NGINX中的线程池加速

转载请注明出处。

本文介绍了关于事件驱动的一些细节,对于理解阻塞,非阻塞,异步,同步都有一定的帮助,我特地翻译了一下。注:本文不是完全翻译,但是精华都翻译了。

原文地址

众所周知NGINX使用异步,事件驱动的方式来处理连接请求。也就是说和传统服务器架构不同,NGNIX不会为每个请求专门创建一个进程,而是在一个工作进程中处理多个连接请求。为了实现这种处理方式,NGINX使用非阻塞模式来处理sockets,并且使用效率很高的epollkqueue

因为高负荷进程的数量很小并且波动不大(一般来说一个CPU核只有一个这种进程),所以内存消耗很小并且CPU的轮询不会被消耗在进程切换上。从NGINX上就可以看出这种方式的受欢迎程度。它可以处理百万级别的并发请求并且扩展性也很出色。

http://cdn.nginx.com/wp-content/uploads/2015/06/Traditional-Server-and-NGINX-Worker.png

但是这种异步,事件驱动的方式仍然有问题。或者,我更喜欢把这个问题当成“敌人”(-_-!),即阻塞。不幸的是,很多第三方的模块是阻塞方式的函数调用,但是用户(有时甚至是模块开发人员)并没有察觉到这样做的坏处。阻塞操作会极大程度上的损坏NGINX的性能,因此我们必须尽最大努力来避免。

即使在目前官方的NGINX代码中,也不可能完全避免阻塞操作。为了解决这个缺陷,线程池机制在1.7.11的NGINX中被实现了。我们会在下文中介绍它是如何解决这个问题和如何使用它。首先我们来看一下这个问题是如何产生的。

问题

首先,为了更好的理解问题本质,我们来谈一谈NGINX。

总体来说,NGINX是一个事件处理器(event handler),即一个控制者,这个控制者会从内核接收所有连接中发生的事件,并且向操作系统发送命令来处理这些事件。实际上,NGINX的主要工作就是在协调操作系统,而操作系统只是在进行读写数据的常规操作。因此,及时响应对NGINX来说是至关重要的。

NGINX-Event-Loop2

所谓的事件可以是超时,socket可读或可写的通知或者错误通知等。NGINX接收这些事件后挨个对他们进行处理。可以看出所有的处理实际上就是在一个线程上进行简单的循环操作(loop over a thread)。通常情况下,对一个事件的处理可以在极短的时间内完成,然后NGINX立即进行下一个事件的处理。

但是如果事件处理是高负荷的操作会产生什么结果呢?整个循环都会被阻塞。

因此,我们所说的“一个阻塞操作”就是指那些会使整个循环中断很长时间的操作。比如,长时间的高负荷CPU操作,或者访问硬盘,又或者访问同步数据库等等。总之,工作进程为了完成这个这个操作,它不能做任何其他工作也不能处理其他事件,尽管队列中的其他事件完全可以使用系统其余的资源。

举个简单的类比,有很多人在超市柜台前等待付款。到其中一名顾客了,这个顾客要的商品并没有在超市中,因此收银员就跑到仓库中去为他取。现在,整个队伍都在等待收银员去取货,但同时其他顾客所需要的商品很可能就在超市中。

Faraway Warehouse

基本上完全类似的场景也会发生在NGINX中,比如当它需要读取一个文件,而这个文件又不在内存中时。

一些操作系统提供了异步接口来读取文件,NGINX可以使用这些接口来解决上述问题,比如FreeBSD。但不幸的是Linux所提供的异步接口有一些严重的缺陷。例如,想使用异步接口的话,文件的O_DIRECT属性必须要设置。也就是说,任何对该文件的访问都只能通过硬盘读取,而不能使用内存。这显然不是我们所希望的。

为了解决这个问题,线程池的概念被引入了1.7.11版本的NGINX。接下来,我们来看看它是如何工作的。

线程池

让我们回顾一下收银员的例子。与上次不同,这次超市雇佣了专门的送货员来送货,这样一来如果遇到商品不在超市的情况,收银员只需要下个订单给送货员,然后就可以继续为后面的顾客服务了。

在NGINX中,线程池就充当了送货员的角色。线程池由多个任务队列和许多的线程组成。当工作进程(译者注:本文中的工作进程都是指事件处理器)需要处理一个可能会很耗时的操作时,它会把这个操作提交到线程池中的任务队列中,由线程池来处理。

Thread Pool

虽然看起来我们多了一个队列,但是这个队列并不会影响NGINX接收请求和处理那些不需要文件IO的事件。读取文件只是最常见的一种高负载操作,实际上NGINX的线程池可以处理任何不适合在主工作进程中完成的操作。不过目前只提供了两种核心操作:在大多数系统中可以使用read(),在Linux中可以使用sendfile()。我们会在后续引入更多有价值的操作到线程池中。

测试

(译者注:测试部分,废话较多,我简单总结了一下)

Load Generators

如图所示,服务器端有很多4M的小文件,左边两个为模拟用户。上边的时随机访问用户,用来使内存命中失败,从而从硬盘读文件产生阻塞。下边的是固定访问用户,只访问预先设定的文件,即模拟内存命中。

情况一不使用线程池

% ifstat -bi eth2
eth2
Kbps in  Kbps out
5531.24  1.03e+06
4855.23  812922.7
5994.66  1.07e+06
5476.27  981529.3
6353.62  1.12e+06
5166.17  892770.3
5522.81  978540.8
6208.10  985466.7
6370.79  1.12e+06
6123.33  1.07e+06

<pre><code class="terminal">Running 1m test @ http://192.0.2.1:8000/1/1/1
  12 threads and 50 connections
  Thread Stats   Avg    Stdev     Max  +/- Stdev
    Latency     7.42s  5.31s   24.41s   74.73%
    Req/Sec     0.15    0.36     1.00    84.62%
  488 requests in 1.01m, 2.01GB read
Requests/sec:      8.08
Transfer/sec:     34.07MB</code>

 第二种情况使用线程池 

% ifstat -bi eth2
eth2
Kbps in  Kbps out
60915.19  9.51e+06
59978.89  9.51e+06
60122.38  9.51e+06
61179.06  9.51e+06
61798.40  9.51e+06
57072.97  9.50e+06
56072.61  9.51e+06
61279.63  9.51e+06
61243.54  9.51e+06
59632.50  9.50e+06

<pre><code class="terminal">Running 1m test @ http://192.0.2.1:8000/1/1/1
  12 threads and 50 connections
  Thread Stats   Avg      Stdev     Max  +/- Stdev
    Latency   226.32ms  392.76ms   1.72s   93.48%
    Req/Sec    20.02     10.84    59.00    65.91%
  15045 requests in 1.00m, 58.86GB read
Requests/sec:    250.57
Transfer/sec:      0.98GB</code>

 可以看出无论是服务器的吞吐量还是客户端的响应时间都有了数量级的提升。 

不过。。。。。

在实际的应用中,文件IO大多数情况下都不会直接从硬盘读取。如果内存够大,操作系统完全能够把常用的文件都缓存在缓存页中。从缓存页中读取文件非常迅速基本上不会产生阻塞,而使用线程池的话会产生一些额外开销。

因此如果你的服务器有足够大的RAM,那么就没有必要使用线程池。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值