引言: 随便写写
(1)什么是惊群效应
惊群问题又名惊群效应。简单来说就是多个进程或者线程在等待同一个事件,当事件发生时,所有线程和进程都会被内核唤醒。唤醒后通常只有一个进程获得了该事件并进行处理,其他进程发现获取事件失败后又继续进入了等待状态,在一定程度上造成了资源浪费,降低了系统性能。
(2)惊群问题(thundering herd)的产生
在建立连接的时候,Nginx出于充分发挥多核CPU架构性能的考虑,使用了多个worker子进程监听相同端口的设计,这样多个子进程在accept建立新连接时会有争抢,这会带来著名的“惊群”问题,子进程数量越多越明显,这会造成系统性能的下降。
一般情况下,有多少CPU核心就有配置多少个worker子进程。假设现在没有用户连入服务器,某一时刻恰好所有的子进程都休眠且等待新连接的系统调用(如epoll_wait),这时有一个用户向服务器发起了连接,内核在收到TCP的SYN包时,会激活所有的休眠worker子进程。最终只有最先开始执行accept的子进程可以成功建立新连接,而其他worker子进程都将accept失败。这些accept失败的子进程被内核唤醒是不必要的,他们被唤醒会的执行很可能是多余的,那么这一时刻他们占用了本不需要占用的资源,引发了不必要的进程切换,增加了系统开销。
(3)惊群效应影响
惊群效应会占用系统资源,降低系统性能。多进程/线程的唤醒,涉及到的一个问题是上下文切换问题。频繁的上下文切换带来的一个问题是数据将频繁的在寄存器与运行队列中流转。极端情况下,时间更多的消耗在进程/线程的调度上,而不是执行。
(4)常见的惊群效应
A、accept 惊群
以多进程为例,在主进程创建监听描述符listenfd后,fork()多个子进程,多个进程共享listenfd,accept是在每个子进程中,当一个新连接来的时候,会发生惊群。
在内核2.6之前,所有进程accept都会惊醒,但只有一个可以accept成功,其他返回EGAIN。
在内核2.6及之后,解决了惊群问题,办法是在内核中增加了一个互斥等待变量。一个互斥等待的行为与睡眠基本类似,主要的不同点在于:
a)当一个等待队列入口有 WQ_FLAG_EXCLUSEVE 标志置位, 它被添加到等待队列的尾部。若没有这个标志的入口项,则添加到队首。
b)当wake up被在一个等待队列上调用时,它在唤醒第一个有 WQ_FLAG_EXCLUSIVE 标志的进程后停止。
执行步骤:
/*
* The core wakeup function. Non-exclusive wakeups (nr_exclusive == 0) just
* wake everything up. If it's an exclusive wakeup (nr_exclusive == small +ve
* number) then we wake all the non-exclusive tasks and one exclusive task.
*
* There are circumstances in which we can try to wake a task which has already
* started to run but is not in state TASK_RUNNING. try_to_wake_up() returns
* zero in this (rare) case, and we handle it by continuing to scan the queue.
*/
static void __wake_up_common(wait_queue_head_t *q, unsigned int mode,
int nr_exclusive, int wake_flags, void *key)
{
wait_queue_t *curr, *next;
list_for_each_entry_safe(curr, next, &q->task_list, task_list) {
unsigned flags = curr->flags;
if (curr->func(curr, mode, wake_flags, key) &&
(flags & WQ_FLAG_EXCLUSIVE) && !--nr_exclusive)
break;
}
}
对于互斥等待的行为,比如对一个 listen 后的socket描述符,多线程阻塞 accept 时,系统内核只会唤醒所有正在等待此时间的队列的第一个,队列中的其他人则继续等待下一次事件的发生。这样就避免的多个线程同时监听同一个socket描述符时的惊群问题。
B、epoll惊群
epoll惊群分两种:
a)是在fork之前创建 epollfd,所有进程共用一个epoll。
b)是在fork之后创建 epollfd,每个进程独用一个epoll。
a)fork之前创建epollfd(新版内核已解决)
主进程创建 listenfd,创建epollfd;
主进程 fork多个子进程;
每个子进程把listenfd,加到epollfd 中。
当一个连接进来时,会触发epoll惊群,多个子进程的epoll同时会触发。
这里的epoll惊群跟 accept 惊群是类似的,共享一个 epollfd,加锁或标记解决,在新版本的epoll中已解决,但在内核2.6及之前是存在的。
b)fork之后创建epollfd(内核未解决)
主进程创建 listendfd;
主进程创建多个子进程;
每个子进程创建自已的epollfd;
每个子进程把listenfd加入到epollfd中;
当一个连接进来时,会触发epoll 惊群,多个子进程epoll同时会触发。
因为每个子进程的epoll是不同的epoll, 虽然listenfd是同一个,但新连接过来时,accept会触发惊群。因为内核不知道该发给哪个监听进程,因为不是同一个epoll。所以这种惊群内核并没有处理,惊群还是会出现。