解决多进程模式下引起的“惊群”效应
導(dǎo)語:?對不起,我是標(biāo)題黨,本文解決的不是我們理解的“驚群”效應(yīng),先為我們操作系統(tǒng)組的正下名,因為騰訊服務(wù)器的內(nèi)核版本,已經(jīng)解決epoll模式下的驚群現(xiàn)象(本文描述的現(xiàn)象跟驚群其實基本一致)。接下來容我詳細(xì)道來這個是什么形式的“驚群”效應(yīng)并如何解決。
最近很無聊,突然登錄線上的某臺機(jī)器,發(fā)現(xiàn)服務(wù)進(jìn)程的CPU占用率很不一樣,詳細(xì)如下圖:
為什么會出現(xiàn)這種情況呢?那到底會不會造成線上服務(wù)不穩(wěn)定。然后自己通過webbench壓測了一波,發(fā)現(xiàn)所有請求都很正常,一個請求都沒丟失,而且時延也非常完成。好吧,心頭大石放下,還以為還要背個事故呢。
目前調(diào)度不均衡的情況,是請求量比較少導(dǎo)致的。猜想,如果我把請求量壓上來,進(jìn)程的調(diào)度均衡情況會不會被改善呢?再壓測了一波,把所有進(jìn)程都壓到一個相對高的CPU占用率,竟然發(fā)現(xiàn)各個進(jìn)程的調(diào)度情況均衡了,但還是有一些差異。
然后自己一直糾結(jié)著是不是因為Linux的驚群導(dǎo)致。先分析下系統(tǒng)的內(nèi)核版本,本來測試的機(jī)器是前段時間才重裝的系統(tǒng),應(yīng)該已經(jīng)解決了驚群的了啊。咋一看,內(nèi)核版本已經(jīng)是修復(fù)了驚群現(xiàn)象版本(3.10>2.6)。
驚群簡單來說就是多個進(jìn)程或者線程在等待同一個事件,當(dāng)事件發(fā)生時,所有線程和進(jìn)程都會被內(nèi)核喚醒。喚醒后通常只有一個進(jìn)程獲得了該事件并進(jìn)行處理,其他進(jìn)程發(fā)現(xiàn)獲取事件失敗后又繼續(xù)進(jìn)入了等待狀態(tài),在一定程度上降低了系統(tǒng)性能。
好吧,本來想甩下鍋給操作系統(tǒng)組的,直接去問操作系統(tǒng)組為什么會出現(xiàn)驚群的,看來還是保留點自尊吧。
那,到底是什么原因?qū)е碌哪?#xff1f;先strace看看某個進(jìn)程什么情況:?
怎么進(jìn)程會accept失敗了……這不科學(xué)啊,壓測工具的響應(yīng)也是正常的,線上也沒人反饋過出問題,應(yīng)該這個是正常的邏輯。
進(jìn)程為啥會出現(xiàn)競爭效應(yīng)呢?先看看所有進(jìn)程的情況:
同一個設(shè)備id?所有進(jìn)程都在同一個隊列競爭資源。分析了下服務(wù)的代碼創(chuàng)建多進(jìn)程,具體流程是這樣的:
這種方式創(chuàng)建多進(jìn)程,在父進(jìn)程創(chuàng)建完socket以后才fork出來,內(nèi)核肯定clone同一個設(shè)備id啊。那為什么同一個設(shè)備id就會導(dǎo)致資源分配不均衡呢?下面我們分析下:
首先,進(jìn)程epoll模式是設(shè)置了LT模式,LT模式下,每接收一個請求,內(nèi)核都會喚醒進(jìn)程進(jìn)行接收。然而,所有子進(jìn)程都是共享一個設(shè)備id,換句話來說,只能由一個進(jìn)程把請求讀出并處理,其他進(jìn)程只是一個空轉(zhuǎn)狀態(tài)。因此,就會出現(xiàn)上面各個子進(jìn)程的調(diào)度不均衡的情況,其實,這種情況我自己認(rèn)為也是驚群效應(yīng),所有服務(wù)進(jìn)程都被驚醒,但是accept出來是EAGAIN。但具體為什么都是某個進(jìn)程占用的CPU更高,這個應(yīng)該是由內(nèi)核決定,具體原因我也不太清楚。
好吧,稍微修改一下:
fork();
create_listen_socket();
loop();
?
不幸的是,這樣是啟動不起來的!
詳細(xì)分析了下:socket創(chuàng)建的時候,設(shè)置了REUSEADDR,所以原來創(chuàng)建多進(jìn)程的,是能啟動起來,因為端口公用了一個設(shè)備id。但是先fork在createsocket,端口被占用了,那能不能設(shè)置REUSEPORT。兩者的區(qū)別,網(wǎng)上稍微搜一下就有相關(guān)資料了,具體差別是在于:SO_REUSEADDR主要改變了系統(tǒng)對待通配符IP地址沖突的方式,而SO_REUSEPORT允許將任意數(shù)目的socket綁定到完全相同的源地址端口對上。
然后嘗試設(shè)置REUSEPORT參數(shù),結(jié)果也是不盡人意,編譯出錯。
?原來,REUSEPORT是只有在3.9以上的內(nèi)核版本才支持,我的開發(fā)機(jī)是2.6,應(yīng)該不支持這次編譯。
好吧,問題還是不能解決,請教了一些操作系統(tǒng)組的高手,建議使用ET模式去解決一下這個驚群效應(yīng)。修改了框架代碼,編譯了一個ET模式的服務(wù)進(jìn)程,進(jìn)行壓測,webbench丟包非常嚴(yán)重,1000/s的包只能處理幾個。ET模式是不是不通用呢?我們先看下ET的具體說明:
ET模式下accept存在的問題,考慮這種情況:多個連接同時到達(dá),服務(wù)器的TCP就緒隊列瞬間積累多個就緒連接,由于是邊緣觸發(fā)模式,epoll只會通知一次,accept只處理一個連接,導(dǎo)致TCP就緒隊列中剩下的連接都得不到處理。
解決辦法是用while循環(huán)抱住accept調(diào)用,處理完TCP就緒隊列中的所有連接后再退出循環(huán)。如何知道是否處理完就緒隊列中的所有連接呢?accept返回-1并且errno設(shè)置為EAGAIN就表示所有連接都處理完。
這樣就明了了,多個請求同時喚醒一個進(jìn)程,而我的accept是沒有循環(huán)處理的,?ET邊緣觸發(fā)導(dǎo)致多個請求操作系統(tǒng)只通知了一次,而邏輯才進(jìn)行了一次處理,所以導(dǎo)致隊列的包沒被接收處理,從而導(dǎo)致丟包。
接下來改造系統(tǒng),ET模式下,循環(huán)accept。OK,請求都被accept成功,但是,還是會觸發(fā)EAGAIN,而且多進(jìn)程之間也是調(diào)度不均衡的。(https://blog.csdn.net/dog250/article/details/80837278里面是某大神總結(jié)的,是可以通過ET模式解決LT模式的驚群現(xiàn)象,但是我把代碼編譯測試了一遍,確實還是會觸發(fā)驚群,但頻率沒那么高,估計大神的測試代碼是因為業(yè)務(wù)邏輯是空的原因?qū)е?#xff09;。
?什么ET/LT模式都嘗試過了,還是解決不了多進(jìn)程之前調(diào)度不均衡的問題,想著反正不影響(原理上來說,空轉(zhuǎn)情況下確實是會浪費(fèi)點CPU),準(zhǔn)備想放棄。但最后還是想嘗試一下,把socket句柄設(shè)置成15(SO_REUSEPORT=15),自己定義了一個宏,然后修改了fork邏輯:
fork();
socket = create_listen_socket();
set_sock_opt(socket,SO_REUSERPORT);
loop();
?
啟動進(jìn)程,成功了!但是具體原因是為啥,機(jī)器的操作系統(tǒng)是不支持SO_REUSEPORT的,問下了操作系統(tǒng)的同事,給到的答復(fù)是目前的操作系統(tǒng)是打了上游的patch。再細(xì)問一下,標(biāo)準(zhǔn)庫的頭文件也沒有SO_REUSEPORT的定義。給到的答復(fù)是頭文件和內(nèi)核不同步。好吧,其實我很不愿意接受了這個答復(fù)。最后的一個問題,那這樣我如何確保我的所有機(jī)器是否支持SO_REUSEPORT,給到的答復(fù)是只能測試了。
經(jīng)過一輪發(fā)布,發(fā)現(xiàn)所有機(jī)器都支持這個參數(shù),而且進(jìn)程已經(jīng)支持了多進(jìn)程之間的調(diào)度均衡。
另外,通過fork的順序,也確認(rèn)了每個進(jìn)程管理自身的設(shè)備id,也不會出現(xiàn)驚群現(xiàn)象(不會再出現(xiàn)accept EAGAIN),原因是REUSEPORT,偵聽同一個IP地址端口對的多個socket本身在socket層就是相互隔離的,在它們之間的事件分發(fā)是TCP/IP協(xié)議棧完成的,所以不會再有驚群發(fā)生。
多進(jìn)程情況下,都建議使用REUSEPORT,就不會出現(xiàn)那么多不穩(wěn)定的問題。
總結(jié)
以上是生活随笔為你收集整理的解决多进程模式下引起的“惊群”效应的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 游戏AI探索之旅:从AlphaGo到MO
- 下一篇: 清华大学团队与腾讯AI Lab专项合作夺