1、前言
我从事Linux系统下网络开发将近4年了,经常还是遇到一些问题,只是知其然而不知其所以然,有时候和其他人交流,搞得非常尴尬。如今计算机都是多核了,网络编程框架也逐步丰富多了,我所知道的有多进程、多线程、异步事件驱动常用的三种模型。最经典的模型就是Nginx中所用的Master-Worker多进程异步驱动模型。今天和大家一起讨论一下网络开发中遇到的“惊群”现象。之前只是听说过这个现象,网上查资料也了解了基本概念,在实际的工作中还真没有遇到过。今天周末,结合自己的理解和网上的资料,彻底将“惊群”弄明白。需要弄清楚如下几个问题:
(1)什么是“惊群”,会产生什么问题?
(2)“惊群”的现象怎么用代码模拟出来?
(3)如何处理“惊群”问题,处理“惊群”后的现象又是怎么样呢?
2、何为惊群
如今网络编程中经常用到多进程或多线程模型,大概的思路是父进程创建socket,bind、listen后,通过fork创建多个子进程,每个子进程继承了父进程的socket,调用accpet开始监听等待网络连接。这个时候有多个进程同时等待网络的连接事件,当这个事件发生时,这些进程被同时唤醒,就是“惊群”。这样会导致什么问题呢?我们知道进程被唤醒,需要进行内核重新调度,这样每个进程同时去响应这一个事件,而最终只有一个进程能处理事件成功,其他的进程在处理该事件失败后重新休眠或其他。网络模型如下图所示:
简而言之,惊群现象(thundering herd)就是当多个进程和线程在同时阻塞等待同一个事件时,如果这个事件发生,会唤醒所有的进程,但最终只可能有一个进程/线程对该事件进行处理,其他进程/线程会在失败后重新休眠,这种性能浪费就是惊群。
3、编码模拟“惊群”现象
我们已经知道了“惊群”是怎么回事,那么就按照上面的图编码实现看一下效果。我尝试使用多进程模型,创建一个父进程绑定一个端口监听socket,然后fork出多个子进程,子进程们开始循环处理(比如accept)这个socket。测试代码如下所示:
1 #include <stdio.h>
2 #include <unistd.h>
3 #include <sys/types.h>
4 #include <sys/socket.h>
5 #include <netinet/in.h>
6 #include <arpa/inet.h>
7 #include <assert.h>
8 #include <sys/wait.h>
9 #include <string.h>
10 #include <errno.h>
IP "127.0.0.1"
13 #define PORT 8888
14 #define WORKER 4
worker(int listenfd, int i)
17 {
18
while (1) {
, i);
20
struct sockaddr_in client_addr;
21
socklen_t client_addrlen = sizeof( client_addr );
22
int connfd = accept( listenfd, ( struct sockaddr* )&client_addr, &client_addrlen );
23
if (connfd != -1) {
, i);
,inet_ntoa(client_addr.sin_addr));
,client_addr.sin_port);
27
} else {
, i, strerror(errno));
close(connfd);
29
}
30 }
;
32 }
main()
35 {
36
int i = 0;
37
struct sockaddr_in address;
38
bzero(&address, sizeof(address));
39
address.sin_family = AF_INET;
40
inet_pton( AF_INET, IP, &address.sin_addr);
41
address.sin_port = htons(PORT);
42
int listenfd = socket(PF_INET, SOCK_STREAM, 0);
43
assert(listenfd >= 0);
ret = bind(listenfd, (struct sockaddr*)&address, sizeof(address));
46
assert(ret != -1);
47
48
ret = listen(listenfd, 5);
49
assert(ret != -1);
(i = 0; i < WORKER; i++) {
, i+1);
53
pid_t pid = fork();
(pid == 0) {
56
worker(listenfd, i);
57
}
(pid < 0) {
);
61
}
62 }
status;
66
wait(&status);
;
68 }
编译执行,在本机上使用telnet 127.0.0.1 8888测试,结果如下所示: