epoll的惊群效应

1.epoll惊群效应产生的原因

很多朋友都在Linux下使用epoll编写过socket的服务端程序,在多线程环境下可能会遇到epoll的惊群效应。那么什么是惊群效应呢。其产生的原因是什么呢?
在多线程或者多进程环境下,有些人为了提高程序的稳定性,往往会让多个线程或者多个进程同时在epoll_wait监听的socket描述符。当一个新的链接请求进来时,操作系统不知道选派那个线程或者进程处理此事件,则干脆将其中几个线程或者进程给唤醒,而实际上只有其中一个进程或者线程能够成功处理accept事件,其他线程都将失败,且errno错误码为EAGAIN。这种现象称为惊群效应,结果是肯定的,惊群效应肯定会带来资源的消耗和性能的影响。
那么如何解决这个问题。

2.惊群问题的解决方法

2.1多线程环境下解决惊群解决方法

这种情况,不建议让多个线程同时在epoll_wait监听的socket,而是让其中一个线程epoll_wait监听的socket,当有新的链接请求进来之后,由epoll_wait的线程调用accept,建立新的连接,然后交给其他工作线程处理后续的数据读写请求,这样就可以避免了由于多线程环境下的epoll_wait惊群效应问题。

2.2多进程下的解决方法

目前很多开源软件,如lighttpd,nginx等都采用master/workers的模式提高软件的吞吐能力及并发能力,在nginx中甚至还采用了负载均衡的技术,在某个子进程的处理能力达到一定负载之后,由其他负载较轻的子进程负责epoll_wait的调用,那么nginx和Lighttpd是如何避免epoll_wait的惊群效用的。
lighttpd的解决思路是无视惊群效应,仍然采用master/workers模式,每个子进程仍然管自己在监听的socket上调用epoll_wait,当有新的链接请求发生时,操作系统仍然只是唤醒其中部分的子进程来处理该事件,仍然只有一个子进程能够成功处理此事件,那么其他被惊醒的子进程捕获EAGAIN错误,并无视。
nginx的解决思路:在同一时刻,永远都只有一个子进程在监听的socket上epoll_wait,其做法是,创建一个全局的pthread_mutex_t,在子进程进行epoll_wait前,则先获取锁。代码如下:
ngx_int_t  ngx_trylock_accept_mutex(ngx_cycle_t *cycle)  
{  
    if (ngx_shmtx_trylock(&ngx_accept_mutex))
    {
        if (ngx_enable_accept_events(cycle) == NGX_ERROR) 
        {  
            ngx_shmtx_unlock(&ngx_accept_mutex);  
            return NGX_ERROR;  
        }  
  
        ngx_accept_mutex_held = 1;  
        return NGX_OK;  
    }  
  
    if (ngx_accept_mutex_held)
    {  
        if (ngx_disable_accept_events(cycle) == NGX_ERROR)
        {  
            return NGX_ERROR;  
        }  
  
        ngx_accept_mutex_held = 0;  
    }  
    return NGX_OK;  
}  
且只有在ngx_accept_disabled < 0 时,才会去获取全局锁,及只有在子进程的负载能力在一定的范围下才会尝试去获取锁,并进入epoll_wait监听的socket。

void  ngx_process_events_and_timers(ngx_cycle_t *cycle)  
{   
    if (ngx_accept_disabled > 0)
    {  
        ngx_accept_disabled--;    
    }

else

{  
        if (ngx_trylock_accept_mutex(cycle) == NGX_ERROR) {  
            return;  
        }     
    }  
}


ngx_accept_disabled = ngx_cycle->connection_n / 8  - ngx_cycle->free_connection_n;

表示当子进程的连接数达到连接总数的7/8时,是不会尝试去获取全局锁,只会专注于自己的连接事件请求。

### 什么是 epoll 现象? 在 Linux 中,`epoll` 是一种高效的 I/O 多路复用机制,用于监控多个文件描述符上的事件。当 `epoll_wait` 被唤醒时,如果多个线程或进程都在等待同一个文件描述符的事件,则可能会引发所谓的 **现象 (Thundering Herd Problem)** 。具体来说,现象是指在一个事件发生时,所有正在阻塞于该事件的线程都会被唤醒,但实际上只有一个线程能够真正处理这个事件[^1]。 --- ### 现象的原因 #### 1. 文件描述符共享 在多进程或多线程环境中,多个工作单元可能共享同一个监听套接字(listen socket)。当有新的连接到达时,操作系统会通知所有的监听者,而实际上只需要其中一个去接受新连接即可。其他被唤醒的工作单元最终又重新进入休眠状态,这浪费了大量的 CPU 时间和资源[^2]。 #### 2. 唤醒机制 传统的 `select` 和早期版本的 `poll` 都存在类似的问题。虽然 `epoll` 设计上更高效,但如果未正确配置其行为模式或者使用不当,仍然会出现这种情况。例如,在水平触发模式 (`EPOLLET`) 下,如果没有采取额外措施防止重复唤醒,也可能导致现象的发生[^3]。 --- ### 解决方案 以下是几种常见的解决方法: #### 方法一:引入 WQ_FLAG_EXCLUSIVE 标记 通过修改内核代码,在调用 `ep_poll` 函数时为其设置 `WQ_FLAG_EXCLUSIVE` 标志位,可以有效减少不必要的线程唤醒次数。此标志告诉内核只应该唤醒一个合适的消费者来处理特定类型的事件,从而显著缓解甚至完全消除效应的影响。 ```c // 修改 ep_poll 的实现逻辑以支持独占队列标志 struct wake_q_head wqh; wake_q_init(&wqh); list_add_tail(&wait->entry, &wqh.head); __wake_up_common_lock(wait_queue_head_t *q, unsigned int mode, int nr_exclusive, void *key, int wake_flags | WQ_FLAG_EXCLUSIVE); ``` #### 方法二:采用 ET 边沿触发模式 相比默认使用的 LT (Level Triggered)模式,ET (Edge Triggered)模式更适合高性能场景下的应用开发。它要求应用程序显式读取完数据后再继续接收后续的通知消息,因此减少了频繁轮询带来的开销并间接抑制了潜在的风险。 需要注意的是,切换到 ET 模式后需要特别注意边界条件管理,比如部分读写操作可能导致遗漏某些更新信号等问题。 #### 方法三:单进程/单线程负责监听 另一种简单粗暴的办法就是让整个系统里唯一的一个工作者专门承担起监听职责,其余成员则专注于业务计算任务本身。这样做的好处在于彻底规避掉了因竞争同一资源而导致的各种麻烦事;不过缺点也很明显——扩展性和灵活性较差些。 对于 Nginx 来说,默认情况下它的 Worker Processes 并不会直接参与到 Listen Socket 上面来的 Read Event 监听当中去,而是借助 Master Process 统筹安排好了之后才分发给对应的子实例执行实际的任务流程。这样的架构设计很好地兼顾到了效率与稳定性两方面的需求。 #### 方法四:加锁控制访问权限 还可以考虑利用互斥量或者其他同步原语强制规定任何时候最多只能有一个实体有权尝试获取来自某个指定 File Descriptor 的最新动态变化情况报告。尽管这种方法理论上可行,但在实践中往往因为增加了额外的协调成本反而得不偿失。 --- ### 总结 综上所述,针对 Linux 系统中的 Epoll 现象提供了多种有效的应对策略。其中既包括底层技术改进层面的内容(如启用 Exclusive Wakeup),也有高层次框架调整方面的建议(如改用 Edge Trigged Mode 或限定单一 Listener)。开发者应当依据具体的项目需求权衡利弊选取最恰当的一种方式加以实施。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值