今天打开 OneNote,发现里面躺着一篇很久以前写的笔记,现在将它贴出来。

1. 什么叫惊群现象

首先,我们看看维基百科对惊群的定义:

The thundering herd problem occurs when a large number of processes waiting for an event are awoken when that event occurs, but only one process is able to proceed at a time. After the processes wake up, they all demand the resource and a decision must be made as to which process can continue. After the decision is made, the remaining processes are put back to sleep, only to all wake up again to request access to the resource.

This occurs repeatedly, until there are no more processes to be woken up. Because all the processes use system resources upon waking, it is more efficient if only one process was woken up at a time.

This may render the computer unusable, but it can also be used as a technique if there is no other way to decide which process should continue (for example when programming with semaphores).

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

2. accept 惊群

考虑如下场景:
主进程创建socket, bind, listen之后,fork出多个子进程,每个子进程都开始循环处理(accept)这个socket。每个进程都阻塞在accpet上,当一个新的连接到来时,所有的进程都会被唤醒,但其中只有一个进程会accept成功,其余皆失败,重新休眠。这就是accept惊群。

那么这个问题真的存在吗?

事实上,历史上,Linux 的 accpet 确实存在惊群问题,但现在的内核都解决该问题了。即,当多个进程/线程都阻塞在对同一个 socket 的 accept 调用上时,当有一个新的连接到来,内核只会唤醒一个进程,其他进程保持休眠,压根就不会被唤醒。

测试代码如下:

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/wait.h>
#include <stdio.h>
#include <string.h>
#define PROCESS_NUM 10
int main()
{
int fd = socket(PF_INET, SOCK_STREAM, 0);
int connfd;
int pid;
char sendbuff[1024];
struct sockaddr_in serveraddr;
serveraddr.sin_family = AF_INET;
serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
serveraddr.sin_port = htons(1234);
bind(fd, (struct sockaddr*)&serveraddr, sizeof(serveraddr));
listen(fd, 1024);
int i;
for(i = 0; i < PROCESS_NUM; i++)
{
int pid = fork();
if(pid == 0)
{
while(1)
{
connfd = accept(fd, (struct sockaddr*)NULL, NULL);
snprintf(sendbuff, sizeof(sendbuff), "accept PID is %d\n", getpid()); send(connfd, sendbuff, strlen(sendbuff) + 1, 0);
printf("process %d accept success!\n", getpid());
close(connfd);
}
}
}
int status;
wait(&status);
return 0;
}

当我们对该服务器发起连接请求(用 telnet/curl 等模拟)时,会看到只有一个进程被唤醒。

关于 accept 惊群的一些帖子或文章:

3. epoll惊群

如上所述,accept 已经不存在惊群问题,但 epoll 上还是存在惊群问题。即,如果多个进程/线程阻塞在监听同一个 listening socket fd 的 epoll_wait 上,当有一个新的连接到来时,所有的进程都会被唤醒。

考虑如下场景:

主进程创建 socket, bind, listen 后,将该 socket 加入到 epoll 中,然后 fork 出多个子进程,每个进程都阻塞在 epoll_wait 上,如果有事件到来,则判断该事件是否是该 socket 上的事件,如果是,说明有新的连接到来了,则进行 accept 操作。为了简化处理,忽略后续的读写以及对 accept 返回的新的套接字的处理,直接断开连接。

那么,当新的连接到来时,是否每个阻塞在 epoll_wait 上的进程都会被唤醒呢?

很多博客中提到,测试表明虽然 epoll_wait 不会像 accept 那样只唤醒一个进程/线程,但也不会把所有的进程/线程都唤醒。例如这篇文章:关于多进程 epoll 与 “惊群”问题

为了验证这个问题,我自己写了一个测试程序:

