关于多线程/多进程下的惊群问题笔记

  • 内容
  • 评论
  • 相关

什么是“惊群”?

假设你养了一百只小鸡,现在你有一粒粮食,你把这粒粮食直接扔到小鸡中间,一百只小鸡一起上来抢,最终只有一只小鸡能得手,其它九十九只小鸡只能铩羽而归,这就所谓的惊群效应。

惊群效应会带来什么影响?

如今网络编程中经常用到多进程或多线程模型,大概的思路是父进程创建socket,bind、listen后,通过fork创建多个子进程,每个子进程继承了父进程的socket,调用accpet开始监听等待网络连接。这个时候有多个进程同时等待网络的连接事件,当这个事件发生时,这些进程被同时唤醒,就是“惊群”。这样会导致什么问题呢?我们知道进程被唤醒,需要进行内核重新调度,这样每个进程同时去响应这一个事件,而最终只有一个进程能处理事件成功,其他的进程在处理该事件失败后重新休眠或其他。网络模型如下图所示:

简而言之,惊群现象(thundering herd)就是当多个进程和线程在同时阻塞等待同一个事件时,如果这个事件发生,会唤醒所有的进程,但最终只可能有一个进程/线程对该事件进行处理,其他进程/线程会在失败后重新休眠,这种性能浪费就是惊群。

操作系统的惊群问题

在多进程/多线程等待同一资源时,也会出现惊群。即当某一资源可用时,多个进程/线程会惊醒,竞争资源。这就是操作系统中的惊群。

网络编程下存在惊群现象的几种情况

在高并发(多线程/多进程/多连接)中,会产生惊群的情况有:

  • accept惊群
  • epoll惊群
    • fork之前创建epollfd
    • fork之后创建epollfd
  • 线程池惊群

accept惊群(在Linux 2.6以及之后的内核版本已经解决这个问题)

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

首先我们来看下多进程程序处理连接的流程:

  1. 主线程创建了监听描述符listenfd
  2. 主线程fork 三个子进程共享listenfd
  3. 当有新连接进来时,内核进行处理

在内核2.6之前,所有进程accept都会惊醒,但只有一个可以accept成功,其他返回EGAIN。

在内核2.6及之后,解决了惊群,在内核中增加了一个互斥等待变量。一个互斥等待的行为与睡眠基本类似,主要的不同点在于:
1)当一个等待队列入口有 WQ_FLAG_EXCLUSEVE 标志置位, 它被添加到等待队列的尾部. 没有这个标志的入口项, 相反, 添加到开始.
2)当 wake_up 被在一个等待队列上调用时, 它在唤醒第一个有 WQ_FLAG_EXCLUSIVE 标志的进程后停止。
对于互斥等待的行为,比如如对一个listen后的socket描述符,多线程阻塞accept时,系统内核只会唤醒所有正在等待此时间的队列 的第一个,队列中的其他人则继续等待下一次事件的发生,这样就避免的多个线程同时监听同一个socket描述符时的惊群问题。

epoll惊群

epoll惊群分两种:

  1. 在fork之前创建epollfd,所有进程共用一个epoll
  2. 在fork之后创建epollfd,每个进程独用一个epoll

fork之前创建epollfd(2.6以及之后的内核已解决)

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

分析:

这里的epoll惊群跟accept惊群是类似的,共享一个epollfd, 加锁或标记解决。在新版本的epoll中已解决。但在内核2.6及之前是存在的。

fork之后创建epollfd(内核未解决)

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

分析:

因为每个子进程的epoll是不同的epoll, 虽然listenfd是同一个,但新连接过来时, accept会触发惊群,但内核不知道该发给哪个监听进程,因为不是同一个epoll。所以这种惊群内核并没有处理。惊群还是会出现。

关于nginx下的惊群问题

nginx有两类进程,一类称为master进程(相当于管理进程),另一类称为worker进程(实际工作进程)。启动方式有两种:

(1)单进程启动:此时系统中仅有一个进程,该进程既充当master进程的角色,也充当worker进程的角色。

(2)多进程启动:此时系统有且仅有一个master进程,至少有一个worker进程工作。

master进程主要进行一些全局性的初始化工作和管理worker的工作;事件处理是在worker中进行的。

首先简要的浏览一下nginx的启动过程,如下图:

由于nginx中使用的epoll,是在创建进程后创建的epollfd,因此也会出现上面的惊群问题。那么nginx中如何来避免惊群问题呢?这里涉及到一个并不是很常见的nginx配置项:accept_mutex

如果accept_mutex的值被设为off,那么当有一个请求需要处理时,所有的worker进程都会从waiting状态中唤醒,但是只有一个worker进程能处理请求,这样就会出现一定程度的惊群现象,这个现象每一秒钟会发生多次。它使服务器的性能下降,因为所有被唤醒的worker进程在重新进入waiting状态前会占用一段CPU时间。但是如果你的网站访问量比较大,为了系统的吞吐量,奶嘴还是建议大家关闭它。

举个栗子:

还是小鸡吃米,假设你养了一百只小鸡,现在你有一盆粮食,那么有两种喂食方法:

  1. 你主动抓一只小鸡过来,把一粒粮食塞到它嘴里,其它九十九只小鸡对此浑然不知,该睡觉睡觉,然后又抓过另一只小鸡采用同样的方法来喂粮食,一直往复。这就相当于激活了accept_mutex。
  2. 你把这盆粮食直接放到小鸡中间,一百只小鸡一起上来抢,抢到的小鸡都能得手。这就相当于关闭了accept_mutex。

可以看到在高并发下其实关闭accept_mutex其实整体的效率无疑大大增强了。

线程池惊群

在多线程设计中,经常会用到互斥和条件变量的问题。当一个线程解锁并通知其他线程的时候,就会出现惊群的现象。

  • pthread_mutex_lock/pthread_mutex_unlock:线程互斥锁的加锁及解锁函数。
  • pthread_cond_wait:线程池中的消费者线程等待线程条件变量被通知;
  • pthread_cond_signal/pthread_cond_broadcast:生产者线程通知线程池中的某个或一些消费者线程池,接收处理任务;

pthread_cond_signal,语义上看,是通知一个线程。调用此函数后,系统会唤醒在相同条件变量上等待的一个或多个线程(可参看手册)。如果通知了多个线程,则发生了惊群。

正常的用法:
所有线程共用一个锁,共用一个条件变量
当pthread_cond_signal通知时,就可能会出现惊群

解决惊群的方法:
所有线程共用一个锁,每个线程有自已的条件变量
pthread_cond_signal通知时,定向通知某个线程的条件变量,不会出现惊群


本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可,非商业性质可转载须署名链接,详见本站版权声明。

评论

0条评论

发表评论

电子邮件地址不会被公开。 必填项已用*标注