Nginx-Web-Server-Deep-Dive-1200x630-155541714-copy.png

事件驱动模型是Nginx服务器保障完整功能和具有良好性能的重要机制之一。 
 
事件驱动模型概述
 
实际上,事件驱动并不是计算机编程领域的专业词汇,它是一种比较古老的响应事件的模型,在计算机编程、公共关系、经济活动等领域均有很广泛的应用。顾名思义,事件驱动就是在持续事务管理过程中,由当前时间点上出现的事件引发的调动可用资源执行相关任务,解决不断出现的问题,防止事务堆积的一种策略。在计算机编程领域,事件驱动模型对应一种程序设计方式,Event-driven programming,即事件驱动程序设计。

事件驱动模型一般是由事件收集器事件发送器事件处理器三部分基本单元组成。
其中, 事件收集器专门负责收集所有的事件,包括来自用户的(如鼠标单击事件、键盘输入事件等)、来自硬件的(如时钟事件等)和来自软件的(如操作系统、应用程序本身等)。
 
事件发送器负责将收集器收集到的事件分发到目标对象中。目标对象就是事件处理器所处的位置。事件处理器主要负责具体事件的响应工作,它往往要到实现阶段才完全确定。

在程序设计过程中,对事件驱动机制的实现方式有多种,这里介绍batch programming,即批次程序设计。批次的程序设计是一种比较初级的程序设计方式。使用批次程序设计的软件,其流程是由程序设计师在实际编码过程中决定的,也就是说,在程序运行的过程中,事件的发生、事件的发送和事件的处理都是预先设计好的。由此可见,事件驱动程序设计更多的关注了事件产生的随机性,使得应用程序能够具备相当的柔性,可以应付种种来自用户、硬件和系统的离散随机事件,这在很大程度上增强了用户和软件的交互性和用户操作的灵活性。

事件驱动程序可以由任何编程语言来实现,只是难易程度有别。如果一个系统是以事件驱动程序模型作为编程基础的,那么,它的架构基本上是这样的:预先设计一个事件循环所形成的程序,这个事件循环程序构成了“事件收集器”,它不断地检查目前要处理的事件信息,然后使用“事件发送器”传递给“事件处理器”。“事件处理器”一般运用虚函数机制来实现。
 
 Nginx中的事件驱动模型
 
Nginx服务器响应和处理Web请求的过程,就是基于事件驱动模型的,它也包含事件收集器、事件发送器和事件处理器等三部分基本单元。Nginx的“事件收集器”和“事件发送器”的实现没有太大的特点,重点介绍一下它的“事件处理器”。

通常,我们在编写服务器处理模型的程序时,基于事件驱动模型,“目标对象”中的“事件处理器”可以有以下几种实现办法:
 
  • “事件发送器”每传递过来一个请求,“目标对象”就创建一个新的进程,调用“事件处理器”来处理该请求。
  • “事件发送器”每传递过来一个请求,“目标对象”就创建一个新的线程,调用“事件处理器”来处理该请求。
  • “事件发送器”每传递过来一个请求,“目标对象”就将其放入一个待处理事件的列表,使用非阻塞I/O方式调用“事件处理器”来处理该请求。

 以上的三种处理方式,各有特点,第一种方式,由于创建新的进程的开销比较大,会导致服务器性能比较差,但其实现相对来说比较简单。
第二种方式,由于要涉及到线程的同步,故可能会面临死锁、同步等一系列问题,编码比较复杂。

第三种方式,在编写程序代码时,逻辑比前面两种都复杂。大多数网络服务器采用了第三种方式,逐渐形成了所谓的“事件驱动处理库”。
事件驱动处理库又被称为多路IO复用方法,最常见的包括以下三种:select模型,poll模型和epoll模型。Nginx服务器还支持rtsig模型、kqueue模型、dev/poll模型和eventport模型等。通过Nginx配置可以使得Nginx服务器支持这几种事件驱动处理模型。这里详细介绍以下它们。 
 
select库
 
select库,是各个版本的Linux和Windows平台都支持的基本事件驱动模型库,并且在接口的定义上也基本相同,只是部分参数的含义略有差异。使用select库的步骤一般是:

首先,创建所关注事件的描述符集合。对于一个描述符,可以关注其上面的(Read)事件、写(Write)事件以及异常发送(Exception)事件,所以要创建三类事件描述符集合,分别用来收集读事件的描述符、写事件的描述符和异常事件的描述符。

其次,调用底层提供的select()函数,等待事件发生。这里需要注意的一点是,select的阻塞与是否设置非阻塞I/O是没有关系的。
然后,轮询所有事件描述符集合中的每一个事件描述符,检查是否有相应的事件发生,如果有,就进行处理。

Nginx服务器在编译过程中如果没有为其指定其他高性能事件驱动模型库,它将自动编译该库。我们可以使用--with-select_module和--without-select_module两个参数强制Nginx是否编译该库。 
 
poll库
 
poll库,作为Linux平台上的基本事件驱动模型,实在Linux2.1.23中引入的。Windows平台不支持poll库。
poll与select的基本工作方式是相同的,都是现创建一个关注事件的描述符集合,再去等待这些事件发生,然后在轮询描述符集合,检查有没有事件发生,如果有,就进行处理。

poll库与select库的主要区别在于,select库需要为读事件、写事件和异常事件分别创建一个描述符集合,因此在最后轮询的时候,需要分别轮询这三个集合。而poll库只需要创建一个集合,在每个描述符对应的结构上分别设置读事件、写事件或者异常事件,最后轮询的时候,可以同时检查这三种事件是否发生。可以说,poll库是select库的优化实现。