#include <sys/types.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <netdb.h>
#include <string.h>
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>
#include <errno.h>
#include <sys/wait.h>
#define PROCESS_NUM 10
static int
create_and_bind (char *port)
{
int fd = socket(PF_INET, SOCK_STREAM, 0);
struct sockaddr_in serveraddr;
serveraddr.sin_family = AF_INET;
serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
serveraddr.sin_port = htons(atoi(port));
bind(fd, (struct sockaddr*)&serveraddr, sizeof(serveraddr));
return fd;
}
static int
make_socket_non_blocking (int sfd)
{
int flags, s; flags = fcntl (sfd, F_GETFL, 0);
if (flags == -1)
{
perror ("fcntl");
return -1;
} flags |= O_NONBLOCK;
s = fcntl (sfd, F_SETFL, flags);
if (s == -1)
{
perror ("fcntl");
return -1;
} return 0;
} #define MAXEVENTS 64 int
main (int argc, char *argv[])
{
int sfd, s;
int efd;
struct epoll_event event;
struct epoll_event *events; sfd = create_and_bind("1234");
if (sfd == -1)
abort (); s = make_socket_non_blocking (sfd);
if (s == -1)
abort (); s = listen(sfd, SOMAXCONN);
if (s == -1)
{
perror ("listen");
abort ();
} efd = epoll_create(MAXEVENTS);
if (efd == -1)
{
perror("epoll_create");
abort();
} event.data.fd = sfd;
//event.events = EPOLLIN | EPOLLET;
event.events = EPOLLIN;
s = epoll_ctl(efd, EPOLL_CTL_ADD, sfd, &event);
if (s == -1)
{
perror("epoll_ctl");
abort();
} /* Buffer where events are returned */
events = calloc(MAXEVENTS, sizeof event);
int k;
for(k = 0; k < PROCESS_NUM; k++)
{
int pid = fork();
if(pid == 0)
{ /* The event loop */
while (1)
{
int n, i;
n = epoll_wait(efd, events, MAXEVENTS, -1);
printf("process %d return from epoll_wait!\n", getpid());
/* sleep here is very important!*/
//sleep(2);
for (i = 0; i < n; i++)
{
if ((events[i].events & EPOLLERR) || (events[i].events & EPOLLHUP) || (!(events[i].events & EPOLLIN)))
{
/* An error has occured on this fd, or the socket is not
ready for reading (why were we notified then?) */
fprintf (stderr, "epoll error\n");
close (events[i].data.fd);
continue;
}
else if (sfd == events[i].data.fd)
{
/* We have a notification on the listening socket, which
means one or more incoming connections. */
struct sockaddr in_addr;
socklen_t in_len;
int infd;
char hbuf[NI_MAXHOST], sbuf[NI_MAXSERV]; in_len = sizeof in_addr;
infd = accept(sfd, &in_addr, &in_len);
if (infd == -1)
{
printf("process %d accept failed!\n", getpid());
break;
}
printf("process %d accept successed!\n", getpid()); /* Make the incoming socket non-blocking and add it to the
list of fds to monitor. */
close(infd);
}
}
}
}
}
int status;
wait(&status);
free (events);
close (sfd);
return EXIT_SUCCESS;
}

发现确实如上面那篇博客里所说,当我模拟发起一个请求时,只有一个或少数几个进程被唤醒了。

也就是说,到目前为止,还没有得到一个确定的答案。但后来,在下面这篇博客中看到这样一个评论:http://blog.csdn.net/spch2008/article/details/18301357

这个总结,需要进一步阐述,你的实验,看上去是只有4个进程唤醒了,而事实上,其余进程没有被唤醒的原因是你的某个进程已经处理完这个 accept,内核队列上已经没有这个事件,无需唤醒其他进程。你可以在 epoll 获知这个 accept 事件的时候,不要立即去处理,而是 sleep 下,这样所有的进程都会被唤起

看到这个评论后,我顿时如醍醐灌顶,重新修改了上面的测试程序,即在 epoll_wait 返回后,加了个 sleep 语句,这时再测试,果然发现所有的进程都被唤醒了。

所以,epoll_wait上的惊群确实是存在的。

4. 为什么内核不处理 epoll 惊群

看到这里,我们可能有疑惑了,为什么内核对 accept 的惊群做了处理,而现在仍然存在 epoll 的惊群现象呢?

我想,应该是这样的:
accept 确实应该只能被一个进程调用成功,内核很清楚这一点。但 epoll 不一样,他监听的文件描述符,除了可能后续被 accept 调用外,还有可能是其他网络 IO 事件的,而其他 IO 事件是否只能由一个进程处理,是不一定的,内核不能保证这一点,这是一个由用户决定的事情,例如可能一个文件会由多个进程来读写。所以,对 epoll 的惊群,内核则不予处理。

