惊群效应
[toc]
1. 惊群效应
1.1 简介
惊群问题又名惊群效应。简单来说就是多个进程或者线程在等待同一个事件,当事件发生时,所有线程和进程都会被内核唤醒。唤醒后通常只有一个进程获得了该事件并进行处理,其他进程发现获取事件失败后又继续进入了等待状态,在一定程度上降低了系统性能。
打个比方就是:当你往一群鸽子中间扔一块食物,虽然最终只有一个鸽子抢到食物,但所有鸽子都会被惊动来争夺,没有抢到食物的鸽子只好回去继续睡觉, 等待下一块食物到来。这样,每扔一块食物,都会惊动所有的鸽子,即为惊群。
1.2 引发的问题
惊群效应会占用系统资源,降低系统性能。
多进程/线程的唤醒,涉及到的一个问题是上下文切换问题。频繁的上下文切换带来的一个问题是数据将频繁的在寄存器与运行队列中流转。极端情况下,时间更多的消耗在进程/线程的调度上,而不是执行。
2. 常见的惊群效应
在 Linux 下,我们常见的惊群效应发生于我们使用 accept 以及我们 select 、poll 或 epoll 等系统提供的 API 来处理我们的网络链接。
2.1 accept 惊群
以多进程为例,在主进程创建监听描述符listenfd后,fork()多个子进程,多个进程共享listenfd,accept是在每个子进程中,当一个新连接来的时候,会发生惊群。

由上图所示:
- 主线程创建了监听描述符listenfd = 3
- 主线程fork 三个子进程共享listenfd=3
- 当有新连接进来时,内核进行处理
在内核2.6之前,所有进程accept都会惊醒,但只有一个可以accept成功,其他返回EGAIN。
在内核2.6及之后,解决了惊群,在内核中增加了一个互斥等待变量。一个互斥等待的行为与睡眠基本类似,主要的不同点在于:
1.当一个等待队列入口有 WQ_FLAG_EXCLUSEVE 标志置位, 它被添加到等待队列的尾部. 没有这个标志的入口项, 相反, 添加到开始.
2.当 wake_up 被在一个等待队列上调用时, 它在唤醒第一个有 WQ_FLAG_EXCLUSIVE 标志的进程后停止。(内核只会唤醒一个用户进程 task 就会退出唤醒过程,从而不存在了”惊群”现象)
3.对于互斥等待的行为,比如如对一个listen后的socket描述符,多线程阻塞accept时,系统内核只会唤醒所有正在等待此事件的队列 的第一个,队列中的其他人则继续等待下一次事件的发生,这样就避免的多个线程同时监听同一个socket描述符时的惊群问题。
2.2 epoll惊群
epoll惊群分两种:
- 在fork之前创建epollfd,所有进程共用一个epoll;
- 在fork之后创建epollfd,每个进程独用一个epoll.
2.2.1 fork之前创建epollfd(内核2.6已解决)
- 主进程创建listenfd, 创建epollfd
- 主进程fork多个子进程
- 每个子进程把listenfd,加到epollfd中
- 当一个连接进来时,会触发epoll惊群,多个子进程的epoll同时会触发
分析:这里的epoll惊群跟accept惊群是类似的,共享一个epollfd, 加锁或标记解决。在新版本的epoll中已解决。但在内核2.6及之前是存在的。
2.2.2 fork之后创建epollfd(内核未解决)
- 主进程创建listendfd
- 主进程创建多个子进程
- 每个子进程创建自已的epollfd
- 每个子进程把listenfd加入到epollfd中
- 当一个连接进来时,会触发epoll惊群,多个子进程epoll同时会触发
分析:因为每个子进程的epoll是不同的epoll, 虽然listenfd是同一个,但新连接过来时, accept会触发惊群,但内核不知道该发给哪个监听进程,因为不是同一个epoll。所以这种惊群内核并没有处理。惊群还是会出现。
3. 内核解决惊群问题详解
首先如前面所说,Accept 的惊群问题在 Linux Kernel 2.6 之后就被从内核的层面上解决了。但是 EPOLL 怎么办?在 2016 年一月,Linux 之父 Linus 向内核提交了一个补丁,其中的关键代码是
1 | if (epi->event.events & EPOLLEXCLUSIVE) |
简而言之,通过增加一个 EPOLLEXCLUSIVE 标志位作为辅助。如果用户开启了 EPOLLEXCLUSIVE ,那么在加入内核等待队列时,使用 add_wait_queue_exclusive 否则则使用 add_wait_queue。 EPOLLEXCLUSIVE 只保证唤醒的进程数小于等于我们开启的进程数,而不是直接唤醒所有进程,也不是只保证唤醒一个进程。
4. Nginx解决惊群效应
目前而言,应用解决惊群有两种策略
- 这是可以接受的代价,那么我们暂时不管。这是我们大多数的时候的策略
- 通过加锁或其余的手段来解决这个问题,最典型的例子是 Nginx
我们来看看 Nginx 怎么解决这样的问题的:
Nginx通过 控制争抢处理socket的进程数量 和 抢占ngx_accept_mutex锁 解决惊群现象。只有一个ngx_accept_mutex锁,谁拿到锁,谁处理该socket的请求。
同时:如果当前进程的连接数>最大连接数*7/8,则该进程不参与本轮竞争。
1 | //nginx的每个worker进程在函数ngx_process_events_and_timers中处理事件。下面代码是ngx_process_events_and_timers()函数的核心部分。 |
nginx从抢锁、释放锁到处理事件的整个过程,我已经结合代码做了注释,相信大家对整个过程应该已经不陌生了。至于pthread_mutex_trylock()中进程是如何抢占锁的,这就有赖于实现抢占的算法了,此处只是解释处理过程,并不关心抢占实现原理。感兴趣的同学可以自己搜索相关资料。
- 先处理新用户的连接事件,再释放处理新连接的锁:如果刚释放锁,就有新连接,刚获得锁的进程要给等待队列中添加sockfd时,此时原获得锁的进程也要从等待队列中删除sockfd,TCP的三次握手的连接是非线程安全的。为了避免产生错误,使得将sockfd从等待队列中删除后,再让新的进程抢占锁,处理新连接。
- 拿到锁,将任务放在任务队列中,不是立刻去处理:每个进程要处理新连接事件,必须拿到锁,当前进程将新连接事件的sokect添加到任务队列中,立即释放锁,让其他进程尽快获得锁,处理用户的连接。
你可能有个疑问,如果没有加锁,有新事件连接时,所有的进程都会被唤醒执行accept,有且仅有一个进程会accept返回成功,其他进程都重新进入睡眠状态。现在有了锁,在发生accept之前,进程们要去抢占锁,也是有且仅有一个进程会抢到锁,其他进程也是重新进入睡眠状态。即:不论是否有accept锁,都会有很多进程被唤醒再重新进入睡眠状态的过程,那惊群现象如何解释?
其实,锁不能解决惊群现象,惊群现象是没办法解决的,很多进程被同时唤醒是一个必然的过程。Nginx中通过检查当前进程的连接数是否>最大连接数*7/8来判断当前进程是否能处理新连接,减少被唤醒的进程数量,也实现了简单的负载均衡。锁只能保证不让所有的进程去调用accept函数,解决了很多进程调用accept返回错误,锁解决的是惊群现象的错误,并不是解决了惊群现象!
转载自:
https://blog.csdn.net/fedorafrog/article/details/114068524?spm=1001.2014.3001.5502