scrapy:TypeError: ‘float’ object is not iterable

使用scrapy进行网页爬取时,出现:TypeError: ‘float’ object is not iterable错误,竟然是由于Twisted版本不兼容导致的。具体可参考这里:https://github.com/scrapy/scrapy/issues/2461

修复方法,将Twisted的版本降级到Twisted-16.6.0版本或者小于16.6.0的版本即可。

每日一个linux命令 df

每日一个linux命令:df

linux中df命令的功能是用来检查linux服务器的文件系统的磁盘空间占用情况。可以利用该命令来获取硬盘被占用了多少空间,目前还剩下多少空间等信息。

1.命令格式:

df [选项] [文件]

2.命令功能:

显示指定磁盘文件的可用空间。如果没有文件名被指定,则所有当前被挂载的文件系统的可用空间将被显示。默认情况下,磁盘空间将以 1KB 为单位进行显示,除非环境变量 POSIXLY_CORRECT 被指定,那样将以512字节为单位进行显示

3.命令参数:

必要参数:

[/crayon]

选择参数:

[/crayon]

4.使用实例:

实例1:显示磁盘使用情况

命令:
df
输出:

[/crayon]

说明:

linux中df命令的输出清单的第1列是代表文件系统对应的设备文件的路径名(一般是硬盘上的分区);第2列给出分区包含的数据块(1024字节)的数目;第3,4列分别表示已用的和可用的数据块数目。用户也许会感到奇怪的是,第3,4列块数之和不等于第2列中的块数。这是因为缺省的每个分区都留了少量空间供系统管理员使用。即使遇到普通用户空间已满的情况,管理员仍能登录和留有解决问题所需的工作空间。清单中Use% 列表示普通用户空间使用的百分比,即使这一数字达到100%,分区仍然留有系统管理员使用的空间。最后,Mounted on列表示文件系统的挂载点。

实例2:以inode模式来显示磁盘使用情况

命令:
df -i
输出:

[/crayon]

说明:

实例3:显示指定类型磁盘

命令:
df -t ext3
输出:

[/crayon]

说明:

实例4:列出各文件系统的i节点使用情况

命令:
df -ia
输出:

[/crayon]

说明:

实例5:列出文件系统的类型

命令:
df -T
输出:

[/crayon]

说明:

实例6:以更易读的方式显示目前磁盘空间和使用情况

命令:

输出:

[/crayon]

说明:

-h更具目前磁盘空间和使用情况 以更易读的方式显示
-H根上面的-h参数相同,不过在根式化的时候,采用1000而不是1024进行容量转换
-k以单位显示磁盘的使用情况
-l显示本地的分区的磁盘空间使用率,如果服务器nfs了远程服务器的磁盘,那么在df上加上-l后系统显示的是过滤nsf驱动器后的结果
-i显示inode的使用情况。linux采用了类似指针的方式管理磁盘空间影射.这也是一个比较关键应用

Nginx Web服务器 多维度优化策略

Nginx(读音engine x)服务器由于性能优秀稳定、配置简单以及跨平台,被越来越多的公司和个人所采用,现已成为市场份额继Apache之后的第二大Web服务器。各大小网站论坛博客也介绍说明了Nginx从安装到优化的各种配置。
不过看了很多这些相关Nginx的文档之后,发现一个比较大的问题,就是这些文档基本也就从两个方面着手,一是修改Nginx的配置文件,二是调整操作系统的相关内核参数;而且文档说明也不够明了,缺乏比较系统级别的优化。本文将从Nginx源码编译安装开始,到修改配置文件,调整系统内核参数以及架构四个方面着手分别介绍如何优化。

一. 安装

(1) 精简模块

Nginx由于不断添加新的功能,附带的模块也越来越多。很多操作系统厂商为了用户方便安装管理,都增加了rpm、deb或者其他自有格式软件包,可以本地甚至在线安装。不过我不太建议使用这种安装方式。这虽然简化了安装,在线安装甚至可以自动解决软件依赖关系,但是安装后软件的文件布局过于分散,不便管理维护;同时也正是由于存在软件包之间的依赖关系,导致当有安全漏洞、或者其它问题,想要通过更新升级Nginx新版本时却发现yum、deb源还未发布新版本(一般都落后于官网发布的软件版本)。最重要的是采用非源码编译安装的方式,默认会添加入许多模块,比如邮件相关、uwsgi、memcache等等,很多网站运行时这些模块根本未用到,虽然平时占用的资源很小,但是仍然可能是压弯骆驼的一根稻草。各种非必需模块默认安装运行的同时,也给Web系统带来了安全隐患。尽量保持软件的轻装上阵,是每个运维应当尽力做到的,所以我建议一般常用的服务器软件使用源码编译安装管理。。我一般使用的编译参数如下,PHP相关模块fastcgi被保留用作后文优化说明,:

"--prefix=/App/nginx" \
"--with-http_stub_status_module" \
"--without-http_auth_basic_module" \
"--without-http_autoindex_module" \
"--without-http_browser_module" \
"--without-http_empty_gif_module" \
"--without-http_geo_module" \
"--without-http_limit_conn_module" \
"--without-http_limit_req_module" \
"--without-http_map_module" \
"--without-http_memcached_module" \
"--without-http_proxy_module" \
"--without-http_referer_module" \
"--without-http_scgi_module" \
"--without-http_split_clients_module" \
"--without-http_ssi_module" \
"--without-http_upstream_ip_hash_module" \
"--without-http_upstream_keepalive_module" \
"--without-http_upstream_least_conn_module" \
"--without-http_userid_module" \
"--without-http_uwsgi_module" \
"--without-mail_imap_module" \
"--without-mail_pop3_module" \
"--without-mail_smtp_module" \
"--without-poll_module" \
"--without-select_module" \
"--with-cc-opt='-O2'"

编译参数根据网站是否真正用到的原则增添或者减少,比如我们公司如果需要用到ssi模块,从而能够实现访问shtml页面,可以将第17行删除,那么Nginx将默认安装。大家可以通过运行 "./configure –help" 查看编译帮助,决定是否需要安装哪些模块。

