Linux惊群效应详解(最详细的了吧)
https://blog.csdn.net/lyztyycode/article/details/78648798?locationNum=6&fps=1
linux驚群效應(yīng)
詳細(xì)的介紹什么是驚群,驚群在線程和進(jìn)程中的具體表現(xiàn),驚群的系統(tǒng)消耗和驚群的處理方法。1、驚群效應(yīng)是什么?
驚群效應(yīng)也有人叫做雷鳴群體效應(yīng),不過(guò)叫什么,簡(jiǎn)言之,驚群現(xiàn)象就是多進(jìn)程(多線程)在同時(shí)阻塞等待同一個(gè)事件的時(shí)候(休眠狀態(tài)),如果等待的這個(gè)事件發(fā)生,那么他就會(huì)喚醒等待的所有進(jìn)程(或者線程),但是最終卻只可能有一個(gè)進(jìn)程(線程)獲得這個(gè)時(shí)間的“控制權(quán)”,對(duì)該事件進(jìn)行處理,而其他進(jìn)程(線程)獲取“控制權(quán)”失敗,只能重新進(jìn)入休眠狀態(tài),這種現(xiàn)象和性能浪費(fèi)就叫做驚群。 為了更好的理解何為驚群,舉一個(gè)很簡(jiǎn)單的例子,當(dāng)你往一群鴿子中間扔一粒谷子,所有的各自都被驚動(dòng)前來(lái)?yè)寠Z這粒食物,但是最終注定只可能有一個(gè)鴿子滿意的搶到食物,沒(méi)有搶到的鴿子只好回去繼續(xù)睡覺(jué),等待下一粒谷子的到來(lái)。這里鴿子表示進(jìn)程(線程),那粒谷子就是等待處理的事件。看一下:WIKI的雷鳴群體效應(yīng)的解釋
2.驚群效應(yīng)到底消耗了什么?
我想你應(yīng)該也會(huì)有跟我一樣的問(wèn)題,那就是驚群效應(yīng)到底消耗了什么? ? ???(1)、系統(tǒng)對(duì)用戶進(jìn)程/線程頻繁地做無(wú)效的調(diào)度,上下文切換系統(tǒng)性能大打折扣。 (2)、為了確保只有一個(gè)線程得到資源,用戶必須對(duì)資源操作進(jìn)行加鎖保護(hù),進(jìn)一步加大了系統(tǒng)開(kāi)銷。 是不是還是覺(jué)得不夠深入,概念化?看下面: ? ? ???? *1、上下文切換(context? switch)過(guò)高會(huì)導(dǎo)致cpu像個(gè)搬運(yùn)工,頻繁地在寄存器和運(yùn)行隊(duì)列之間奔波,更多的時(shí)間花在了進(jìn)程(線程)切換,而不是在真正工作的進(jìn)程(線程)上面。直接的消耗包括cpu寄存器要保存和加載(例如程序計(jì)數(shù)器)、系統(tǒng)調(diào)度器的代碼需要執(zhí)行。間接的消耗在于多核cache之間的共享數(shù)據(jù)。 看一下:wiki上下文切換 *2、通過(guò)鎖機(jī)制解決驚群效應(yīng)是一種方法,在任意時(shí)刻只讓一個(gè)進(jìn)程(線程)處理等待的事件。但是鎖機(jī)制也會(huì)造成cpu等資源的消耗和性能損耗。目前一些常見(jiàn)的服務(wù)器軟件有的是通過(guò)鎖機(jī)制解決的,比如nginx(它的鎖機(jī)制是默認(rèn)開(kāi)啟的,可以關(guān)閉);還有些認(rèn)為驚群對(duì)系統(tǒng)性能影響不大,沒(méi)有去處理,比如lighttpd。3.驚群效應(yīng)的廬山真面目。
讓我們從進(jìn)程和線程兩個(gè)方面來(lái)揭開(kāi)驚群效應(yīng)的廬山真面目:*1)accept()驚群:
首先讓我們先來(lái)考慮一個(gè)場(chǎng)景: ? ? ? ??主進(jìn)程創(chuàng)建了socket、bind、listen之后,fork()出來(lái)多個(gè)進(jìn)程,每個(gè)子進(jìn)程都開(kāi)始循環(huán)處理(accept)這個(gè)listen_fd。每個(gè)進(jìn)程都阻塞在accept上,當(dāng)一個(gè)新的連接到來(lái)時(shí)候,所有的進(jìn)程都會(huì)被喚醒,但是其中只有一個(gè)進(jìn)程會(huì)接受成功,其余皆失敗,重新休眠。 那么這個(gè)問(wèn)題真的存在嗎? ? ? ? ?歷史上,Linux的accpet確實(shí)存在驚群?jiǎn)栴},但現(xiàn)在的內(nèi)核都解決該問(wèn)題了。即,當(dāng)多個(gè)進(jìn)程/線程都阻塞在對(duì)同一個(gè)socket的接受調(diào)用上時(shí),當(dāng)有一個(gè)新的連接到來(lái),內(nèi)核只會(huì)喚醒一個(gè)進(jìn)程,其他進(jìn)程保持休眠,壓根就不會(huì)被喚醒。 ? ? ?? 不妨寫個(gè)程序測(cè)試一下,眼見(jiàn)為實(shí): fork_thunder_herd.c: [cpp]?view plaincopy很明顯當(dāng)telnet連接的時(shí)候只有一個(gè)進(jìn)程accept成功,你會(huì)不會(huì)和我有同樣的疑問(wèn),就是會(huì)不會(huì)內(nèi)核中喚醒了所有的進(jìn)程只是沒(méi)有獲取到資源失敗了,就好像驚群被“隱藏”?
這個(gè)問(wèn)題很好證明,我們修改一下代碼:
[cpp]?view plaincopy沒(méi)錯(cuò),就是增加了一個(gè)accept失敗的返回信息,按照上面的步驟運(yùn)行,這里我就不截圖了,我只告訴你運(yùn)行結(jié)果與上面的運(yùn)行結(jié)果無(wú)異,增加的失敗信息并沒(méi)有輸出,也就說(shuō)明了這里并沒(méi)有發(fā)生驚群,所以注意阻塞和驚群的喚醒的區(qū)別。
Google了一下:其實(shí)在linux2.6版本以后,linux內(nèi)核已經(jīng)解決了accept()函數(shù)的“驚群”現(xiàn)象,大概的處理方式就是,當(dāng)內(nèi)核接收到一個(gè)客戶連接后,只會(huì)喚醒等待隊(duì)列上的第一個(gè)進(jìn)程(線程),所以如果服務(wù)器采用accept阻塞調(diào)用方式,在最新的linux系統(tǒng)中已經(jīng)沒(méi)有“驚群效應(yīng)”了
accept函數(shù)的驚群解決了,下面來(lái)讓我們看看存在驚群現(xiàn)象的另一種情況:epoll驚群
*2)epoll驚群:
概述:如果多個(gè)進(jìn)程/線程阻塞在監(jiān)聽(tīng)同一個(gè)監(jiān)聽(tīng)socket?fd的epoll_wait上,當(dāng)有一個(gè)新的連接到來(lái)時(shí),所有的進(jìn)程都會(huì)被喚醒。同樣讓我們假設(shè)一個(gè)場(chǎng)景:
主進(jìn)程創(chuàng)建socket,bind,listen后,將該socket加入到epoll中,然后fork出多個(gè)子進(jìn)程,每個(gè)進(jìn)程都阻塞在epoll_wait上,如果有事件到來(lái),則判斷該事件是否是該socket上的事件如果是,說(shuō)明有新的連接到來(lái)了,則進(jìn)行接受操作。為了簡(jiǎn)化處理,忽略后續(xù)的讀寫以及對(duì)接受返回的新的套接字的處理,直接斷開(kāi)連接。
那么,當(dāng)新的連接到來(lái)時(shí),是否每個(gè)阻塞在epoll_wait上的進(jìn)程都會(huì)被喚醒呢?
很多博客中提到,測(cè)試表明雖然epoll_wait不會(huì)像接受那樣只喚醒一個(gè)進(jìn)程/線程,但也不會(huì)把所有的進(jìn)程/線程都喚醒。這究竟是問(wèn)什么呢?看一下:多進(jìn)程epoll和“驚群”
我們還是眼見(jiàn)為實(shí),一步步解決上面的疑問(wèn):
代碼實(shí)例:epoll_thunder_herd.c:
[cpp]?view plaincopy上面的代碼編譯gcc epoll_thunder_herd.c -o server?
一個(gè)終端運(yùn)行代碼 ./server 1234? 另一個(gè)終端telnet 127.0.0.1 1234
運(yùn)行結(jié)果:
這里我們看到只有一個(gè)進(jìn)程返回了,似乎并沒(méi)有驚群效應(yīng),讓我們用strace -f? ./server 8888追蹤執(zhí)行過(guò)程(這里只給出telnet之后的截圖,之前的截圖參考accept,不同的就是進(jìn)程阻塞在epoll_wait)
截圖(部分):
運(yùn)行結(jié)果顯示了部分個(gè)進(jìn)程被喚醒了,返回了“process accept failed”只是后面因?yàn)槟承┰蚴×恕K赃@里貌似存在部分“驚群”。
怎么判斷發(fā)生了驚群呢?
我們根據(jù)strace的返回信息可以確定:
1)系統(tǒng)只會(huì)讓一個(gè)進(jìn)程真正的接受這個(gè)連接,而剩余的進(jìn)程會(huì)獲得一個(gè)EAGAIN信號(hào)。圖中有體現(xiàn)。
2)通過(guò)返回結(jié)果和進(jìn)程執(zhí)行的系統(tǒng)調(diào)用判斷。
這究竟是什么原因?qū)е碌哪?#xff1f;
看我們的代碼,看似部分進(jìn)程被喚醒了,而事實(shí)上其余進(jìn)程沒(méi)有被喚醒的原因是因?yàn)槟硞€(gè)進(jìn)程已經(jīng)處理完這個(gè)事件,無(wú)需喚醒其他進(jìn)程,你可以在epoll獲知這個(gè)事件的時(shí)候sleep(2);這樣所有的進(jìn)程都會(huì)被喚起。看下面改正后的代碼結(jié)果更加清晰:
代碼修改:
[cpp]?view plaincopy運(yùn)行結(jié)果:
如圖所示:所有的進(jìn)程都被喚醒了。所以epoll_wait的驚群確實(shí)存在。
為什么內(nèi)核處理了accept的驚群,卻不處理epoll_wait的驚群呢?
我想,應(yīng)該是這樣的:
accept確實(shí)應(yīng)該只能被一個(gè)進(jìn)程調(diào)用成功,內(nèi)核很清楚這一點(diǎn)。但epoll不一樣,他監(jiān)聽(tīng)的文件描述符,除了可能后續(xù)被accept調(diào)用外,還有可能是其他網(wǎng)絡(luò)IO事件的,而其他IO事件是否只能由一個(gè)進(jìn)程處理,是不一定的,內(nèi)核不能保證這一點(diǎn),這是一個(gè)由用戶決定的事情,例如可能一個(gè)文件會(huì)由多個(gè)進(jìn)程來(lái)讀寫。所以,對(duì)epoll的驚群,內(nèi)核則不予處理。
*3)線程驚群:
進(jìn)程的驚群已經(jīng)介紹的很詳細(xì)了,這里我就舉一個(gè)線程驚群的簡(jiǎn)單例子,我就截取上次紅包代碼中的代碼片段,如下 [cpp]?view plaincopy4.我們?cè)趺唇鉀Q“驚群”呢?你有什么高見(jiàn)?
這里通常代碼加鎖的處理機(jī)制我就不詳述了,來(lái)看一下常見(jiàn)軟件的處理機(jī)制和linux最新的避免和解決的辦法(1)、Nginx的解決:
如上所述,如果采用epoll,則仍然存在該問(wèn)題,nginx就是這種場(chǎng)景的一個(gè)典型,我們接下來(lái)看看其具體的處理方法。 nginx的每個(gè)worker進(jìn)程都會(huì)在函數(shù)ngx_process_events_and_timers()中處理不同的事件,然后通過(guò)ngx_process_events()封裝了不同的事件處理機(jī)制,在Linux上默認(rèn)采用epoll_wait()。 在主要ngx_process_events_and_timers()函數(shù)中解決驚群現(xiàn)象。 [cpp]?view plaincopy(2)、SO_REUSEPORT
Linux內(nèi)核的3.9版本帶來(lái)了SO_REUSEPORT特性,該特性支持多個(gè)進(jìn)程或者線程綁定到同一端口,提高服務(wù)器程序的性能,允許多個(gè)套接字bind()以及l(fā)isten()同一個(gè)TCP或UDP端口,并且在內(nèi)核層面實(shí)現(xiàn)負(fù)載均衡。
在未開(kāi)啟SO_REUSEPORT的時(shí)候,由一個(gè)監(jiān)聽(tīng)socket將新接收的連接請(qǐng)求交給各個(gè)工作者處理,看圖示:
下面讓我們好好比較一下多進(jìn)程(線程)服務(wù)器編程傳統(tǒng)方法和使用SO_REUSEPORT的區(qū)別
運(yùn)行在Linux系統(tǒng)上的網(wǎng)絡(luò)應(yīng)用程序,為了利用多核的優(yōu)勢(shì),一般使用以下典型的多進(jìn)程(多線程)服務(wù)器模型:
1.單線程listener/accept,多個(gè)工作線程接受任務(wù)分發(fā),雖然CPU工作負(fù)載不再成為問(wèn)題,但是仍然存在問(wèn)題:
? ? ?? (1)、單線程listener(圖一),在處理高速率海量連接的時(shí)候,一樣會(huì)成為瓶頸
? ? ? ? (2)、cpu緩存行丟失套接字結(jié)構(gòu)現(xiàn)象嚴(yán)重。
2.所有工作線程都accept()在同一個(gè)服務(wù)器套接字上呢?一樣存在問(wèn)題:
? ? ? ? (1)、多線程訪問(wèn)server socket鎖競(jìng)爭(zhēng)嚴(yán)重。
? ? ? ? (2)、高負(fù)載情況下,線程之間的處理不均衡,有時(shí)高達(dá)3:1。
? ? ? ? (3)、導(dǎo)致cpu緩存行跳躍(cache line bouncing)。
? ? ? ? (4)、在繁忙cpu上存在較大延遲。
上面兩種方法共同點(diǎn)就是很難做到cpu之間的負(fù)載均衡,隨著核數(shù)的提升,性能并沒(méi)有提升。甚至服務(wù)器的吞吐量CPS(Connection Per Second)會(huì)隨著核數(shù)的增加呈下降趨勢(shì)。
下面我們就來(lái)看看SO_REUSEPORT解決了什么問(wèn)題:
? ? ? ? (1)、允許多個(gè)套接字bind()/listen()同一個(gè)tcp/udp端口。每一個(gè)線程擁有自己的服務(wù)器套接字,在服務(wù)器套接字上沒(méi)有鎖的競(jìng)爭(zhēng)。
? ? ? ? (2)、內(nèi)核層面實(shí)現(xiàn)負(fù)載均衡
? ? ? ? (3)、安全層面,監(jiān)聽(tīng)同一個(gè)端口的套接字只能位于同一個(gè)用戶下面。
? ? ? ? (4)、處理新建連接時(shí),查找listener的時(shí)候,能夠支持在監(jiān)聽(tīng)相同IP和端口的多個(gè)sock之間均衡選擇。
當(dāng)一個(gè)連接到來(lái)的時(shí)候,系統(tǒng)到底是怎么決定那個(gè)套接字來(lái)處理它?
對(duì)于不同內(nèi)核,存在兩種模式,這兩種模式并不共存,一種叫做熱備份模式,另一種叫做負(fù)載均衡模式,3.9內(nèi)核以后,全部改為負(fù)載均衡模式。
熱備份模式:一般而言,會(huì)將所有的reuseport同一個(gè)IP地址/端口的套接字掛在一個(gè)鏈表上,取第一個(gè)即可,工作的只有一個(gè),其他的作為備份存在,如果該套接字掛了,它會(huì)被從鏈表刪除,然后第二個(gè)便會(huì)成為第一個(gè)。
負(fù)載均衡模式:和熱備份模式一樣,所有reuseport同一個(gè)IP地址/端口的套接字會(huì)掛在一個(gè)鏈表上,你也可以認(rèn)為是一個(gè)數(shù)組,這樣會(huì)更加方便,當(dāng)有連接到來(lái)時(shí),用數(shù)據(jù)包的源IP/源端口作為一個(gè)HASH函數(shù)的輸入,將結(jié)果對(duì)reuseport套接字?jǐn)?shù)量取模,得到一個(gè)索引,該索引指示的數(shù)組位置對(duì)應(yīng)的套接字便是工作套接字。這樣就可以達(dá)到負(fù)載均衡的目的,從而降低某個(gè)服務(wù)的壓力。
參考資料:https://pureage.info/2015/12/22/thundering-herd.htmlhttp://www.tuicool.com/articles/2aumqehttp://blog.163.com/leyni@126/blog/static/16223010220122611523786/http://baike.baidu.com/link?url=6x0zTazmBxTYE9ngPt_boKjS8ivdQnRlfhHj-STCnqG9tjKwfCluPsKlq-ASUkdQTPW3XrD8FtyilBaI75GJCKhttp://m.blog.csdn.net/tuantuanls/article/details/41205739tcp對(duì)so_reuseport的優(yōu)化?
總結(jié)
以上是生活随笔為你收集整理的Linux惊群效应详解(最详细的了吧)的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 2022年世界杯赛程进行到哪了?
- 下一篇: Linux C++ 回射服务器