Contents

惊群效应

1.简介

当你往一群鸽子中间扔一块食物,虽然最终只有一个鸽子抢到食物,但所有鸽子都会被惊动来争夺,没有抢到食物的鸽子只好回去继续睡觉, 等待下一块食物到来。这样,每扔一块食物,都会惊动所有的鸽子,即为惊群。

操作系统的惊群

在多进程/多线程等待同一资源时,也会出现惊群。即当某一资源可用时,多个进程/线程会惊醒,竞争资源。

2.惊群的影响

  1. 惊醒所有进程/线程,导致n-1个进程/线程做了无效的调度,上下文切换,cpu瞬时增高
  2. 多个进程/线程争抢资源,所以涉及到同步问题,需对资源进行加锁保护,加解锁加大系统CPU开销
  3. 但在某些情况,惊群次数少,进(线)程负载不高,惊群可以忽略不计

3.惊群的情况

  1. accept惊群
  2. epoll惊群
  3. nginx惊群
  4. 线程池惊群

3.1 accept惊群

新版内核已解决

以多进程为例,在主进程创建监听描述符listenfd后,fork()多个子进程,多个进程共享listenfd,accept是在每个子进程中,当一个新连接来的时候,会发生惊群。

https://cdn.staticaly.com/gh/Hongtao-Xu/note@main/img/202208281803046.png

  1. 主线程创建了监听描述符listenfd = 3
  2. 主线程fork三个子进程,共享listenfd=3
  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之后)已解决

  1. 主进程创建listenfd, 创建epollfd
  2. 主进程fork多个子进程
  3. 每个子进程把listenfd,加到epollfd中
  4. 当一个连接进来时,会触发epoll惊群,多个子进程的epoll同时会触发

这里的epoll惊群跟accept惊群是类似的,共享一个epollfd, 可以通过加锁或标记解决。

3.2.2 fork之后创建epollfd

内核未解决

  1. 主进程创建listendfd
  2. 主进程fork创建多个子进程
  3. 每个子进程创建自已的epollfd
  4. 每个子进程把listenfd加入到epollfd中
  5. 当一个连接进来时,会触发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 循环监听
  1. nginx里采用了主动的方法去把监听描述符放到epoll中或从epoll移出(这个是nginx的精髓所在,因为大部份的并发架构都是被动的)
  2. nginx中用采互斥锁去解决谁来accept问题,保证了同一时刻,只有一个worker接收新连接(所以nginx并没有惊群问题)
  3. nginx根据自已的载负(最大连接的7/8)情况,决定去不去抢锁,简单方便地解决负载,防止进程因业务太多而导致所有业务都不及时处理

3.4线程池惊群

当一个线程解锁并通知其他线程的时候,就会出现惊群的现象

正常的用法:

  1. 所有线程共用一个锁,共用一个条件变量
  2. 当pthread_cond_signal通知时,就可能会出现惊群

解决惊群的方法:

  1. 所有线程共用一个锁,每个线程有自已的条件变量
  2. pthread_cond_signal通知时,定向通知某个线程的条件变量,不会出现惊群