深入Netty邏輯架構,從Reactor線程模型開始
本文是Netty系列第6篇
上一篇文章我們從一個Netty的使用Demo,了解了用Netty構建一個Server服務端應用的基本方式。并且從這個Demo出發(fā),簡述了Netty的邏輯架構。
今天主要是深入學習下 邏輯架構 中的EventLoop 和 EventLoopGroup,掌握Netty的線程模型,這是Netty最精髓的知識點之一。
本文預計閱讀時間約 「15分鐘」,將重點圍繞以下幾個問題展開:
1.什么是Reactor線程模型?
2.EventLoopGroup、EventLoop 怎么實現(xiàn)Reactor線程模型?
3.深入Netty的線程模型優(yōu)化
- Netty3和Netty4的線程模型變化
- 什么是Netty4線程模型的無鎖串行化
4.從線程模型看最佳實踐
先簡單回顧下上一篇的邏輯架構圖,看看EventLoop 和 EventLoopGroup是在什么位置。
1.什么是Reactor線程模型?
先來回顧下我們在Netty系列的第2篇介紹的I/O線程模型,包括BIO、NIO、I/O多路復用、信號驅(qū)動IO、AIO。IO多路復用在Java中有專門的NIO包封裝了相關的方法。
前面的文章也說過,使用Netty而不是直接使用Java NIO包,就是因為Netty幫我們封裝了許多對NIO包的使用細節(jié),做了許多優(yōu)化。
其中非常著名的,就是Netty的「Reactor線程模型」。
- 前置知識如果還不太清楚,可以回頭看看前面幾篇文章:
- 《沒搞清楚網(wǎng)絡I/O模型?那怎么入門Netty》
- 《從網(wǎng)絡I/O模型到Netty,先深入了解下I/O多路復用》
- 《從I/O多路復用到Netty,還要跨過Java NIO包》
Reactor模式 是一種「事件驅(qū)動」模式。
「Reactor線程模型」就是通過 單個線程 使用Java NIO包中的Selector的select()方法,進行監(jiān)聽。當獲取到事件(如accept、read等)后,就會分配(dispatch)事件進行相應的事件處理(handle)。
如果要給 Reactor線程模型 下一個更明確的定義,應該是:
- Reactor線程模式 = Reactor(I/O多路復用)+ 線程池
其中Reactor負責監(jiān)聽和分配事件,線程池負責處理事件。
然后,根據(jù)Reactor的數(shù)量和線程池的數(shù)量,又可以將Reactor分為三種模型
- 單Reactor單線程模型 (固定大小為1的線程池)
- 單Reactor多線程模型
- 多Reactor多線程模型 (一般是主從2個Reactor)
1.1 單Reactor單線程模型
Reactor內(nèi)部通過 selector 監(jiān)聽連接事件,收到事件后通過dispatch進行分發(fā)。
- 如果是連接建立的事件,通過accept接受連接,并創(chuàng)建一個Handler來處理連接后續(xù)的各種事件。
- 如果是讀寫事件,直接調(diào)用連接對應的Handler來處理,Handler完成 read => (decode => compute => encode) => send 的全部流程
這個過程中,無論是事件監(jiān)聽、事件分發(fā)、還是事件處理,都始終只有 一個線程 執(zhí)行所有的事情。
缺點:
在請求過多時,會無法支撐。因為只有一個線程,無法發(fā)揮多核CPU性能。
而且一旦某個Handler發(fā)生阻塞,服務端就完全無法處理其他連接事件。
1.2 單Reactor多線程模型
為了提高性能,我們可以把復雜的事件處理handler交給線程池,那就可以演進為 「單Reactor多線程模型」 。
這種模型和第一種模型的主要區(qū)別是把業(yè)務處理從之前的單一線程脫離出來,換成線程池處理。
1)Reactor線程
通過select監(jiān)聽客戶請求,如果是連接建立的事件,通過accept接受連接,并創(chuàng)建一個Handler來處理連接后續(xù)的讀寫事件。這里的Handler只負責響應事件、read和write事件,會將具體的業(yè)務處理交由Worker線程池處理。
只處理連接事件、讀寫事件。
2)Worker線程池
處理所有業(yè)務事件,包括(decode => compute => encode) 過程。
充分利用多核機器的資源,提高性能。
缺點:
在極個別特殊場景中,一個Reactor線程負責監(jiān)聽和處理所有的客戶端連接可能會存在性能問題。例如并發(fā)百萬客戶端連接(雙十一、春運搶票)
1.3 多Reactor多線程模型
為了充分利用多核能力,可以構建兩個 Reactor,這就演進為 「主從Reactor線程模型」 。
1)主Reactor
主 Reactor 單獨監(jiān)聽server socket,accept新連接,然后將建立的 SocketChannel 注冊給指定的 從Reactor,
2)從Reactor
從Reactor 將連接加入到連接隊列進行監(jiān)聽,并創(chuàng)建handler進行事件處理。執(zhí)行事件的讀寫、分發(fā),把業(yè)務處理就扔給worker線程池完成。
3)Worker線程池
處理所有業(yè)務事件,充分利用多核機器的資源,提高性能。
輕松處理百萬并發(fā)。
缺點:
實現(xiàn)比較復雜。
不過有了Netty,一切都變得簡單了。
Netty幫我們封裝好了一切,可以快速使用主從Reactor線程模型(Netty4的實現(xiàn)上增加了無鎖串行化設計),具體代碼這里就不貼了,可以看看上一篇的Demo。
2. EventLoop、EventLoopGroup 怎么實現(xiàn)Reactor線程模型?
上面我們已經(jīng)了解了Reactor線程模型,了解了它的核心就是:
- Reactor線程模式 = Reactor(I/O多路復用)+ 線程池
它的運行模式包括四個步驟:
- 連接注冊:建立連接后,將channel注冊到selector上
- 事件輪詢:selcetor上輪詢(select()函數(shù))獲取已經(jīng)注冊的channel的所有I/O事件(多路復用)
- 事件分發(fā):把準備就緒的I/O事件分配到對應線程進行處理
- 事件處理:每個worker線程執(zhí)行事件任務
那這樣的模型在Netty中具體怎么實現(xiàn)呢?
這就需要我們了解下EventLoop和EventLoopGroup了。
2.1 EventLoop是什么
EventLoop 不是Netty獨有的,它本身是一個通用的 事件等待和處理的程序模型。主要用來解決多線程資源消耗高的問題。例如 Node.js 就采用了 EventLoop 的運行機制。
那么,在Netty中,EventLoop是什么呢?
- 一個Reactor模型的事件處理器。
- 單獨一個線程。
- 一個EventLoop內(nèi)部會維護一個selector和一個「taskQueue任務隊列」,分別負責處理 「I/O事件」 和 「任務」。
「taskQueue任務隊列」是多生產(chǎn)者單消費者隊列,在多線程并發(fā)添加任務時,可以保證線程安全。
「I/O事件」即selectionKey中的事件,如accept、connect、read、write等;
「任務」包括 普通任務、定時任務等。
- 普通任務:通過 NioEventLoop 的 execute() 方法向任務隊列 taskQueue 中添加任務。例如 Netty 在寫數(shù)據(jù)時會封裝 WriteAndFlushTask 提交給 taskQueue。
- 定時任務:通過調(diào)用 NioEventLoop 的 schedule() 方法向 定時任務隊列 scheduledTaskQueue 添加一個定時任務,用于周期性執(zhí)行該任務(如心跳消息發(fā)送等)。定時任務隊列的任務 到了執(zhí)行時間后,會合并到 普通任務 隊列中進行真正執(zhí)行。
一圖勝千言:
EventLoop單線程運行,循環(huán)往復執(zhí)行三個動作:
- selector事件輪詢
- I/O事件處理
- 任務處理
2.2 EventLoopGroup是什么
EventLoopGroup比較簡單,可以簡單理解為一個“EventLoop線程池”。

