注册 登录  
 加关注
   显示下一条  |  关闭
温馨提示!由于新浪微博认证机制调整,您的新浪微博帐号绑定已过期,请重新绑定!立即重新绑定新浪微博》  |  关闭

还东国的博客

行之苟有恒,久久自芬芳

 
 
 

日志

 
 

(转载)关于 多进程epoll 与 “惊群”问题  

2015-04-21 19:32:04|  分类: 网络服务编程技术 |  标签: |举报 |字号 订阅

  下载LOFTER 我的照片书  |

关于 多进程epoll “惊群”问题

http://blog.163.com/pandalove@126/blog/static/9800324520122633515612/

 

【遇到问题】

    手头原来有一个单进程的linux epoll服务器程序,近来希望将它改写成多进程版本,主要原因有:

在服务高峰期间 并发的 网络请求非常海量,目前的单进程版本的程序有点吃不消:单进程时只有一个循环先后处理epoll_wait()到的事件,使得某些不幸排队靠后的socket fd的事件处理不及时(担心有些客户端等不耐烦甚至超时断开);

希望充分利用到服务器的多颗CPU

 

    但随着改写工作的深入,便第一次碰到了“惊群”问题,一开始我的程序设想如下:

主进程先监听端口, listen_fd = socket(...);

创建epollepoll_fd = epoll_create(...);

然后开始fork(),每个子进程进入大循环,去等待new  acceptepoll_wait(...),处理事件等。

 

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

    打个比方,街边有一家麦当劳餐厅,里面有4个服务小窗口,各有一位服务员。当大门口进来一位新客人时,门铃响了,4个服务员于是都抬起头(相当于操作系统唤醒了所有服务进程)希望将客人招呼过去自己所在的服务窗口。但结果可想而知,客人最终会走向其中某一个窗口,而其他3个窗口的服务员只能“失望叹息”(这一声无奈的叹息就相当于acceptEAGAIN错误),埋头继续忙自己的事去。

    这样子“惊群”现象必然造成资源浪费,那有木有好的解决办法呢?

【寻找办法】

    看了网上N多帖子和网页,阅读多款优秀开源程序的源代码,再结合自己的实验测试,总结如下:

 实际情况中,在发生惊群时,并非全部子进程都会被唤醒,而是一部分子进程被唤醒。但被唤醒的进程仍然只有1个成功accept,其他皆失败。

所有基于linux epoll机制的服务器程序在多进程时都受惊群问题的困扰,包括 lighttpd nginx 等程序,各家程序的处理办法也不一样。

lighttpd的解决思路:无视惊群。采用Watcher/Workers模式,具体措施有优化fork()epoll_create()的位置(让每个子进程自己去epoll_create()epoll_wait()),捕获accept()抛出来的错误并忽视等。这样子一来,当有新accept时仍将有多个lighttpd子进程被唤醒。

nginx的解决思路:避免惊群。具体措施有使用全局互斥锁,每个子进程在epoll_wait()之前先去申请锁,申请到则继续处理,获取不到则等待,并设置了一个负载均衡的算法(当某一个子进程的任务量达到总设置量的7/8时,则不会再尝试去申请锁)来均衡各个进程的任务量。

一款国内的优秀商业MTA服务器程序(不便透露名称):采用Leader/Followers线程模式,各个线程地位平等,轮流做Leader来响应请求。

对比lighttpdnginx两套方案,前者实现方便,逻辑简单,但那部分无谓的进程唤醒带来的资源浪费的代价如何仍待商榷(有网友测试认为这部分开销不大 http://www.iteye.com/topic/382107)。后者逻辑较复杂,引入互斥锁和负载均衡算分也带来了更多的程序开销。所以这两款程序在解决问题的同时,都有其他一部分计算开销,只是哪一个开销更大,未有数据对比。

坊间也流传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()成为原子操作,避免被多个进程都调用了。

【采用方案】

    多方考量,最后选择参考lighttpdWatcher/Workers模型,实现了我需要的那款多进程epoll程序,核心流程如下:

主进程先监听端口, listen_fd = socket(...); setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR,...)setnonblocking(listen_fd)listen(listen_fd,...)

开始fork(),到达子进程数上限(建议根据服务器实际的CPU核数来配置)后,主进程变成一个Watcher,只做子进程维护和信号处理等全局性工作。

每一个子进程(Worker)中,都创建属于自己的epollepoll_fd = epoll_create(...);,接着将listen_fd加入epoll_fd中,然后进入大循环,epoll_wait()等待并处理事件。千万注意, epoll_create()这一步一定要在fork()之后。

大胆设想(未实现):每个Worker进程采用多线程方式来提高大循环的socket fd处理速度,必要时考虑加入互斥锁来做同步,但也担心这样子得不偿失(进程+线程频繁切换带来的额外操作系统开销),这一步尚未实现和测试,但看到nginx源码中貌似有此逻辑。

【小结】

    纵观现如今的Linux服务器程序开发(无论是游戏服务器/WebServer服务器/balabala各类应用服务器),epoll可谓大行其道,当红炸子鸡一枚。它也确实是一个好东西,单进程时的事件处理能力就已经大大强于poll/select,难怪Nginx/Lighttpd等生力军程序都那么喜欢它。

    但毕竟只有一个进程的话,晾着服务器的多个CPU实在是罪过,为追求更高的机器利用率和更短的请求响应处理时间,还是折腾着搞出了多进程epoll。从新程序在线上服务器上的表现看,效果也确实不错 ,开心。。。

    感谢诸多网友的帖子分享,现在新程序已经上线,小弟也将心得整理成这篇博文,希望能帮到有需要的童鞋。仓促成文,若有错漏恳请指正,也请诸位不吝赐教给建议,灰常感谢!

  评论这张
 
阅读(391)| 评论(0)
推荐 转载

历史上的今天

在LOFTER的更多文章

评论

<#--最新日志,群博日志--> <#--推荐日志--> <#--引用记录--> <#--博主推荐--> <#--随机阅读--> <#--首页推荐--> <#--历史上的今天--> <#--被推荐日志--> <#--上一篇,下一篇--> <#-- 热度 --> <#-- 网易新闻广告 --> <#--右边模块结构--> <#--评论模块结构--> <#--引用模块结构--> <#--博主发起的投票-->
 
 
 
 
 
 
 
 
 
 
 
 
 
 

页脚

网易公司版权所有 ©1997-2017