Java IO復(fù)用_動(dòng)力節(jié)點(diǎn)Java學(xué)院整理
對(duì)于服務(wù)器的并發(fā)處理能力,我們需要的是:每一毫秒服務(wù)器都能及時(shí)處理這一毫秒內(nèi)收到的數(shù)百個(gè)不同TCP連接上的報(bào)文,與此同時(shí),可能服務(wù)器上還有數(shù)以十萬(wàn)計(jì)的最近幾秒沒(méi)有收發(fā)任何報(bào)文的相對(duì)不活躍連接。同時(shí)處理多個(gè)并行發(fā)生事件的連接,簡(jiǎn)稱為并發(fā);同時(shí)處理萬(wàn)計(jì)、十萬(wàn)計(jì)的連接,則是高并發(fā)。服務(wù)器的并發(fā)編程所追求的就是處理的并發(fā)連接數(shù)目無(wú)限大,同時(shí)維持著高效率使用CPU等資源,直至物理資源首先耗盡。
并發(fā)編程有很多種實(shí)現(xiàn)模型,最簡(jiǎn)單的就是與“線程”捆綁,1個(gè)線程處理1個(gè)連接的全部生命周期。優(yōu)點(diǎn):這個(gè)模型足夠簡(jiǎn)單,它可以實(shí)現(xiàn)復(fù)雜的業(yè)務(wù)場(chǎng)景,同時(shí),線程個(gè)數(shù)是可以遠(yuǎn)大于CPU個(gè)數(shù)的。然而,線程個(gè)數(shù)又不是可以無(wú)限增大的,為什么呢?因?yàn)榫€程什么時(shí)候執(zhí)行是由操作系統(tǒng)內(nèi)核調(diào)度算法決定的,調(diào)度算法并不會(huì)考慮某個(gè)線程可能只是為了一個(gè)連接服務(wù)的,它會(huì)做大一統(tǒng)的玩法:時(shí)間片到了就執(zhí)行一下,哪怕這個(gè)線程一執(zhí)行就會(huì)不得不繼續(xù)睡眠。這樣來(lái)回的喚醒、睡眠線程在次數(shù)不多的情況下,是廉價(jià)的,但如果操作系統(tǒng)的線程總數(shù)很多時(shí),它就是昂貴的(被放大了),因?yàn)檫@種技術(shù)性的調(diào)度損耗會(huì)影響到線程上執(zhí)行的業(yè)務(wù)代碼的時(shí)間。舉個(gè)例子,這時(shí)大部分擁有不活躍連接的線程就像我們的國(guó)企,它們執(zhí)行效率太低了,它總是喚醒就睡眠在做無(wú)用功,而它喚醒爭(zhēng)到CPU資源的同時(shí),就意味著處理活躍連接的民企線程減少獲得了CPU的機(jī)會(huì),CPU是核心競(jìng)爭(zhēng)力,它的無(wú)效率進(jìn)而影響了GDP總吞吐量。我們所追求的是并發(fā)處理數(shù)十萬(wàn)連接,當(dāng)幾千個(gè)線程出現(xiàn)時(shí),系統(tǒng)的執(zhí)行效率就已經(jīng)無(wú)法滿足高并發(fā)了。
對(duì)高并發(fā)編程,目前只有一種模型,也是本質(zhì)上唯一有效的玩法。連接上的消息處理,可以分為兩個(gè)階段:等待消息準(zhǔn)備好、消息處理。當(dāng)使用默認(rèn)的阻塞套接字時(shí)(例如上面提到的1個(gè)線程捆綁處理1個(gè)連接),往往是把這兩個(gè)階段合而為一,這樣操作套接字的代碼所在的線程就得睡眠來(lái)等待消息準(zhǔn)備好,這導(dǎo)致了高并發(fā)下線程會(huì)頻繁的睡眠、喚醒,從而影響了CPU的使用效率。
高并發(fā)編程方法當(dāng)然就是把兩個(gè)階段分開處理。即,等待消息準(zhǔn)備好的代碼段,與處理消息的代碼段是分離的。當(dāng)然,這也要求套接字必須是非阻塞的,否則,處理消息的代碼段很容易導(dǎo)致條件不滿足時(shí),所在線程又進(jìn)入了睡眠等待階段。那么問(wèn)題來(lái)了,等待消息準(zhǔn)備好這個(gè)階段怎么實(shí)現(xiàn)?它畢竟還是等待,這意味著線程還是要睡眠的!解決辦法就是,主動(dòng)查詢,或者讓1個(gè)線程為所有連接而等待!這就是IO多路復(fù)用了。多路復(fù)用就是處理等待消息準(zhǔn)備好這件事的,但它可以同時(shí)處理多個(gè)連接!它也可以“等待”,所以它也可能導(dǎo)致線程睡眠,然而這不要緊,因?yàn)樗粚?duì)多、它可以監(jiān)控所有連接。這樣,當(dāng)我們的線程被喚醒執(zhí)行時(shí),就一定是有一些連接準(zhǔn)備好被我們的代碼執(zhí)行了,這是有效率的!沒(méi)有那么多個(gè)線程都在爭(zhēng)搶處理“等待消息準(zhǔn)備好”階段,整個(gè)世界終于清凈了!
多路復(fù)用有很多種實(shí)現(xiàn),在linux上,2.4內(nèi)核前主要是select和poll,現(xiàn)在主流是epoll,它們的使用方法似乎很不同,但本質(zhì)是一樣的。
效率卻也不同,這也是epoll完全替代了select的原因。
簡(jiǎn)單的談下epoll為何會(huì)替代select。
前面提到過(guò),高并發(fā)的核心解決方案是1個(gè)線程處理所有連接的“等待消息準(zhǔn)備好”,這一點(diǎn)上epoll和select是無(wú)爭(zhēng)議的。但select預(yù)估錯(cuò)誤了一件事,就像我們開篇所說(shuō),當(dāng)數(shù)十萬(wàn)并發(fā)連接存在時(shí),可能每一毫秒只有數(shù)百個(gè)活躍的連接,同時(shí)其余數(shù)十萬(wàn)連接在這一毫秒是非活躍的。select的使用方法是這樣的:
返回的活躍連接 ==select(全部待監(jiān)控的連接)
什么時(shí)候會(huì)調(diào)用select方法呢?在你認(rèn)為需要找出有報(bào)文到達(dá)的活躍連接時(shí),就應(yīng)該調(diào)用。所以,調(diào)用select在高并發(fā)時(shí)是會(huì)被頻繁調(diào)用的。這樣,這個(gè)頻繁調(diào)用的方法就很有必要看看它是否有效率,因?yàn)?,它的輕微效率損失都會(huì)被“頻繁”二字所放大。它有效率損失嗎?顯而易見(jiàn),全部待監(jiān)控連接是數(shù)以十萬(wàn)計(jì)的,返回的只是數(shù)百個(gè)活躍連接,這本身就是無(wú)效率的表現(xiàn)。被放大后就會(huì)發(fā)現(xiàn),處理并發(fā)上萬(wàn)個(gè)連接時(shí),select就完全力不從心了。
看幾個(gè)圖。當(dāng)并發(fā)連接為一千以下,select的執(zhí)行次數(shù)不算頻繁,與epoll似乎并無(wú)多少差距:

