正如咱们所知,NGINX采用了异步、事件驱动的方法来处理链接。这种处理方式无需(像使用传统架构的服务器同样)为每一个请求建立额外的专用进程或者线程,而是在一个工做进程中处理多个链接和请求。为此,NGINX工做在非阻塞的socket模式下,并使用了epoll 和 kqueue这样有效的方法。 html
由于满负载进程的数量不多(一般每核CPU只有一个)并且恒定,因此任务切换只消耗不多的内存,并且不会浪费CPU周期。经过NGINX自己的实例,这种方法的优势已经为众人所知。NGINX能够很是好地处理百万级规模的并发请求。 linux
每一个进程都消耗额外的内存,并且每次进程间的切换都会消耗CPU周期并丢弃CPU高速缓存中的数据。 nginx
可是,异步、事件驱动方法仍然存在问题。或者,我喜欢将这一问题称为“敌兵”,这个敌兵的名字叫阻塞(blocking)。不幸的是,不少第三方模块使用了阻塞调用,然而用户(有时甚至是模块的开发者)并不知道阻塞的缺点。阻塞操做能够毁掉NGINX的性能,咱们必须不惜一切代价避免使用阻塞。 git
即便在当前官方的NGINX代码中,依然没法在所有场景中避免使用阻塞,NGINX1.7.11中实现的线程池机制解决了这个问题。咱们将在后面讲述这个线程池是什么以及该如何使用。如今,让咱们先和咱们的“敌兵”进行一次面对面的碰撞。 github
首先,为了更好地理解这一问题,咱们用几句话说明下NGINX是如何工做的。 服务器
一般状况下,NGINX是一个事件处理器,即一个接收来自内核的全部链接事件的信息,而后向操做系统发出作什么指令的控制器。实际上,NGINX干了编排操做系统的所有脏活累活,而操做系统作的是读取和发送字节这样的平常工做。因此,对于NGINX来讲,快速和及时的响应是很是重要的。 网络
工做进程监听并处理来自内核的事件
事件能够是超时、socket读写就绪的通知,或者发生错误的通知。NGINX接收大量的事件,而后一个接一个地处理它们,并执行必要的操做。所以,全部的处理过程是经过一个线程中的队列,在一个简单循环中完成的。NGINX从队列中取出一个事件并对其作出响应,好比读写socket。在多数状况下,这种方式是很是快的(也许只须要几个CPU周期,将一些数据复制到内存中),NGINX能够在一瞬间处理掉队列中的全部事件。
全部处理过程是在一个简单的循环中,由一个线程完成
可是,若是NGINX要处理的操做是一些又长又重的操做,又会发生什么呢?整个事件处理循环将会卡住,等待这个操做执行完毕。
所以,所谓“阻塞操做”是指任何致使事件处理循环显著中止一段时间的操做。操做能够因为各类缘由成为阻塞操做。例如,NGINX可能因长时间、CPU密集型处理,或者可能等待访问某个资源(好比硬盘,或者一个互斥体,亦或要从处于同步方式的数据库得到相应的库函数调用等)而繁忙。关键是在处理这样的操做期间,工做进程没法作其余事情或者处理其余事件,即便有更多的可用系统资源能够被队列中的一些事件所利用。
咱们来打个比方,一个商店的营业员要接待他面前排起的一长队顾客。队伍中的第一位顾客想要的某件商品不在店里而在仓库中。这位营业员跑去仓库把东西拿来。如今整个队伍必须为这样的配货方式等待数个小时,队伍中的每一个人都很不爽。你能够想见人们的反应吧?队伍中每一个人的等待时间都要增长这些时间,除非他们要买的东西就在店里。
队伍中的每一个人不得不等待第一我的的购买
在NGINX中会发生几乎一样的状况,好比当读取一个文件的时候,若是该文件没有缓存在内存中,就要从磁盘上读取。从磁盘(特别是旋转式的磁盘)读取是很慢的,而当队列中等待的其余请求可能不须要访问磁盘时,它们也得被迫等待。致使的结果是,延迟增长而且系统资源没有获得充分利用。
一个阻塞操做足以显著地延缓全部接下来的操做
一些操做系统为读写文件提供了异步接口,NGINX可使用这样的接口(见AIO指令)。FreeBSD就是个很好的例子。不幸的是,咱们不能在Linux上获得相同的福利。虽然Linux为读取文件提供了一种异步接口,可是存在明显的缺点。其中之一是要求文件访问和缓冲要对齐,但NGINX很好地处理了这个问题。可是,另外一个缺点更糟糕。异步接口要求文件描述符中要设置O_DIRECT标记,就是说任何对文件的访问都将绕过内存中的缓存,这增长了磁盘的负载。在不少场景中,这都绝对不是最佳选择。
为了有针对性地解决这一问题,在NGINX 1.7.11中引入了线程池。默认状况下,NGINX+尚未包含线程池,可是若是你想试试的话,能够联系销售人员,NGINX+ R6是一个已经启用了线程池的构建版本。
如今,让咱们走进线程池,看看它是什么以及如何工做的。
让咱们回到那个可怜的,要从大老远的仓库去配货的售货员那儿。这回,他已经变聪明了(或者也许是在一群愤怒的顾客教训了一番以后,他才变得聪明的?),雇用了一个配货服务团队。如今,当任何人要买的东西在大老远的仓库时,他再也不亲自去仓库了,只须要将订单丢给配货服务,他们将处理订单,同时,咱们的售货员依然能够继续为其余顾客服务。所以,只有那些要买仓库里东西的顾客须要等待配货,其余顾客能够获得即时服务。
传递订单给配货服务不会阻塞队伍
对NGINX而言,线程池执行的就是配货服务的功能。它由一个任务队列和一组处理这个队列的线程组成。
当工做进程须要执行一个潜在的长操做时,工做进程再也不本身执行这个操做,而是将任务放到线程池队列中,任何空闲的线程均可以从队列中获取并执行这个任务。
工做进程将阻塞操做卸给线程池
那么,这就像咱们有了另一个队列。是这样的,可是在这个场景中,队列受限于特殊的资源。磁盘的读取速度不能比磁盘产生数据的速度快。无论怎么说,至少如今磁盘再也不延误其余事件,只有访问文件的请求须要等待。
“从磁盘读取”这个操做一般是阻塞操做最多见的示例,可是实际上,NGINX中实现的线程池可用于处理任何不适合在主循环中执行的任务。
目前,卸载到线程池中执行的两个基本操做是大多数操做系统中的read()系统调用和Linux中的sendfile()。接下来,咱们将对线程池进行测试(test)和基准测试(benchmark),在将来的版本中,若是有明显的优点,咱们可能会卸载其余操做到线程池中。
如今让咱们从理论过分到实践。咱们将进行一次模拟基准测试(synthetic benchmark),模拟在阻塞操做和非阻塞操做的最差混合条件下,使用线程池的效果。
另外,咱们须要一个内存确定放不下的数据集。在一台48GB内存的机器上,咱们已经产生了每文件大小为4MB的随机数据,总共256GB,而后配置NGINX,版本为1.9.0。
配置很简单:
worker_processes 16; events { accept_mutex off; } http { include mime.types; default_type application/octet-stream; access_log off; sendfile on; sendfile_max_chunk 512k; server { listen 8000; location / { root /storage; } } }
如上所示,为了达到更好的性能,咱们调整了几个参数:禁用了logging和accept_mutex,同时,启用了sendfile并设置了sendfile_max_chunk的大小。最后一个指令能够减小阻塞调用sendfile()所花费的最长时间,由于NGINX不会尝试一次将整个文件发送出去,而是每次发送大小为512KB的块数据。
这台测试服务器有2个Intel Xeon E5645处理器(共计:12核、24超线程)和10-Gbps的网络接口。磁盘子系统是由4块西部数据WD1003FBYX 磁盘组成的RAID10阵列。全部这些硬件由Ubuntu服务器14.04.1 LTS供电。
为基准测试配置负载生成器和NGINX
客户端有2台服务器,它们的规格相同。在其中一台上,在wrk中使用Lua脚本建立了负载程序。脚本使用200个并行链接向服务器请求文件,每一个请求均可能未命中缓存而从磁盘阻塞读取。咱们将这种负载称做随机负载。
在另外一台客户端机器上,咱们将运行wrk的另外一个副本,使用50个并行链接屡次请求同一个文件。由于这个文件将被频繁地访问,因此它会一直驻留在内存中。在正常状况下,NGINX可以很是快速地服务这些请求,可是若是工做进程被其余请求阻塞的话,性能将会降低。咱们将这种负载称做恒定负载。
性能将由服务器上ifstat监测的吞吐率(throughput)和从第二台客户端获取的wrk结果来度量。
如今,没有使用线程池的第一次运行将不会带给咱们很是振奋的结果:
% ifstat -bi eth2 eth2 Kbps in Kbps out5531.241.03e+064855.23812922.75994.661.07e+065476.27981529.36353.621.12e+065166.17892770.35522.81978540.86208.10985466.76370.791.12e+066123.331.07e+06
如上所示,使用这种配置,服务器产生的总流量约为1Gbps。从下面所示的top输出,咱们能够看到,工做进程的大部分时间花在阻塞I/O上(它们处于top的D状态):
top- 10:40:47up 11 days, 1:32, 1 user, loadaverage: 49.61, 45.77 62.89Tasks: 375 total, 2 running, 373 sleeping, 0 stopped, 0 zombie %Cpu(s): 0.0us, 0.3sy, 0.0ni, 67.7id, 31.9wa, 0.0hi, 0.0si, 0.0stKiBMem: 49453440 total, 49149308 used, 304132 free, 98780 buffersKiBSwap: 10474236 total, 20124 used, 10454112 free, 46903412 cachedMemPIDUSERPRNIVIRTRESSHRS %CPU %MEMTIME+ COMMAND 4639 vbart 20 0 47180 28152 496 D 0.7 0.1 0:00.17nginx 4632 vbart 20 0 47180 28196 536 D 0.3 0.1 0:00.11nginx 4633 vbart 20 0 47180 28324 540 D 0.3 0.1 0:00.11nginx 4635 vbart 20 0 47180 28136 480 D 0.3 0.1 0:00.12nginx 4636 vbart 20 0 47180 28208 536 D 0.3 0.1 0:00.14nginx 4637 vbart 20 0 47180 28208 536 D 0.3 0.1 0:00.10nginx 4638 vbart 20 0 47180 28204 536 D 0.3 0.1 0:00.12nginx 4640 vbart 20 0 47180 28324 540 D 0.3 0.1 0:00.13nginx 4641 vbart 20 0 47180 28324 540 D 0.3 0.1 0:00.13nginx 4642 vbart 20 0 47180 28208 536 D 0.3 0.1 0:00.11nginx 4643 vbart 20 0 47180 28276 536 D 0.3 0.1 0:00.29nginx 4644 vbart 20 0 47180 28204 536 D 0.3 0.1 0:00.11nginx 4645 vbart 20 0 47180 28204 536 D 0.3 0.1 0:00.17nginx 4646 vbart 20 0 47180 28204 536 D 0.3 0.1 0:00.12nginx 4647 vbart 20 0 47180 28208 532 D 0.3 0.1 0:00.17nginx 4631 vbart 20 0 47180 756 252 S 0.0 0.1 0:00.00nginx 4634 vbart 20 0 47180 28208 536 D 0.0 0.1 0:00.11nginx 4648 vbart 20 0 25232 1956 1160 R 0.0 0.0 0:00.08top 25921 vbart 20 0 121956 2232 1056 S 0.0 0.0 0:01.97sshd 25923 vbart 20 0 40304 4160 2208 S 0.0 0.0 0:00.53zsh
在这种状况下,吞吐率受限于磁盘子系统,而CPU在大部分时间里是空闲的。从wrk得到的结果也很是低:
Running 1m test @ http://192.0.2.1:8000/1/1/112 threads and50 connections Thread Stats Avg Stdev Max +/- Stdev Latency 7.42s 5.31s 24.41s 74.73% Req/Sec 0.150.361.0084.62% 488 requests in1.01m, 2.01GB read Requests/sec: 8.08 Transfer/sec: 34.07MB
请记住,文件是从内存送达的!第一个客户端的200个链接建立的随机负载,使服务器端的所有的工做进程忙于从磁盘读取文件,所以产生了过大的延迟,而且没法在合理的时间内处理咱们的请求。
如今,咱们的线程池要登场了。为此,咱们只需在location块中添加aio threads指令:
location / { root /storage; aio threads; }
接着,执行NGINX reload从新加载配置。
而后,咱们重复上述的测试:
% ifstat -bi eth2 eth2 Kbps in Kbps out60915.199.51e+0659978.899.51e+0660122.389.51e+0661179.069.51e+0661798.409.51e+0657072.979.50e+0656072.619.51e+0661279.639.51e+0661243.549.51e+0659632.509.50e+06
如今,咱们的服务器产生的流量是9.5Gbps,相比之下,没有使用线程池时只有约1Gbps!
理论上还能够产生更多的流量,可是这已经达到了机器的最大网络吞吐能力,因此在此次NGINX的测试中,NGINX受限于网络接口。工做进程的大部分时间只是休眠和等待新的事件(它们处于top的S状态):
top- 10:43:17up 11 days, 1:35, 1 user, loadaverage: 172.71, 93.84, 77.90Tasks: 376 total, 1 running, 375 sleeping, 0 stopped, 0 zombie %Cpu(s): 0.2us, 1.2sy, 0.0ni, 34.8id, 61.5wa, 0.0hi, 2.3si, 0.0stKiBMem: 49453440 total, 49096836 used, 356604 free, 97236 buffersKiBSwap: 10474236 total, 22860 used, 10451376 free, 46836580 cachedMemPIDUSERPRNIVIRTRESSHRS %CPU %MEMTIME+ COMMAND 4654 vbart 20 0 309708 28844 596 S 9.0 0.1 0:08.65nginx 4660 vbart 20 0 309748 28920 596 S 6.6 0.1 0:14.82nginx 4658 vbart 20 0 309452 28424 520 S 4.3 0.1 0:01.40nginx 4663 vbart 20 0 309452 28476 572 S 4.3 0.1 0:01.32nginx 4667 vbart 20 0 309584 28712 588 S 3.7 0.1 0:05.19nginx 4656 vbart 20 0 309452 28476 572 S 3.3 0.1 0:01.84nginx 4664 vbart 20 0 309452 28428 524 S 3.3 0.1 0:01.29nginx 4652 vbart 20 0 309452 28476 572 S 3.0 0.1 0:01.46nginx 4662 vbart 20 0 309552 28700 596 S 2.7 0.1 0:05.92nginx 4661 vbart 20 0 309464 28636 596 S 2.3 0.1 0:01.59nginx 4653 vbart 20 0 309452 28476 572 S 1.7 0.1 0:01.70nginx 4666 vbart 20 0 309452 28428 524 S 1.3 0.1 0:01.63nginx 4657 vbart 20 0 309584 28696 592 S 1.0 0.1 0:00.64nginx 4655 vbart 20 0 30958 28476 572 S 0.7 0.1 0:02.81nginx 4659 vbart 20 0 309452 28468 564 S 0.3 0.1 0:01.20nginx 4665 vbart 20 0 309452 28476 572 S 0.3 0.1 0:00.71nginx 5180 vbart 20 0 25232 1952 1156 R 0.0 0.0 0:00.45top 4651 vbart 20 0 20032 752 252 S 0.0 0.0 0:00.00nginx 25921 vbart 20 0 121956 2176 1000 S 0.0 0.0 0:01.98sshd 25923 vbart 20 0 40304 3840 2208 S 0.0 0.0 0:00.54zsh
如上所示,基准测试中还有大量的CPU资源剩余。
wrk的结果以下:
Running 1m test @ http://192.0.2.1:8000/1/1/112 threads and50 connections Thread Stats Avg Stdev Max +/- Stdev Latency 226.32ms 392.76ms 1.72s 93.48% Req/Sec 20.0210.8459.0065.91% 15045 requests in1.00m, 58.86GB read Requests/sec: 250.57 Transfer/sec: 0.98GB
服务器处理4MB文件的平均时间从7.42秒降到226.32毫秒(减小了33倍),每秒请求处理数提高了31倍(250 vs 8)!
对此,咱们的解释是请求再也不由于工做进程被阻塞在读文件,而滞留在事件队列中,等待处理,它们能够被空闲的进程处理掉。只要磁盘子系统能作到最好,就能服务好第一个客户端上的随机负载,NGINX可使用剩余的CPU资源和网络容量,从内存中读取,以服务于上述的第二个客户端的请求。
在抛出咱们对阻塞操做的担心并给出一些使人振奋的结果后,可能大部分人已经打算在你的服务器上配置线程池了。先别着急。
实际上,最幸运的状况是,读取和发送文件操做不去处理缓慢的硬盘驱动器。若是咱们有足够多的内存来存储数据集,那么操做系统将会足够聪明地在被称做“页面缓存”的地方,缓存频繁使用的文件。
“页面缓存”的效果很好,可让NGINX在几乎全部常见的用例中展现优异的性能。从页面缓存中读取比较快,没有人会说这种操做是“阻塞”。而另外一方面,卸载任务到一个线程池是有必定开销的。
所以,若是内存有合理的大小而且待处理的数据集不是很大的话,那么无需使用线程池,NGINX已经工做在最优化的方式下。
卸载读操做到线程池是一种适用于很是特殊任务的技术。只有当常常请求的内容的大小,不适合操做系统的虚拟机缓存时,这种技术才是最有用的。至于可能适用的场景,好比,基于NGINX的高负载流媒体服务器。这正是咱们已经模拟的基准测试的场景。
咱们若是能够改进卸载读操做到线程池,将会很是有意义。咱们只须要知道所需的文件数据是否在内存中,只有不在内存中时,读操做才应该卸载到一个单独的线程中。
再回到售货员那个比喻的场景中,这回,售货员不知道要买的商品是否在店里,他必需要么老是将全部的订单提交给配货服务,要么老是亲自处理它们。
人艰不拆,操做系统缺乏这样的功能。第一次尝试是在2010年,人们试图将这一功能添加到Linux做为fincore()系统调用,可是没有成功。后来还有一些尝试,是使用RWF_NONBLOCK标记做为preadv2()系统调用来实现这一功能(详情见LWN.net上的非阻塞缓冲文件读取操做和异步缓冲读操做)。但全部这些补丁的命运目前还不明朗。悲催的是,这些补丁尚没有被内核接受的主要缘由,貌似是由于旷日持久的撕逼大战(bikeshedding)。
另外一方面,FreeBSD的用户彻底没必要担忧。FreeBSD已经具有足够好的异步读取文件接口,咱们应该用这个接口而不是线程池。
因此,若是你确信在你的场景中使用线程池能够带来好处,那么如今是时候深刻了解线程池的配置了。
线程池的配置很是简单、灵活。首先,获取NGINX 1.7.11或更高版本的源代码,使用--with-threads配置参数编译。在最简单的场景中,配置看起来很朴实。咱们只须要在http、 server,或者location上下文中包含aio threads指令便可:
aio threads;
这是线程池的最简配置。实际上的精简版本示例以下:
thread_pooldefault threads=32 max_queue=65536;aio threads=default;
这里定义了一个名为“default”,包含32个线程,任务队列最多支持65536个请求的线程池。若是任务队列过载,NGINX将输出以下错误日志并拒绝请求:
thread pool "NAME"queue overflow: N tasks waiting
错误输出意味着线程处理做业的速度有可能低于任务入队的速度了。你能够尝试增长队列的最大值,可是若是这无济于事,那么这说明你的系统没有能力处理如此多的请求了。
正如你已经注意到的,你可使用thread_pool指令,配置线程的数量、队列的最大值,以及线程池的名称。最后要说明的是,能够配置多个独立的线程池,将它们置于不一样的配置文件中,用作不一样的目的:
http { thread_pool one threads=128 max_queue=0; thread_pool two threads=32; server { location /one { aio threads=one; } location /two { aio threads=two; } } … }
若是没有指定max_queue参数的值,默认使用的值是65536。如上所示,能够设置max_queue为0。在这种状况下,线程池将使用配置中所有数量的线程,尽量地同时处理多个任务;队列中不会有等待的任务。
如今,假设咱们有一台服务器,挂了3块硬盘,咱们但愿把该服务器用做“缓存代理”,缓存后端服务器的所有响应信息。预期的缓存数据量远大于可用的内存。它其实是咱们我的CDN的一个缓存节点。毫无疑问,在这种状况下,最重要的事情是发挥硬盘的最大性能。
咱们的选择之一是配置一个RAID阵列。这种方法毁誉参半,如今,有了NGINX,咱们能够有其余的选择:
# 咱们假设每块硬盘挂载在相应的目录中:/mnt/disk一、/mnt/disk二、/mnt/disk3 proxy_cache_path /mnt/disk1 levels=1:2 keys_zone=cache_1:256m max_size=1024G use_temp_path=off; proxy_cache_path /mnt/disk2 levels=1:2 keys_zone=cache_2:256m max_size=1024G use_temp_path=off; proxy_cache_path /mnt/disk3 levels=1:2 keys_zone=cache_3:256m max_size=1024G use_temp_path=off; thread_pool pool_1 threads=16; thread_pool pool_2 threads=16; thread_pool pool_3 threads=16; split_clients $request_uri$disk { 33.3% 1; 33.3% 2; * 3; } location / { proxy_pass http://backend; proxy_cache_key $request_uri; proxy_cache cache_$disk; aio threads=pool_$disk; sendfile on; }
在这份配置中,使用了3个独立的缓存,每一个缓存专用一块硬盘,另外,3个独立的线程池也各自专用一块硬盘。
缓存之间(其结果就是磁盘之间)的负载均衡使用split_clients模块,split_clients很是适用于这个任务。
在 proxy_cache_path指令中设置use_temp_path=off,表示NGINX会将临时文件保存在缓存数据的同一目录中。这是为了不在更新缓存时,磁盘之间互相复制响应数据。
这些调优将带给咱们磁盘子系统的最大性能,由于NGINX经过单独的线程池并行且独立地与每块磁盘交互。每块磁盘由16个独立线程和读取和发送文件专用任务队列提供服务。
我敢打赌,你的客户喜欢这种量身定制的方法。请确保你的磁盘也持有一样的观点。
这个示例很好地证实了NGINX能够为硬件专门调优的灵活性。这就像你给NGINX下了一道命令,让机器和数据用最佳姿式来搞基。并且,经过NGINX在用户空间中细粒度的调优,咱们能够确保软件、操做系统和硬件工做在最优模式下,尽量有效地利用系统资源。
综上所述,线程池是一个伟大的功能,将NGINX推向了新的性能水平,除掉了一个众所周知的长期危害——阻塞——尤为是当咱们真正面对大量内容的时候。
甚至,还有更多的惊喜。正如前面提到的,这个全新的接口,有可能没有任何性能损失地卸载任何长期阻塞操做。NGINX在拥有大量的新模块和新功能方面,开辟了一方新天地。许多流行的库仍然没有提供异步非阻塞接口,此前,这使得它们没法与NGINX兼容。咱们能够花大量的时间和资源,去开发咱们本身的无阻塞原型库,但这么作始终都是值得的吗?如今,有了线程池,咱们能够相对容易地使用这些库,而不会影响这些模块的性能。