(2) GCC编译参数优化 [可选项】

GCC总共提供了5级编译优化级别:

-O0: 无优化。

-O和-O1: 使用能减少目标代码尺寸以及执行时间并且不会使编译时间明显增加的优化。在编译大型程序的时候会显著增加编译时内存的使用。

-O2: 包含-O1的优化并增加了不需要在目标文件大小和执行速度上进行折衷的优化。编译器不执行循环展开以及函数内联。此选项将增加编译时间和目标文件的执行性能。

-Os: 可以看成 -O2.5,专门优化目标文件大小,执行所有的不增加目标文件大小的-O2优化选项,并且执行专门减小目标文件大小的优化选项。适用于磁盘空间紧张时使用。但有可能有未知的问题发生,况且目前硬盘容量很大,常用程序无必要使用。

-O3: 打开所有 -O2 的优化选项外增加 -finline-functions、-funswitch-loops、-fgcse-after-reload 优化选项。相对于 -O2 性能并未有较多提高,编译时间也最长,生成的目标文件也更大更占内存,有时性能不增反而降低,甚至产生不可预知的问题(包括错误),所以并不被大多数软件安装推荐,除非有绝对把握方可使用此优化级别。

修改GCC编译参数,提高编译优化级别,此方法适用于所有通过GCC编译安装的程序,不止Nginx。稳妥起见用 -O2,这也是大多数软件编译推荐的优化级别。查看Nginx源码文件 auto/cc/gcc,搜索NGX_GCC_OPT,默认GCC编译参数为-O,可以直接修改内容为NGX_GCC_OPT="-O2"或者在 ./configure配置时添加–with-cc-opt='-O2'选项。

二. 配置

应用服务器的性能优化主要在合理使用CPU、内存、磁盘IO和网络IO四个方面,现在我们从Nginx配置文件 nginx.conf 入手进行优化:

(1) 工作进程数的选择

指令:worker_processes

定义了Nginx对外提供web服务时的工作进程数。最优值取决于许多因素,包括(但不限于)CPU核心的数量、存储数据的硬盘数量及负载模式。不能确定的时候,将其设置为可用的CPU内核数将是一个好的开始(设置为“auto”将尝试自动检测它)。Shell执行命令 ps ax | grep "nginx: worker process" | grep -v "grep" 可以看到运行中的Nginx工作进程数,一般建议设置成服务器逻辑核心数,Shell执行命令 cat /proc/cpuinfo | grep processor | wc -l 可以检测出服务器逻辑核心总数,偷懒可以直接写auto,Nginx自适应。

(2) 是否绑定CPU

指令:worker_cpu_affinity

绑定工作进程到对应CPU核心,Nginx默认未开启CPU绑定。目前的服务器一般为多核CPU,当并发很大时,服务器各个CPU的使用率可能出现严重不均衡的局面,这时候可以考虑使用CPU绑定,以达到CPU使用率相对均匀的状态,充分发挥多核CPU的优势。top、htop等程序可以查看所有CPU核心的使用率状况。绑定样例:

worker_processes 4;
worker_cpu_affinity 0001 0010 0100 1000;

(3) 打开文件数限制

指令:worker_rlimit_nofile

设定了每个Nginx工作进程打开的最大文件数,受限于系统的用户进程打开文件数限制,未设置则使用系统默认值。理论上应该设置为当前Shell启动进程的最大打开文件数除以Nginx的工作进程数。由于Nginx的工作进程打开文件数并不一完全均匀,所以可以将其设置成Shell启动进程的最大打开文件数。Shell执行命令 ulimit -n 可以查看当前登录Shell会话最大打开文件数数限制。Linux系统用户进程默认同时打开文件最大数为1024,这个值太小,访问量稍大就报“too many open files"。Shell执行命令先修改用户打开文件数限制:

echo "* – nofile 65536" >> /etc/security/limits.conf
然后添加入/etc/profile如下两行内容,修改所有Shell和通过Shell启动的进程打开文件数限制:

echo "ulimit -n 65536" >> /etc/profile
Shell执行命令使当前Shell临时会话立即生效:

ulimit -n 65536

(4) 惊群问题

指令:accept_mutex

如果 accept_mutex 指令值为 on 启用,那么将轮流唤醒一个工作进程接收处理新的连接,其余工作进程继续保持睡眠;如果值为 off 关闭,那么将唤醒所有工作进程,由系统通过use指令指定的网络IO模型调度决定由哪个工作进程处理,未接收到连接请求的工作进程继续保持睡眠,这就是所谓的“惊群问题”。Web服务器Apache的进程数很多,成百上千也是时有的事,“惊群问题”也尤为明显。Nginx为了稳定,参数值保守的设置为 on 开启状态。可以将其设置成Off 提高性能和吞吐量,但这样也会带来上下文切换增多或者负载升高等等其它资源更多消耗的后果。

(5) 网络IO模型

指令:use

定义了Nginx设置用于复用客户端线程的轮询方法(也可称多路复用网络IO模型)。这自然是选择效率更高的优先,Linux 2.6+内核推荐使用epoll,FreeBSD推荐使用kqueue,安装时Nginx会自动选择。

(6) 连接数

指令:worker_connections

定义了Nginx一个工作进程的最大同时连接数,不仅限于客户端连接,包括了和后端被代理服务器等其他的连接。官网文档还指出了该参数值不能超过 worker_rlimit_nofile 值,所以建议设置成和 worker_rlimit_nofile 值相等。

(7) 打开文件缓存

指令:open_file_cache

开启关闭打开文件缓存,默认值 off 关闭,强烈建议开启,可以避免重新打开同一文件带来的系统开销,节省响应时间。如需开启必须后接参数 max=数字,设置缓存元素的最大数量。当缓存溢出时,使用LRU(最近最少使用)算法删除缓存中的元素;可选参数 inactive=时间 设置超时,在这段时间内缓存元素如果没有被访问,将从缓存中删除。示例:open_file_cache max=65536 inactive=60s。

指令:open_file_cache_valid

设置检查open_file_cache缓存的元素的时间间隔。

指令:open_file_cache_min_uses

设置在由open_file_cache指令的inactive参数配置的超时时间内, 文件应该被访问的最小次数。如果访问次数大于等于此值,文件描述符会保留在缓存中,否则从缓存中删除。

(8) 日志相关

指令:access_log 和 error_log

当并发很大时,Nginx的访问日志和错误日志的保存肯定会造成对磁盘的大量读写,也将影响Nginx的性能。并发量越大,IO越高。这时候可以考虑关闭访问日志和错误日志,或者将日志保存到tmpfs文件系统里,或者减少保存的访问日志条目和错误日志的级别,从而避免磁盘IO的影响。关闭日志使用 access_log off。如必须保存日志,可以按每日或者每时或者其它时间段对日志做切割,这也可以减小IO,虽然可能效果不是特别大,不过因为日志文件尺寸变小了很多,也方便查阅或归档分析日志。一般线上环境建议错误日志设置为 error 或者 crit。自定义访问日志的条目和错误日志的级别,详细信息可以参阅官网或者网上其它文档,按需修改。

(9) 隐藏Nginx版本号

指令:server_tokens

开启或关闭“Server”响应头中输出的Nginx版本号。推介设置为 off,关闭显示响应头的版本号,对性能的提高有小小的裨益,主要还是为了安全起见,不被骇客找到版本号对应的漏洞,从而被攻击。

(10) 压缩相关

指令:gzip

Nginx默认开启了gzip压缩功能。有可能很多人认为,开启gzip压缩会增加CPU的处理时间和负载。但是经过我们网站的测试发现,关闭了gzip压缩功能的Nginx虽然减少了CPU计算,节省了服务器的响应时间,但网站页面总体响应时间反而加长了,原因在于js和css、xml、json、html等等这些静态文件的数据传输时间的增长大大超过了服务器节省出来的响应时间,得不偿失。gzip on 开启压缩后,大约可以减少75%的文件尺寸,不但节省了比较多的带宽流量,也提高了页面的整体响应时间。所有建议还是开启。当然也不是所有的静态文件都需要压缩,比如静态图片和PDF、视频,文件本身就应当做压缩处理后保存到服务器。这些文件再次使用gzip压缩,压缩的比例并不高,甚至适得其反,压缩后文件尺寸增大了。CPU压缩处理这些静态文件增加占用的服务器响应时间绝大部分时候会超过了被压缩减小的文件尺寸减少的数据传输时间,不划算。是否需要对Web网站开启压缩,以及对哪些文件过滤压缩,大家可以通过使用HttpWatch、Firebug等等网络分析工具对比测试。

指令:gzip_comp_level

指定压缩等级,其值从1到9,数字越大,压缩率越高,越消耗CPU,负载也越高。9等级无疑压缩率最高,压缩后的文件尺寸也最小,但也是最耗CPU资源,负载最高,速度最慢的,这对于用户访问有时是无法忍受的。一般推荐使用1-4等级,比较折衷的方案。我们公司网站使用等级2。

指令:gzip_min_length

指定压缩的文件最小尺寸,单位 bytes 字节,低于该值的不压缩,超过该值的将被压缩。我们网站设置为1k,太小的文件没必要压缩,压缩过小尺寸文件带来增加的CPU消耗时间和压缩减少的文件尺寸降低的数据下载时间互相抵消,并有可能增加总体的响应时间。

指令:gzip_types

指定允许压缩的文件类型,Nginx配置目录 conf 下的 mime.types 文件存放了Nginx支持的文件类型,text/html类型文件,文件后缀为html htm shtml默认压缩。推荐配置:gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript。

(11) 浏览器缓存

指令:expires

设置HTTP应答中的“Expires”和“Cache-Control”头标。"Expires"一般结合"Last-Modified"使用。当设置了合理的expires配置时,浏览器第一次访问Web页面元素,会下载页面中的的静态文件到本机临时缓存目录下。第二次及之后再次访问相同URL时将发送带头标识"If-Modified-Since"和本地缓存文件时间属性值的请求给服务器,服务器比对服务器本地文件时间属性值,如果未修改,服务器直接返回http 304状态码,浏览器直接调用本地已缓存的文件;如果时间属性值修改了,重新发送新文件。这样就避免了从服务器再次传送文件内容,减小了服务器压力,节省了带宽,同时也提高了用户访问速度,一举三得。指令后接数字加时间单位,即为缓存过期时间;-1 表示永远过期,不缓存。强烈建议添加expires配置,过期时间的选择具体分析。我们公司的部分Nginx配置如下:

{
    expires 30d;
}
{
    expires 30d;
}

或者统一将静态文件放在固定目录下再对目录做location和expires,示例:

location /static/
{
expires 30d;
}

(12) 持久连接

指令:keepalive_timeout

启用Http的持久连接Keepalive属性,复用之前已建立的TCP连接接收请求、发送回应,减少重新建立TCP连接的资源时间开销。在此的建议是当网站页面内容以静态为主时,开启持久连接;若主要是动态网页,且不能被转化为静态页面,则关闭持久连接。后接数字和时间单位符号。正数为开启持久连接,0关闭。

(13) 减少HTTP请求次数

网站页面中存在大量的图片、脚本、样式表、Flash等静态元素,减少访问请求次数最大的优点就是减少用户首次访问页面的加载时间。可以采用合并相同类型文件为一个文件的办法减少请求次数。这其实属于Web前端优化范畴,应当由Web前段工程师做好相关静态文件的规划管理,而不是由运维来做。不过Nginx也可以通过安装阿里巴巴提供的Concat或者Google的PageSpeed模块实现这个合并文件的功能。我们公司并未使用合并功能,具体安装配置信息请查询网上相关文档,这里不再累述。Concat源代码网址:https://github.com/alibaba/nginx-http-concat/,PageSpeed源代码网址:https://github.com/pagespeed/ngx_pagespeed。

(14) PHP相关

Nginx不能直接解析PHP代码文件,需要调用FastCGI接口转给PHP解释器执行,然后将结果返回给Nginx。PHP优化本文暂不介绍。Nginx可以开启FastCGI的缓存功能,从而提高性能。

指令:fastcgi_temp_path

定义FastCGI缓存文件保存临时路径。

指令:fastcgi_cache_path

定义FastCGI缓存文件保存路径和缓存的其它参数。缓存数据以二进制数据文件形式存储,缓存文件名和key都是通过对访问URL使用MD5计算获得的结果。缓存文件先保存至fastcgi_temp_path指定的临时目录下,然后通过重命名操作移至fastcgi_cache_path指定的缓存目录。levels指定了目录结构,子目录数以16为基数;keys_zone指定了共享内存区名和大小,用于保存缓存key和数据信息;inactive指定了缓存数据保存的时间,当这段时间内未被访问,将被移出;max_size指定了缓存使用的最大磁盘空间,超过容量时将最近最少使用数据删除。建议fastcgi_temp_path和fastcgi_cache_path设为同一分区,同分区移动操作效率更高。示例:

fastcgi_temp_path /tmp/fastcgi_temp;
fastcgi_cache_path /tmp/fastcgi_cache levels=1:2 keys_zone=cache_fastcgi:16m inactive=30m max_size=1g;
示例中使用/tmp/fastcgi_temp作为FastCGI缓存的临时目录;/tmp/fastcgi_cache作为FastCGI缓存保存的最终目录;一级子目录为16的一次方16个,二级子目录为16的2次方256个;共享内存区名为cache_fastcgi,占用内存128MB;缓存过期时间为30分钟;缓存数据保存于磁盘的最大空间大小为1GB。

指令:fastcgi_cache_key

定义FastCGI缓存关键字。启用FastCGI缓存必须加上这个配置,不然访问所有PHP的请求都为访问第一个PHP文件URL的结果。

指令:fastcgi_cache_valid

为指定的Http状态码指定缓存时间。

指令:fastcgi_cache_min_uses

指定经过多少次请求相同的URL将被缓存。

指令:fastcgi_cache_use_stale

指定当连接FastCGI服务器发生错误时,哪些情况使用过期数据回应。

指令:fastcgi_cache

缓存使用哪个共享内存区。

我常用nginx.conf模板,大家根据情况做适当修改:

worker_processes  auto;

error_log  logs/error.log error;

pid        logs/nginx.pid;
worker_rlimit_nofile    65536;

events
{
    use epoll;
    accept_mutex off;
    worker_connections  65536;
}


http
{
    include       mime.types;
    default_type  text/html;

    charset UTF-8;
    server_names_hash_bucket_size   128;
    client_header_buffer_size       4k;
    large_client_header_buffers  4  32k;
    client_max_body_size            8m;

    open_file_cache max=65536  inactive=60s;
    open_file_cache_valid      80s;
    open_file_cache_min_uses   1;

    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  logs/access.log  main;

    sendfile    on;
    server_tokens off;

    fastcgi_temp_path  /tmp/fastcgi_temp;
    fastcgi_cache_path /tmp/fastcgi_cache levels=1:2 keys_zone=cache_fastcgi:128m inactive=30m max_size=1g;
    fastcgi_cache_key $request_method://$host$request_uri;
    fastcgi_cache_valid 200 302 1h;
    fastcgi_cache_valid 301     1d;
    fastcgi_cache_valid any     1m;
    fastcgi_cache_min_uses 1;
    fastcgi_cache_use_stale error timeout http_500 http_503 invalid_header;

    keepalive_timeout  60;

    gzip  on;
    gzip_min_length 1k;
    gzip_buffers  4 64k;
    gzip_http_version   1.1;
    gzip_comp_level 2;
    gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript;

    server
    {
        listen       80;
        server_name  localhost;
        index        index.html;
        root         /App/web;

        location ~ .+\.(php|php5)$
        {
            fastcgi_pass   unix:/tmp/php.sock;
            fastcgi_index  index.php;
            include        fastcgi.conf;
            fastcgi_cache  cache_fastcgi;
        }

        location ~ .+\.(gif|jpg|jpeg|png|bmp|swf|txt|csv|doc|docx|xls|xlsx|ppt|pptx|flv)$
        {
            expires 30d;
        }

        location ~ .+\.(js|css|html|xml)$
        {
            expires 30d;
        }

        location /nginx-status
        {
            stub_status on;
            allow 192.168.1.0/24;
            allow 127.0.0.1;
            deny all;
        }
    }
}

三. 内核

Linux内核参数部分默认值不适合高并发,一般临时方法可以通过调整/Proc文件系统,或者直接修改/etc/sysctl.conf配置文件永久保存。调整/Proc文件系统,系统重启后还原至默认值,所以不推荐。Linux内核调优,主要涉及到网络和文件系统、内存等的优化,下面是我常用的内核调优配置:

########################################
net.core.rmem_default = 262144
net.core.rmem_max = 16777216
net.core.wmem_default = 262144
net.core.wmem_max = 16777216
net.core.somaxconn = 262144
net.core.netdev_max_backlog = 262144
net.ipv4.tcp_max_orphans = 262144
net.ipv4.tcp_max_syn_backlog = 262144
net.ipv4.tcp_max_tw_buckets = 10000
net.ipv4.ip_local_port_range = 1024 65500
net.ipv4.tcp_tw_recycle = 1
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_syncookies = 1
net.ipv4.tcp_synack_retries = 1
net.ipv4.tcp_syn_retries = 1
net.ipv4.tcp_fin_timeout = 30
net.ipv4.tcp_keepalive_time = 600
net.ipv4.tcp_keepalive_intvl = 30
net.ipv4.tcp_keepalive_probes = 3
net.ipv4.tcp_mem = 786432 1048576 1572864
fs.aio-max-nr = 1048576
fs.file-max = 6815744
kernel.sem = 250 32000 100 128
vm.swappiness = 10
EOF
sysctl -p

详细说明大家可以查看我的Linux内核优化文章:http://dongsong.blog.51cto.com/916653/1631085。

四. 架构

Nginx的最大优势在于处理静态文件和代理转发功能,支持7层负载均衡和故障隔离。 动静分离是每个网站发展到一定规模之后必然的结果。静态请求则应当最好将其拆分,并启用独立的域名,既便于管理的需要,也便于今后能够快速支持CDN。如果一台Nginx性能无法满足,则可以考虑在Nginx前端添加LVS负载均衡,或者F5等硬件负载均衡(费用昂贵,适合土豪公司单位),由多台Nginx共同分担网站请求。还可以考虑结合Varnish或者Squid缓存静态文件实现类似CDN功能。新版Nginx目前已经支持直接读写Memcache,可以编译安装时候选择添加此类模块,从而节省了转交给PHP或者JPS等动态程序服务器处理时间,提高效率的同时,减小了动态服务器的负载。

Elasticsearch 架构以及源码概览

Elasticsearch 是最近两年异军突起的一个兼有搜索引擎和NoSQL数据库功能的开源系统,基于Java/Lucene构建。最近研究了一下,感觉 Elasticsearch 的架构以及其开源的生态构建都有许多可借鉴之处,所以整理成文章分享下。本文的代码以及架构分析主要基于 Elasticsearch 2.X 最新稳定版。

Elasticsearch 看名字就能大概了解下它是一个弹性的搜索引擎。首先弹性隐含的意思是分布式,单机系统是没法弹起来的,然后加上灵活的伸缩机制,就是这里的 Elastic 包含的意思。它的搜索存储功能主要是 Lucene 提供的,Lucene 相当于其存储引擎,它在之上封装了索引,查询,以及分布式相关的接口。

Elasticsearch 中的几个概念

  1. 集群(Cluster)一组拥有共同的 cluster name 的节点。
  2. 节点(Node) 集群中的一个 Elasticearch 实例。
  3. 索引(Index) 相当于关系数据库中的database概念,一个集群中可以包含多个索引。这个是个逻辑概念。
  4. 主分片(Primary shard) 索引的子集,索引可以切分成多个分片,分布到不同的集群节点上。分片对应的是 Lucene 中的索引。
  5. 副本分片(Replica shard)每个主分片可以有一个或者多个副本。
  6. 类型(Type)相当于数据库中的table概念,mapping是针对 Type 的。同一个索引里可以包含多个 Type。
  7. Mapping 相当于数据库中的schema,用来约束字段的类型,不过 Elasticsearch 的 mapping 可以自动根据数据创建。
  8. 文档(Document) 相当于数据库中的row。
  9. 字段(Field)相当于数据库中的column。
  10. 分配(Allocation) 将分片分配给某个节点的过程,包括分配主分片或者副本。如果是副本,还包含从主分片复制数据的过程。

分布式以及 Elastic

分布式系统要解决的第一个问题就是节点之间互相发现以及选主的机制。如果使用了 Zookeeper/Etcd 这样的成熟的服务发现工具,这两个问题都一并解决了。但 Elasticsearch 并没有依赖这样的工具,带来的好处是部署服务的成本和复杂度降低了,不用预先依赖一个服务发现的集群,缺点当然是将复杂度带入了 Elasticsearch 内部。

服务发现以及选主 ZenDiscovery

  1. 节点启动后先ping(这里的ping是 Elasticsearch 的一个RPC命令。如果 discovery.zen.ping.unicast.hosts 有设置,则ping设置中的host,否则尝试ping localhost 的几个端口, Elasticsearch 支持同一个主机启动多个节点)
  2. Ping的response会包含该节点的基本信息以及该节点认为的master节点。
  3. 选举开始,先从各节点认为的master中选,规则很简单,按照id的字典序排序,取第一个。
  4. 如果各节点都没有认为的master,则从所有节点中选择,规则同上。这里有个限制条件就是 discovery.zen.minimum_master_nodes,如果节点数达不到最小值的限制,则循环上述过程,直到节点数足够可以开始选举。
  5. 最后选举结果是肯定能选举出一个master,如果只有一个local节点那就选出的是自己。
  6. 如果当前节点是master,则开始等待节点数达到 minimum_master_nodes,然后提供服务。
  7. 如果当前节点不是master,则尝试加入master。

Elasticsearch 将以上服务发现以及选主的流程叫做 ZenDiscovery 。由于它支持任意数目的集群(1-N),所以不能像 Zookeeper/Etcd 那样限制节点必须是奇数,也就无法用投票的机制来选主,而是通过一个规则,只要所有的节点都遵循同样的规则,得到的信息都是对等的,选出来的主节点肯定是一致的。但分布式系统的问题就出在信息不对等的情况,这时候很容易出现脑裂(Split-Brain)的问题,大多数解决方案就是设置一个quorum值,要求可用节点必须大于quorum(一般是超过半数节点),才能对外提供服务。而 Elasticsearch 中,这个quorum的配置就是 discovery.zen.minimum_master_nodes 。 说到这里要吐槽下 Elasticsearch 的方法和变量命名,它的方法和配置中的master指的是master的候选节点,也就是说可能成为master的节点,并不是表示当前的master,我就被它的一个 isMasterNode 方法坑了,开始一直没能理解它的选举规则。

弹性伸缩 Elastic

Elasticsearch 的弹性体现在两个方面: 1. 服务发现机制让节点很容易加入和退出。 2. 丰富的设置以及allocation API。

Elasticsearch 节点启动的时候只需要配置discovery.zen.ping.unicast.hosts,这里不需要列举集群中所有的节点,只要知道其中一个即可。当然为了避免重启集群时正好配置的节点挂掉,最好多配置几个节点。节点退出时只需要调用 API 将该节点从集群中排除 (Shard Allocation Filtering),系统会自动迁移该节点上的数据,然后关闭该节点即可。当然最好也将不可用的已知节点从其他节点的配置中去除,避免下次启动时出错。

分片(Shard)以及副本(Replica)  分布式存储系统为了解决单机容量以及容灾的问题,都需要有分片以及副本机制。Elasticsearch 没有采用节点级别的主从复制,而是基于分片。它当前还未提供分片切分(shard-splitting)的机制,只能创建索引的时候静态设置。

ElasticSearch-01

(elasticsearch 官方博客的图片)

比如上图所示,开始设置为5个分片,在单个节点上,后来扩容到5个节点,每个节点有一个分片。如果继续扩容,是不能自动切分进行数据迁移的。官方文档的说法是分片切分成本和重新索引的成本差不多,所以建议干脆通过接口重新索引

Elasticsearch 的分片默认是基于id 哈希的,id可以用户指定,也可以自动生成。但这个可以通过参数(routing)或者在mapping配置中修改。当前版本默认的哈希算法是MurmurHash3

Elasticsearch 禁止同一个分片的主分片和副本分片在同一个节点上,所以如果是一个节点的集群是不能有副本的。

恢复以及容灾

分布式系统的一个要求就是要保证高可用。前面描述的退出流程是节点主动退出的场景,但如果是故障导致节点挂掉,Elasticsearch 就会主动allocation。但如果节点丢失后立刻allocation,稍后节点恢复又立刻加入,会造成浪费。Elasticsearch的恢复流程大致如下:

  1. 集群中的某个节点丢失网络连接
  2. master提升该节点上的所有主分片的在其他节点上的副本为主分片
  3. cluster集群状态变为 yellow ,因为副本数不够
  4. 等待一个超时设置的时间,如果丢失节点回来就可以立即恢复(默认为1分钟,通过 index.unassigned.node_left.delayed_timeout 设置)。如果该分片已经有写入,则通过translog进行增量同步数据。
  5. 否则将副本分配给其他节点,开始同步数据。

但如果该节点上的分片没有副本,则无法恢复,集群状态会变为red,表示可能要丢失该分片的数据了。

分布式集群的另外一个问题就是集群整个重启后可能导致不预期的分片重新分配(部分节点没有启动完成的时候,集群以为节点丢失),浪费带宽。所以 Elasticsearch 通过以下静态配置(不能通过API修改)控制整个流程,以10个节点的集群为例:

  • gateway.recover_after_nodes: 8
  • gateway.expected_nodes: 10
  • gateway.recover_after_time: 5m

比如10个节点的集群,按照上面的规则配置,当集群重启后,首先系统等待 minimum_master_nodes(6)个节点加入才会选出master, recovery操作是在 master节点上进行的,由于我们设置了 recover_after_nodes(8),系统会继续等待到8个节点加入, 才开始进行recovery。当开始recovery的时候,如果发现集群中的节点数小于expected_nodes,也就是还有部分节点未加入,于是开始recover_after_time 倒计时(如果节点数达到expected_nodes则立刻进行 recovery),5分钟后,如果剩余的节点依然没有加入,则会进行数据recovery。

Elasticsearch 除了支持 Lucene 本身的检索功能外,在之上做了一些扩展。 1. 脚本支持
Elasticsearch 默认支持groovy脚本,扩展了 Lucene 的评分机制,可以很容易的支持复杂的自定义评分算法。它默认只支持通过sandbox方式实现的脚本语言(如lucene expression,mustache),groovy必须明确设置后才能开启。Groovy的安全机制是通过java.security.AccessControlContext设置了一个class白名单来控制权限的,1.x版本的时候是自己做的一个白名单过滤器,但限制策略有漏洞,导致一个远程代码执行漏洞。 2. 默认会生成一个 _all 字段,将所有其他字段的值拼接在一起。这样搜索时可以不指定字段,并且方便实现跨字段的检索。 3. Suggester Elasticsearch 通过扩展的索引机制,可以实现像google那样的自动完成suggestion以及搜索词语错误纠正的suggestion。

NoSQL 数据库

Elasticsearch 可以作为数据库使用,主要依赖于它的以下特性:

  1. 默认在索引中保存原始数据,并可获取。这个主要依赖 Lucene 的store功能。
  2. 实现了translog,提供了实时的数据读取能力以及完备的数据持久化能力(在服务器异常挂掉的情况下依然不会丢数据)。Lucene 因为有 IndexWriter buffer, 如果进程异常挂掉,buffer中的数据是会丢失的。所以 Elasticsearch 通过translog来确保不丢数据。同时通过id直接读取文档的时候,Elasticsearch 会先尝试从translog中读取,之后才从索引中读取。也就是说,即便是buffer中的数据尚未刷新到索引,依然能提供实时的数据读取能力。Elasticsearch 的translog 默认是每次写请求完成后统一fsync一次,同时有个定时任务检测(默认5秒钟一次)。如果业务场景需要更大的写吞吐量,可以调整translog相关的配置进行优化。
  3. dynamic-mapping 以及 schema-free
    Elasticsearch 的dynamic-mapping相当于根据用户提交的数据,动态检测字段类型,自动给数据库表建立表结构,也可以动态增加字段,所以它叫做schema-free,而不是schema-less。这种方式的好处是用户能一定程度享受schema-less的好处,不用提前建立表结构,同时因为实际上是有schema的,可以做查询上的优化,检索效率要比纯schema-less的数据库高许多。但缺点就是已经创建的索引不能变更数据类型(Elasticsearch 写入数据的时候如果类型不匹配会自动尝试做类型转换,如果失败就会报错,比如数字类型的字段写入字符串”123”是可以的,但写入”abc”就不可以。),要损失一定的自由度。另外 Elasticsearch 提供的index-template功能方便用户动态创建索引的时候预先设定索引的相关参数以及type mapping,比如按天创建日志库,template可以设置为对 log-* 的索引都生效。这两个功能我建议新的数据库都可以借鉴下。
  4. 丰富的QueryDSL功能
    Elasticsearch 的query语法基本上和sql对等的,除了join查询,以及嵌套临时表查询不能支持。不过 Elasticsearch 支持嵌套对象以及parent外部引用查询,所以一定程度上可以解决关联查询的需求。另外group by这种查询可以通过其aggregation实现。Elasticsearch 提供的aggregation能力非常强大,其生态圈里的 Kibana 主要就是依赖aggregation来实现数据分析以及可视化的。

系统架构

Elasticsearch 的依赖注入用的是guice,网络使用netty,提供http rest和RPC两种协议。

Elasticsearch 之所以用guice,而不是用spring做依赖注入,关键的一个原因是guice可以帮它很容易的实现模块化,通过代码进行模块组装,可以很精确的控制依赖注入的管理范围。比如 Elasticsearch 给每个shard单独生成一个injector,可以将该shard相关的配置以及组件注入进去,降低编码和状态管理的复杂度,同时删除shard的时候也方便回收相关对象。这方面有兴趣使用guice的可以借鉴。

ClusterState

前面我们分析了 Elasticsearch 的服务发现以及选举机制,它是内部自己实现的。服务发现工具做的事情其实就是跨服务器的状态同步,多个节点修改同一个数据对象,需要有一种机制将这个数据对象同步到所有的节点。Elasticsearch 的ClusterState 就是这样一个数据对象,保存了集群的状态,索引/分片的路由表,节点列表,元数据等,还包含一个ClusterBlocks,相当于分布式锁,用于实现分布式的任务同步。

主节点上有个单独的进程处理 ClusterState 的变更操作,每次变更会更新版本号。变更后会通过PRC接口同步到其他节点。主节知道其他节点的ClusterState 的当前版本,发送变更的时候会做diff,实现增量更新。

Rest 和 RPC

elasticsearch-rest

Elasticsearch 的rest请求的传递流程如上图(这里对实际流程做了简化): 1. 用户发起http请求,Elasticsearch 的9200端口接受请求后,传递给对应的RestAction。 2. RestAction做的事情很简单,将rest请求转换为RPC的TransportRequest,然后调用NodeClient,相当于用客户端的方式请求RPC服务,只不过transport层会对本节点的请求特殊处理。

这样做的好处是将http和RPC两层隔离,增加部署的灵活性。部署的时候既可以同时开启RPC和http服务,也可以用client模式部署一组服务专门提供http rest服务,另外一组只开启RPC服务,专门做data节点,便于分担压力。

Elasticsearch 的RPC的序列化机制使用了 Lucene 的压缩数据类型,支持vint这样的变长数字类型,省略了字段名,用流式方式按顺序写入字段的值。每个需要传输的对象都需要实现:

设计高并发下的读服务?一个电商老兵的10条经验

本文作者是一个一线的电商老兵,任职于京东商城。在本文中,他将会分享他在构建以读为主的系统时总结的经验和教训,内容包括使用HTTP协议对外通讯、使用短连接、数据异构、巧用缓存、流量控制、防刷、降级、多域名等,作者老马不带遮掩的,把自己总结的经验,包括代码都放到这里了,欢迎各位检阅!

几乎所有的互联网系统从开始都是一体化设计的,基本上所有的功能代码都是耦合在一起的。后续随着用户的不断增多业务也越来越多样化,系统需要的维护人员也会越来越多,相应的系统的复杂度、稳定性、可维护性也就越来越难控制,这时系统的拆分以及服务化就成了必然的选择。

系统被拆分后实现方式也就多样化起来,各个系统可以根据自己的业务需求、技术特性、方便程度甚至个人喜好来选择使用不同的语言。服务化后各种功能被拆分的越来越细,原来可能一次请求能够完成的事,现在就需要多次请求并将结果进行融合。

服务化的好处是系统的职责变得清晰,可以突破单一资源限制等,比如突破数据库连接资源的限制(包括关系型数据库、非关系型数据库);不太友好的地方如服务分化、治理复杂等,比如页面要展示一个商品就需要调用库存、商品、价格、促销等各种服务。

像库存、商品、价格等这些体量(访问量+数据量)非常大的服务将他们拆分为单一系统(一个系统只提供一种服务)是很有必要的。对于体量不够大的或者职责划分不清的服务,为了便于维护和使用,一般会将其融合在一个系统中(暂且称它为”非单一系统”)。这些服务一个共同的特点是读大于写,比如京东首页的全部分类、热搜索词等, 可以说是一个彻彻底底的读服务,这些信息数据量小而且很少改动,读取量远远高于写入(或更新)量,像单品页要用到的延保、pop套装等服务,虽然对于单个商品他们的读写不频繁,但他们会涉及很多(亿级别)sku,所以整体加起来他们的访问量、数据量、更新频率都不小。那么针对这些五花八门的服务,怎么才能在一个系统里,既要保证高可用,又保证高性能,还要保证数据一致性等问题,下面我们就来一一解答。

系统特点

  1. 提供的服务多
  2. 依赖的数据源多样化,数据库、HTTP接口、JSF(公司内部RPC框架)接口等
  3. 系统以读为主
  4. 整体服务加起来体量大(访问量+数据量)
  5. 需要快速响应
  6. 服务之间相互影响性要小

基本原则

根据以上系统特点,我们实现该系统时遵循以下几个大的原则:

  1. 使用HTTP协议对外通信
  2. 使用短连接
  3. 数据异构
  4. 巧用缓存
  5. 流量控制
  6. 异步、并行
  7. 数据托底
  8. 防刷
  9. 降级
  10. 多域名

使用HTTP协议对外通信

前面提到服务化后各个系统使用的语言可以不相同,对于使用同一种语言实现的不同系统,可以指定语言相关的协议进行通信,比如JSF(公司内部RPC框架),不同语言的系统之间就需要找一个通用的协议来通信。SOAP简单对象访问协议是一种非语言相关的通信协议, 以HTTP协议为载体进行传输,虽然有各种辅助框架,但它还是太重了,相比较HTTP从便捷和使用范围上有绝对的优势,所以本系统以HTTP协议对外提供服务。

使用短连接

HTTP协议本身是工作在TCP协议上的,这里说的长连接短连接本质上只的是TCP的长短连接。所谓的长连接顾名思义就是用完之后不立即断开连接,何时断开取决于上层业务设置和底层协议是否发生异常,短连接就比较干脆,干完活马上就将连接关闭,过完河就拆桥。

在HTTP中开启长连接需要在协议头中加上Connection:keep-alive,当然最终是否使用长连接通信是需要双方进行协商的,客户端和服务端只要有一方不同意,则开启失败。长连接因为可以复用链路,所以如果请求频繁,可以减少连接的建立和关闭时间,从而节省资源。

HTTP 1.0默认使用短连接,HTTP 1.1中开启短连接需要在协议头上加上Connection:close,如何单个客户请求频繁,TCP链接的建立和关闭多少会浪费点资源。

既然长连接这么『好』,短连接这么『不好』为什么还要使用短连接呢?我们知道这个『连接』实际上是TCP连接。TCP连接是有一个四元组表示的,如[源ip:源port—目标ip:目标port]。从这个四元组可以看到理论上可以有无数个连接, 但是操作系统能够承受的连接可是有限的,假设我们设置了长连接,那么不管这个时间有多短,在高并发下server端都会产生大量的TCP连接,操作系统维护每个连接不但要消耗内存也会消耗CPU,在高并发下维护过多的活跃连接风险可想而知。

而且在长连接的情况下如果有人搞恶意攻击,创建完连接后什么都不做,势必会对Server产生不小的压力。所以在互联网这种高并发系统中,使用短连接是一个明智的选择。对于服务端因短连接产生的大量的TIME_WAIT状态的连接,可以更改系统的一些内核参数来控制,比如net.ipv4.tcp_max_tw_buckets、net.ipv4.tcp_tw_recycle、net.ipv4.tcp_tw_reuse等参数(注:非专业人士调优内核参数要慎重)。

具体TIME_WAIT等TCP的各种状态这里不再详述,给出一个简单状态转换图供参考:

gaobingfa-01

数据异构

一个大的原则,如果依赖的服务不可靠,那系统就可能随时出问题。对于依赖服务的数据,能异构的就要拿过来,有了数据就可以做任何你想做的事,有了数据,依赖服务再怎么变着花的挂对你的影响也是有限的。

异构时可以将数据打散,将数据原子化,这样在向外提供服务时,可以任意组装拼合。

巧用缓存

应对高并发系统,缓存是必不可少的利器,巧妙的使用缓存会使系统的性能有质的飞跃,下面就介绍一下本系统使用缓存的几种方式。

使用Redis缓存

首先看一下使用Redis缓存的简单数据流向图:

gaobingfa-02很典型的使用缓存的一种方式,这里先重点介绍一下在缓存命中与不命中时都做了哪些事。

当用户发起请求后,首先在Nginx这一层直接从Redis获取数据, 这个过程中Nginx使用lua-resty-redis操作Redis,该模块支持网络Socket和unix domain socket。如果命中缓存,则直接返回客户端。如果没有则回源请求数据,这里要记住另一个原则,不可『随意回源』(为了保护后端应用)。为了解决高并发下缓存失效后引发的雪崩效应,我们使用lua-resty-lock(异步非阻塞锁)来解决这个问题。

很多人一谈到锁就心有忌惮,认为一旦用上锁必然会影响性能,这种想法的不妥的。我们这里使用的lua-resty-lock是一个基于Nginx共享内存(ngx.shared.DICT)的非阻塞锁(基于Nginx的时间事件实现),说它是非阻塞的是因为它不会阻塞Nginx的worker进程,当某个key(请求)获取到该锁后,后续试图对该key再一次获取锁时都会『阻塞』在这里,但不会阻塞其它的key。当第一个获取锁的key将获取到的数据更新到缓存后,后续的key就不会再回源后端应用了,从而可以起到保护后端应用的作用。

下面贴一段从官网弄过来的简化代码,详细使用请移步https://github.com/openresty/lua-resty-lock

gaobingfa-03

使用Nginx共享缓存

上面使用到的Redis缓存,即使Redis部署在本地仍然会有进程间通信、内核态和用户态的数据拷贝,使用Nginx的共享缓存可以将这些动作都省略掉。

Nginx共享缓存是worker共享的,也就是说它是一个全局的缓存,使用Nginx的lua_shared_dict配置指令定义。语法如下:

#指定一个100m的共享缓存
lua_shared_dict cache 100m;

数据流向图如下:

gaobingfa-04

缓存分片

当缓存数据的总量大到一定程度后,单个Redis实例就会成为瓶颈,这时候就要考虑分片了,具体如何分片可以根据自己的系统特性来定,如果不是对性能有苛刻的要求,可以直接使用一些Redis代理(如temproxy),因为代理对Redis性能有一定的损耗。

使用代理的另一个好处是它支持多种分片算法,而且对用户是透明的。我们这里没有选择代理,而是自己实现了一个简单的分片算法。

该分片算法在Java端基于Jedis扩展出一套取摸算法,向Redis写数据。Nginx这端使用lua+c实现同样的算法,从Redis读数据。

另一种是对Nginx的共享缓存(dict)做分片,dict本身使用自旋锁加红黑树实现的,它这个锁是一个阻塞锁。同样当缓存在dict中的数据量和访问并发量大到一定程度后,对其分片也是必须的了。

缓存数据切割

早前阅读Redis代码发现Redis在每个事件循环中,一次最多向某个连接吐64K的数据,也就是说当缓存的数据大于64K时,至少需要两个事件循环才能将数据吐完。当然,在网络发生拥堵或者对端处理数据慢时,即使缓存数据小于64K,也不能保证在一个事件循环内吐完数据。基于这种情况我们可以考虑,当数据大于某个阀值时,将数据切割成多个小块,然后将其放到不同的Redis上。

简单描述下实现方式:

  1. 存储时先判断数据大小(数据大小用n表示,阀值用a表示),如果n大于a则代表需要将数据切割存储,切割的块数用b表示,b是Redis的实例个数,用n整除b得出的数c是要切割的数据块(前b-1块)的大小,最后一块数据的大小是n-c*2。存储前生成一个版本号,将这个版本号放到被切割块的第一个字节,然后按照顺序异步将其存入各个Redis中,最后再为代表该数据的key打上标记,标记该key的数据是被切割的。
  2. 取数据时先检查该key是否被标记,如果被标记则使用ngx.thread.spawn(),按照顺序异步并行向各个Redis发送get命令,然后对比获取到的所有数据块的第一个字节,如果比对一直,则拼装输出。

注:这种算法用在Nginx的共享缓存不会有性能的提升,因为共享缓存的操作都是阻塞操作,只有支持非阻塞操作的网络通信才会对性能有提升。

缓存更新

根据业务的不同,缓存更新的方式也各有不同,一种容易带来隐患的方式是被动更新,这种更新方式在缓存失效后,需要通过回源的方式来更新缓存,这时需要运用多种手段来控制回源量(比如前面说到的非阻塞锁)。

另一种我们称之为主动更新,主动更新一般有消息、worker(定时器)等方式,使用消息方式可以确保数据实时性比较高,worker方式实时性要少差一点。实际项目中使用哪种方式更新缓存,可以从可维护性、安全性、业务性、实时性等方便找一个平衡点以便选择合适的更新方式。

数据一致性

为了保证服务快速响应,我们的Redis都是部署在本地的,这样可以减少网络传输消耗的时间,也可以避免缓存和应用之间网络故障造成的风险。这个单机部署会造成相互之间数据不一致,为了解决这个问题,我们使用了Redis的主从功能,并且Redis以树形结构进行部署,这时每个集群一个主Redis,同一个集群中的服务都向主Redis写数据, 由主Redis将数据逐个同步下去,每个服务器只读自己本机的Redis。

Redis的部署结构像这样:

gaobingfa-05

在描述缓存更新时,提到了worker更新,基于上述的Redis部署方式,我们用worker更新缓存时会存在一定的问题。如果所有的机器都部署了worker,那么当这些worker会在某个时刻同时执行,这显然是不可行的。如果我们每个集群部署一个worker,那么势必造成单点问题。基于以上问题我们实现了一种分布式worker,这种worker基于Redis以集群为单位,在一个时间段内(比如3分钟)只会有一个worker被启动。这样既可以避免worker单点,又可以保持代码的统一。

流量控制

这一原则主要为了避免系统过载,可以采用多种方式达到此目的。流量控制可以在前端做(Nginx),也可以在后端做。

我们知道servelt 2.x在处理请求时用的是多线程同步模型,每一个请求都会创建一个线程,然后同步的执行该请求,这个模型受限于线程资源的限制,很难产生大的吞吐量,而且某个业务阻塞就会引起连锁反应。基于servlet 2.x的容器我们一般采用池化技术和同步并行操作,使用池化技术可以将资源进行配额分配,比如数据库连接池。同步并行需要业务特性的支持,比如一个请求依赖多个后端服务,如果这些后端服务在业务上没有一个先后顺序的依赖,那么我们完全可以将这些服务放到一个线程池中去并行执行,说它同步是因为我们需要等到所有的服务都返回结果后才能继续向下执行。

目前servlet 3支持异步请求,是一种多线程异步模型, 它的每个请求仍然要使用一个线程,只不过可以进行异步操作了。这种模型的一个优点是可以按业务来分配请求资源了,比如你的系统要向外提供10中服务,你可以为每种服务分配一个固定的线程池,这样服务之间可以相互隔离。

缺点是由于是异步,所以就需要各种回调,开发和维护成本高。同事有一个项目用到了servlet 3,测试结果显示这种方式不会获得更短的响应时间,反而会有稍微下降,但是吞吐量确实有提升。所以最终是否使用这种方式,取决于你的系统更倾向于完成哪种特性。

除了在后端进行流量控制,还可以在Nginx层做控制。目前在Nginx层有多种模块可以支持流量控制,如ngx_http_limit_conn_module
、ngx_http_limit_req_module、lua-resty-limit-traffic(需安装lua模块)等,限于篇幅如何使用就不在详述,感兴趣可到官网查看。

数据托底

生产环境中有些服务可能非常重要,需要保证绝对可用,这时如果业务允许,我们就可以为其做个数据托底。

托底方式非常多,这里简单介绍几种,一种是在应用后端进行数据托底,这种方式比较灵活,可以将数据存储在内存、磁盘等各种设备上,当发生异常时可以返回托底数据,缺点是和后端应用高度耦合,一旦应用容器挂掉托底也就不起作用了。

另一种方式将托底功能跟应用剥离出来,可以使用Lua的方式在Nginx做一层拦截,用每次请求回源返回的正确数据来更新托底数据(这个过程可以做各种校验),当服务或应用出问题时可以直接从Nginx层返回数据。

还有一种是使用Nginx的error_page指令,简单配置如下:

gaobingfa-06

降级

降级的意义其实和流量控制的意义差不多,都是为了确保系统负载稳定。当线上流量超过我们预期时,为了降低系统负载就可以实施降级了。

降级的方式可以是自动降级,比如我们对一个依赖服务可以设置一个超时,当超过这个时间时就可以自动的返回一个默认值(前提是业务允许)。

手动降级,提前为某些服务设置降级开关,出现问题是可以将开关打开,比如前面我们说到了有些服务是有托底数据的,当系统过载后我们可以将其降级到直接走托底数据。

防刷

对于一些有规律入参的请求,我们可以用严格检验入参的方式,来规避非法入参穿透缓存的行为(比如一些爬虫程序无限制的猜测商品价格),这种方式可以做在前端(Nginx),也可以做在后端(Tomcat),推荐在Nginx层做。 在Nginx层做入参校验的例子:

gaobingfa-07

使用计数器识别恶意用户,比如在一段时间内为每个用户或IP等记录访问次数,如果在规定的时间内超过规定的次数,则做一些对应策略。

对恶意用户设置黑名单,每次访问都检查是否在黑名单中,存在就直接拒绝。

使用Cookie,如果用户访问是没有带指定的Cookie,或者和规定的Cookie规则不符,则做一些对应策略。

通过访问日志实时计算用户的行为,发现恶意行为后对其做相应的对策。

多域名

除正常域名外,为系统提供其它访问域名。使用CDN域名缩短用户请求链路,使用不带Cookie的域名,降低用户请求流量。

总结

以上大致介绍了开发以读为主系统的一些基本原则,用好这些原则,单台机器每天抗几十亿流量不是问题。另外上面提到的好多原则,限于篇幅并没有详细展开描述,后续有时间再详细展开。

作者介绍

马顺风,目前就职于京东商城,曾参与开发并设计过多个亿级流量系统,擅长解决大并发、大流量问题。作为一个不安分的Java码农,懂点Lua会点C,业余时间喜欢在Redis、Nginx、Nginx+Lua等方面瞎折腾。