Py学习  »  NGINX

月考成绩+NGINX的线程池提升性能9倍!

Python程序员 • 6 年前 • 691 次点击  

Python部落(python.freelycode.com)组织翻译,禁止转载,欢迎转发。

Nginx不会为每一个请求创建一个专用的进程或线程(如使用传统架构的服务器那样),它是通过异步和事件驱动来进行连接处理的,并且是在一个工作进程中处理多个请求和连接。为了实现这一点,Nginx在非阻塞模式下使用socket,而且配合其他的高效方法,如 epoll 和 kqueue 。

因为全权重(full-weight)的进程数量很少(一般一个CPU内核只有一个)而且一般都是固定的,所以占用内存更少,并且CPU周期不会浪费在切换任务上面。通过Nginx本身这个例子,大部分人知道了Nginx的这个优点并且广泛接受。它成功地处理了数百万的并发请求并且有良好的扩展性。

每个进程消耗额外的内存,并且它们之间的每次切换都会消耗CPU并且丢弃L-cache

但是异步和事件驱动的方法仍非完美,依旧存在问题。我喜欢把它比喻为,一个“敌人”,一个名字叫“阻塞”的敌人。不幸的是,许多第三方模块都使用的是阻塞调用,但是用户(有时候甚至开发人员)并不知道这些缺陷。阻塞操作可能会破坏Nginx的性能,所以我们需要不惜一切代价来避免。

即使在当前的Nginx代码中,所有情况中都不可能避免掉组阻塞操作,为了解决这个问题,在Nginx 1.7.11 和 Nginx Plus Release 7 中实现了新的“线程池”机制。后面我们会讲到这到底是个什么并且教大家如何使用。现在让我们来正面看看“阻塞”这个敌人。

编辑 - 有关NGINX Plus R7的概述,请参阅我们的博客上的“ Announcing NGINX Plus R7”

有关NGINX Plus R7中其他新功能的详细讨论,请参阅这些相关博文:

        --HTTP/2 Now Fully Supported in NGINX Plus

        --Socket Sharding in NGINX

        --The New NGINX Plus Dashboard in Release 7

        --TCP Load Balancing in NGINX Plus R7

问题

首先,为了更好的理解这个问题,我们先来了解Nginx的工作原理。

一般来说,Nginx是一个事件处理程序,它是一个从内核接收信息的控制器,处理发生在连接上面的事,然后再向操作系统发出相应操作的命令。而事实上,Nginx通过编排操作系统完成了所有的繁重工作,而操作系统则完成了读取和发送字节的日常工作。因此Nginx能快速及时的做出响应非常重要。

工作进程监听并处理来自内核的事件

事件可以是超时、关于准备读取或写入的socket的通知,或出现错误的通知。Nginx接收一组事件,然后逐个进行处理,执行必要的操作。因此,所有的处理都是在一个线程中的一个简单的循环中完成的。Nginx从队列中去除某个事件,然后通过如写入或读取一个socket来进行反应。大多数情况下,这非常快(可能只需要几个CPU周期来复制内存中的一些数据),Nginx在一瞬间完成了队列中的所有事件。

所有的处理都是在一个线程中的一个简单的循环中完成

但是,如果发生漫长而繁重的操作,会发生什么?事件处理的整个周期会被卡住等待该操作完成。

因此,通常我们说的“阻塞操作”,是指可以在相当长时间内停止处理事件操作的操作(读起来比较绕口)。各种原因都可能导致操作阻塞。例如,Nginx可能会忙于冗长的CPU密集型处理,或者它不得不等待访问资源(例如硬盘驱动,或互斥锁或库函数调用,以同步方式从数据库获取响应,等等)。关键是在处理这些操作时,即使有很多其他资源可用,甚至队列中的很多事件可以使用其他那些资源来完成工作,工作进程也还是不能做其他任何事情,也不能处理额外的事件。

