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

2021-06-12 21:57:10 字數 2882 閱讀 3742

【遇到問題】

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

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

希望充分利用到伺服器的多顆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實在是罪過,為追求更高的機器利用率和更短的請求響應處理時間

關於驚群問題

簡單說來,多執行緒 多程序 linux下執行緒程序也沒多大區別 等待同乙個socket事件,當這個事件發生時,這些執行緒 程序被同時喚醒,就是驚群。可以想見,效率很低下,許多程序被核心重新排程喚醒,同時去響應這乙個事件,當然只有乙個程序能處理事件成功,其他的程序在處理該事件失敗後重新休眠 也有其他選...

高效能伺服器 epoll驚群與lighttpd改良

關於高效能server,unix網路程式設計 堪稱經典。根據說明,有如下三種相對高效的模型 模型1 最強 乙個程序中有預先建立多個執行緒都阻塞在accept函式 為了免accept驚群,可以在accept前先thread lock 任何乙個監聽執行緒從accept返回得到乙個socket就自己處理這...

關於多程序與多執行緒

首先,要實現多工,通常我們會設計master worker模式,master負責分配任務,worker負責執行任務,因此,多工環境下,通常是乙個master,多個worker。如果用多程序實現master worker,主程序就是master,其他程序就是worker。如果用多執行緒實現master...