9.8 事件驱动框架的处理流程

本节开始讨论事件处理流程。在9.5.1节中已经看到,图9-4的第12步会将监听连接的读事件设为ngx_event_accept方法,在第13步会把监听连接的读事件添加到ngx_epoll_module事件驱动模块中。这样,在执行ngx_epoll_process_events方法时,如果有新连接事件出现,则会调用ngx_event_accept方法来建立新连接。在9.8.1节中将会讨论ngx_event_accept方法的执行流程。

当然,建立连接其实没有那么简单。Nginx出于充分发挥多核CPU架构性能的考虑,使用了多个worker子进程监听相同端口的设计,这样多个子进程在accept建立新连接时会有争抢,这会带来著名的“惊群”问题,子进程数量越多问题越明显,这会造成系统性能下降。在9.8.2节中,我们会讲到在建立新连接时Nginx是如何避免出现“惊群”现象的。

另外,建立连接时还会涉及负载均衡问题。在多个子进程争抢处理一个新连接事件时,一定只有一个worker子进程最终会成功建立连接,随后,它会一直处理这个连接直到连接关闭。那么,如果有的子进程很“勤奋”,它们抢着建立并处理了大部分连接,而有的子进程则“运气不好”,只处理了少量连接,这对多核CPU架构下的应用是很不利的,因为子进程间应该是平等的,每个子进程应该尽量地独占一个CPU核心。子进程间负载不均衡,必然影响整个服务的性能。在9.8.3节中,我们会看到Nginx是如何解决负载均衡问题的。

实际上,上述问题的解决离不开Nginx的post事件处理机制。这个post事件是什么意思呢?它表示允许事件延后执行。Nginx设计了两个post队列,一个是由被触发的监听连接的读事件构成的ngx_posted_accept_events队列,另一个是由普通读/写事件构成的ngx_posted_events队列。这样的post事件可以让用户完成什么样的功能呢?

❑将epoll_wait产生的一批事件,分到这两个队列中,让存放着新连接事件的ngx_posted_accept_events队列优先执行,存放普通事件的ngx_posted_events队列最后执行,这是解决“惊群”和负载均衡两个问题的关键。

❑如果在处理一个事件的过程中产生了另一个事件,而我们希望这个事件随后执行(不是立刻执行),就可以把它放到post队列中。在9.8.3节中会介绍post队列。

我们在9.8.5节中将本章的网络事件、定时器事件进行综合考虑,以说明ngx_process_events_and_timers事件框架执行流程是如何把连接的建立、事件的执行结合在一起的。

9.8.1 如何建立新连接

上文提到过,处理新连接事件的回调函数是ngx_event_accept,其原型如下。


void ngx_event_accept(ngx_event_t*ev)


下面简单介绍一下它的流程,如图9-6所示。

下面对流程中的7个步骤进行说明。

1)首先调用accept方法试图建立新连接,如果没有准备好的新连接事件,ngx_event_accept方法会直接返回。

2)设置负载均衡阈值ngx_accept_disabled,这个阈值是进程允许的总连接数的1/8减去空闲连接数,它的具体用法参见9.8.3节。

3)调用ngx_get_connection方法由连接池中获取一个ngx_connection_t连接对象。

4)为ngx_connection_t中的pool指针建立内存池。在这个连接释放到空闲连接池时,释放pool内存池。

5)设置套接字的属性,如设为非阻塞套接字。

6)将这个新连接对应的读事件添加到epoll等事件驱动模块中,这样,在这个连接上如果接收到用户请求epoll_wait,就会收集到这个事件。

7)调用监听对象ngx_listening_t中的handler回调方法。ngx_listening_t结构体的handler回调方法就是当新的TCP连接刚刚建立完成时在这里调用的。

最后,如果监听事件的available标志位为1,再次循环到第1步,否则ngx_event_accept方法结束。事件的available标志位对应着multi_accept配置项。当available为1时,告诉Nginx一次性尽量多地建立新连接,它的实现原理也就在这里。

9.8.1 如何建立新连接 - 图1

图 9-6 ngx_event_accept方法建立新连接的流程