Linux 惊群详解

本文转载于https://jin-yang.github.io/post/linux-details-of-thundering-herd.html

先说结论(参考):
1、linux多进程accept系统调用的惊群问题(注意,这里没有使用select、epoll等事件机制),在linux 2.6版本之前的版本存在,在之后的版本中解决掉了。
2、使用select epoll等事件机制,在linux早期的版本中,惊群问题依然存在(epoll_create在fork之前)。 原因与之前单纯使用accept导致惊群,原因类似。Epoll的惊群问题,同样在之后的某个版本部分解决了。
3、Epoll_create在fork之后调用,不能避免惊群问题,Nginx使用互斥锁,解决epoll惊群问题。

1、accept()

    常见的场景如下:
    主进程执行 socket()+bind()+listen() 后,fork() 多个子进程,每个子进程都通过 accept() 循环处理这个 socket;此时,每个进程都阻塞在 accpet() 调用上,当一个新连接到来时,所有的进程都会被唤醒,但其中只有一个进程会 accept() 成功,其余皆失败,重新休眠。这就是 accept 惊群。
    如果只用一个进程去 accept 新连接,并通过消息队列等同步方式使其他子进程处理这些新建的连接,那么将会造成效率低下;因为这个进程只能用来 accept 连接,该进程可能会造成瓶颈。而实际上,对于 Linux 来说,这只是历史上的问题,现在的内核都解决该问题,也即只会唤醒一个进程。可以通过如下程序进行测试,只会激活一个进程。

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#include <netinet/in.h>

#define PROCESS_NUM 10

int main()
{
    int fd = socket(PF_INET, SOCK_STREAM, 0);
    int connfd;
    int pid, i, status;
    char sendbuff[1024];
    struct sockaddr_in serveraddr;

    printf("Listening 0.0.0.0:1234\n");
    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);
    for(i = 0; i < PROCESS_NUM; i++) {
        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);
            }
        }
    }
    wait(&status);
    return 0;
}

2、epoll()

    另外还有一个是关于 epoll_wait() 的(epoll_create()咱fork之前):
    主进程仍执行 socket()+bind()+listen() 后,将该 socket 加入到 epoll 中,然后 fork 出多个子进程,每个进程都阻塞在epoll_wait() 上,如果有事件到来,则判断该事件是否是该socket 上的事件,如果是,说明有新的连接到来了,则进行 accept 操作。为了简化处理,忽略后续的读写以及对 accept 返回的新的套接字的处理,直接断开连接。Epoll部分修复了惊群问题,与accept惊群的解决类似,epoll后来的版本(具体哪个版本,有待考证),修复了这个问题。

#include <netdb.h>
#include <stdio.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/epoll.h>
#include <sys/types.h>
#include <sys/socket.h>

#define PROCESS_NUM 10
#define MAXEVENTS 64