Tips:
監(jiān)聽一個端口,只會綁定到 BossEventLoopGroup 中的一個 Eventloop,所以, BossEventLoopGroup 配置多個線程也無用,除非你同時監(jiān)聽多個端口。
2.3 具體實現(xiàn)
Netty可以通過簡單配置,支持單Reactor單線程模型 、單Reactor多線程模型 、多Reactor多線程模型。
我們以 「多Reactor多線程模型」 為例,來看看Netty是如何通過EventLoop來實現(xiàn)的。
還是一圖勝千言:
我們結合Reactor線程模型的四個步驟來梳理一下:
1)連接注冊
master EventLoopGroup中有一個EventLoop,綁定某個特定端口進行監(jiān)聽。
一旦有新的連接進來觸發(fā)accept類型事件,就會在當前EventLoop的I/O事件處理階段,將這個連接分配給slave EventLoopGroup中的某一個EventLoop,進行后續(xù) 事件的監(jiān)聽。
2)事件輪詢
slave EventLoopGroup中的EventLoop,會通過selcetor對綁定到自身的channel進行輪詢,獲取已經(jīng)注冊的channel的所有I/O事件(多路復用)。
當然,EventLoopGroup中會有 多個EventLoop 運行,各自循環(huán)處理。具體EventLoop數(shù)量是由 用戶指定的線程數(shù) 或者 默認為核數(shù)的2倍。
3)事件分發(fā)
當slave EventLoopGroup中的EventLoop獲取到I/O事件后,會在EventLoop的 I/O事件處理(processSelectedKeys) 階段分發(fā)給對應ChannelPipeline進行處理。
注意,仍然在當前線程進行串行處理
4)事件處理
在ChannelPipeline中對I/O事件進行處理。
I/O事件處理完后,EventLoop在 任務處理(runAllTasks) 階段,對隊列中的任務進行消費處理。
至此,我們就能完全梳理清楚EventLoopGroup/EventLoop 和 Reactor線程模型的關系了。
咦,好像有什么地方不對勁?
沒錯,細心的朋友可能會發(fā)現(xiàn),slave EventLoopGroup中并不是
- 一個selector + 線程池
而是有多個EventLoop組成的
- 多selector + 多個單線程
這是為什么呢?
那就得繼續(xù)深入了解下Netty4的線程模型優(yōu)化了。
3. 深入Netty的線程模型優(yōu)化
上文說過,對每個EventLoop來說,都是單線程運行,并循環(huán)往復執(zhí)行三個動作:
- selector事件輪詢
- I/O事件處理
- 任務處理
在slave EventLoopGroup中,并不是 “一個selector + 線程池”模式,而是有多個EventLoop組成的 “多selector + 多個單線程“ 模型,這是為什么呢?
這主要是因為我們分析的是Netty4的線程模型,跟Netty3的傳統(tǒng)Reactor模型相比有了不同之處。
3.1 Netty3和Netty4的線程模型變化
在Netty3的線程模型中,分為 讀事件處理模型 和 寫事件處理模型。
- read事件的ChannelHandler都是由Netty的 I/O 線程(對應Netty 4 中的 EventLoop)中負責執(zhí)行。
- I/O線程調(diào)度執(zhí)行ChannelPipeline中Handler鏈的對應方法,直到業(yè)務實現(xiàn)的End Handler。
- End Handler將消息封裝成Runnable,放入到業(yè)務線程池中執(zhí)行,I/O線程返回,繼續(xù)讀/寫等I/O操作。
- write事件是由調(diào)用線程處理,可能是 I/O 線程,也可能是業(yè)務線程。
- 如果是業(yè)務線程,那么業(yè)務線程會執(zhí)行ChannelPipeline中的Channel Handler。
- 執(zhí)行到系統(tǒng)最后一個ChannelHandler,將編碼后的消息Push到發(fā)送隊列中,業(yè)務線程返回。
- Netty的I/O線程從發(fā)送消息隊列中取出消息,調(diào)用SocketChannel的write方法進行消息發(fā)送。
由上文可以看到,在Netty3的線程模型中,是采用“selector + 業(yè)務線程池”的模型。
注意,在這種模型下,讀寫模型不一致。尤其是讀事件、寫事件的「執(zhí)行線程」是不一樣的。
但是在Netty4的線程模型中,采用了“多selector + 多個單線程”模型。
讀事件:
- I/O線程NioEventLoop從SocketChannel中讀取數(shù)據(jù),將ByteBuf投遞到ChannelPipeline,觸發(fā)ChannelRead事件;
- I/O線程NioEventLoop調(diào)用ChannelHandler鏈,直到將消息投遞到業(yè)務線程,然后I/O線程返回,繼續(xù)后續(xù)的操作。
寫事件:
- 業(yè)務線程調(diào)用ChannelHandlerContext.write(Object msg)方法進行消息發(fā)送。
- ChannelHandlerInvoker將發(fā)送消息封裝成 任務,放入到EventLoop的Mpsc任務隊列中,業(yè)務線程返回。后續(xù)由EventLoop在循環(huán)中統(tǒng)一調(diào)度和執(zhí)行。
- I/O線程EventLoop在進行 任務處理 時,從Mpsc任務隊列中獲取任務,調(diào)用ChannelPipeline進行處理,處理Outbound事件,直到將消息放入發(fā)送隊列,然后喚醒Selector,執(zhí)行寫操作。
Netty4中,無論讀寫,都是通過I/O線程(也就是EventLoop)來統(tǒng)一處理。
為什么Netty4的線程模型做了這樣的變化?答案就是 無鎖串行化設計。
3.2 什么是Netty4線程模型的無鎖串行化
我們先看看Netty3的線程模型存在什么問題:
讀/寫線程模型 不一致,帶來額外的開發(fā)心智負擔。
寫操作由業(yè)務線程發(fā)起時,通常業(yè)務會使用 線程池多線程并發(fā)執(zhí)行 某個業(yè)務流程,所以某一個時刻會有多個業(yè)務線程同時操作ChannelHandler,我們需要對ChannelHandler進行并發(fā)保護,大大降低了開發(fā)效率。
頻繁的線程上下文切換,會帶來額外的性能損耗。
而Netty4線程模型的 「無鎖串行化」設計,就很好地解決了這些問題。
一圖勝千言:
從事件輪詢、消息的讀取、編碼以及后續(xù)Handler的執(zhí)行,始終都由I/O線程NioEventLoop內(nèi)部進行串行操作,這就意味著整個流程不會進行線程上下文的切換,避免多線程競爭導致的性能下降,數(shù)據(jù)也不會面臨被并發(fā)修改的風險。
表面上看,串行化設計似乎CPU利用率不高,并發(fā)程度不夠。但是,通過調(diào)整slave EventLoopGroup的線程參數(shù),可以同時啟動多個NioEventLoop,串行化的線程并行運行,這種局部無鎖化的串行線程設計相比「一個隊列-多個工作線程模型」性能更優(yōu)。
總結下Netty4無鎖串行化設計的優(yōu)點:
- 一個EventLoop會處理一個channel全生命周期的所有事件。從消息的讀取、編碼以及后續(xù)Handler的執(zhí)行,始終都由I/O線程NioEventLoop負責。
- 每個EventLoop會有自己獨立的任務隊列。
- 整個流程不會進行線程上下文的切換,數(shù)據(jù)也不會面臨被并發(fā)修改的風險。
- 對于用戶而言,統(tǒng)一的讀寫線程模型,也降低了使用的心智負擔。
4. 從線程模型看最佳實踐
NioEventLoop 無鎖串行化的設計這么好,它就完美無缺了嗎?
不是的!
在特定的場景下,Netty3的線程模型可能性能更高。比如編碼和其它寫操作非常耗時,由多個業(yè)務線程并發(fā)執(zhí)行,性能肯定高于單個EventLoop線程串行執(zhí)行。
因此,雖然單線程執(zhí)行避免了線程切換,但是它的缺陷就是不能執(zhí)行時間過長的 I/O 操作,一旦某個 I/O 事件發(fā)生阻塞,那么后續(xù)的所有 I/O 事件都無法執(zhí)行,甚至造成事件積壓。
所以,Netty4的線程模型的最佳實踐需要注意以下兩點:
- 無論讀/寫,不在自定義ChannelHandler中做耗時操作。
- 不把耗時操作放進 任務隊列。
本文從Reactor線程模型開始說起,到Netty如何用EventLoop實現(xiàn)Reactor線程模型。
然后對Netty4的線程模型優(yōu)化做了詳細介紹,尤其是「無鎖串行化設計」。
最后從EventLoop線程模型出發(fā),說明了日常開發(fā)中使用Netty4開發(fā)的最佳實踐。
希望大家能對EventLoop有全面的認識。