epoll在多进程下产生的“惊群”现象

epoll在多进程下产生的“惊群”现象_如何避免_多进程因为文件描述符继承问题导致  



问题:


         有一个单进程的linux epoll服务器程序,近来希望将它改写成多进程版本,


         主要原因有: 
                1、在服务高峰期间 并发的 网络请求非常大,目前的单进程版本的支撑不了:单进程时只有一个循环先后处理epoll_wait()到的事件,使得某些不幸排队靠后的socket fd的网络事件得不到及时处理;
                 2、希望充分利用到服务器的多颗CPU;


 


但随着改写工作的深入,便第一次碰到了“惊群”问题,程序设想如下: 
主进程先监听端口: listen_fd = socket(...);
创建epoll,epoll_fd = epoll_create(...);
然后开始fork(),每个子进程进入大循环,去等待新的accept,epoll_wait(...),处理事件等。




           接着就遇到了“惊群”现象:当listen_fd有新的accept()请求过来,操作系统会唤醒所有子进程(因为这些进程都epoll_wait()同 一个listen_fd,操作系统又无从判断由谁来负责accept,索性干脆全部叫醒……),但最终只会有一个进程成功accept,其他进程 accept失败。外国IT友人认为所有子进程都是被“吓醒”的,所以称之为Thundering Herd(惊群)。


           打个比方,街边有一家麦当劳餐厅,里面有4个服务小窗口,每个窗口各有一名服务员。当大门口进来一位新客人,“欢迎光临!”餐厅大门的感应式门铃自动响了 (相当于操作系统底层捕抓到了一个网络事件),于是4个服务员都抬起头(相当于操作系统唤醒了所有服务进程)希望将客人招呼过去自己所在的服务窗口。但结 果可想而知,客人最终只会走向其中某一个窗口,而其他3个窗口的服务员只能“失望叹息”(这一声无奈的叹息就相当于accept()返回EAGAIN错 误),然后埋头继续忙自己的事去。 
这样子“惊群”现象必然造成资源浪费,那有木有好的解决办法呢?


 


寻找解决方法:


        看了网上N多帖子和网页,阅读多款优秀开源程序的源代码,再结合自己的实验测试,总结如下: 
        1、实际情况中,在发生惊群时,并非全部子进程都会被唤醒,而是一部分子进程被唤醒。但被唤醒的进程仍然只有1个成功accept,其他皆失败,errno=EAGAIN。
        2、所有基于linux epoll机制的服务器程序在多进程时都受惊群问题的困扰,包括 lighttpd 和 nginx 等程序,各家程序的处理办法也不一样。


           lighttpd的解决思路:无视惊群。采 用Watcher/Workers模式,具体措施有优化fork()与epoll_create()的位置(让每个子进程自己去 epoll_create()和epoll_wait()),捕获accept()抛出来的错误并忽视等。这样子一来,当有新accept时仍将有多个 lighttpd子进程被唤醒。
    
           nginx的解决思路:避免惊群。具体措施有使用全局互斥锁,每个子进程在epoll_wait()之前先去申请锁,申请到则继续处理,获取不到则等待,并设置了一个负载均衡的算法(当某一个子进程的任务量达到总设置量的7/8时,则不会再尝试去申请锁)来均衡各个进程的任务量。
        3、也流传Linux 2.6.x之后的内核,就已经解决了accept的惊群问题,论文地址 http://static.usenix.org/event/usenix2000/freenix/full_papers/molloy/molloy.pdf 。
     但其实不然,这篇论文里提到的改进并未能彻底解决实际生产环境中的惊群问题,因为大多数多进程服务器程序都是在fork()之后,再对 epoll_wait(listen_fd,...)的事件,这样子当listen_fd有新的accept请求时,进程们还是会被唤醒。论文的改进主要 是在内核级别让accept()成为原子操作,避免被多个进程都调用了。


 


多方考量,最后选择参考lighttpd的Watcher/Workers模型,实现了我需要的那款多进程epoll程序,核心流程如下: 
主 进程先监听端口, listen_fd = socket(...); ,setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR,...),setnonblocking(listen_fd),listen(listen_fd,...)。
开始fork(),到达子进程数上限(建议根据服务器实际的CPU核数来配置)后,主进程变成一个Watcher,只做子进程维护和信号处理等全局性工作。
每 一个子进程(Worker)中,都创建属于自己的epoll,epoll_fd = epoll_create(...);,接着将listen_fd加入epoll_fd中,然后进入大循环,epoll_wait()等待并处理事件。千 万注意, epoll_create()这一步一定要在fork()之后。
大胆设想(未实现):每个Worker进程采用多线程方式来提高大循环的socket fd处理速度,必要时考虑加入互斥锁来做同步。


你可能感兴趣的:(epoll在多进程下产生的“惊群”现象)