遇到問(wèn)題】
? ? 手頭原來(lái)有一個(gè)單進(jìn)程的linux epoll服務(wù)器程序,近來(lái)希望將它改寫(xiě)成多進(jìn)程版本,主要原因有:
- 在服務(wù)高峰期間 并發(fā)的 網(wǎng)絡(luò)請(qǐng)求非常海量,目前的單進(jìn)程版本的程序有點(diǎn)吃不消:?jiǎn)芜M(jìn)程時(shí)只有一個(gè)循環(huán)先后處理epoll_wait()到的事件,使得某些不幸排隊(duì)靠后的socket fd的網(wǎng)絡(luò)事件處理不及時(shí)(擔(dān)心有些socket客戶端等不耐煩而超時(shí)斷開(kāi));
- 希望充分利用到服務(wù)器的多顆CPU;
?
? ? 但隨著改寫(xiě)工作的深入,便第一次碰到了“驚群”問(wèn)題,一開(kāi)始我的程序設(shè)想如下:
- 主進(jìn)程先監(jiān)聽(tīng)端口, listen_fd = socket(...);
- 創(chuàng)建epoll,epoll_fd =?epoll_create(...);
- 然后開(kāi)始fork(),每個(gè)子進(jìn)程進(jìn)入大循環(huán),去等待new??accept,epoll_wait(...),處理事件等。
?
? ? 接著就遇到了“驚群”現(xiàn)象:當(dāng)listen_fd有新的accept()請(qǐng)求過(guò)來(lái),操作系統(tǒng)會(huì)喚醒所有子進(jìn)程(因?yàn)檫@些進(jìn)程都epoll_wait()同一個(gè)listen_fd,操作系統(tǒng)又無(wú)從判斷由誰(shuí)來(lái)負(fù)責(zé)accept,索性干脆全部叫醒……),但最終只會(huì)有一個(gè)進(jìn)程成功accept,其他進(jìn)程accept失敗。外國(guó)IT友人認(rèn)為所有子進(jìn)程都是被“嚇醒”的,所以稱之為Thundering Herd(驚群)。
? ??打個(gè)比方,街邊有一家麥當(dāng)勞餐廳,里面有4個(gè)服務(wù)小窗口,每個(gè)窗口各有一名服務(wù)員。當(dāng)大門(mén)口進(jìn)來(lái)一位新客人,“歡迎光臨!”餐廳大門(mén)的感應(yīng)式門(mén)鈴自動(dòng)響了(相當(dāng)于操作系統(tǒng)底層捕抓到了一個(gè)網(wǎng)絡(luò)事件),于是4個(gè)服務(wù)員都抬起頭(相當(dāng)于操作系統(tǒng)喚醒了所有服務(wù)進(jìn)程)希望將客人招呼過(guò)去自己所在的服務(wù)窗口。但結(jié)果可想而知,客人最終只會(huì)走向其中某一個(gè)窗口,而其他3個(gè)窗口的服務(wù)員只能“失望嘆息”(這一聲無(wú)奈的嘆息就相當(dāng)于accept()返回EAGAIN錯(cuò)誤),然后埋頭繼續(xù)忙自己的事去。
? ? 這樣子“驚群”現(xiàn)象必然造成資源浪費(fèi),那有木有好的解決辦法呢?
?
?
【尋找辦法】
? ? 看了網(wǎng)上N多帖子和網(wǎng)頁(yè),閱讀多款優(yōu)秀開(kāi)源程序的源代碼,再結(jié)合自己的實(shí)驗(yàn)測(cè)試,總結(jié)如下:
- ?實(shí)際情況中,在發(fā)生驚群時(shí),并非全部子進(jìn)程都會(huì)被喚醒,而是一部分子進(jìn)程被喚醒。但被喚醒的進(jìn)程仍然只有1個(gè)成功accept,其他皆失敗。
- 所有基于linux epoll機(jī)制的服務(wù)器程序在多進(jìn)程時(shí)都受驚群?jiǎn)栴}的困擾,包括 lighttpd 和 nginx 等程序,各家程序的處理辦法也不一樣。
- lighttpd的解決思路:無(wú)視驚群。采用Watcher/Workers模式,具體措施有優(yōu)化fork()與epoll_create()的位置(讓每個(gè)子進(jìn)程自己去epoll_create()和epoll_wait()),捕獲accept()拋出來(lái)的錯(cuò)誤并忽視等。這樣子一來(lái),當(dāng)有新accept時(shí)仍將有多個(gè)lighttpd子進(jìn)程被喚醒。
- nginx的解決思路:避免驚群。具體措施有使用全局互斥鎖,每個(gè)子進(jìn)程在epoll_wait()之前先去申請(qǐng)鎖,申請(qǐng)到則繼續(xù)處理,獲取不到則等待,并設(shè)置了一個(gè)負(fù)載均衡的算法(當(dāng)某一個(gè)子進(jìn)程的任務(wù)量達(dá)到總設(shè)置量的7/8時(shí),則不會(huì)再嘗試去申請(qǐng)鎖)來(lái)均衡各個(gè)進(jìn)程的任務(wù)量。
- 一款國(guó)內(nèi)的優(yōu)秀商業(yè)MTA服務(wù)器程序(不便透露名稱):采用Leader/Followers線程模式,各個(gè)線程地位平等,輪流做Leader來(lái)響應(yīng)請(qǐng)求。
- 對(duì)比lighttpd和nginx兩套方案,前者實(shí)現(xiàn)方便,邏輯簡(jiǎn)單,但那部分無(wú)謂的進(jìn)程喚醒帶來(lái)的資源浪費(fèi)的代價(jià)如何仍待商榷(有網(wǎng)友測(cè)試認(rèn)為這部分開(kāi)銷(xiāo)不大 http://www./topic/382107)。后者邏輯較復(fù)雜,引入互斥鎖和負(fù)載均衡算分也帶來(lái)了更多的程序開(kāi)銷(xiāo)。所以這兩款程序在解決問(wèn)題的同時(shí),都有其他一部分計(jì)算開(kāi)銷(xiāo),只是哪一個(gè)開(kāi)銷(xiāo)更大,未有數(shù)據(jù)對(duì)比。
- 坊間也流傳Linux 2.6.x之后的內(nèi)核,就已經(jīng)解決了accept的驚群?jiǎn)栴},論文地址?http://static./event/usenix2000/freenix/full_papers/molloy/molloy.pdf?。
- 但其實(shí)不然,這篇論文里提到的改進(jìn)并未能徹底解決實(shí)際生產(chǎn)環(huán)境中的驚群?jiǎn)栴},因?yàn)榇蠖鄶?shù)多進(jìn)程服務(wù)器程序都是在fork()之后,再對(duì)epoll_wait(listen_fd,...)的事件,這樣子當(dāng)listen_fd有新的accept請(qǐng)求時(shí),進(jìn)程們還是會(huì)被喚醒。論文的改進(jìn)主要是在內(nèi)核級(jí)別讓accept()成為原子操作,避免被多個(gè)進(jìn)程都調(diào)用了。
?
【采用方案】
? ? 多方考量,最后選擇參考lighttpd的Watcher/Workers模型,實(shí)現(xiàn)了我需要的那款多進(jìn)程epoll程序,核心流程如下:
- 主進(jìn)程先監(jiān)聽(tīng)端口, listen_fd = socket(...); ,setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR,...),setnonblocking(listen_fd),listen(listen_fd,...)。
- 開(kāi)始fork(),到達(dá)子進(jìn)程數(shù)上限(建議根據(jù)服務(wù)器實(shí)際的CPU核數(shù)來(lái)配置)后,主進(jìn)程變成一個(gè)Watcher,只做子進(jìn)程維護(hù)和信號(hào)處理等全局性工作。
- 每一個(gè)子進(jìn)程(Worker)中,都創(chuàng)建屬于自己的epoll,epoll_fd =?epoll_create(...);,接著將listen_fd加入epoll_fd中,然后進(jìn)入大循環(huán),epoll_wait()等待并處理事件。千萬(wàn)注意,epoll_create()這一步一定要在fork()之后。
- 大膽設(shè)想(未實(shí)現(xiàn)):每個(gè)Worker進(jìn)程采用多線程方式來(lái)提高大循環(huán)的socket fd處理速度,必要時(shí)考慮加入互斥鎖來(lái)做同步,但也擔(dān)心這樣子得不償失(進(jìn)程 線程頻繁切換帶來(lái)的額外操作系統(tǒng)開(kāi)銷(xiāo)),這一步尚未實(shí)現(xiàn)和測(cè)試,但看到nginx源碼中貌似有此邏輯。
【小結(jié)】
? ? 縱觀現(xiàn)如今的Linux服務(wù)器程序開(kāi)發(fā)(無(wú)論是游戲服務(wù)器/WebServer服務(wù)器/balabala各類(lèi)應(yīng)用服務(wù)器),epoll可謂大行其道,當(dāng)紅炸子雞一枚。它也確實(shí)是一個(gè)好東西,單進(jìn)程時(shí)的事件處理能力就已經(jīng)大大強(qiáng)于poll/select,難怪Nginx/Lighttpd等生力軍程序都那么喜歡它。
? ? 但畢竟只有一個(gè)進(jìn)程的話,晾著服務(wù)器的多個(gè)CPU實(shí)在是罪過(guò),為追求更高的機(jī)器利用率和更短的請(qǐng)求響應(yīng)處理時(shí)間,還是折騰著搞出了多進(jìn)程epoll。
來(lái)源:http://www./content-3-104601.html
|