5. Nginx 是如何处理惊群问题的

在思考这个问题之前,我们应该以前对前面所讲几点有所了解,即先弄清楚问题的背景,并能自己复现出来,而不仅仅只是看书或博客,然后再来看看 Nginx 的解决之道。这个顺序不应该颠倒。

首先,我们先大概梳理一下 Nginx 的网络架构,几个关键步骤为:

  1. Nginx 主进程解析配置文件,根据 listen 指令,将监听套接字初始化到全局变量 ngx_cycle 的 listening 数组之中。此时,监听套接字的创建、绑定工作早已完成。
  2. Nginx 主进程 fork 出多个子进程。
  3. 每个子进程在 ngx_worker_process_init 方法里依次调用各个 Nginx 模块的 init_process 钩子,其中当然也包括 NGX_EVENT_MODULE 类型的 ngx_event_core_module 模块,其 init_process 钩子为 ngx_event_process_init。
  4. ngx_event_process_init 函数会初始化 Nginx 内部的连接池,并把 ngx_cycle 里的监听套接字数组通过连接池来获得相应的表示连接的 ngx_connection_t 数据结构,这里关于 Nginx 的连接池先略过。我们主要看 ngx_event_process_init 函数所做的另一个工作:如果在配置文件里没有开启accept_mutex锁,就通过 ngx_add_event 将所有的监听套接字添加到 epoll 中。
  5. 每一个 Nginx 子进程在执行完 ngx_worker_process_init 后,会在一个死循环中执行 ngx_process_events_and_timers,这就进入到事件处理的核心逻辑了。
  6. 在 ngx_process_events_and_timers 中,如果在配置文件里开启了 accept_mutext 锁,子进程就会去获取 accet_mutext 锁。如果获取成功,则通过 ngx_enable_accept_events 将监听套接字添加到 epoll 中,否则,不会将监听套接字添加到 epoll 中,甚至有可能会调用 ngx_disable_accept_events 将监听套接字从 epoll 中删除(如果在之前的连接中,本worker子进程已经获得过accept_mutex锁)。
  7. ngx_process_events_and_timers 继续调用 ngx_process_events,在这个函数里面阻塞调用 epoll_wait。

至此,关于 Nginx 如何处理 fork 后的监听套接字,我们已经差不多理清楚了,当然还有一些细节略过了,比如在每个 Nginx 在获取 accept_mutex 锁前,还会根据当前负载来判断是否参与 accept_mutex 锁的争夺。

把这个过程理清了之后,Nginx 解决惊群问题的方法也就出来了,就是利用 accept_mutex 这把锁。

如果配置文件中没有开启 accept_mutex,则所有的监听套接字不管三七二十一,都加入到每子个进程的 epoll中,这样当一个新的连接来到时,所有的 worker 子进程都会惊醒。

如果配置文件中开启了 accept_mutex,则只有一个子进程会将监听套接字添加到 epoll 中,这样当一个新的连接来到时,当然就只有一个 worker 子进程会被唤醒了。

6. 小结

现在我们对惊群及 Nginx 的处理总结如下:

  • accept 不会有惊群,epoll_wait 才会。
  • Nginx 的 accept_mutex,并不是解决 accept 惊群问题,而是解决 epoll_wait 惊群问题。
  • 说Nginx 解决了 epoll_wait 惊群问题,也是不对的,它只是控制是否将监听套接字加入到epoll 中。监听套接字只在一个子进程的 epoll 中,当新的连接来到时,其他子进程当然不会惊醒了。

7. 其他参考文章