然而,并發(fā)數(shù)一旦上去,select的缺點(diǎn)被“執(zhí)行頻繁”無(wú)限放大了,且并發(fā)數(shù)越多越明顯:

再來(lái)說(shuō)說(shuō)epoll是如何解決的。它很聰明的用了3個(gè)方法來(lái)實(shí)現(xiàn)select方法要做的事:
新建的epoll描述符==epoll_create()
epoll_ctrl(epoll描述符,添加或者刪除所有待監(jiān)控的連接)
返回的活躍連接 ==epoll_wait( epoll描述符 )
這么做的好處主要是:分清了頻繁調(diào)用和不頻繁調(diào)用的操作。例如,epoll_ctrl是不太頻繁調(diào)用的,而epoll_wait是非常頻繁調(diào)用的。這時(shí),epoll_wait卻幾乎沒(méi)有入?yún)?,這比select的效率高出一大截,而且,它也不會(huì)隨著并發(fā)連接的增加使得入?yún)⒃桨l(fā)多起來(lái),導(dǎo)致內(nèi)核執(zhí)行效率下降。
epoll是怎么實(shí)現(xiàn)的呢?其實(shí)很簡(jiǎn)單,從這3個(gè)方法就可以看出,它比select聰明的避免了每次頻繁調(diào)用“哪些連接已經(jīng)處在消息準(zhǔn)備好階段”的 epoll_wait時(shí),是不需要把所有待監(jiān)控連接傳入的。這意味著,它在內(nèi)核態(tài)維護(hù)了一個(gè)數(shù)據(jù)結(jié)構(gòu)保存著所有待監(jiān)控的連接。這個(gè)數(shù)據(jù)結(jié)構(gòu)就是一棵紅黑樹,它的結(jié)點(diǎn)的增加、減少是通過(guò)epoll_ctrl來(lái)完成的。它是非常簡(jiǎn)單的:

