nginx系列第七篇:结合nginx讨论“惊群”问题

目录

1.什么是惊群

2.linux下socket通信之accept"惊群"现象

3.select/poll/epoll"惊群"现象

4.nginx中的惊群处理


1.什么是惊群

"惊群"是多个进程(线程)阻塞在某个系统调用上等待事件触发,当事件触发后,这些睡眠的进程(线程)会被同时唤醒,多个进程(线程)从阻塞的系统调用返回。"惊群"效率低下,
大量的CPU时间浪费在被唤醒发现无事可做,然后又继续睡眠的反复切换上。

2.linux下socket通信之accept"惊群"现象

        socket网络通信中,网络数据包的接收是异步进行的,数据包到来的处理分为两部分:
一是网卡通知数据包到来,中断协议栈收包。
二是协议栈将数据包填充socket的接收队列,通知应用程序有数据可读。
应用程序是通过socket和协议栈交互的,socket是应用程序和协议栈之间联系的桥梁,socket是两者之间的接口,
当数据包到达协议栈的时候,发生下面两个过程:
(1)协议栈将数据包放入socket的接收缓冲区队列,并通知持有该socket的应用程序;
(2)持有该socket的应用程序响应通知事件,将数据包从socket的接收缓冲区队列中取出

此处引用别出的一个图进行说明:

nginx系列第七篇:结合nginx讨论“惊群”问题_第1张图片

    对于高性能的服务器而言,为了利用多 CPU 核的优势,基本上都采用多个进程(线程)的方式同时accept在一个listen socket上。多个进程(线程)阻塞在accept调用上,在协议栈将client的请求socket放入listen socket的accept队列的时候,是要唤醒一个进程还是全部进程来处理呢?
linux内核通过睡眠队列来组织所有等待某个事件的task,而 wakeup 机制则可以异步唤醒整个睡眠队列上的 task,wakeup 逻辑在唤醒睡眠队列时,会遍历该队列链表上的每一个节点,调用每一个节点的 callback,从而唤醒睡眠队列上的每个 task。这样,在一个 connect 到达这个 lisent socket 的时候,内核会唤醒所有睡眠在 accept 队列上的 task。N 个 task 进程(线程)同时从 accept 返回,但是,只有一个 task 返回这个 connect 的 fd,其他 task 都返回-1(EAGAIN)。这是典型的 accept"惊群"现象。这个是 linux 上困扰了大家很长时间的一个经典问题,在 linux2.6(又说是2.4.1)以后的内核中得到彻底的解决,通过添加了一个 WQ_FLAG_EXCLUSIVE 标记告诉内核进行排他性的唤醒,即唤醒一个进程后即退出唤醒的过。用户进程 task 对 listen socket 进行 accept 操作,如果这个时候如果没有新的 connect 请求过来,用户进程 task 会阻塞睡眠在 listent fd 的睡眠队列上。这个时候,用户进程 Task 会被设置 WQ_FLAG_EXCLUSIVE 标志位,并加入到 listen socket 的睡眠队列尾部(这里要确保所有不带 WQ_FLAG_EXCLUSIVE 标志位的 non-exclusive waiters 排在带 WQ_FLAG_EXCLUSIVE 标志位的 exclusive waiters 前面)。根据前面的唤醒逻辑,一个新的 connect 到来,内核只会唤醒一个用户进程 task 就会退出唤醒过程,从而不存在了"惊群"现象。

3.select/poll/epoll"惊群"现象

        linux系统内核优化以后,accept系统调用已经不存在"惊群"现象。但是在实际服务端开发中,我们会存在另外一种惊群问题,通常一个服务端有很多网络IO需要处理,为了提高server的并发能力,不会让server阻塞在accept调用上,一般会使用 select/poll/epoll I/O 多路复用技术,同时为了充分利用多核CPU的有害,服务器上会起多个进程(线程)同时提供服务。因此在某一时刻多个进程(线程)阻塞在 select/poll/epoll_wait 系统调用上,当一个请求上来的时候,多个进程都会被 select/poll/epoll_wait 唤醒去 accept,然而只有一个进程(线程)accept 成功,其他进程(线程accept失败,然后重新阻塞在select/poll/epoll_wait系统调用上。因此尽管 accept 不存在"惊群"了,但是我们出现了另外的"惊群"。针对这个问题(只让一个进程(线程)去监听 listen socket 的可读事件),我们可以看一些nginx是如何处理惊群的。

4.nginx中的惊群处理

       nginx master不会接受任何请求,所有的连接都在worker中处理。nginx所有worker并不能同时accept多个新连接,nginx实现了一个叫accept锁的东西,同一时刻只有一个worker能够获取到这个accept锁,只有获取到锁的这个worker才能够accept新连接。代码实现如下:

nginx系列第七篇:结合nginx讨论“惊群”问题_第2张图片

      当 ngx_use_accept_mutex 为 1 的时候(当 nginx worker 进程数>1 时且配置文件中打开 accept_mutex 时,这个标志置为 1),表示要规避 listen fd"惊群"。nginx 的 worker 进程在进行 event 模块的初始化的时候, core event 模块的 process_init 函数中(ngx_event_process_init)将 listen fd 加入到 epoll 中并监听其 READ 事件。

       nginx 通过一次仅允许一个进程将 listen fd 放入自己的 epoll 来监听其 READ 事件的方式来达到 listen fd"惊群"避免。然而做好这一点并不容易,作为一个高性能 web 服务器,需要尽量避免阻塞,并且要很好平衡各个工作 worker 的请求,避免饿死情况。

(1)避免新请求不能及时得到处理的饿死现象。worker进程在抢夺到accept权限,加锁成功的时候,要将事件的处理delay到释放锁后在处理(为什么ngx_posted_accept_events队列上的事件处理不需要延迟呢? 因为ngx_posted_accept_events上的事件就是listen fd的可读事件,本来就是我抢到的accept权限,还没accept就释放锁,这个时候被别人抢走了怎么办呢?)。否则,获得锁的工作worker由于在处理一个耗时事件,这个时候大量请求过来,其他worker进程空闲却没有处理权限在干着急的等着。

(2)避免总是某个worker进程抢到锁的现象。大量请求被同一个进程抢到,而其他worker进程却很清闲。 nginx有个简单的负载均衡,ngx_accept_disabled表示此时满负荷程度,没必要再处理新连接了,nginx.conf配置了每个worker进程能够处理的最大连接数,当达到最大数的7/8时,ngx_accept_disabled为正,说明本nginx worker进程非常繁忙,将不再去处理新连接。每次要进行抢夺accept权限的时候,如果ngx_accept_disabled大于0,则递减1,不进行抢夺逻辑。

你可能感兴趣的:(nginx,网络,服务器)