驚群問題和解決思路
?
【遇到問題】
? ? 手頭原來有一個單進(jìn)程的linux epoll服務(wù)器程序,近來希望將它改寫成多進(jìn)程版本,主要原因有:
- 在服務(wù)高峰期間 并發(fā)的 網(wǎng)絡(luò)請求非常海量,目前的單進(jìn)程版本的程序有點(diǎn)吃不消:單進(jìn)程時只有一個循環(huán)先后處理epoll_wait()到的事件,使得某些不幸排隊靠后的socket fd的網(wǎng)絡(luò)事件處理不及時(擔(dān)心有些socket客戶端等不耐煩而超時斷開);
- 希望充分利用到服務(wù)器的多顆CPU;
?
? ? 但隨著改寫工作的深入,便第一次碰到了“驚群”問題,一開始我的程序設(shè)想如下:
- 主進(jìn)程先監(jiān)聽端口, listen_fd = socket(...);
- 創(chuàng)建epoll,epoll_fd =?epoll_create(...);
- 然后開始fork(),每個子進(jìn)程進(jìn)入大循環(huán),去等待new? accept,epoll_wait(...),處理事件等。
?
? ? 接著就遇到了“驚群”現(xiàn)象:當(dāng)listen_fd有新的accept()請求過來,操作系統(tǒng)會喚醒所有子進(jìn)程(因為這些進(jìn)程都epoll_wait()同一個listen_fd,操作系統(tǒng)又無從判斷由誰來負(fù)責(zé)accept,索性干脆全部叫醒……),但最終只會有一個進(jìn)程成功accept,其他進(jìn)程accept失敗。外國IT友人認(rèn)為所有子進(jìn)程都是被“嚇醒”的,所以稱之為Thundering Herd(驚群)。
?
? ? 這樣子“驚群”現(xiàn)象必然造成資源浪費(fèi),那有木有好的解決辦法呢?
?
【尋找辦法】
? ? 看了網(wǎng)上N多帖子和網(wǎng)頁,閱讀多款優(yōu)秀開源程序的源代碼,再結(jié)合自己的實(shí)驗測試,總結(jié)如下:
- ?實(shí)際情況中,在發(fā)生驚群時,并非全部子進(jìn)程都會被喚醒,而是一部分子進(jìn)程被喚醒。但被喚醒的進(jìn)程仍然只有1個成功accept,其他皆失敗。
- 所有基于linux epoll機(jī)制的服務(wù)器程序在多進(jìn)程時都受驚群問題的困擾,包括 lighttpd 和 nginx 等程序,各家程序的處理辦法也不一樣。
- lighttpd的解決思路:無視驚群。采用Watcher/Workers模式,具體措施有優(yōu)化fork()與epoll_create()的位置(讓每個子進(jìn)程自己去epoll_create()和epoll_wait()),捕獲accept()拋出來的錯誤并忽視等。這樣子一來,當(dāng)有新accept時仍將有多個lighttpd子進(jìn)程被喚醒。
- nginx的解決思路:避免驚群。具體措施有使用全局互斥鎖,每個子進(jìn)程在epoll_wait()之前先去申請鎖,申請到則繼續(xù)處理,獲取不到則等待,并設(shè)置了一個負(fù)載均衡的算法(當(dāng)某一個子進(jìn)程的任務(wù)量達(dá)到總設(shè)置量的7/8時,則不會再嘗試去申請鎖)來均衡各個進(jìn)程的任務(wù)量。
- 一款國內(nèi)的優(yōu)秀商業(yè)MTA服務(wù)器程序(不便透露名稱):采用Leader/Followers線程模式,各個線程地位平等,輪流做Leader來響應(yīng)請求。
- 對比lighttpd和nginx兩套方案,前者實(shí)現(xiàn)方便,邏輯簡單,但那部分無謂的進(jìn)程喚醒帶來的資源浪費(fèi)的代價如何仍待商榷(有網(wǎng)友測試認(rèn)為這部分開銷不大 http://www.iteye.com/topic/382107)。后者邏輯較復(fù)雜,引入互斥鎖和負(fù)載均衡算分也帶來了更多的程序開銷。所以這兩款程序在解決問題的同時,都有其他一部分計算開銷,只是哪一個開銷更大,未有數(shù)據(jù)對比。
- 坊間也流傳Linux 2.6.x之后的內(nèi)核,就已經(jīng)解決了accept的驚群問題,論文地址???http://static.usenix.org/event/usenix2000/freenix/full_papers/molloy/molloy.pdf???。
- 但其實(shí)不然,這篇論文里提到的改進(jìn)并未能徹底解決實(shí)際生產(chǎn)環(huán)境中的驚群問題,因為大多數(shù)多進(jìn)程服務(wù)器程序都是在fork()之后,再對epoll_wait(listen_fd,...)的事件,這樣子當(dāng)listen_fd有新的accept請求時,進(jìn)程們還是會被喚醒。論文的改進(jìn)主要是在內(nèi)核級別讓accept()成為原子操作,避免被多個進(jìn)程都調(diào)用了。
?
?
【采用方案】
? ? 多方考量,最后選擇參考lighttpd的Watcher/Workers模型,實(shí)現(xiàn)了我需要的那款多進(jìn)程epoll程序,核心流程如下:
- 主進(jìn)程先監(jiān)聽端口, listen_fd = socket(...); ,setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR,...),setnonblocking(listen_fd),listen(listen_fd,...)。
- 開始fork(),到達(dá)子進(jìn)程數(shù)上限(建議根據(jù)服務(wù)器實(shí)際的CPU核數(shù)來配置)后,主進(jìn)程變成一個Watcher,只做子進(jìn)程維護(hù)和信號處理等全局性工作。
- 每一個子進(jìn)程(Worker)中,都創(chuàng)建屬于自己的epoll,epoll_fd =?epoll_create(...);,接著將listen_fd加入epoll_fd中,然后進(jìn)入大循環(huán),epoll_wait()等待并處理事件。千萬注意, epoll_create()這一步一定要在fork()之后。
- 大膽設(shè)想(未實(shí)現(xiàn)):每個Worker進(jìn)程采用多線程方式來提高大循環(huán)的socket fd處理速度,必要時考慮加入互斥鎖來做同步,但也擔(dān)心這樣子得不償失(進(jìn)程+線程頻繁切換帶來的額外操作系統(tǒng)開銷),這一步尚未實(shí)現(xiàn)和測試,但看到nginx源碼中貌似有此邏輯。
?
?
?
【小結(jié)】
? ? 縱觀現(xiàn)如今的Linux服務(wù)器程序開發(fā)(無論是游戲服務(wù)器/WebServer服務(wù)器/balabala各類應(yīng)用服務(wù)器),epoll可謂大行其道,當(dāng)紅炸子雞一枚。它也確實(shí)是一個好東西,單進(jìn)程時的事件處理能力就已經(jīng)大大強(qiáng)于poll/select,難怪Nginx/Lighttpd等生力軍程序都那么喜歡它。
? ? 但畢竟只有一個進(jìn)程的話,晾著服務(wù)器的多個CPU實(shí)在是罪過,為追求更高的機(jī)器利用率和更短的請求響應(yīng)處理時間,還是折騰著搞出了多進(jìn)程epoll。從新程序在線上服務(wù)器上的表現(xiàn)看,效果也確實(shí)不錯 ,開心。。。
? ? 感謝諸多網(wǎng)友的帖子分享,現(xiàn)在新程序已經(jīng)上線,小弟也將心得整理成這篇博文,希望能幫到有需要的童鞋。倉促成文,若有錯漏懇請指正,也請諸位不吝賜教給建議,灰常感謝!
?
?
epoll驚群測試
?
???https://www.jianshu.com/p/362b56b573f4??
?
本文摘自 :https://blog.51cto.com/l