圖中左下方的紅黑樹由所有待監(jiān)控的連接構(gòu)成。左上方的鏈表,同是目前所有活躍的連接。于是,epoll_wait執(zhí)行時(shí)只是檢查左上方的鏈表,并返回左上方鏈表中的連接給用戶。這樣,epoll_wait的執(zhí)行效率能不高嗎?
最后,再看看epoll提供的2種玩法ET和LT,即翻譯過(guò)來(lái)的邊緣觸發(fā)和水平觸發(fā)。其實(shí)這兩個(gè)中文名字倒也有些貼切。這2種使用方式針對(duì)的仍然是效率問(wèn)題,只不過(guò)變成了epoll_wait返回的連接如何能夠更準(zhǔn)確些。
例如,我們需要監(jiān)控一個(gè)連接的寫緩沖區(qū)是否空閑,滿足“可寫”時(shí)我們就可以從用戶態(tài)將響應(yīng)調(diào)用write發(fā)送給客戶端 。但是,或者連接可寫時(shí),我們的“響應(yīng)”內(nèi)容還在磁盤上呢,此時(shí)若是磁盤讀取還未完成呢?肯定不能使線程阻塞的,那么就不發(fā)送響應(yīng)了。但是,下一次epoll_wait時(shí)可能又把這個(gè)連接返回給你了,你還得檢查下是否要處理??赡埽覀兊某绦蛴辛硪粋€(gè)模塊專門處理磁盤IO,它會(huì)在磁盤IO完成時(shí)再發(fā)送響應(yīng)。那么,每次epoll_wait都返回這個(gè)“可寫”的、卻無(wú)法立刻處理的連接,是否符合用戶預(yù)期呢?
于是,ET和LT模式就應(yīng)運(yùn)而生了。LT是每次滿足期待狀態(tài)的連接,都得在epoll_wait中返回,所以它一視同仁,都在一條水平線上。ET則不然,它傾向更精確的返回連接。在上面的例子中,連接第一次變?yōu)榭蓪懞?,若是程序未向連接上寫入任何數(shù)據(jù),那么下一次epoll_wait是不會(huì)返回這個(gè)連接的。ET叫做 邊緣觸發(fā),就是指,只有連接從一個(gè)狀態(tài)轉(zhuǎn)到另一個(gè)狀態(tài)時(shí),才會(huì)觸發(fā)epoll_wait返回它??梢?jiàn),ET的編程要復(fù)雜不少,至少應(yīng)用程序要小心的防止epoll_wait的返回的連接出現(xiàn):可寫時(shí)未寫數(shù)據(jù)后卻期待下一次“可寫”、可讀時(shí)未讀盡數(shù)據(jù)卻期待下一次“可讀”。
當(dāng)然,從一般應(yīng)用場(chǎng)景上它們性能是不會(huì)有什么大的差距的,ET可能的優(yōu)點(diǎn)是,epoll_wait的調(diào)用次數(shù)會(huì)減少一些,某些場(chǎng)景下連接在不必要喚醒時(shí)不會(huì)被喚醒(此喚醒指epoll_wait返回)。但如果像我上面舉例所說(shuō)的,有時(shí)它不單純是一個(gè)網(wǎng)絡(luò)問(wèn)題,跟應(yīng)用場(chǎng)景相關(guān)。當(dāng)然,大部分開源框架都是基于ET寫的,框架嘛,它追求的是純技術(shù)問(wèn)題,當(dāng)然力求盡善盡美
相關(guān)文章
詳解Java中的反射機(jī)制和動(dòng)態(tài)代理
本文將詳細(xì)介紹反射機(jī)制以及動(dòng)態(tài)代理機(jī)制,而且基本現(xiàn)在的主流框架都應(yīng)用了反射機(jī)制,如spring、MyBatis、Hibernate等等,這就有非常重要的學(xué)習(xí)意義2021-06-06
java通過(guò)信號(hào)量實(shí)現(xiàn)限流的示例
本文主要介紹了java通過(guò)信號(hào)量實(shí)現(xiàn)限流的示例,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2023-06-06
基于springboot2集成jpa,創(chuàng)建dao的案例
這篇文章主要介紹了基于springboot2集成jpa,創(chuàng)建dao的案例,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2021-01-01
Java泛型之協(xié)變與逆變及extends與super選擇
這篇文章主要介紹了Java泛型之協(xié)變與逆變及extends與super選擇,文章圍繞主題內(nèi)容展開詳細(xì)內(nèi)容介紹,需要的小伙伴可以參考一下2022-05-05
Java:com.netflix.client.ClientException錯(cuò)誤解決
本文主要介紹了Java:com.netflix.client.ClientException錯(cuò)誤解決,主要是指出客戶端?module-sso?試圖通過(guò)負(fù)載均衡器訪問(wèn)服務(wù)時(shí),負(fù)載均衡器沒(méi)有找到可用的服務(wù)器來(lái)處理請(qǐng)求,下面就來(lái)介紹一下解決方法2024-08-08

