關於 多程式epoll 與 “驚群”問題
【遇到問題】
手頭原來有一個單程式的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錯誤),然後埋頭繼續忙自己的事去。
這樣子“驚群”現象必然造成資源浪費,那有木有好的解決辦法呢?
【尋找辦法】
看了網上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來響應請求。
- 對比lighttpd和nginx兩套方案,前者實現方便,邏輯簡單,但那部分無謂的程式喚醒帶來的資源浪費的代價如何仍待商榷(有網友測試認為這部分開銷不大 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()成為原子操作,避免被多個程式都呼叫了。
【採用方案】
多方考量,最後選擇參考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處理速度,必要時考慮加入互斥鎖來做同步,但也擔心這樣子得不償失(程式+執行緒頻繁切換帶來的額外作業系統開銷),這一步尚未實現和測試,但看到nginx原始碼中貌似有此邏輯。
【小結】
縱觀現如今的Linux伺服器程式開發(無論是遊戲伺服器/WebServer伺服器/balabala各類應用伺服器),epoll可謂大行其道,當紅炸子雞一枚。它也確實是一個好東西,單程式時的事件處理能力就已經大大強於poll/select,難怪Nginx/Lighttpd等生力軍程式都那麼喜歡它。
但畢竟只有一個程式的話,晾著伺服器的多個CPU實在是罪過,為追求更高的機器利用率和更短的請求響應處理時間,還是折騰著搞出了多程式epoll。從新程式線上上伺服器上的表現看,效果也確實不錯 ,開心。
感謝諸多網友的帖子分享,現在新程式已經上線,小弟也將心得整理成這篇博文,希望能幫到有需要的童鞋。倉促成文,若有錯漏懇請指正,也請諸位不吝賜教給建議,灰常感謝!
相關文章
- 驚群問題|復現|解決
- 關於kindeditor插入程式碼問題
- redis驚群Redis
- 關於CleanMyMac常見問題與解答Mac
- 多程式問題
- 關於陣列的物件獲取及排序問題/小程式的多層頁面返回問題陣列物件排序
- python關於多級包之間的引用問題Python
- 有個關於多執行緒的識別問題執行緒
- 關於公司程式碼許可權的問題
- 關於this指向的問題
- 關於跨域問題跨域
- 關於PMOS與NMOS電流公式的方向問題公式
- MOGDB/openGauss與PostgreSQL關於GBK字符集問題SQL
- MOGDB/openGauss與PostgreSQL關於GDK字符集問題SQL
- 關於程式碼如何執行的五個問題
- [20191202]關於hugepages相關問題.txt
- 關於 go-micro 相關問題Go
- 關於盒模型相關的問題模型
- 多執行緒相關問題執行緒
- 關於 Puerts 的效能問題
- 關於django跨域問題Django跨域
- 關於並查集問題並查集
- 關於 swoole 除錯問題除錯
- 關於dcat-admin問題
- 關於JQuery操作checkbox問題jQuery
- 關於rem佈局問題REM
- 關於MQTT 使用遇到問題MQQT
- 關於DrawerLayout的小問題
- 關於javascript的this指向問題JavaScript
- epoll使用與原理
- 多程式通訊系列問題
- [併發程式設計]-關於 CAS 的幾個問題程式設計
- 【epoll問題】EPOLLRDHUP使用導致無法接受資料
- 請教您關於 Nginx 下多個 Laravel 專案的部署問題NginxLaravel
- CV關於Mysql中ON與Where區別問題詳解buaMySql
- 關於安裝nbextensions的問題
- 關於 a 標籤跳轉問題
- 關於影像識別的問題
- 關於redis配置找不到問題Redis