在说nginx前,先来看看什么是“惊群”?简单说来,多线程/多进程(linux下线程进程也没多大区别)等待同一个socket事件,当这个事件发生时,这些线程/进程被同时唤醒,就是惊群。可以想见,效率很低下,许多进程被内核重新调度唤醒,同时去响应这一个事件,当然只有一个进程能处理事件成功,其他的进程在处理该事件失败后重新休眠(也有其他选择)。这种性能浪费现象就是惊群。
惊群通常发生在server 上,当父进程绑定一个端口监听socket,然后fork出多个子进程,子进程们开始循环处理(比如accept)这个socket。每当用户发起一个TCP连接时,多个子进程同时被唤醒,然后其中一个子进程accept新连接成功,余者皆失败,重新休眠。
那么,我们不能只用一个进程去accept新连接么?然后通过消息队列等同步方式使其他子进程处理这些新建的连接,这样惊群不就避免了?没错,惊群是避免了,但是效率低下,因为这个进程只能用来accept连接。对多核机器来说,仅有一个进程去accept,这也是程序员在自己创造accept瓶颈。所以,我仍然坚持需要多进程处理accept事件。
其实,在linux2.6内核上,accept系统调用已经不存在惊群了(至少我在2.6.18内核版本上已经不存在)。大家可以写个简单的程序试下,在父进程中bind,listen,然后fork出子进程,所有的子进程都accept这个监听句柄。这样,当新连接过来时,大家会发现,仅有一个子进程返回新建的连接,其他子进程继续休眠在accept调用上,没有被唤醒。
但是很不幸,通常我们的程序没那么简单,不会愿意阻塞在accept调用上,我们还有许多其他网络读写事件要处理,linux下我们爱用epoll解决非阻塞socket。所以,即使accept调用没有惊群了,我们也还得处理惊群这事,因为epoll有这问题。上面说的测试程序,如果我们在子进程内不是阻塞调用accept,而是用epoll_wait,就会发现,新连接过来时,多个子进程都会在epoll_wait后被唤醒!
nginx就是这样,master进程监听端口号(例如80),所有的nginx worker进程开始用epoll_wait来处理新事件(linux下),如果不加任何保护,一个新连接来临时,会有多个worker进程在epoll_wait后被唤醒,然后发现自己accept失败。现在,我们可以看看nginx是怎么处理这个惊群问题了。
nginx的每个worker进程在函数ngx_process_events_and_timers中处理事件,(void) ngx_process_events(cycle, timer, flags);封装了不同的事件处理机制,在linux上默认就封装了epoll_wait调用。我们来看看ngx_process_events_and_timers为解决惊群做了什么:
从上面的注释可以看到,无论有多少个nginx
worker进程,同一时刻只能有一个worker进程在自己的epoll中加入监听的句柄。这个处理accept的nginx
worker进程置flag为NGX_POST_EVENTS,这样它在接下来的ngx_process_events函数(在linux中就是ngx_epoll_process_events函数)中不会立刻处理事件,延后,先处理完所有的accept事件后,释放锁,然后再处理正常的读写socket事件。我们来看下ngx_epoll_process_events是怎么做的:
看看ngx_use_accept_mutex在何种情况下会被打开:
当nginx
worker数量大于1时,也就是多个进程可能accept同一个监听的句柄,这时如果配置文件中accept_mutex开关打开了,就将ngx_use_accept_mutex置为1。
再看看有些负载均衡作用的ngx_accept_disabled是怎么维护的,雅思评分在ngx_event_accept函数中:
表明,当已使用的连接数占到在nginx.conf里配置的worker_connections总数的7/8以上时,ngx_accept_disabled为正,这时本worker将ngx_accept_disabled减1,而且本次不再处理新连接。
最后,我们看下ngx_trylock_accept_mutex函数是怎么玩的:
OK,关于锁的细节是如何实现的,这篇限于篇幅就不说了,下篇帖子再来讲。现在大家清楚nginx是怎么处理惊群了吧?简单了说,就是同一时刻只允许一个nginx
worker在自己的epoll中处理监听句柄。它的负载均衡也很简单,当达到最大connection的7/8时,本worker不会去试图拿accept锁,也不会去处理新连接,这样其他nginx
worker进程就更有机会去处理监听句柄,建立新连接了。而且,由于timeout的设定,使得没有拿到锁的worker进程,去拿锁的频繁更高。