1. 背景
1.1 傳統(tǒng)線程模型
特點(diǎn):
- 基于阻塞式 I/O 模型;
- 每個(gè)連接都需要獨(dú)立的線程完成數(shù)據(jù)輸入,業(yè)務(wù)處理诡渴,數(shù)據(jù)返回的完整操作浸策。
存在問題:
- 當(dāng)并發(fā)數(shù)較大時(shí)冯键,需要?jiǎng)?chuàng)建大量線程來(lái)處理連接,系統(tǒng)資源占用較大庸汗;
- 連接建立后惫确,如果當(dāng)前線程暫時(shí)沒有數(shù)據(jù)可讀,則線程就阻塞在 read 操作上蚯舱,造成線程資源浪費(fèi)改化。
1.2 Reactor模型
針對(duì)傳統(tǒng)阻塞 I/O 服務(wù)模型的缺點(diǎn),我們一般基于 I/O 復(fù)用模型來(lái)進(jìn)行改進(jìn):所有連接的事件都由IO多路復(fù)用器(使用select/poll/epoll操作系統(tǒng)函數(shù)實(shí)現(xiàn))管理枉昏,線程只需要在復(fù)用器上等待陈肛,不需要阻塞等待所有連接。當(dāng)某條連接有新的數(shù)據(jù)可以處理時(shí)兄裂,操作系統(tǒng)通知應(yīng)用程序句旱,等待在復(fù)用器上的線程就會(huì)從阻塞狀態(tài)返回,開始進(jìn)行業(yè)務(wù)處理晰奖。通過(guò)這種方式谈撒,可以實(shí)現(xiàn)一個(gè)線程處理多個(gè)連接。
Reactor模型就是基于多路復(fù)用IO的匾南。
維基百科上對(duì)reactor的描述是啃匿,“The reactor design pattern is an event handling pattern for handling service requests delivered concurrently by one or more inputs. The service handler then demultiplexes the incoming requests and dispatches them synchronously to associated request handlers.”。從這個(gè)描述中,我們知道Reactor模式首先是事件驅(qū)動(dòng)的溯乒,有一個(gè)或多個(gè)并發(fā)輸入源夹厌,有一個(gè)Service Handler,有多個(gè)Request Handlers橙数,這個(gè)Service Handler會(huì)同步的將輸入的請(qǐng)求(Event)多路復(fù)用的分發(fā)給相應(yīng)的Request Handler尊流。所以reactor模式也叫dispatcher模式。如下圖所示:
從結(jié)構(gòu)上灯帮,這有點(diǎn)類似生產(chǎn)者消費(fèi)者模式崖技,即有一個(gè)或多個(gè)生產(chǎn)者將事件放入一個(gè)Queue中,而一個(gè)或多個(gè)消費(fèi)者主動(dòng)的從這個(gè)Queue中Poll事件來(lái)處理钟哥,而Reactor模式則并沒有Queue來(lái)做緩沖迎献,每當(dāng)一個(gè)Event輸入到Service Handler之后,該Service Handler會(huì)主動(dòng)的根據(jù)不同的Event類型將其分發(fā)給對(duì)應(yīng)的EventHandler來(lái)處理腻贰。
Reactor 模式中有 2 個(gè)關(guān)鍵組成:
- Reactor:即上圖中的ServiceHandler吁恍,處于單獨(dú)的線程中,封裝selector(多路復(fù)用器)播演,負(fù)責(zé)監(jiān)聽和分發(fā)事件冀瓦,分發(fā)給適當(dāng)?shù)奶幚砥鱽?lái)對(duì) IO 事件做出反應(yīng)。
- Handlers:即上圖中的eventHandler写烤,handler執(zhí)行 I/O 事件對(duì)應(yīng)的要完成的實(shí)際操作翼闽。
Reactor 模型有 3 種典型的實(shí)現(xiàn),分別為:
1)Reactor 單線程洲炊;2)Reactor 多線程感局;3)主從 Reactor 多線程。
1.2.1 單線程模型
reactor暂衡、accpet/read/write及業(yè)務(wù)處理共用一個(gè)線程询微。
方案說(shuō)明:
- Reactor 對(duì)象通過(guò) Select 監(jiān)控客戶端請(qǐng)求事件,收到事件后通過(guò) Dispatch 進(jìn)行分發(fā)狂巢;
- 如果是建立連接請(qǐng)求事件撑毛,則由 Acceptor 通過(guò) Accept 處理連接請(qǐng)求,獲取到連接對(duì)象(socket)唧领,然后創(chuàng)建一個(gè) Handler 對(duì)象與連接進(jìn)行綁定藻雌;
- 如果不是建立連接事件,則 Reactor 會(huì)分發(fā)調(diào)用連接對(duì)應(yīng)的 Handler 來(lái)響應(yīng)疹吃;
- Handler 會(huì)完成 Read→業(yè)務(wù)處理→Send 的完整業(yè)務(wù)流程。
優(yōu)點(diǎn):模型簡(jiǎn)單西雀,沒有多線程萨驶、線程通信、競(jìng)爭(zhēng)的問題艇肴,全部都在一個(gè)線程中完成腔呜。
缺點(diǎn):性能問題叁温,只有一個(gè)線程,無(wú)法發(fā)揮多核 CPU 的性能核畴。Handler 在處理某個(gè)連接上的業(yè)務(wù)時(shí)膝但,整個(gè)進(jìn)程無(wú)法處理其他連接事件,很容易導(dǎo)致性能瓶頸谤草。
使用場(chǎng)景:客戶端的數(shù)量有限跟束,業(yè)務(wù)處理非常快速丑孩。比如 Redis冀宴,純內(nèi)存操作,數(shù)據(jù)結(jié)構(gòu)時(shí)間復(fù)雜度低温学。
1.2.2 多線程模型
抽出單獨(dú)的線程池進(jìn)行業(yè)務(wù)的處理略贮。
方案說(shuō)明:
- Reactor 對(duì)象通過(guò) Select 監(jiān)控客戶端請(qǐng)求事件,收到事件后通過(guò) Dispatch 進(jìn)行分發(fā)仗岖;
- 如果是建立連接請(qǐng)求事件逃延,則由 Acceptor 通過(guò) Accept 處理連接請(qǐng)求,然后創(chuàng)建一個(gè) Handler 對(duì)象處理連接完成后續(xù)的各種事件轧拄;
- 如果不是建立連接事件揽祥,則 Reactor 會(huì)分發(fā)調(diào)用連接對(duì)應(yīng)的 Handler 來(lái)響應(yīng);
- Handler 只負(fù)責(zé)響應(yīng)事件紧帕,不做具體業(yè)務(wù)處理盔然,通過(guò) Read 讀取數(shù)據(jù)后,會(huì)分發(fā)給后面的 Worker 線程池進(jìn)行業(yè)務(wù)處理是嗜;
- Worker 線程池會(huì)分配獨(dú)立的線程完成真正的業(yè)務(wù)處理愈案,然后將響應(yīng)結(jié)果發(fā)給 Handler 進(jìn)行處理;
- Handler 收到響應(yīng)結(jié)果后通過(guò) Send 將響應(yīng)結(jié)果返回給 Client鹅搪。
優(yōu)點(diǎn):可以充分利用多核 CPU 的處理能力站绪。業(yè)務(wù)操作不會(huì)影響IO事件的響應(yīng)。
缺點(diǎn):Reactor 承擔(dān)所有事件的監(jiān)聽和響應(yīng)丽柿,在單線程中運(yùn)行恢准,高并發(fā)場(chǎng)景下容易成為性能瓶頸。
1.2.2 主從多線程模型
針對(duì)單 Reactor 多線程模型中甫题,Reactor 在單線程中運(yùn)行馁筐,高并發(fā)場(chǎng)景下容易成為性能瓶頸,可以讓 Reactor 在多線程中運(yùn)行坠非。
方案說(shuō)明:
- Reactor 主線程 MainReactor 對(duì)象通過(guò) Select 監(jiān)控建立連接事件敏沉,收到事件后通過(guò) Acceptor 接收,處理建立連接事件;
- Acceptor 處理建立連接事件后盟迟,MainReactor 將連接分配 Reactor 子線程給 SubReactor 進(jìn)行處理秋泳;
- SubReactor 將連接加入連接隊(duì)列進(jìn)行監(jiān)聽,并創(chuàng)建一個(gè) Handler 用于處理各種連接事件攒菠;
- 當(dāng)有新的事件發(fā)生時(shí)迫皱,SubReactor 會(huì)調(diào)用連接對(duì)應(yīng)的 Handler 進(jìn)行響應(yīng);
- Handler 通過(guò) Read 讀取數(shù)據(jù)后辖众,會(huì)分發(fā)給后面的 Worker 線程池進(jìn)行業(yè)務(wù)處理卓起;
- Worker 線程池會(huì)分配獨(dú)立的線程完成真正的業(yè)務(wù)處理,如何將響應(yīng)結(jié)果發(fā)給 Handler 進(jìn)行處理赵辕;
- Handler 收到響應(yīng)結(jié)果后通過(guò) Send 將響應(yīng)結(jié)果返回給 Client既绩。
優(yōu)點(diǎn):父線程與子線程的數(shù)據(jù)交互簡(jiǎn)單職責(zé)明確,父線程只需要接收新連接还惠,子線程完成后續(xù)的業(yè)務(wù)處理饲握。
父線程與子線程的數(shù)據(jù)交互簡(jiǎn)單,Reactor 主線程只需要把新連接傳給子線程蚕键,子線程無(wú)需返回?cái)?shù)據(jù)救欧。
這種模型在許多項(xiàng)目中廣泛使用,包括 Nginx 主從 Reactor 多進(jìn)程模型锣光,Memcached 主從多線程笆怠,Netty 主從多線程模型的支持。
2. Netty線程模型
其實(shí)Netty的默認(rèn)線程模型是Reactor模型的變種誊爹,就是去掉工作線程池的第三種形式(主從Reactor模型)的變種蹬刷。
Netty中Reactor模式的參與者主要有下面2大組件:
NioEventLoopGroup/NioEventLoop
ChannelPipeline
2.1 NioEventLoopGroup / NioEventLoop
EventLoop 本質(zhì)是一個(gè)單線程執(zhí)行器(同時(shí)維護(hù)了一個(gè) Selector)。
它的繼承關(guān)系比較復(fù)雜,重要的是:
- 一條線是繼承自 j.u.c.ScheduledExecutorService 因此包含了線程池中所有的方法
- 另一條線是繼承自 netty 自己的 EventExecutor频丘,
- 提供了 boolean inEventLoop(Thread thread) 方法判斷一個(gè)線程是否屬于此 EventLoop
- 提供了 parent 方法來(lái)看看自己屬于哪個(gè) EventLoopGroup
EventLoopGroup 是一組 EventLoop办成,Channel 一般會(huì)調(diào)用 EventLoopGroup 的 register 方法來(lái)綁定到其中一個(gè) EventLoop,后續(xù)這個(gè) Channel 上的 io 事件都由此 EventLoop 來(lái)處理搂漠,每個(gè)NioEventLoop負(fù)責(zé)可以處理多個(gè)Channel上的事件迂卢,而一個(gè)Channel只對(duì)應(yīng)于一個(gè)EventLoop。這是一種串行化的設(shè)計(jì)理念桐汤,每條連接從消息的讀取而克、編碼以及后續(xù)Handler的執(zhí)行,默認(rèn)情況下始終都由IO線程EventLoop負(fù)責(zé)怔毛,這就意味著整個(gè)流程不會(huì)進(jìn)行線程上下文的切換员萍,數(shù)據(jù)也不會(huì)面臨被并發(fā)修改的風(fēng)險(xiǎn)。
- 繼承自 netty 自己的 EventExecutorGroup
- 實(shí)現(xiàn)了 Iterable 接口提供遍歷 EventLoop 的能力
- 另有 next 方法獲取集合中下一個(gè) EventLoop
NioEventLoop 肩負(fù)著兩種任務(wù), 第一個(gè)是作為 IO 線程, 執(zhí)行與 Channel 相關(guān)的 IO 操作,包括 調(diào)用 select 等待就緒的 IO 事件拣度、讀寫數(shù)據(jù)與數(shù)據(jù)的處理等碎绎,由processSelectedKeys方法觸發(fā)蜂莉。而第二個(gè)任務(wù)是作為任務(wù)執(zhí)行者, 執(zhí)行TaskQueue 中的任務(wù), 例如用戶調(diào)用 eventLoop.submit或eventLoop.schedule 提交的普通或定時(shí)任務(wù)也是這個(gè)線程執(zhí)行的。兩種任務(wù)的執(zhí)行時(shí)間比由變量ioRatio控制混卵,默認(rèn)為50,則表示允許非IO任務(wù)執(zhí)行的時(shí)間與IO任務(wù)的執(zhí)行時(shí)間相等窖张。
Netty抽象了兩組線程池BossGroup和WorkerGroup幕随,其類型都是NioEventLoopGroup,BossGroup為MainReactor線程(只需設(shè)置一個(gè)線程)宿接,WorkerGroup為SubReactor線程(線程的個(gè)數(shù)默認(rèn)為 2 * CPU Core)赘淮。
Boss NioEventLoop線程的執(zhí)行步驟:
- 處理accept事件與client建立連接, 生成NioSocketChannel。
- 將NioSocketChannel注冊(cè)到某個(gè)worker NIOEventLoop上的selector
- 處理任務(wù)隊(duì)列的任務(wù) 即runAllTasks睦霎。
Worker NioEventLoop線程的執(zhí)行步驟:
- 輪詢注冊(cè)到自己Selector上的所有NioSocketChannel的read和write事件梢卸。
- 處理read和write事件在對(duì)應(yīng)NioSocketChannel處理業(yè)務(wù)。
- runAllTasks處理任務(wù)隊(duì)列TaskQueue的任務(wù)副女,一些耗時(shí)的業(yè)務(wù)處理可以放入TaskQueue中慢慢處理這樣不影響數(shù)據(jù)在pipeline中的流動(dòng)處理蛤高。
- 每個(gè)worker NIOEventLoop處理NioSocketChannel業(yè)務(wù)時(shí),會(huì)使用 pipeline (管道)碑幅,管道中維護(hù)了很多 handler處理器用來(lái)處理 channel 中的數(shù)據(jù)戴陡。
2.1 ChannelPipeline
ChannelPipeline是保存 ChannelHandler 的 List,用于處理或攔截 Channel 的入站事件和出站操作沟涨⌒襞可以方便的新增和刪除ChannelHandler來(lái)實(shí)現(xiàn)不同的業(yè)務(wù)邏輯定制,不需要對(duì)已有的ChannelHandler進(jìn)行修改裹赴,能夠?qū)崿F(xiàn)對(duì)修改封閉和對(duì)擴(kuò)展的支持喜庞。
ChannelHandler 是一個(gè)接口,處理 I/O 事件或攔截 I/O 操作棋返,并將其轉(zhuǎn)發(fā)到其 ChannelPipeline(業(yè)務(wù)處理鏈)中的下一個(gè)處理程序延都。
包含2個(gè)重要的子類:
- ChannelInboundHandler 用于處理入站 I/O 事件。
- ChannelOutboundHandler 用于處理出站 I/O 操作懊昨。
比如白名單校驗(yàn)窄潭,數(shù)據(jù)解碼,業(yè)務(wù)處理等邏輯等需要對(duì)輸入的數(shù)據(jù)進(jìn)行處理的酵颁,可以分別封裝為入站handler嫉你,實(shí)現(xiàn)ChannelInboundHandler 接口或者繼承ChannelInboundHandlerAdapter。而數(shù)據(jù)編碼等對(duì)輸出的數(shù)據(jù)進(jìn)行處理的躏惋,可以分別封裝為出站handler幽污,實(shí)現(xiàn)ChannelOutboundHandler 接口或者繼承ChannelOutboundHandlerAdapter。
入站事件由自下而上方向的入站處理程序處理簿姨,如下圖左側(cè)所示距误。 入站Handler處理程序通常處理由圖底部的I / O線程生成的入站數(shù)據(jù)簸搞。 通常通過(guò)實(shí)際輸入操作(例SocketChannel.read(ByteBuffer))從遠(yuǎn)程讀取入站數(shù)據(jù)。出站事件由上下方向處理准潭,如下圖右側(cè)所示趁俊。 出站Handler處理程序通常會(huì)生成或轉(zhuǎn)換出站傳輸,例如write請(qǐng)求刑然。 I/O線程通常執(zhí)行實(shí)際的輸出操作寺擂,例如SocketChannel.write(ByteBuffer)。
在 Netty 中每個(gè) Channel 都有且僅有一個(gè) ChannelPipeline 與之對(duì)應(yīng), 它們的組成關(guān)系如下:
一個(gè) Channel 包含了一個(gè) ChannelPipeline, 而 ChannelPipeline 中又維護(hù)了一個(gè)由 ChannelHandlerContext 組成的雙向鏈表, 并且每個(gè) ChannelHandlerContext 中又關(guān)聯(lián)著一個(gè) ChannelHandler泼掠。入站事件和出站事件在一個(gè)雙向鏈表中怔软,入站事件會(huì)從鏈表head往后傳遞到最后一個(gè)入站的handler,出站事件會(huì)從鏈表tail往前傳遞到最前一個(gè)出站的handler择镇,兩種類型的handler互不干擾挡逼。
默認(rèn)情況下ChannelHandler 是在 IO 線程中執(zhí)行,在將事件處理器添加到事件鏈時(shí)可以指定在哪個(gè)線程池中執(zhí)行腻豌。
那么當(dāng)不同的handler由不同的eventLoop執(zhí)行的話家坎,是如何在eventLoop中切換的?
關(guān)鍵代碼 io.netty.channel.AbstractChannelHandlerContext#invokeChannelRead()
static void invokeChannelRead(final AbstractChannelHandlerContext next, Object msg) { final Object m = next.pipeline.touch(ObjectUtil.checkNotNull(msg, "msg"), next); // 下一個(gè) handler 的事件循環(huán)是否與當(dāng)前的事件循環(huán)是同一個(gè)線程 EventExecutor executor = next.executor(); // 是吝梅,直接調(diào)用 if (executor.inEventLoop()) { next.invokeChannelRead(m); } // 不是乘盖,將要執(zhí)行的代碼作為任務(wù)提交給下一個(gè)事件循環(huán)處理(換人) else { executor.execute(new Runnable() { @Override public void run() { next.invokeChannelRead(m); } }); } }
- 如果兩個(gè) handler 綁定的是同一個(gè)線程,那么就直接調(diào)用
- 否則憔涉,把要調(diào)用的代碼封裝為一個(gè)任務(wù)對(duì)象订框,由下一個(gè) handler 的線程來(lái)調(diào)用
通常呢,我們還會(huì)專門開辟業(yè)務(wù)一個(gè)線程池來(lái)處理耗時(shí)的業(yè)務(wù)邏輯兜叨,那業(yè)務(wù)處理完成之后穿扳,如何將響應(yīng)結(jié)果通過(guò) IO 線程寫入到網(wǎng)卡中呢?
業(yè)務(wù)線程調(diào)用 Channel 對(duì)象的 write 方法并不會(huì)立即寫入網(wǎng)絡(luò)国旷,只是將數(shù)據(jù)放入一個(gè)待寫入隊(duì)列(緩沖區(qū))矛物,然后IO線程每次執(zhí)行事件選擇后,會(huì)從待寫入緩存區(qū)中獲取寫入任務(wù)跪但,將數(shù)據(jù)真正寫入到網(wǎng)絡(luò)中履羞,數(shù)據(jù)到達(dá)網(wǎng)卡之前會(huì)經(jīng)過(guò)一系列的 Channel Handler(Netty事件傳播機(jī)制),最終寫入網(wǎng)卡屡久。
總結(jié)
Netty的線程模型基于主從多Reactor模型忆首。通常由一個(gè)線程負(fù)責(zé)處理OP_ACCEPT事件,擁有 CPU 核數(shù)的兩倍的IO線程處理讀寫事件被环。
一個(gè)通道的IO操作會(huì)綁定在一個(gè)IO線程中糙及,而一個(gè)IO線程可以注冊(cè)多個(gè)通道。
在一個(gè)網(wǎng)絡(luò)通信中通常會(huì)包含網(wǎng)絡(luò)數(shù)據(jù)讀寫筛欢,編碼浸锨、解碼唇聘、業(yè)務(wù)處理。默認(rèn)情況下這些操作會(huì)在IO線程中運(yùn)行柱搜,但也可以指定其他線程池迟郎。
通常業(yè)務(wù)處理會(huì)單獨(dú)開啟業(yè)務(wù)線程池,但也可以進(jìn)一步細(xì)化聪蘸,例如心跳包可以直接在IO線程中處理谎亩,而需要再轉(zhuǎn)發(fā)給業(yè)務(wù)線程池,避免線程切換宇姚。
在一個(gè)IO線程中所有通道的事件是串行處理的。