一、單線程Reactor模式
????????Netty線程模型總體上可以說(shuō)是Reactor模式的一種變種,我們先看看什么是Reactor模式。這里主要參考維基百科上對(duì)Ractor的定義與描述。
????????Reactor模式是一種事件處理模式,單個(gè)或多個(gè)事件(Event)并發(fā)地投遞到事件處理服務(wù)(Service Handler)藐俺,事件處理服務(wù)將事件進(jìn)行分離炊甲,同步的將他們分發(fā)到對(duì)應(yīng)的事件處理器中去處理。Reactor模式有下面幾種參與者:
? ? ? ? 1欲芹、資源:任何提供系統(tǒng)的輸入或者消費(fèi)系統(tǒng)的輸出的資源卿啡,如:Socket句柄。
? ? ? ? 2菱父、同步事件分離器:通常使用event loop來(lái)進(jìn)行對(duì)資源的阻塞等待颈娜,當(dāng)有資源就緒的時(shí)候事件分離器將資源傳遞給事件分發(fā)器。
? ? ? ? 3浙宜、事件分發(fā)器:處理請(qǐng)求處理器的注冊(cè)或者反注冊(cè)官辽,將資源從時(shí)間分離器分發(fā)到資源對(duì)應(yīng)的請(qǐng)求處理器中同步執(zhí)行。
? ? ? ? 4粟瞬、請(qǐng)求處理器:應(yīng)用定義的對(duì)相關(guān)資源的請(qǐng)求處理同仆。
? ??????下面用一張圖表示通用Reactor模式的示意圖:
????????Reactor模式的優(yōu)點(diǎn)與缺點(diǎn):
????????Reactor模式使得應(yīng)用代碼和Reactor實(shí)現(xiàn)相分離,這使得用戶(hù)可以將應(yīng)用代碼設(shè)計(jì)成最大程度可復(fù)用的模塊裙品,由于對(duì)于請(qǐng)求處理器的調(diào)用的是同步的俗批,用戶(hù)不需要去考慮并發(fā)問(wèn)題,同時(shí)也減少了多線程對(duì)系統(tǒng)資源的消耗市怎。另一方面扶镀,相比于過(guò)程化模式的程序,Reactor模式下的程序相對(duì)比較難于Debug焰轻,同時(shí)單線程的設(shè)計(jì)在多核時(shí)代不能夠充分利用多核處理器資源,影響了系統(tǒng)的擴(kuò)展性昆雀。
????????這是最簡(jiǎn)單的單線程Reactor模式辱志,網(wǎng)上也有對(duì)于多線程Reactor模式的一些介紹,本文不做過(guò)多介紹狞膘,多線程Reactor模式也是在原有的模型基礎(chǔ)上進(jìn)行的變種揩懒。
二、Netty線程模型
????????Netty是一款高效的NIO框架和工具挽封,基于JAVA NIO提供的API實(shí)現(xiàn)已球。在JAVA NIO方面Selector給Reactor模式提供了基礎(chǔ),Netty結(jié)合Selector和Reactor模式設(shè)計(jì)了高效的線程模型辅愿,Reactor模式的參與者主要有下面一些組件:
????????Selector
? ? ? ? EventLoopGroup/EventLoop
? ? ? ? ChannelPipeline
????????下面對(duì)其功能和其在Netty之Reactor模式中扮演的角色進(jìn)行介紹智亮。
1、Selector
????????Selector是JAVA NIO提供的SelectableChannel多路復(fù)用器点待,它內(nèi)部維護(hù)著三個(gè)SelectionKey集合阔蛉,負(fù)責(zé)配合select操作將就緒的IO事件分離出來(lái),落地為SelectionKey癞埠。在Netty線程模型中状原,我認(rèn)為Selector充當(dāng)著demultiplexer的角色聋呢,而對(duì)于SelectionKey我們可以將它看成Reactor模式中的資源。
2颠区、EventLoopGroup/EventLoop
????????EventLoopGroup是一組EventLoop的抽象削锰,由于Netty對(duì)Reactor模式進(jìn)行了變種,實(shí)際上為更好的利用多核CPU資源毕莱,Netty實(shí)例中一般會(huì)有多個(gè)EventLoop同時(shí)工作器贩,每個(gè)EventLoop維護(hù)著一個(gè)Selector實(shí)例,類(lèi)似單線程Reactor模式地工作著央串。至于多少線程可有用戶(hù)決定磨澡,Netty也根據(jù)實(shí)際上的處理器核數(shù)提供了一個(gè)默認(rèn)的數(shù)字,我們也建議使用這個(gè)數(shù)字:
private static final int DEFAULT_EVENT_LOOP_THREADS;
static {
????????DEFAULT_EVENT_LOOP_THREADS = Math.max(1, SystemPropertyUtil.getInt( "io.netty.eventLoopThreads",? ? ????????Runtime.getRuntime().availableProcessors() * 2));
????????if (logger.isDebugEnabled()) { logger.debug("-Dio.netty.eventLoopThreads: {}", DEFAULT_EVENT_LOOP_THREADS);
????????}
}
????????EventLoopGroup提供next接口质和,可以總一組EventLoop里面按照一定規(guī)則獲取其中一個(gè)EventLoop來(lái)處理任務(wù)稳摄,對(duì)于EventLoopGroup這里需要了解的是在Netty中,在Netty服務(wù)器編程中我們需要BossEventLoopGroup和WorkerEventLoopGroup兩個(gè)EventLoopGroup來(lái)進(jìn)行工作饲宿。通常一個(gè)服務(wù)端口即一個(gè)ServerSocketChannel對(duì)應(yīng)一個(gè)Selector和一個(gè)EventLoop線程厦酬,也就是我們建議BossEventLoopGroup的線程數(shù)參數(shù)這是為1。BossEventLoop負(fù)責(zé)接收客戶(hù)端的連接并將SocketChannel交給WorkerEventLoopGroup來(lái)進(jìn)行IO處理瘫想。下面是他們的工作示意圖:
????????如上圖仗阅,BossEventLoopGroup通常是一個(gè)單線程的EventLoop,EventLoop維護(hù)著一個(gè)注冊(cè)了ServerSocketChannel的Selector實(shí)例国夜,BoosEventLoop不斷輪詢(xún)Selector將連接事件分離出來(lái)减噪,通常是OP_ACCEPT事件,然后將accept得到的SocketChannel交給WorkerEventLoopGroup车吹,WorkerEventLoopGroup會(huì)由next選擇其中一個(gè)EventLoopGroup來(lái)將這個(gè)SocketChannel注冊(cè)到其維護(hù)的Selector并對(duì)其后續(xù)的IO事件進(jìn)行處理筹裕。在Reactor模式中BossEventLoopGroup主要是對(duì)多線程的擴(kuò)展,而每個(gè)EventLoop的實(shí)現(xiàn)涵蓋IO事件的分離窄驹,和分發(fā)(Dispatcher)朝卒。
3、ChannelPipeline
????????在Netty中ChannelPipeline維護(hù)著一個(gè)ChannelHandler的鏈表隊(duì)列乐埠,每個(gè)SocketChannel都有一個(gè)維護(hù)著一個(gè)ChannelPipeline實(shí)例抗斤,而每個(gè)ChannelPipeline實(shí)例通常維護(hù)著一個(gè)ChannelHandler鏈表隊(duì)列,由于SocketChannel是和SelectionKey關(guān)聯(lián)的丈咐,也就是Reactor模式中的資源瑞眼,當(dāng)EventLoop將SelectionKey分離出來(lái)的時(shí)候會(huì)將SelectionKey關(guān)聯(lián)的Channel交給Channel關(guān)聯(lián)的ChannelHandler鏈來(lái)處理,那么ChannelPipeline其實(shí)是擔(dān)任著Reactor模式中的請(qǐng)求處理器這個(gè)角色棵逊。既然提到ChannelPipeline负拟,這里對(duì)其也進(jìn)行一些簡(jiǎn)單的介紹吧。
????????ChannelPipeline的默認(rèn)實(shí)現(xiàn)是DefaultChannelPipeline歹河,DefaultChannelPipeline本身維護(hù)著一個(gè)用戶(hù)不可見(jiàn)的tail和head的ChannelHandler掩浙,他們分別位于鏈表隊(duì)列的頭部和尾部花吟。tail在更上從的部分,而head在靠近網(wǎng)絡(luò)層的方向厨姚。在Netty中關(guān)于ChannelHandler有兩個(gè)重要的接口衅澈,ChannelInBoundHandler和ChannelOutBoundHandler。inbound可以理解為網(wǎng)絡(luò)數(shù)據(jù)從外部流向系統(tǒng)內(nèi)部谬墙,而outbound可以理解為網(wǎng)絡(luò)數(shù)據(jù)從系統(tǒng)內(nèi)部流向系統(tǒng)外部今布。用戶(hù)實(shí)現(xiàn)的ChannelHandler可以根據(jù)需要實(shí)現(xiàn)其中一個(gè)或多個(gè)接口,將其放入Pipeline中的鏈表隊(duì)列中拭抬,ChannelPipeline會(huì)根據(jù)不同的IO事件類(lèi)型來(lái)找到相應(yīng)的Handler來(lái)處理部默,同時(shí)鏈表隊(duì)列是責(zé)任鏈模式的一種變種,自上而下或自下而上所有滿(mǎn)足事件關(guān)聯(lián)的Handler都會(huì)對(duì)事件進(jìn)行處理造虎。
????????上面部分主要是對(duì)比Reactor模式對(duì)Netty的線程模型進(jìn)行相應(yīng)的對(duì)比介紹傅蹂,下面主要會(huì)結(jié)合JavaScript單線程模型多介紹一些Netty對(duì)EventLoop的實(shí)現(xiàn)及相應(yīng)的思考。
4算凿、JavaScript單線程模型
????????眾所周知份蝴,JavaScript是單線程的,也就是任何時(shí)刻同時(shí)只能有一個(gè)線程堆棧在執(zhí)行氓轰,那么對(duì)于下面這段代碼可能有同學(xué)會(huì)疑惑這婚夫,這個(gè)是怎么執(zhí)行的:
console.log("A");
setTimeout(function timeout() {
????????console.log("B");
}, 10);
console.log("C"); ....
//biz code console.log("D");
????????最初的想法是我們?cè)O(shè)置了一個(gè)定時(shí)任務(wù),10ms之后執(zhí)行署鸡,如果在biz code處的code需要執(zhí)行20ms以上案糙,那么timeout怎么能夠順利執(zhí)行呢,而且單線程是如何做到既執(zhí)行下面的biz code又執(zhí)行timeout的呢靴庆。事實(shí)上如果biz code的部分如果執(zhí)行時(shí)間大于10ms侍筛,那么timeout并不會(huì)立即準(zhǔn)時(shí)執(zhí)行的。要明白其中的原因撒穷,我們可以從一張圖來(lái)理解JavaScript的單線程模型:
????????首先簡(jiǎn)單理解下eventloop機(jī)制,即一個(gè)線程在執(zhí)行完主線程后會(huì)不斷輪詢(xún)callback隊(duì)列裆熙,取出就緒任務(wù)執(zhí)行端礼,每個(gè)循環(huán)稱(chēng)為一個(gè)tick。因?yàn)镴avaScript只有一個(gè)線程執(zhí)行入录,因此也只有一個(gè)線程堆棧蛤奥,結(jié)合上面的code實(shí)例接單說(shuō)明一下對(duì)應(yīng)堆棧的變動(dòng):
????????console.log("A")入棧執(zhí)行,輸出"A"僚稿,console.log("A")出棧凡桥。setTimeout入棧,WebAPIs后臺(tái)不斷檢查timeout對(duì)象的超時(shí)時(shí)間是否已經(jīng)到達(dá)蚀同,如果到達(dá)則會(huì)將對(duì)于的callback也即timeout放入callback隊(duì)列缅刽。接下來(lái)console.log("C")會(huì)入棧執(zhí)行啊掏,輸出"C",然后出棧衰猛。...最后console.log("D")會(huì)入棧執(zhí)行迟蜜,輸出"D",然后出棧啡省。主區(qū)域代碼執(zhí)行完畢線程會(huì)不斷輪詢(xún)callback隊(duì)列來(lái)查詢(xún)是否有就緒callback娜睛,如果有則取出執(zhí)行,如果沒(méi)有則繼續(xù)輪詢(xún)卦睹。而對(duì)于超時(shí)或者是我們使用ajax的callback畦戒,后臺(tái)會(huì)根據(jù)IO操作或超時(shí)時(shí)間是否完畢來(lái)決定是否將callback放入callback隊(duì)列,這就是EventLoop機(jī)制结序。Node的單線程EventLoop模型相比于JavaScript的單線程EventLoop模型類(lèi)似障斋,但是更復(fù)雜一些,整體模型可以作為參考去理解笼痹。
5配喳、Netty EventLoop
????????理解完JavaScript的EventLoop機(jī)制之后我們?cè)倩剡^(guò)頭來(lái)看看Netty EventLoop機(jī)制的具體實(shí)現(xiàn)。對(duì)比JavaScript單線程模型圖凳干,我畫(huà)了一張Netty的單線程模型圖:
????????在Netty的EventLoop線程中晴裹,這個(gè)線程主要需要處理IO事件和其他兩種任務(wù),分別為定時(shí)任務(wù)和一般任務(wù)救赐。Netty提供可一個(gè)參數(shù)ioRatio用于用戶(hù)調(diào)整單線程對(duì)于IO處理時(shí)間和任務(wù)處理時(shí)間的分配的比率涧团。這樣根據(jù)實(shí)際應(yīng)用場(chǎng)景用戶(hù)可以對(duì)這個(gè)值進(jìn)行調(diào)整,默認(rèn)值是50经磅,也就是這個(gè)線程會(huì)將處理IO的時(shí)間和處理任務(wù)的時(shí)間控制為1:1泌绣。
final long ioStartTime = System.nanoTime();
processSelectedKeys();
//處理IO事件 final long ioTime = System.nanoTime() - ioStartTime;
//處理IO事件的時(shí)間
runAllTasks(ioTime * (100 - ioRatio) / ioRatio);
//計(jì)算用于處理任務(wù)的時(shí)間
????????這樣盡管一個(gè)EventLoop會(huì)關(guān)聯(lián)多個(gè)Channel,這些Channel在單個(gè)線程下并不會(huì)出現(xiàn)并發(fā)問(wèn)題预厌,同時(shí)對(duì)于異步任務(wù)的處理也一樣阿迈,Netty這樣設(shè)計(jì)即免去了并發(fā)問(wèn)題的煩惱,有減少了多線程上下文切換帶來(lái)的性能損耗轧叽,同時(shí)基于EventLoopGroup實(shí)現(xiàn)的有限的線程數(shù)能夠充分利用CPU處理能力苗沧。
6、關(guān)于IO密集型和CPU密集型的思考
????????Netty基于單線程設(shè)計(jì)的EventLoop能夠同時(shí)處理成千上萬(wàn)的客戶(hù)端連接的IO事件炭晒,缺點(diǎn)是單線程不能夠處理時(shí)間過(guò)長(zhǎng)的任務(wù)待逞,這樣會(huì)阻塞使得IO事件的處理被阻塞,嚴(yán)重的時(shí)候回造成IO事件堆積网严,服務(wù)不能夠高效響應(yīng)客戶(hù)端請(qǐng)求识樱。所謂時(shí)間過(guò)長(zhǎng)的任務(wù)通常是占用CPU資源比較長(zhǎng)的任務(wù),也即CPU密集型,對(duì)于業(yè)務(wù)應(yīng)用也可能是業(yè)務(wù)代碼的耗時(shí)怜庸。這點(diǎn)和Node是極其相似的当犯,我可以認(rèn)為這是基于單線程的EventLoop模型的通病,我們不能夠?qū)⑦^(guò)長(zhǎng)的任務(wù)交給這個(gè)單線程來(lái)處理休雌,也就是不適合CPU密集型應(yīng)用灶壶。那么問(wèn)題怎么解決呢,參照Node的解決方案杈曲,當(dāng)我們遇到需要處理時(shí)間很長(zhǎng)的任務(wù)的時(shí)候驰凛,我們可以將它交給子線程來(lái)處理,主線程繼續(xù)去EventLoop担扑,當(dāng)子線程計(jì)算完畢再講結(jié)果交給主線程恰响。這也是通常基于Netty的應(yīng)用的解決方案涌献,通常業(yè)務(wù)代碼執(zhí)行時(shí)間比較長(zhǎng)胚宦,我們不能夠把業(yè)務(wù)邏輯交給這個(gè)單線程來(lái)處理,因此我們需要額外的線程池來(lái)分配線程資源來(lái)專(zhuān)門(mén)處理耗時(shí)較長(zhǎng)的業(yè)務(wù)邏輯燕垃,這是比較通用的設(shè)計(jì)方案枢劝。