int main (int argc, char *argv[])
{
    int sfd, efd;
    int flags;
    int n, i, k;
    struct epoll_event event;
    struct epoll_event *events;
    struct sockaddr_in serveraddr;

    sfd = socket(PF_INET, SOCK_STREAM, 0);
    serveraddr.sin_family = AF_INET;
    serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
    serveraddr.sin_port = htons(atoi("1234"));
    bind(sfd, (struct sockaddr*)&serveraddr, sizeof(serveraddr));

    flags = fcntl (sfd, F_GETFL, 0);
    flags |= O_NONBLOCK;
    fcntl (sfd, F_SETFL, flags);

    if (listen(sfd, SOMAXCONN) < 0) {
        perror ("listen");
        exit(EXIT_SUCCESS);
    }

    if ((efd = epoll_create(MAXEVENTS)) < 0) {
        perror("epoll_create");
        exit(EXIT_SUCCESS);
    }

    event.data.fd = sfd;
    event.events = EPOLLIN; // | EPOLLET;
    if (epoll_ctl(efd, EPOLL_CTL_ADD, sfd, &event) < 0) {
        perror("epoll_ctl");
        exit(EXIT_SUCCESS);
    }

    /* Buffer where events are returned */
    events = (struct epoll_event*)calloc(MAXEVENTS, sizeof event);

    for(k = 0; k < PROCESS_NUM; k++) {
        if (fork() == 0) { /* children process */
            while (1) {    /* The event loop */
                n = epoll_wait(efd, events, MAXEVENTS, -1);
                printf("process #%d return from epoll_wait!\n", getpid());
                sleep(2);  /* sleep here is very important!*/
                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;
}

    Epoll_create()在Fork之前还是之后,有神马区别呢?

    fork之前epoll_create的话,所有进程共享一个epoll红黑数。如果我们只需要处理accept事件的话,貌似世界一片美好了。但是,epoll并不是只处理accept事件,accept后续的读写事件都需要处理,还有定时或者信号事件。当连接到来时,我们需要选择一个进程来accept,这个时候,任何一个accept都是可以的。当连接建立以后,后续的读写事件,却与进程有了关联。一个请求与a进程建立连接后,后续的读写也应该由a进程来做。
    当读写事件发生时,应该通知哪个进程呢?Epoll并不知道,因此,事件有可能错误通知另一个进程,这是不对的。因此,我们使用epoll_create()在fork之后创建,每个进程的读写事件,只注册在自己进程的epoll中。我们知道epoll对惊群的修复,是建立在共享在同一个epoll结构上的。Epoll_create在fork之后执行,每个进程有单独的epoll 红黑树,等待队列,ready事件列表。因此,惊群再次出现了。 

3、Nginx解决方案

    针对Epoll_create()在fork之后出现的惊群现象,nginx 的每个 worker 进程都会在函数 ngx_process_events_and_timers() 中处理不同的事件,然后通过 ngx_process_events() 封装了不同的事件处理机制,在 Linux 上默认采用 epoll_wait()。主要在 ngx_process_events_and_timers() 函数中解决惊群现象。

void ngx_process_events_and_timers(ngx_cycle_t *cycle)
{
    ... ...
    // 是否通过对accept加锁来解决惊群问题,需要工作线程数>1且配置文件打开accetp_mutex
    if (ngx_use_accept_mutex) {
        // 超过配置文件中最大连接数的7/8时,该值大于0,此时满负荷不会再处理新连接,简单负载均衡
        if (ngx_accept_disabled > 0) {
            ngx_accept_disabled--;
        } else {
            // 多个worker仅有一个可以得到这把锁。获取锁不会阻塞过程,而是立刻返回,获取成功的话
            // ngx_accept_mutex_held被置为1。拿到锁意味着监听句柄被放到本进程的epoll中了,如果
            // 没有拿到锁,则监听句柄会被从epoll中取出。
            if (ngx_trylock_accept_mutex(cycle) == NGX_ERROR) {
                return;
            }
            if (ngx_accept_mutex_held) {
                // 此时意味着ngx_process_events()函数中,任何事件都将延后处理,会把accept事件放到
                // ngx_posted_accept_events链表中,epollin|epollout事件都放到ngx_posted_events链表中
                flags |= NGX_POST_EVENTS;
            } else {
                // 拿不到锁,也就不会处理监听的句柄,这个timer实际是传给epoll_wait的超时时间,修改
                // 为最大ngx_accept_mutex_delay意味着epoll_wait更短的超时返回,以免新连接长时间没有得到处理
                if (timer == NGX_TIMER_INFINITE || timer > ngx_accept_mutex_delay) {
                    timer = ngx_accept_mutex_delay;
                }
            }
        }
    }
    ... ...
    (void) ngx_process_events(cycle, timer, flags);   // 实际调用ngx_epoll_process_events函数开始处理
    ... ...
    if (ngx_posted_accept_events) { //如果ngx_posted_accept_events链表有数据,就开始accept建立新连接
        ngx_event_process_posted(cycle, &ngx_posted_accept_events);
    }

    if (ngx_accept_mutex_held) { //释放锁后再处理下面的EPOLLIN EPOLLOUT请求
        ngx_shmtx_unlock(&ngx_accept_mutex);
    }

    if (delta) {
        ngx_event_expire_timers();
    }

    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, cycle->log, 0, "posted events %p", ngx_posted_events);
	// 然后再处理正常的数据读写请求。因为这些请求耗时久,所以在ngx_process_events里NGX_POST_EVENTS标
    // 志将事件都放入ngx_posted_events链表中,延迟到锁释放了再处理。

    从上面的注释可以看到,无论有多少个nginx worker进程,同一时刻只能有一个worker进程在自己的epoll中加入监听的句柄listenfd。这个处理accept的nginx worker进程置flag为NGX_POST_EVENTS,这样它在接下来的ngx_process_events函数(在linux中就是ngx_epoll_process_events函数)中不会立刻处理事件,延后,先处理完所有的accept事件后,释放锁,然后再处理正常的读写socket事件。
 

参考:https://jin-yang.github.io/post/linux-details-of-thundering-herd.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值