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. 坊間也流傳Linux 2.6.x之后的內核,就已經解決了accept的驚群問題,論文地址 http://static.usenix.org/event/usenix2000/freenix/full_papers/molloy/molloy.pdf 。
  8. 但其實不然,這篇論文里提到的改進並未能徹底解決實際生產環境中的驚群問題,因為大多數多進程服務器程序都是在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。從新程序在線上服務器上的表現看,效果也確實不錯 ,開心。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM