惊群(thundering herd)

惊群(thundering herd)

本篇只是对惊群做一个实验性的学习.了解下epoll的为什么会产生惊群现象,并且可以如何处理.不过我的描述都是比较浅显的,暂时没有深入到内核中去学习.

wiki-惊群

维基百科上对于惊群的解释是:

In computer science, the thundering herd problem occurs when a large number of processes or threads waiting for an event are awoken when that event occurs, but only one process is able to handle the event. When the processes wake up, they will each try to handle the event, but only one will win. All processes will compete for resources, possibly freezing the computer, until the herd is calmed down again.

所谓惊群意思就是说,多个线程或者进程同时等待同一个事件发生,当事件发生后,所有的线程都被唤醒,但是最终只会有一个进程能够处理事件,其他进程被唤醒,但是无法获得资源.因此,很多进程被唤醒所带来的频繁上下文切换的开销很大.

比如说,fork()了很多进程,每个进程都调用accept()来等待sockfd上的请求建立.当客户端连接到达的时候,会怎么样呢?

如下代码来做个实验:

#include <stdio.h>
#include <zconf.h>
#include <netinet/in.h>
#include <strings.h>
#include <memory.h>
#include <sys/wait.h>

#define PROCESS_NUM 10
#define BUFFER_SIZE 128
int main(int argc, char *argv[]) {
    char buf[BUFFER_SIZE];
    struct sockaddr_in server_addr,client_addr;
    int sockfd = socket(AF_INET,SOCK_STREAM,0); //面向tcp的socket
    bzero(&server_addr,sizeof(struct sockaddr_in));

    server_addr.sin_family = AF_INET; //ipv4地址格式
    server_addr.sin_port = htons(8080); //绑定80端口
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);

    //将socket和地址绑定
    bind(sockfd, (const struct sockaddr *) &server_addr, sizeof(struct sockaddr_in));

    //sockfd进入到监听状态,等待客户端链接
    listen(sockfd,5);
    socklen_t client_len = sizeof(struct sockaddr_in);

    //创建10个线程来准备接受链接
    for (int i = 0; i < PROCESS_NUM; i++) {
        int pid = fork();
        if (pid == 0) {
            //子进程
            while (1) {
                int connfd = accept(sockfd,(struct sockaddr_in*)&client_addr,&client_len);
                snprintf(buf,sizeof(buf),"accept PID is %d\n",getpid());
                send(connfd,buf,strlen(buf) + 1,0);
                printf("process %d accept success\n",getpid());
                close(connfd);
            }
        }
    }
    int status;
    wait(&status);
    return 0;
}

如果此时使用telnet来建立链接,输入telnet localhost 8080.控制台输出结果如下:

//我输入了两次telnet
client fd:4
process 9991 accept success
client fd:4
process 9992 accept success

可以看到最多只有一个进程被唤醒.因为如果其他进程也被唤醒但是无法建立链接的话,accept()应该会返回-1.

linux内核对于单个文件描述的相应是串行化的,所以最多只有一个进程会被唤醒.

The Linux-kernel will serialize responses for requests to a single file descriptor, so only one thread (process) is woken up ------wiki thundering herd

epoll的惊群

当多个epoll描述符都监听了相同的描述符的时候,大致代码如下:

int epfd1 = epoll_create(10);
int epfd2 = epoll_create(10);

int sockfd = socket(AF_INET,SOCK_STREAM,0);
ev.data.fd = sockfd;
ev.events = EPOLLIN;

epoll_ctl(epfd1,EPOLL_CTL_ADD,ev.data.fd,&ev);
epoll_ctl(epfd2,EPOLL_CTL_ADD,ev.data.fd,&ev);

在默认情况下,当来自客户端的请求到达的时候,所有的epoll_wait()都会结束阻塞.但是只有一个可以建立链接.

在linux4.5中引入了一个EPOLLEXCLUSIVE来解决这个问题,EPOLLIN|EPOLLEXCLUSIVE标志后,只有一个或者多个进程会被唤醒,而不是所有进程.所以在很大程度上避免了epoll 惊群问题.

man page中提到:

Sets an exclusive wakeup mode for the epoll file descriptor that is being attached to the target file
descriptor, fd. When a wakeup event occurs and multiple epoll file descriptors are attached to the same target file using EPOLLEXCLUSIVE, one or more of the epoll file descriptors will receive an event with epoll_wait(2). The default in this scenario (when EPOLLEXCLUSIVE is not set is for all epoll file descriptors to receive an event. EPOLLEXCLUSIVE is thus useful for avoiding thundering herd problems in certain scenarios.

下面来做一个实验来验证一下这个情况.在实验代码中,我们用循环创建了10个epollfd,并且都去监控sockfd,然后等待链接的建立.完整的代码在这里:

for (int i = 0; i < PROCESS_NUM; i++) {
    int pid = fork();
    if (pid == 0) {
        //子进程
        int epfd = epoll_create(10);
        bzero(&ev,sizeof(struct epoll_event));
        ev.data.fd = sockfd;
        ev.events = EPOLLIN; 
        epoll_ctl(epfd,EPOLL_CTL_ADD,ev.data.fd,&ev);
        while (1) {
            int num;
            num = epoll_wait(epfd,events,EVENTS_NUM,-1);
            printf("handle events\n");
            sleep(3);
            handle_events(events,num,epfd,sockfd);
        }
    }
}
//output:
handle events 会出现10次

telnet localhost 8080来建立网络连接,可以发现handle events输出了10次.如果将ev.events = EPOLLIN; 修改为ev.events = EPOLLIN | EPOLLEXCLUSIVE;,那么只会出现一次.

有一个问题就是,为什么上述代码中为什么要sleep,这里说到了是因为,如果不休眠的话,那么很快会进入到handle_events中,进而链接被建立.所以不会引起其他epoll_wait()的返回,因为内核中的半连接队列中的资源已经被取走了.因此,我这里休眠了3秒来模拟资源没有被取走的情况.

暂无评论

发送评论 编辑评论

|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