前言
本小节将看到nginx中解决惊群现象的方法。所谓惊群现象,简单的来说就是当多个进程或线程在同时阻塞等待同一个事件时,如果该事件发生,会唤醒在等待的所有的进程/线程,但最终只可能有一个进程/线程对该事件进行处理,其他进程/线程会在失败后重新休眠,唤醒多个进程/线程这种不必要的行为会造成系统资源的浪费(涉及到进程的上下文切换)。
而常见的惊群问题有accept
惊群、epoll
惊群。
accept导致的惊群问题
当多个进程/线程调用accept
监听同一个socket
上时,一个新连接的到来就会导致所有阻塞在该socket
上的进程/线程都被唤醒,但是最后只有一个进程/线程可以accept
成功,其余的又会重新休眠,这样就产生了惊群现象。
这个问题其实在linux2.6内核版本就已经解决了,它维护了一个等待队列(队列的元素为进程),并且使用了WQ_FLAG_EXCLUSIVE
标志位(互斥标志位),非exclusive
元素会加在等待队列的前面,而exclusive
元素会加在等待队列的末尾,当有新连接到来时,会遍历等待队列,并且只唤醒第一个exclusive
进程(非互斥的进程由于排在队列前面也会被唤醒)就退出遍历。
阻塞在accept
上的进程都是互斥的(也就是WQ_FLAG_EXCLUSIVE
标志位会被置位),因此现在的linux内核调用accept
时,多个进程/线程只有一个会被唤醒并建立新连接。
而nginx中处理的主要是另外一种,epoll导致的惊群问题(确切的来说,是解决多个epfd
(epfd
是指调用epoll_create
获取的描述符)共同监听同一个socket
造成的惊群问题)。
epoll导致的惊群问题
虽然accept
上已经不存在惊群问题了,但是以目前的服务器架构,都不会简单的使用accept
阻塞等待新的连接了,而是使用epoll
等I/O多路复用机制。早期的linux,调用epoll_wait
后,当有读/写事件发生时,会唤醒阻塞在epoll_wait
上的所有进程/线程,造成惊群现象。不过这个问题已经被修复了,使用类似于处理accpet
导致的惊群问题的方法,当有事件发生时,只会唤醒等待队列中的第一个exclusive
进程来处理。不过随后就可以看到,这种方法并不能完全解决惊群问题。
这里需要区分一下两种不同的情况(这两种情况,目前linux内核都有处理的办法)。
1. 多个进程/线程使用同一个epfd
然后调用epoll_wait
2. 多个进程/线程有自己的epfd
,然后监听同一个socket
其实也就是epoll_create
和fork
这两个函数调用的先后顺序问题(下面都以进程为例)。第一种情况,先调用epoll_create
获取epfd
,再使用fork
,各进程共用同一个epfd
;第二种情况,先fork
,再调用epoll_create
,各进程独享自己的epfd
。
而nginx面对的是第二种情况,这点需要分清楚(网上有很多用第一种情况来引入nginx处理惊群问题的方法,不要被混淆了)。因为nginx的每个worker进程相互独立,拥有自己的epfd
,不过根据配置文件中的listen
指令都监听了同一个端口,调用epoll_wait
时,若共同监听的套接字有事件发生,就会造成每个worker进程都被唤醒。
多个进程共用一个epfd
第一种情况,由于多个进程/线程都阻塞在epoll_wait
上,并且它们使用的都是同一个epfd
。epoll
会通过与设备(比如网卡)驱动程序建立回调关系,当有事件就绪时,回调ep_poll_callback
函数将事件放在rdllist
链表上,我们调用epoll_wait
所做的工作就只是检查rdllist
链表上是否有元素,若不为空,则把这里的事件复制到用户态内存中,并将数量作为返回值返回。
对