想想一下一个商店的售货员面前排着长队。队列的第一个人需要的东西不在商店里,而是在仓库里。售货员到仓库去拿货送货,整个队列必须等几个小时来完成交付,队列中的每个人都不满意。你能想象到人们的反应吗?队列中每个人的等待时间都在增加,但是他们想买的商品可能就在商店里。

队列中的每个人都不得不等待第一个人的命令完成

Nginx有类似的情况,当Nginx读取一个没有在内存中缓存的文件时,但是需要从磁盘读取。硬盘驱动器很慢(特别是机械硬盘),而等待队列的其他请求可能不需要访问驱动器,其他请求就被迫等待,因此,延迟增加变大,系统资源未得到充分利用。

仅仅一个操作就可以延迟其他操作很长时间

一些操作系统提供了读取和发送文件的异步接口,Nginx可以使用此接口(请参阅aio)。一个很的例子是FreeBSD。不幸的是,linux却和它并非相同。虽然Linux 提供了文件读取的异步接口,但是有一些显著的缺点。其中一个是对文件访问和缓冲区的对齐要求,但Nginx处理的很好。第二个问题就更蛋疼了,异步接口要求在文件描述符上设置O_DIRECT标志,这意味着对文件的访问绕过内存中的缓存,并增加硬盘上的负载。这样它在绝大多数情况下都不是最优的。

为了专门解决这个问题,在Nginx 1.7.11 和 Nginx Plus Release 7中引入了线程池。

现在让我们来看看线程池是什么以及工作原理。

线程池(Thread Pools)

我们再回到刚刚那个售货员的例子,从仓库提取货物运送交付。但他变聪明了(或许是被客户暴揍了一顿后变聪明的),他雇佣了一个送货服务。现在,如果有人要仓库内的商品,售货员自己不去仓库,而是将订单送到送货服务,送货服务来进行处理订单,而售货员将继续为其他顾客服务,因此,只有那些需要仓库商品的客户在等待交付,而另外一些客户则可以立即服务。

将订单传递给送货服务可以解除阻塞队列

在Nginx中,线程池就是执行上面的送货服务功能。它由一个任务队列和多个处理队列的线程组成。当一个worker进程需要做一个可能很长时间的操作,而不是自己处理这个操作时,它会在池的队列中增加一个任务,在这个队列中,它可以被任何空闲线程处理。

Worker进程将阻塞操作卸载到线程池

看起来我们还有另一个队列,对吧。但是在这种情况下,队列收到特定资源的限制。我们从驱动器上读取速度不会比驱动器产生数据的速度快。现在,至少驱动器不会延迟其他任务的处理,只有需要访问文件的请求会需要等待。

“从磁盘读取”操作通常被用作阻塞操作的最常见示例,但实际上,Nginx中的线程池实现可以用于任何不适合在主工作周期中处理的任务。

目前,卸载到线程池仅仅用到三个操作:read() 大多数操作系统的系统调用,Linux上的sendfile()aio_write(),在编写一些临时文件(例如缓存文件)时使用的。我们将继续测试并进行评估,如果有明显的好处,我们可能会将其他操作卸载到线程池中。

编辑 - 在NGINX 1.9.13和NGINX Plus R9中添加了对系统调用的支持。aio_write()

性能测试(Benchmarking)

是时候从理论转向实践了。为了演示使用线程池的效果,我们将执行一个合成基准,模拟阻塞和非阻塞的最坏组合。

它需要保证不适合内存的数据集。在一台内存48GB的机器上,我们已经在4MB文件中生成了256GB的随机数据,然后配置了Nginx1.9.0来服务。

配置比较简单:

正如你所看到的,为了达到更好的性能做了一些调整:logaccept_mutex被禁用,sendfilesendfile_max_chunk开启设置。最后一个指令可以减少阻止调用sendfile()的最长时间,因为Nginx不会一次尝试发送整个文件,而是切割为512KB的块进行。

该机器有两个Intel Xeon E5645 (12 核,总共24个HT线程)处理器和 一个10Gbps网络接口。磁盘子系统是4块西数WD1003FBYX硬盘组成的RAID10结构。操作系统为Ubuntu Server 14.04.1 LTS