accept与epoll惊群 转载的更多相关文章

  1. epoll 惊群处理

    #include <sys/types.h> #include <sys/socket.h> #include <sys/epoll.h> #include < ...

  2. epoll惊群原因分析

    考虑如下情况(实际一般不会做,这里只是举个例子): 在主线程中创建一个socket.绑定到本地端口并监听 在主线程中创建一个epoll实例(epoll_create(2)) 将监听socket添加到e ...

  3. 源码剖析Linux epoll实现机制及Linux上惊群

    转载:https://blog.csdn.net/tgxallen/article/details/78086360 看源码是对一个技术认识最直接且最有效的方式了,之前用Linux Epoll做过一个 ...

  4. 【转载】“惊群”,看看nginx是怎么解决它的

    原文:http://blog.csdn.net/russell_tao/article/details/7204260 在说nginx前,先来看看什么是“惊群”?简单说来,多线程/多进程(linux下 ...

  5. “惊群”,看看nginx是怎么解决它的

    在说nginx前,先来看看什么是“惊群”?简单说来,多线程/多进程(linux下线程进程也没多大区别)等待同一个socket事件,当这个事件发生时,这些线程/进程被同时唤醒,就是惊群.可以想见,效率很 ...

  6. NGINX怎样处理惊群的

    写在前面 写NGINX系列的随笔,一来总结学到的东西,二来记录下疑惑的地方,在接下来的学习过程中去解决疑惑. 也希望同样对NGINX感兴趣的朋友能够解答我的疑惑,或者共同探讨研究. 整个NGINX系列 ...

  7. Linux网络编程“惊群”问题总结

    1.前言 我从事Linux系统下网络开发将近4年了,经常还是遇到一些问题,只是知其然而不知其所以然,有时候和其他人交流,搞得非常尴尬.如今计算机都是多核了,网络编程框架也逐步丰富多了,我所知道的有多进 ...

  8. Linux惊群效应详解

    Linux惊群效应详解(最详细的了吧)   linux惊群效应 详细的介绍什么是惊群,惊群在线程和进程中的具体表现,惊群的系统消耗和惊群的处理方法. 1.惊群效应是什么?        惊群效应也有人 ...

  9. nginx&http 第三章 惊群

    惊群:概念就不解释了. 直接说正题:惊群问题一般出现在那些web服务器上,Linux系统有个经典的accept惊群问题,这个问题现在已经在内核曾经得以解决,具体来讲就是当有新的连接进入到accept队 ...

随机推荐

  1. 【转】Unity网格合并_材质合并

    原帖请戳:Unity网格合并_材质合并 写在前面: 从优化角度,Mesh需要合并. 从换装的角度(这里指的是换形状.换组成部件的换装,而不是挂点型的换装),都需要网格合并.材质合并.如果是人物的换装, ...

  2. google image

    google图片抓取 google图片是base64加密的,而且base64后的信息放在script信息里面 import pymysql from lxml import etree import ...

  3. php zip扩展的一些基本操作

    public function zip_test() { $zip_obj = new ZipArchive(); $res = $zip_obj->open('/data1/www/www.k ...

  4. windows下搭建vue

    1.windows https://nodejs.org/en/download/ 下载安装包2.点击安装 安装完成3.进入cmd npm -v4.安装淘宝镜像cnpm npm下载速度过慢因为安装完n ...

  5. JAVA语言 第二周

    放假第二周了,时间真快! 上一周配置好了环境变量,这一周就可以做一些测试了.对不同的内容分类进行了测试,包括写入.输出.变量·······还有很多.对于开学的试卷,在第一部分做的还行,第二部分就没什么 ...

  6. linux键盘驱动

    http://blog.csdn.net/beyondhaven/article/details/5753182 http://blog.chinaunix.net/uid-20564848-id-7 ...

  7. npm run build

    [npm run build] npm 会在项目的 package.json 文件中寻找 scripts 区域,其中包括npm test和npm start等命令. 其实npm test和npm st ...

  8. 基于oslo_messaging的RPC通信

    oslo_messaging源于Openstack的一个经典的模块,用以实现服务间的RPC通信.Client端将数据放入rabbitmq中,server端从消息队列中获取传送数据. oslo.mess ...

  9. 第一次登录mysql,使用任何命令都报错ERROR 1820 (HY000): You must reset your password using ALTER USER statement before executing this statement.

    问题: 使用临时密码登录成功后,使用任何myql命令,例如show databases;都提示下面的报错 ERROR 1820 (HY000): You must reset your passwor ...

  10. 安装 pygame,找不到Python version 2.7

    今天在安装pygame时出错,提示“Python version 2.7 required, which was not found in the registry”,经过网上查找资料后发现只需要新建 ...