1. 程式人生 > >epoll在多程序下產生的“驚群”現象——如何避免——多程序因為檔案描述符繼承問題導致

epoll在多程序下產生的“驚群”現象——如何避免——多程序因為檔案描述符繼承問題導致

【遇到問題】     手頭原來有一個單程序的linux epoll伺服器程式,近來希望將它改寫成多程序版本,主要原因有:
  1. 在服務高峰期間 併發的 網路請求非常海量,目前的單程序版本的程式有點吃不消:單程序時只有一個迴圈先後處理epoll_wait()到的事件,使得某些不幸排隊靠後的socket fd的網路事件處理不及時(擔心有些socket客戶端等不耐煩而超時斷開)
  2. 希望充分利用到伺服器的多顆CPU;
    但隨著改寫工作的深入,便第一次碰到了“驚群”問題,一開始我的程式設想如下:
  1. 主程序先監聽埠, listen_fd = socket(...);
  2. 建立epoll,epoll_fd = epoll_create(...);
  3. 然後開始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.  實際情況中,在發生驚群時,並非全部子程序都會被喚醒,而是一部分子程序被喚醒。但被喚醒的程序仍然只有1個成功accept,其他皆失敗。
  2. 所有基於linux epoll機制的伺服器程式在多程序時都受驚群問題的困擾,包括 lighttpd 和 nginx 等程式,各家程式的處理辦法也不一樣。
  3. lighttpd的解決思路:無視驚群。採用Watcher/Workers模式,具體措施有優化fork()與epoll_create()的位置(讓每個子程序自己去
    epoll_create()和epoll_wait()),捕獲accept()丟擲來的錯誤並忽視等。這樣子一來,當有新accept時仍將有多個lighttpd子程序被喚醒。
  4. nginx的解決思路:避免驚群。具體措施有使用全域性互斥鎖,每個子程序在epoll_wait()之前先去申請鎖,申請到則繼續處理,獲取不到則等待,並設定了一個負載均衡的演算法(當某一個子程序的任務量達到總設定量的7/8時,則不會再嘗試去申請鎖)來均衡各個程序的任務量。
  5. 一款國內的優秀商業MTA伺服器程式(不便透露名稱):採用Leader/Followers執行緒模式,各個執行緒地位平等,輪流做Leader來響應請求。
  6. 對比lighttpd和nginx兩套方案,前者實現方便,邏輯簡單,但那部分無謂的程序喚醒帶來的資源浪費的代價如何仍待商榷(有網友測試認為這部分開銷不大 http://www.iteye.com/topic/382107)。後者邏輯較複雜,引入互斥鎖和負載均衡算分也帶來了更多的程式開銷。所以這兩款程式在解決問題的同時,都有其他一部分計算開銷,只是哪一個開銷更大,未有資料對比。
  7. 但其實不然,這篇論文裡提到的改進並未能徹底解決實際生產環境中的驚群問題,因為大多數多程序伺服器程式都是在fork()之後,再對epoll_wait(listen_fd,...)的事件,這樣子當listen_fd有新的accept請求時,程序們還是會被喚醒。論文的改進主要是在核心級別讓accept()成為原子操作,避免被多個程序都呼叫了。
【採用方案】     多方考量,最後選擇參考lighttpd的Watcher/Workers模型,實現了我需要的那款多程序epoll程式,核心流程如下:
  1. 主程序先監聽埠, listen_fd = socket(...); ,setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR,...),setnonblocking(listen_fd),listen(listen_fd,...)。
  2. 開始fork(),到達子程序數上限(建議根據伺服器實際的CPU核數來配置)後,主程序變成一個Watcher,只做子程序維護和訊號處理等全域性性工作。
  3. 每一個子程序(Worker)中,都建立屬於自己的epoll,epoll_fd = epoll_create(...);,接著將listen_fd加入epoll_fd中,然後進入大迴圈,epoll_wait()等待並處理事件。千萬注意,epoll_create()這一步一定要在fork()之後
  4. 大膽設想(未實現):每個Worker程序採用多執行緒方式來提高大迴圈的socket fd處理速度,必要時考慮加入互斥鎖來做同步,但也擔心這樣子得不償失(程序+執行緒頻繁切換帶來的額外作業系統開銷),這一步尚未實現和測試,但看到nginx原始碼中貌似有此邏輯。
【小結】     縱觀現如今的Linux伺服器程式開發(無論是遊戲伺服器/WebServer伺服器/balabala各類應用伺服器),epoll可謂大行其道,當紅炸子雞一枚。它也確實是一個好東西,單程序時的事件處理能力就已經大大強於poll/select,難怪Nginx/Lighttpd等生力軍程式都那麼喜歡它。     但畢竟只有一個程序的話,晾著伺服器的多個CPU實在是罪過,為追求更高的機器利用率更短的請求響應處理時間,還是折騰著搞出了多程序epoll。從新程式在線上伺服器上的表現看,效果也確實不錯 ,開心。。。     感謝諸多網友的帖子分享,現在新程式已經上線,小弟也將心得整理成這篇博文,希望能幫到有需要的童鞋。倉促成文,若有錯漏懇請指正,也請諸位不吝賜教給建議,灰常感謝!