epoll 群驚現象

2021-06-16 13:42:55 字數 2925 閱讀 9158

【遇到問題】

手頭原來有乙個單程序的linux epoll伺服器程式,近來希望將它改寫成多程序版本,主要原因有:

在服務高峰期間 併發的 網路請求非常海量,目前的單程序版本的程式有點吃不消:單程序時只有乙個迴圈先後處理epoll_wait()到的事件,使得某些不幸排隊靠後的socket fd的事件處理不及時(擔心有些客戶端等不耐煩甚至超時斷開);

希望充分利用到伺服器的多顆cpu;

但隨著改寫工作的深入,便第一次碰到了「驚群」問題,一開始我的程式設想如下:

主程序先監聽埠, listen_fd = socket(...);

建立epoll,epoll_fd = epoll_create(...);

然後開始fork(),每個子程序進入大迴圈,去等待new  accept,epoll_wait(...),處理事件等。

接著就遇到了「驚群」現象:當

listen_fd有新的accept()請求過來,作業系統會喚醒所有子程序(因為這些程序都epoll_wait()同乙個

listen_fd,作業系統又無從判斷由誰來負責accept,索性乾脆全部叫醒……

),但最終只會有乙個程序成功accept,其他程序accept失敗。外國it友人認為所有子程序都是被「嚇醒」的,所以稱之為

thundering herd

(驚群)。

打個比方,街邊有一家麥當勞餐廳,裡面有4個服務小視窗,各有一位服務員。當大門口進來一位新客人時,門鈴響了,4個服務員於是都抬起頭(相當於作業系統喚醒了所有服務程序)希望將客人招呼過去自己所在的服務視窗。但結果可想而知,客人最終會走向其中某乙個視窗,而其他3個視窗的服務員只能「失望嘆息」(這一聲無奈的嘆息就相當於accept報eagain錯誤),埋頭繼續忙自己的事去。

這樣子「驚群」現象必然造成資源浪費,那有木有好的解決辦法呢?

【尋找辦法】

實際情況中,在發生驚群時,並非全部子程序都會被喚醒,而是一部分子程序被喚醒。但被喚醒的程序仍然只有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來響應請求。

對比lighttpd和nginx兩套方案,前者實現方便,邏輯簡單,但那部分無謂的程序喚醒帶來的資源浪費的代價如何仍待商榷(有網友測試認為這部分開銷不大 後者邏輯較複雜,引入互斥鎖和負載均衡算分也帶來了更多的程式開銷。所以這兩款程式在解決問題的同時,都有其他一部分計算開銷,只是哪乙個開銷更大,未有資料對比。

坊間也流傳linux 2.6.x之後的核心,就已經解決了accept的驚群問題,**位址 

。 但其實不然,這篇**裡提到的改進並未能徹底解決實際生產環境中的驚群問題,因為大多數多程序伺服器程式都是在fork()之後,再對epoll_wait(listen_fd,...)的事件,這樣子當listen_fd有新的accept請求時,程序們還是會被喚醒。**的改進主要是在核心級別讓accept()成為原子操作,避免被多個程序都呼叫了。

【採用方案】

主程序先監聽埠, 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處理速度,必要時考慮加入互斥鎖來做同步,但也擔心這樣子得不償失(程序+執行緒頻繁切換帶來的額外作業系統開銷),這一步尚未實現和測試,但看到nginx原始碼中貌似有此邏輯。

【小結】

縱觀現如今的linux伺服器程式開發(無論是遊戲伺服器/webserver伺服器/balabala各類應用伺服器),epoll可謂大行其道,當紅炸子雞一枚。它也確實是乙個好東西,單程序時的事件處理能力就已經大大強於poll/select,難怪nginx/lighttpd等生力軍程式都那麼喜歡它。

但畢竟只有乙個程序的話,晾著伺服器的多個cpu實在是罪過,為追求更高的機器利用率和更短的請求響應處理時間

感謝諸多網友的帖子分享,現在新程式已經上線,小弟也將心得整理成這篇博文,希望能幫到有需要的童鞋。倉促成文,若有錯漏懇請指正,也請諸位不吝賜教給建議,灰常感謝!

accpet驚群和epoll驚群現象

epoll的驚群現象解決。想一想nginx解決的應該時epoll的驚群問題具體 網上有就不貼出。得到鎖的可以將accpet放進自己的epoll中然後。沒有得到的移出去 在思考這個問題之前,我們應該以前對前面所講幾點有所了解,即先弄清楚問題的背景,並能自己復現出來,而不僅僅只是看書或部落格,然後再來看...

epoll驚群測試

3.1.1 核心版本3.10測試結果 兩個程序均被喚醒 3.1.2 核心版本4.18測試結果 僅有乙個程序被喚醒。如果在epoll wait之後sleep一秒,結果如下 兩個程序均被喚醒,總共收到乙個包。小結 較新版本的核心做了一些優化,不一定會喚醒所有的程序。核心版本為3.10和4.18結果一致,...

關於 多程序epoll 與 「驚群」問題

遇到問題 手頭原來有乙個單程序的linux epoll伺服器程式,近來希望將它改寫成多程序版本,主要原因有 在服務高峰期間 併發的 網路請求非常海量,目前的單程序版本的程式有點吃不消 單程序時只有乙個迴圈先後處理epoll wait 到的事件,使得某些不幸排隊靠後的socket fd的網路事件處理不...