惊群
发布于
Top
io

多个进程/线程同时阻塞等同一个事件时,事件发生后,唤醒所有的进程

但最终只可能有一个进程/线程对该事件进行处理,其他在失败后重新休眠,这种性能浪费就是惊群

accept 惊群

主进程创建socket, bind, listen后,fork出多个子进程都循环处理(accept)这个socket

每个进程都阻塞在accpet上,当一个新的连接到来时,所有的进程都会被唤醒,但其中只有一个进程会accept成功,其余皆失败,重新休眠。这就是accept惊群

fork出多个进程是为了利用多核CPU

内核早解决这问题了

多个进程/线程都阻塞在对同一个 socket 的 accept 调用上时,新连接到来,内核只唤醒一个进程,其他进程保持休眠,压根就不会被唤醒

epoll惊群

accept 已无惊群问题,但 epoll 还有

即,如果多个进程/线程阻塞在监听同一个 listening socket fd 的 epoll_wait 上,当有一个新的连接到来时,多个子进程被唤醒

为什么内核不处理 epoll 惊群

accept 应该只能被一个进程调用成功,内核很清楚这一点

但 epoll 监听的fd,除可能被 accept 调用外,还有可能是其他网络 IO 事件的

其他 IO 事件是否只能由一个进程处理(如一个文件会由多个进程来读写),得用户决定,内核不能强制

所以,对 epoll 的惊群,内核则不予处理

lighttpd 解决思路:无视惊群

Watcher/Worker 模式,优化了 fork() 和 epoll_create() 的位置(让每个子进程自己去epoll_create()和 epoll_wait()),主动捕获 accept() 抛出的错误并忽视

nginx 解决思路:避免惊群

Nginx 使用全局互斥锁,每个工作进程在 epoll_wait() 之前先去申请锁,得到锁了才继续处理,得不到锁则等待,并设置了一个负载均衡算法来权衡各个进程的任务量(当某个工作进程的任务里达到总设置量的 7/8 时,不再尝试去申请锁)

首先启动进程的时候,不把 listenfd 加入自己的 epoll 中,等待进程初始化完毕,开始处理事件的时候,这时候的第一步就是抢锁,即抢占对 listenfd 的控制权,哪个进程抢到,立刻加入自己的 epoll。没抢到的进程继续自己的处理,但是不会 accept 抢到 listenfd 的进程,就会 accept 新的连接

这个锁是 自旋锁 用原子变量实现的 不会造成进程的睡眠和阻塞

Leader/Followers 线程模式

各个线程地位平等,轮流做 Leader 来响应请求


epoll 的实现中,每次 epoll_ctl/add/del 的时候,通过 ep_modify/insert/unlink/remove 实现,操作中先调用 spin_lock 和获得读写锁,所以在用户态中 epoll 是无锁编程,线程安全的,在内核态中是有锁的。也就是说,即使惊群,也还是安全的

Linux 4.5

提供了 EPOLL_EXCLUSIVE ,在 TCP 三次握手最后一个 ACK 报文调用 sock_def_readable 时只唤醒一个等待源,这样就在内核层面避免了 “惊群” 问题了