epoll在多程序下產生的“驚群”現象——如何避免——多程序因為檔案描述符繼承問題導致
阿新 • • 發佈:2019-01-05
【遇到問題】
手頭原來有一個單程序的linux epoll伺服器程式,近來希望將它改寫成多程序版本,主要原因有:
這樣子“驚群”現象必然造成資源浪費,那有木有好的解決辦法呢?
【尋找辦法】
看了網上N多帖子和網頁,閱讀多款優秀開源程式的原始碼,再結合自己的實驗測試,總結如下:
- 在服務高峰期間 併發的 網路請求非常海量,目前的單程序版本的程式有點吃不消:單程序時只有一個迴圈先後處理epoll_wait()到的事件,使得某些不幸排隊靠後的socket fd的網路事件處理不及時(擔心有些socket客戶端等不耐煩而超時斷開);
- 希望充分利用到伺服器的多顆CPU;
- 主程序先監聽埠, listen_fd = socket(...);
- 建立epoll,epoll_fd = epoll_create(...);
- 然後開始fork(),每個子程序進入大迴圈,去等待new accept,epoll_wait(...),處理事件等。
- 實際情況中,在發生驚群時,並非全部子程序都會被喚醒,而是一部分子程序被喚醒。但被喚醒的程序仍然只有1個成功accept,其他皆失敗。
- 所有基於linux epoll機制的伺服器程式在多程序時都受驚群問題的困擾,包括 lighttpd 和 nginx 等程式,各家程式的處理辦法也不一樣。
- lighttpd的解決思路:無視驚群。採用Watcher/Workers模式,具體措施有優化fork()與epoll_create()的位置(讓每個子程序自己去
- nginx的解決思路:避免驚群。具體措施有使用全域性互斥鎖,每個子程序在epoll_wait()之前先去申請鎖,申請到則繼續處理,獲取不到則等待,並設定了一個負載均衡的演算法(當某一個子程序的任務量達到總設定量的7/8時,則不會再嘗試去申請鎖)來均衡各個程序的任務量。
- 一款國內的優秀商業MTA伺服器程式(不便透露名稱):採用Leader/Followers執行緒模式,各個執行緒地位平等,輪流做Leader來響應請求。
- 對比lighttpd和nginx兩套方案,前者實現方便,邏輯簡單,但那部分無謂的程序喚醒帶來的資源浪費的代價如何仍待商榷(有網友測試認為這部分開銷不大 http://www.iteye.com/topic/382107)。後者邏輯較複雜,引入互斥鎖和負載均衡算分也帶來了更多的程式開銷。所以這兩款程式在解決問題的同時,都有其他一部分計算開銷,只是哪一個開銷更大,未有資料對比。
- 但其實不然,這篇論文裡提到的改進並未能徹底解決實際生產環境中的驚群問題,因為大多數多程序伺服器程式都是在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原始碼中貌似有此邏輯。