为基准测试的负载均衡和NGINX的配置

客户端由两台相同规格的机器来表示。在其中一台机器上,wrk使用Lua脚本创建负载。该脚本使用200个并行连接随机请求服务器文件,并且每个请求都可能导致缓存未命中和从磁盘读取的阻塞。我们称它为随机负载。

在第二台客户机上我们运行另外一个wrk副本,它将使用50个并行连接多次请求相同的文件。由于该文件被频繁访问,所以它将一直保存在内存中。在正常情况下,Nginx会迅速处理完成这些请求。但是如果工作进程被其他请求阻塞,性能就会下降,我们称这个负载为恒定负载。

在第二个机器上通过ifstat命令查看服务器吞吐量和wrk结果来测试机器性能。

现在,在没有线程池的第一次运行并没有给我们满意的结果:

正如你所见,通过这个配置,服务器总共大约可以产生1Gbps的流量。使用top输出,我们可以看到所有的工作进程大部分时间处于阻塞I/O(都处于D状态):

在这种情况下,吞吐量受到了磁盘的限制,而大多数时候CPU处于空闲状态。结果wrk也很低:

记住,这是从内存中请求文件!过大的延迟是因为所有的进程都忙于从驱动器中读取文件,以服务由第一个客户机创建的200个连接所创建的随机负载,且无法解释处理我们的请求。

是时候把线程池加上了。我们只需要把aio threads 添加到location模块:

记得重载nginx让配置生效

重载nginx后,我们再测试:

相比于没有线程池的1Gbps,现在我们的服务器产生了9.5Gbps流量

它可能产生更多,但已经达到了实际的网络最大容量,所以在这个测试中,Nginx受到了网络接口的限制。大部分worker进程都在休眠和等待新事件(top中显示为S):

仍有大量的CPU资源。

Wrk结果如下:

处理4MB文件的平均时间从7.42秒减少到226.32毫秒(少了33次),并且每秒的请求数增加了31倍(250/8)!

解释是,我们的请求不再在事件队列中等待处理,而工作进程在读取时被阻塞,而是由空闲进程处理。只要磁盘子系统尽可能的完成任务,就可以完美解决第一台客户机提供的随机负载,Nginx使用剩余的CPU资源和网络容量从内存中提取数据处理第二个客户端的请求。

仍然不是银弹

在我们对阻塞操作和一些令人兴奋的结果考虑之后,可能大多数人已经在服务器上配置线程池了。先别急。

事实上,幸运的是大多数读取和发送文件操作不通过慢速硬盘处理。如果你有足够的RAM来存储数据集,那么操作系统就运转足够快,可以在所谓的“页面缓存”中缓存经常使用的文件。

良好的页面缓存,允许Nginx在几乎所有常见的用例中表现出出色的性能。从页面缓存中读取文档相当快,没有人可以模拟出这种操作的“阻塞”。另一方面,卸载到线程池也需要消耗一定的开销。

因此,如果你有一个合理的RAM并且你的工作数据集不是很大,那么Nginx已经以最好的方式在工作,而不需要使用线程池。

将读取操作卸载到线程池是适用于非常特定任务的技术。在经常请求的内容的卷不适合操作系统的VM缓存情况下,它是最有用的。例如,可能是基于Nginx的大量负载的流媒体服务器。这是我们在基准测试中模拟的情况。

如果我们能通过将读取操作卸载到线程池中来提高性能,那就太好了。所以我们所需要的是一种有效的方法来知道所需文件数据是否在内存中,只有在后一种情况下,读取操作才会被卸载到单独的线程中。

回到我们的销售类比,售货员无法知道所请求的商品是否在商店内,并且必须始终将所有的订单传递给交付服务,或者总是自己处理。

究其原因是操作系统忽略了这个特性。第一次尝试将其作为fincore() 系统调用添加到Linux,那是2010年,但是并未实现。后来,有一些尝试将它作为一个新的preadv2() 系统调用做具有RWF_NONBLOCK标志的新系统调用程序实现(有关详细信息,请参阅无阻塞缓冲文件读取操作和LWN.net上的异步缓冲读取操作)。这些补丁是否能成功实现还是未知数。为什么补丁还不能被接受到内核的最重要的原因是并不是非常被重视。

另一方面,FreeBSD用户完全不需要担心。FreeBSD已经有一个非常好的,可用于替代线程池的异步接口来读取文件。

配置线程池

因此,如果你确定线程池对你的用例有好处的话,那么是时候来深入研究配置了。

配置非常简单灵活。首先你需要Nginx版本1.7.11或者更高版本,加上- -with-threads 参数来编译。Nginx Plus用户需要7版本或者更高版本。在最简单的情况下,配置看起来很简单。你需要做的是在适当的位置包含该指令:aio threads

这是线程池最小可用配置。其实这是下面配置的简写版:

他定义了名为default的线程池,拥有32个工作线程,并且任务队列的最大长度为65536个任务。如果任务队列过载,Nginx拒绝请求并记录此错误:

这个错误意味着线程可能无法像添加到队列中那样快速处理工作。你可以尝试增加最大队列大小,如果增加了但还是和原来一样,则说明你的系统不能提供这么多请求。

你已经注意到,使用thread_pool指令可以配置线程的数量、队列最大长度和特定的线程池名称。最后一个意味着你可以配置多个独立的线程池,并在配置文件的不同位置提供不同的用途:

如果max_queue未指定参数,则默认值为65536。如上图所示,可以设置max_queue为零。在这种情况下,线程池只能处理与线程池配置一样多的任务,没有任务会在队列中等待。

现在假设你有一个带有三个硬盘的服务器,并且你希望这个服务器作为一个缓存代理来缓存你后端的所有响应。预期的缓存数据量远远超过可用的RAM。它实际上是个人CDN的缓存节点。当然在这种情况下,最重要的是利用磁盘驱动的最大性能。

你可以选择配置RAID阵列。这种方法有利有弊。现在你可以用Nginx做另一个:

在这个配置中,thread_pool指令为每个磁盘定义一个专用的独立线程池,并且proxy_cache_path指令在每个磁盘上定义一个专用的独立缓存。

Split_clients模块用于缓存之间的负载均衡(以及磁盘之间的结果),完全符合这个任务。

use_temp_path=off参数给proxy_cache_path提供指令,只是Nginx将临时文件保存到响应的缓存数据所在的相同目录中。在更新缓存时,需要避免在硬盘驱动器之间复制响应数据。

所有的这些配置让我们可以利用当前磁盘子系统的最大性能。因为Nginx通过单独的线程池并行和独立地与驱动器进行交互。每个驱动器由16个独立线程提供,拥有用于读取和发送文件的专用任务队列

我打赌你的客户也喜欢你的这种定制方法。确保你的硬盘也像这样。

这个例子很好的展示了如何灵活的为硬件调整Nginx。就像你正在给Nginx说明关于机器和数据集交互的最佳方法。通过在用户空间微调Nginx,你可以确保你的软件、操作系统和硬件在最佳模式下协同工作,尽可能有效的利用所有系统资源。

结论

总体来说,线程池这个功能很强大,它通过消除阻塞,来推动Nginx达到新性能水平。

还有更多将会到来,如前所述。这个全新的接口有可能允许任何长期阻塞在不损失性能的情况下运行卸载。Nginx有大量的新模块和功能,开辟了新视野。许多流行的库仍未提供一个异步非阻塞接口,所以它们和Nginx不兼容。我们可能花费大量时间和资源开发一些我们自己的库的非阻塞原型,但是这一直这样做值得吗?现在,我们有了线程池,相对更容易的使用这些库,在模块性能不被影响的情况下。

敬请关注。



英文原文:https://www.nginx.com/blog/thread-pools-boost-performance-9x/
译者:IC



今天看啥 - 高品质阅读平台
本文地址:http://www.jintiankansha.me/t/YUgbBO7DOD
Python社区是高质量的Python/Django开发社区
本文地址:http://www.python88.com/topic/3223
 
691 次点击