nginx如何解决惊群效应

什么是惊群效应

惊群效应(thundering herd)是指多进程(多线程)在同时阻塞等待同一个事件的时候(休眠状态),如果等待的这个事件发生,那么他就会唤醒等待的所有进程(或者线程),但是最终却只能有一个进程(线程)获得这个时间的“控制权”,对该事件进行处理,而其他进程(线程)获取“控制权”失败,只能重新进入休眠状态,这种现象和性能浪费就叫做惊群效应。

惊群效应场景

在早期的Linux版本中,内核对于阻塞在epoll_wait的进程,也是采用全部唤醒的机制,所以存在和accept相似的“惊群”问题。新版本的的解决方案也是只会唤醒等待队列上的个进程或线程,所以,新版本Linux 部分的解决了epoll的“惊群”问题。所谓部分的解决,意思就是:对于部分特殊场景,使用epoll机制,已经不存在“惊群”的问题了,但是对于大多数场景,epoll机制仍然存在“惊群”。

1、场景一:epoll_create()在fork子进程之前:

如果epoll_create()调用在fork子进程之前,那么epoll_create()创建的epfd 会被所有子进程继承。接下来,所有子进程阻塞调用epoll_wait(),等待被监控的描述符(包括用于监听客户连接的监听描述符)出现新事件。如果监听描述符发生可读事件,内核将阻塞队列上排在位的进程/线程唤醒,被唤醒的进程/线程继续执行accept()函数,得到新建立的客户连接描述符connfd。这种情况下,任何一个子进程被唤醒并执行accept()函数都是没有问题的。

但是,接下来,子进程的工作如果是调用epoll_ctl(epfd, EPOLL_CTL_ADD, connfd, &ev);将新建连接的描述符connfd加入到epfd中统一监控的话,因为,当前的epfd是在fork之前创建的,此时系统中只有一个epoll监控文件,即所有子进程共享一个epoll监控文件。任何一个进程(父进程或子进程)向epoll监控文件添加、修改和删除文件描述符时,都会影响到其它进程的epoll_wait。 

后续,当connfd描述符上接收到客户端信息时,内核也无法保证每次都是唤醒同一个进程/线程,来处理这个连接描述符connfd上的读写信息(其它进程可能根本就不认识connfd;或者在不同进程中,相同的描述符对应不同的客户端连接),最终导致连接处理错误。(另外,不同的线程处理同一个连接描述符,也会导致发送的信息乱序)

所以,应该避免epoll_create()在fork子进程之前。关于这一点,据说libevent的文档中有专门的描述。

2、场景二:epoll_create()在fork子进程之后:

如果epoll_create()在fork子进程之后,则每个进程都有自己的epoll监控文件(当某个进程将新建连接的描述符connfd加入到本进程的epfd中统一监控,不会影响其它进程的epoll_wait),但是为了实现并发监听,所有的子进程都会调用 

epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &ev); 

将监听描述符加入到监控文件中,也就是说所有子进程都在通过epoll机制轮询同一个监听描述符。如果有新的客户端请求接入,监听描述符出现POLLIN事件(表示描述符可读,有新连接接入),此时内核会唤醒所有的进程,所以“惊群”的问题依然存在。

源码实现

int ngx_cdecl
main(int argc, char *const *argv)
{
    ...
     cycle = ngx_init_cycle(&init_cycle);
    ...
     ngx_master_process_cycle(cycle); // 创建工作进程
    
}

ngx_cycle_t *
ngx_init_cycle(ngx_cycle_t *old_cycle)
{
    ...
    if (ngx_open_listening_sockets(cycle) != NGX_OK) {
        goto failed;
    }
    
}

ngx_int_t
ngx_open_listening_sockets(ngx_cycle_t *cycle)
{
    ... 
  //ngx_socket定义
  //#define ngx_socket          socket
  //#define ngx_socket_n        "socket()"
    s = ngx_socket(ls[i].sockaddr->sa_family, ls[i].type, 0);

    if (bind(s, ls[i].sockaddr, ls[i].socklen) == -1) {
       err = ngx_socket_errno;
    }    

    if (listen(s, ls[i].backlog) == -1) {
        ...
    }
    ...
}

nginx解决方式

对于这种情况下的“惊群”问题,Nginx的解决方案和《UNP 第三版》第30章中30.7和30.8给出的加锁方案类似,大概就是通过互斥锁对每个进程从epoll_wait到accept之间的处理通过互斥量保护。需要注意的是,对于这种加锁操作,每次只有一个子进程能执行epoll_wait和accept,具体哪个进程得到执行,要看内核调度。所以,为了解决负载均衡的问题,Nginx的解决方案中,每个进程有一个当前连接计数,如果当前连接计数超过大连接的7/8,该进程就停止接收新的连接。

源码实现如下:

void
ngx_process_events_and_timers(ngx_cycle_t *cycle)
{
   ...
   //对监听的sockets事件进行处理

    if (ngx_use_accept_mutex) {
        if (ngx_accept_disabled > 0) {
            ngx_accept_disabled--;

        } else {
            // 加锁成功进行后续处理
            if (ngx_trylock_accept_mutex(cycle) == NGX_ERROR) {
                return;
            }
            //设置网络读写事件延迟处理标志,即在释放锁后处理
            if (ngx_accept_mutex_held) {
                flags |= NGX_POST_EVENTS;

            } else {
                if (timer == NGX_TIMER_INFINITE
                    || timer > ngx_accept_mutex_delay)
                {
                    timer = ngx_accept_mutex_delay;
                }
            }
        }
    }

    ...

    //这里面epollwait等待网络事件
    //网络连接事件,放入ngx_posted_accept_events队列
    //网络读写事件,放入ngx_posted_events队列
    (void) ngx_process_events(cycle, timer, flags);
    ...

    //先处理网络连接事件,只有获取到锁,这里才会有连接事件
    ngx_event_process_posted(cycle, &ngx_posted_accept_events);

    //释放锁,让其他进程也能够拿到
    if (ngx_accept_mutex_held) {
        ngx_shmtx_unlock(&ngx_accept_mutex);
    }

    ngx_event_expire_timers();
     //处理网络读写事件
    ngx_event_process_posted(cycle, &ngx_posted_events);
}

  • SO_REUSEPORT

nginx 在 1.9.1 版本加入了这个功能,其本质是利用了 Linux 的 reuseport 的特性, 内核允许多个进程 listening socket 到同一个端口上,从内核层面做了负载均衡,每次只唤醒其中一个进程。

修改参数如下,只需要在listen 端口后面加上 reuseport即可 :

server {
        listen 80 reuseport;
        listen 443 ssl http2 reuseport;
        server_name   xxx.xxx.com;
        charset utf-8;

官方测试情况如下:

网上这方面的内容也非常多,大家可以自行搜索。详细参考 The SO_REUSEPORT socket option [LWN.net]  https://blog.n0p.me/2018/02/2018-02-20-portsharding/

注意:启用REUSEPORT也可能导致一些意想不到的问题,比如单个请求处理的最大延迟可能会增加,可以参考 https://blog.cloudflare.com/the-sad-state-of-linux-socket-balancing/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值