1、前言
我从事Linux系统下网络开发将近4年了,经常还是遇到一些问题,只是知其然而不知其所以然,有时候和其他人交流,搞得非常尴尬。如今计算机都是多核了,网络编程框架也逐步丰富多了,我所知道的有多进程、多线程、异步事件驱动常用的三种模型。最经典的模型就是Nginx中所用的Master-Worker多进程异步驱动模型。今天和大家一起讨论一下网络开发中遇到的“惊群”现象。之前只是听说过这个现象,网上查资料也了解了基本概念,在实际的工作中还真没有遇到过。今天周末,结合自己的理解和网上的资料,彻底将“惊群”弄明白。需要弄清楚如下几个问题:
(1)什么是“惊群”,会产生什么问题?
(2)“惊群”的现象怎么用代码模拟出来?
(3)如何处理“惊群”问题,处理“惊群”后的现象又是怎么样呢?
2、何为惊群
如今网络编程中经常用到多进程或多线程模型,大概的思路是父进程创建socket,bind、listen后,通过fork创建多个子进程,每个子进程继承了父进程的socket,调用accpet开始监听等待网络连接。这个时候有多个进程同时等待网络的连接事件,当这个事件发生时,这些进程被同时唤醒,就是“惊群”。这样会导致什么问题呢?我们知道进程被唤醒,需要进行内核重新调度,这样每个进程同时去响应这一个事件,而最终只有一个进程能处理事件成功,其他的进程在处理该事件失败后重新休眠或其他。网络模型如下图所示:
简而言之,惊群现象(thundering herd)就是当多个进程和线程在同时阻塞等待同一个事件时,如果这个事件发生,会唤醒所有的进程,但最终只可能有一个进程/线程对该事件进行处理,其他进程/线程会在失败后重新休眠,这种性能浪费就是惊群。
3、编码模拟“惊群”现象
我们已经知道了“惊群”是怎么回事,那么就按照上面的图编码实现看一下效果。我尝试使用多进程模型,创建一个父进程绑定一个端口监听socket,然后fork出多个子进程,子进程们开始循环处理(比如accept)这个socket。测试代码如下所示:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <sys/wait.h>
#include <string.h>
#include <errno.h>
#define PORT 8000
#define WORKER 4
int worker(int listenfd, int i)
{
while (1)
{
printf("I am worker %d, begin to accept connection.\n", i);
struct sockaddr_in client_addr;
socklen_t client_addrlen = sizeof(client_addr);
int connfd = accept(listenfd, (struct sockaddr *)&client_addr, &client_addrlen);
if (connfd != -1)
{
printf("worker %d accept a connection success.\t", i);
printf("ip :%s\t", inet_ntoa(client_addr.sin_addr));
printf("port: %d \n", client_addr.sin_port);
}
else
{
printf("worker %d accept a connection failed,error:%s", i, strerror(errno));
close(connfd);
}
}
return 0;
}
int main()
{
int i = 0;
struct sockaddr_in address;
bzero(&address, sizeof(address));
address.sin_family = AF_INET;
address.sin_addr.s_addr = htonl(INADDR_ANY);
address.sin_port = htons(PORT);
int listenfd = socket(PF_INET, SOCK_STREAM, 0);
assert(listenfd >= 0);
int ret = bind(listenfd, (struct sockaddr *)&address, sizeof(address));
assert(ret != -1);
ret = listen(listenfd, 5);
assert(ret != -1);
for (i = 0; i < WORKER; i++)
{
printf("Create worker %d\n", i + 1);
pid_t pid = fork();
if (pid == 0)
{
worker(listenfd, i);
}
if (pid < 0)
{
printf("fork error");
}
}
int status;
wait(&status);
return 0;
}
输出结果:
按照“惊群"现象,期望结果应该是4个子进程都会accpet到请求,其中只有一个成功,另外三个失败的情况。而实际的结果显示,父进程开始创建4个子进程,每个子进程开始等待accept连接。当telnet连接来的时候,只有worker2 子进程accpet到请求,而其他的三个进程并没有接收到请求。
这是什么原因呢?难道惊群现象是假的吗?于是赶紧google查一下,惊群到底是怎么出现的。
其实在Linux2.6版本以后,内核内核已经解决了accept()函数的“惊群”问题,大概的处理方式就是,当内核接收到一个客户连接后,只会唤醒等待队列上的第一个进程或线程。所以,如果服务器采用accept阻塞调用方式,在最新的Linux系统上,已经没有“惊群”的问题了。
但是,对于实际工程中常见的服务器程序,大都使用select、poll或epoll机制,此时,服务器不是阻塞在accept,而是阻塞在select、poll或epoll_wait,这种情况下的“惊群”仍然需要考虑。接下来以epoll为例分析:
使用epoll非阻塞实现代码如下所示: