惊群效应
1.简介
当你往一群鸽子中间扔一块食物,虽然最终只有一个鸽子抢到食物,但所有鸽子都会被惊动来争夺,没有抢到食物的鸽子只好回去继续睡觉, 等待下一块食物到来。这样,每扔一块食物,都会惊动所有的鸽子,即为惊群。
操作系统的惊群
在多进程/多线程等待同一资源时,也会出现惊群。即当某一资源可用时,多个进程/线程会惊醒,竞争资源。
2.惊群的影响
- 惊醒所有进程/线程,导致n-1个进程/线程做了无效的调度,上下文切换,cpu瞬时增高
- 多个进程/线程争抢资源,所以涉及到同步问题,需对资源进行加锁保护,加解锁加大系统CPU开销
- 但在某些情况,惊群次数少,进(线)程负载不高,惊群可以忽略不计
3.惊群的情况
- accept惊群
- epoll惊群
- nginx惊群
- 线程池惊群
3.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 标志的进程后停止。
对于互斥等待的行为,比如对一个listen后的socket描述符,多线程阻塞accept时,系统内核只会唤醒所有正在等待此时间的队列的第一个,队列中的其他人则继续等待下一次事件的发生,这样就避免的多个线程同时监听同一个socket描述符时的惊群问题。
3.2 epoll惊群
3.2.1 fork之前创建epollfd
新版内核(2.6之后)已解决
- 主进程创建listenfd, 创建epollfd
- 主进程fork多个子进程
- 每个子进程把listenfd,加到epollfd中
- 当一个连接进来时,会触发epoll惊群,多个子进程的epoll同时会触发
这里的epoll惊群跟accept惊群是类似的,共享一个epollfd, 可以通过加锁或标记解决。
3.2.2 fork之后创建epollfd
内核未解决
- 主进程创建listendfd
- 主进程fork创建多个子进程
- 每个子进程创建自已的epollfd
- 每个子进程把listenfd加入到epollfd中
- 当一个连接进来时,会触发epoll惊群,多个子进程epoll同时会触发
因为每个子进程的epoll是不同的epoll,虽然listenfd是同一个,但新连接过来时, accept会触发惊群,但内核不知道该发给哪个监听进程,因为不是同一个epoll。所以这种惊群内核并没有处理。惊群还是会出现。
3.3 Nginx惊群
在nginx中使用的epoll,是在创建进程后创建的epollfd。因些会出现惊群问题。即每个子进程worker都会惊醒。
Nginx流程:
1 | 主线程创建listenfd | |
---|---|---|
2 | 主线程fork多个子进程(根据配置) | |
3 | 子进程创建epollfd | |
4 | 获到accept锁,只有一个子进程把listenfd加到epollfd中 | 同一时间只有一个进程会把监听描述符加到epoll中 |
5 | 循环监听 |
- nginx里采用了主动的方法去把监听描述符放到epoll中或从epoll移出(这个是nginx的精髓所在,因为大部份的并发架构都是被动的)
- nginx中用采互斥锁去解决谁来accept问题,保证了同一时刻,只有一个worker接收新连接(所以nginx并没有惊群问题)
- nginx根据自已的载负(最大连接的7/8)情况,决定去不去抢锁,简单方便地解决负载,防止进程因业务太多而导致所有业务都不及时处理
3.4线程池惊群
当一个线程解锁并通知其他线程的时候,就会出现惊群的现象
正常的用法:
- 所有线程共用一个锁,共用一个条件变量
- 当pthread_cond_signal通知时,就可能会出现惊群
解决惊群的方法:
- 所有线程共用一个锁,每个线程有自已的条件变量
- pthread_cond_signal通知时,定向通知某个线程的条件变量,不会出现惊群