Nginx服务器在编译过程中如果没有为其制定其他高性能事件驱动模型库,它将自动编译该库。我们可以使用--with-poll_module和--without-poll_module两个参数强制Nginx是否编译该库。 
 
epoll库
 
epoll库是Nginx服务器支持的高性能事件驱动库之一,它是公认的非常优秀的事件驱动模型,和poll库及select库有很大的不同。epoll属于poll库的一个变种,是在Linux 2.5.44中引入的,在Linux 2.6以上的版本都可以使用它。poll库和select库在实际工作中,最大的区别在于效率。

从前面的介绍我们知道,它们的处理方式都是创建一个待处理事件列表,然后把这个列表发给内核,返回的时候,再去轮询检查这个列表,以判断事件是否发生。这样在描述符比较多的应用中,效率就显得比较低下了。一种比较好的做法是,把描述符列表的管理交给内核负责,一旦有某种事件发生,内核把发生事件的描述符列表通知给进程,这样就避免了轮询整个描述符列表。epoll库就是这样一种模型。

首先,epoll库通过相关调用通知内核创建一个由N个描述符的事件列表。然后,给这些描述符设置所关注的事件,并把它添加到内核的事件列表中去,在具体的编码过程中也可以通过相关调用对事件列表中的描述符进行修改和删除。

完成设置之后,epoll库就开始等待内核通知事件发生了。某一事件发生后,内核将发生事件的描述符列表上报给epoll库。得到事件列表的epoll库,就可以进行事件处理了。

epoll库在Linux平台上是最高效的。它支持一个进程打开大数目的事件描述符,上限是系统可以打开文件的最大数目。同时,epoll库的IO效率不随描述符数目增加而线性下降,因为它只会对内核上报的“活跃”的描述符进行操作。 
 
rtsig模型
 
rtsig是Real-Time Signal的缩写,是实时信号的意思。从严格意义上说,rtsig模型并不是常用的事件驱动模型,但Nginx服务器使用了使用实时信号对事件进行响应的支持,官方文档中将rtsig模型与其他的事件驱动模型并列。

使用rtsig模型时,工作进程会通过系统内核建立一个rtsig队列用于存放标记事件发生(在Nginx服务器应用中特指客户端请求发生)的信号。每个事件发生时,系统内核就会产生一个信号存放到rtsig队列中等待工作进程的处理。

需要指出的是,rtsig队列有长度限制,超过该长度后就会发生溢出。默认情况下,Linux系统事件信号队列的最大长度设置为1024,也就是同时最多可以存放1024个发生事件的信号。在Linux 2.6.6-mm2之前的版本中,系统各个进程的事件信号队列是由内核统一管理的,用户可以通过修改内核参数/proc/sys/kernel/rtsig-max/来自定义该长度设置。在Linux 2.6.6-mm2之后的版本中,该内核参数被取消,系统各个进程分别拥有各自的事件信号队列,这个队列的大小由Linux系统的RLIMT_SIGPENGIND参数定义,在执行setrlimit()系统调用时确定该大小。Nginx提供了worker_rlimit_sigpending参数用于调节这种情况下的事件信号队列长度。

当rtsig队列发生溢出时,Nginx将暂时停止使用rtsig模型,而调用poll库处理未处理的事件,直到rgsit信号队列全部清空,然后再次启动rtsig模型,以防止新的溢出发生。

Nginx在配置文件中提供了相关参数对rtsig模型的使用配置。编译Nginx服务器时,使用--with-rtsig_module配置选项来启用rtsig模型的编译。
 
 其他事件驱动模型
 
除了以上四种主要的事件驱动模型,Nginx服务器针对特定的Linux平台提供了响应的事件驱动模型支持。目前实现的主要有kqueue模型、/dev/poll模型和eventport模型等。
 
  • kqueue模型,是用于支持BSD系列平台的高效事件驱动模型,主要用在FreeBSD 4.1及以上版本、OpenBSD 2.9及以上版本、NetBSD 2.0及以上版本以及Mac OS X平台上。该模型也是poll库的一个变种,其和epoll库的处理方式没有本质上的区别,都是通过避免轮询操作提供效率。该模型同时支持条件触发(level-triggered,也叫水平触发,只要满足条件就触发一个事件)和边缘触发(edge-triggered,每当状态变化时,触发一个事件)。如果大家在这些平台下使用Nginx服务器,建议选在该模型用于请求处理,以提高Nginx服务器的处理性能。
  • /dev/poll模型,适用于支持Unix衍生平台的高效事件驱动模型,其主要在Solaris711/99及以上版本、HP/UX 11.22及以上版本、IRIX 6.5.15及以上版本和Tru64 UNIX 5.1A及以上版本的平台中使用。该模型是Sun公司在开发Solaris系列平台时提出的用于完成事件驱动机制的方案,它使用了虚拟的/dev/poll设备,开发人员可以将要监视的文件描述符加入这个设备,然后通过ioctl()调用来获取事件通知。在以上提到的平台中,建议使用该模型处理请求。

  • eventport模型,适用于支持Solaris 10及以上版本平台的高效事件驱动模型。该模型也是Sun公司在开发Solaris系列平台时提出的用于完成事件驱动机制的方案,它可以有效防止内核崩溃情况的发生。Nginx服务器为此提供了支持。

 
以上就是Nginx服务器支持的事件驱动库。可以看到,Nginx服务器针对不同的Linux或Unix衍生平台提供了多种事件驱动模型的处理,尽量发挥系统平台本身的优势,最大程度地提高处理客户端请求事件的能力。在实际工作中,我们需要根据具体情况和应用情景选择使用不同的事件驱动模型,以保证Nginx服务器的高效运行。