導語: 對不起,我是標題黨,本文解決的不是我們理解的「驚群」效應,先為我們操作系統組的正下名,因為騰訊伺服器的內核版本,已經解決epoll模式下的驚群現象(本文描述的現象跟驚群其實基本一致)。接下來容我詳細道來這個是什麼形式的「驚群」效應並如何解決。
緣起
最近很無聊,突然登錄線上的某台機器,發現服務進程的CPU佔用率很不一樣,詳細如下圖:
為什麼會出現這種情況呢?那到底會不會造成線上服務不穩定。然後自己通過webbench壓測了一波,發現所有請求都很正常,一個請求都沒丟失,而且時延也非常完成。好吧,心頭大石放下,還以為還要背個事故呢。
目前調度不均衡的情況,是請求量比較少導致的。猜想,如果我把請求量壓上來,進程的調度均衡情況會不會被改善呢?再壓測了一波,把所有進程都壓到一個相對高的CPU佔用率,竟然發現各個進程的調度情況均衡了,但還是有一些差異。
猜測
然後自己一直糾結著是不是因為Linux的驚群導致。先分析下系統的內核版本,本來測試的機器是前段時間才重裝的系統,應該已經解決了驚群的了啊。咋一看,內核版本已經是修復了驚群現象版本(3.10>2.6)。
驚群簡單來說就是多個進程或者線程在等待同一個事件,當事件發生時,所有線程和進程都會被內核喚醒。喚醒後通常只有一個進程獲得了該事件並進行處理,其他進程發現獲取事件失敗後又繼續進入了等待狀態,在一定程度上降低了系統性能。
好吧,本來想甩下鍋給操作系統組的,直接去問操作系統組為什麼會出現驚群的,看來還是保留點自尊吧。
那,到底是什麼原因導致的呢?先strace看看某個進程什麼情況:
怎麼進程會accept失敗了……這不科學啊,壓測工具的響應也是正常的,線上也沒人反饋過出問題,應該這個是正常的邏輯。
分析
進程為啥會出現競爭效應呢?先看看所有進程的情況:
同一個設備id?所有進程都在同一個隊列競爭資源。分析了下服務的代碼創建多進程,具體流程是這樣的:
這種方式創建多進程,在父進程創建完socket以後才fork出來,內核肯定clone同一個設備id啊。那為什麼同一個設備id就會導致資源分配不均衡呢?下面我們分析下:
首先,進程epoll模式是設置了LT模式,LT模式下,每接收一個請求,內核都會喚醒進程進行接收。然而,所有子進程都是共享一個設備id,換句話來說,只能由一個進程把請求讀出並處理,其他進程只是一個空轉狀態。因此,就會出現上面各個子進程的調度不均衡的情況,其實,這種情況我自己認為也是驚群效應,所有服務進程都被驚醒,但是accept出來是EAGAIN。但具體為什麼都是某個進程佔用的CPU更高,這個應該是由內核決定,具體原因我也不太清楚。
好吧,稍微修改一下:
fork();
create_listen_socket();
loop();
不幸的是,這樣是啟動不起來的!
詳細分析了下:socket創建的時候,設置了REUSEADDR,所以原來創建多進程的,是能啟動起來,因為埠公用了一個設備id。但是先fork在createsocket,埠被佔用了,那能不能設置REUSEPORT。兩者的區別,網上稍微搜一下就有相關資料了,具體差別是在於:SO_REUSEADDR主要改變了系統對待通配符IP地址衝突的方式,而SO_REUSEPORT允許將任意數目的socket綁定到完全相同的源地址埠對上。
然後嘗試設置REUSEPORT參數,結果也是不盡人意,編譯出錯。
原來,REUSEPORT是只有在3.9以上的內核版本才支持,我的開發機是2.6,應該不支持這次編譯。
繼續深挖
好吧,問題還是不能解決,請教了一些操作系統組的高手,建議使用ET模式去解決一下這個驚群效應。修改了框架代碼,編譯了一個ET模式的服務進程,進行壓測,webbench丟包非常嚴重,1000/s的包只能處理幾個。ET模式是不是不通用呢?我們先看下ET的具體說明:
ET模式下accept存在的問題,考慮這種情況:多個連接同時到達,伺服器的TCP就緒隊列瞬間積累多個就緒連接,由於是邊緣觸發模式,epoll只會通知一次,accept只處理一個連接,導致TCP就緒隊列中剩下的連接都得不到處理。
這樣就明了了,多個請求同時喚醒一個進程,而我的accept是沒有循環處理的, ET邊緣觸發導致多個請求操作系統只通知了一次,而邏輯才進行了一次處理,所以導致隊列的包沒被接收處理,從而導致丟包。
接下來改造系統,ET模式下,循環accept。OK,請求都被accept成功,但是,還是會觸發EAGAIN,而且多進程之間也是調度不均衡的。(https://blog.csdn.net/dog250/article/details/80837278裡面是某大神總結的,是可以通過ET模式解決LT模式的驚群現象,但是我把代碼編譯測試了一遍,確實還是會觸發驚群,但頻率沒那麼高,估計大神的測試代碼是因為業務邏輯是空的原因導致)。
峰迴路轉
什麼ET/LT模式都嘗試過了,還是解決不了多進程之前調度不均衡的問題,想著反正不影響(原理上來說,空轉情況下確實是會浪費點CPU),準備想放棄。但最後還是想嘗試一下,把socket句柄設置成15(SO_REUSEPORT=15),自己定義了一個宏,然後修改了fork邏輯:
fork(); socket = create_listen_socket(); set_sock_opt(socket,SO_REUSERPORT); loop();
啟動進程,成功了!但是具體原因是為啥,機器的操作系統是不支持SO_REUSEPORT的,問下了操作系統的同事,給到的答覆是目前的操作系統是打了上游的patch。再細問一下,標準庫的頭文件也沒有SO_REUSEPORT的定義。給到的答覆是頭文件和內核不同步。好吧,其實我很不願意接受了這個答覆。最後的一個問題,那這樣我如何確保我的所有機器是否支持SO_REUSEPORT,給到的答覆是只能測試了。
經過一輪發布,發現所有機器都支持這個參數,而且進程已經支持了多進程之間的調度均衡。
另外,通過fork的順序,也確認了每個進程管理自身的設備id,也不會出現驚群現象(不會再出現accept EAGAIN),原因是REUSEPORT,偵聽同一個IP地址埠對的多個socket本身在socket層就是相互隔離的,在它們之間的事件分發是TCP/IP協議棧完成的,所以不會再有驚群發生。
寫在最後
多進程情況下,都建議使用REUSEPORT,就不會出現那麼多不穩定的問題。
TAG:信息技術(IT) | 科技 |