背景
通過(guò)選擇合適的 NIO 框架,加上高性能的壓縮二進(jìn)制編解碼技術(shù),精心的設(shè)計(jì) Reactor 線(xiàn)程模型,達(dá)到支持10W TPS的跨節(jié)點(diǎn)遠(yuǎn)程服務(wù)調(diào)用。
定義
Netty 是一個(gè)高性能、異步事件驅(qū)動(dòng)的 NIO 框架,它提供了對(duì) TCP、UDP 和文件傳輸?shù)闹С?,作為一個(gè)異步 NIO 框架,Netty 的所有 IO 操作都是異步非阻塞的,通過(guò) Future-Listener 機(jī)制,用戶(hù)可以方便的主動(dòng)獲取或者通過(guò)通知機(jī)制獲得 IO 操作結(jié)果。
NIO知識(shí)準(zhǔn)備
緩沖區(qū) Buffer:Buffer 是一個(gè)對(duì)象,它包含一些要寫(xiě)入或者要讀出的數(shù)據(jù)。在 NIO 類(lèi)庫(kù)中加入 Buffer 對(duì)象,體現(xiàn)了新庫(kù)與原 I/O 的一個(gè)重要區(qū)別。在面向流的 I/O 中,可以將數(shù)據(jù)直接寫(xiě)入或者將數(shù)據(jù)直接讀到 Stream 對(duì)象中。在 NIO 庫(kù)中,所有數(shù)據(jù)都是用緩沖區(qū)處理的。在讀取數(shù)據(jù)時(shí),它是直接讀到緩沖區(qū)中的;在寫(xiě)入數(shù)據(jù)時(shí),寫(xiě)入到緩沖區(qū)中。任何時(shí)候訪(fǎng)問(wèn) NIO 中的數(shù)據(jù),都是通過(guò)緩沖區(qū)進(jìn)行操作。
緩沖區(qū)實(shí)質(zhì)上是一個(gè)數(shù)組。通常它是一個(gè)字節(jié)數(shù)組(ByteBuffer),也可以使用其他種類(lèi)的數(shù)組。但是一個(gè)緩沖區(qū)不僅僅是一個(gè)數(shù)組,緩沖區(qū)提供了對(duì)數(shù)據(jù)的結(jié)構(gòu)化訪(fǎng)問(wèn)以及維護(hù)讀寫(xiě)位置(limit)等信息。
最常用的緩沖區(qū)是 ByteBuffer,一個(gè) ByteBuffer 提供了一組功能用于操作 byte 數(shù)組。比較常用的就是 get 和 put 系列方法。
通道 Channel:Channel 是一個(gè)通道,可以通過(guò)它讀取和寫(xiě)入數(shù)據(jù),它就像自來(lái)水管一樣,網(wǎng)絡(luò)數(shù)據(jù)通過(guò) Channel 讀取和寫(xiě)入。通道與流的不同之處在于通道是雙向的,流只是在一個(gè)方向上移動(dòng)(一個(gè)流必須是 InputStream 或者 OutputStream 的子類(lèi)),而且通道可以用于讀、寫(xiě)或者同時(shí)用于讀寫(xiě)。因?yàn)?Channel 是全雙工的,所以它可以比流更好地映射底層操作系統(tǒng)的 API。特別是在 UNIX 網(wǎng)絡(luò)編程模型中,底層操作系統(tǒng)的通道都是全雙工的,同時(shí)支持讀寫(xiě)操作。比較常用的 Channel 是 SocketChannel 和 ServerSocketChannel。
多路復(fù)用器 Selector:Selector 是 Java NIO 編程的基礎(chǔ),熟練地掌握 Selector 對(duì)于掌握 NIO 編程至關(guān)重要。多路復(fù)用器提供選擇已經(jīng)就緒的任務(wù)的能力。簡(jiǎn)單來(lái)講,Selector 會(huì)不斷地輪詢(xún)注冊(cè)在其上的 Channel,如果某個(gè) Channel 上面有新的 TCP 連接接入、讀和寫(xiě)事件,這個(gè) Channel 就處于就緒狀態(tài),會(huì)被 Selector 輪詢(xún)出來(lái),然后通過(guò) SelectionKey 可以獲取就緒 Channel 的集合,進(jìn)行后續(xù)的 I/O 操作。
RPC調(diào)用的性能模型分析
傳統(tǒng) RPC 調(diào)用性能差的三宗罪
網(wǎng)絡(luò)傳輸方式問(wèn)題:傳統(tǒng)的 RPC 框架或者基于 RMI 等方式的遠(yuǎn)程服務(wù)(過(guò)程)調(diào)用采用了同步阻塞 IO,當(dāng)客戶(hù)端的并發(fā)壓力或者網(wǎng)絡(luò)時(shí)延增大之后,同步阻塞 IO 會(huì)由于頻繁的 wait 導(dǎo)致 IO 線(xiàn)程經(jīng)常性的阻塞,由于線(xiàn)程無(wú)法高效的工作,IO 處理能力自然下降。

采用 BIO 通信模型的服務(wù)端,通常由一個(gè)獨(dú)立的 Acceptor 線(xiàn)程負(fù)責(zé)監(jiān)聽(tīng)客戶(hù)端的連接,接收到客戶(hù)端連接之后為客戶(hù)端連接創(chuàng)建一個(gè)新的線(xiàn)程處理請(qǐng)求消息,處理完成之后,返回應(yīng)答消息給客戶(hù)端,線(xiàn)程銷(xiāo)毀,這就是典型的一請(qǐng)求一應(yīng)答模型。該架構(gòu)最大的問(wèn)題就是不具備彈性伸縮能力,當(dāng)并發(fā)訪(fǎng)問(wèn)量增加后,服務(wù)端的線(xiàn)程個(gè)數(shù)和并發(fā)訪(fǎng)問(wèn)數(shù)成線(xiàn)性正比,由于線(xiàn)程是 JAVA 虛擬機(jī)非常寶貴的系統(tǒng)資源,當(dāng)線(xiàn)程數(shù)膨脹之后,系統(tǒng)的性能急劇下降,隨著并發(fā)量的繼續(xù)增加,可能會(huì)發(fā)生句柄溢出、線(xiàn)程堆棧溢出等問(wèn)題,并導(dǎo)致服務(wù)器最終宕機(jī)。
序列化方式問(wèn)題:Java 序列化存在如下幾個(gè)典型問(wèn)題:
- Java 序列化機(jī)制是 Java 內(nèi)部的一種對(duì)象編解碼技術(shù),無(wú)法跨語(yǔ)言使用;例如對(duì)于異構(gòu)系統(tǒng)之間的對(duì)接,Java 序列化后的碼流需要能夠通過(guò)其它語(yǔ)言反序列化成原始對(duì)象(副本),目前很難支持;
- 相比于其它開(kāi)源的序列化框架,Java 序列化后的碼流太大,無(wú)論是網(wǎng)絡(luò)傳輸還是持久化到磁盤(pán),都會(huì)導(dǎo)致額外的資源占用;
- 序列化性能差(CPU 資源占用高)。
線(xiàn)程模型問(wèn)題:由于采用同步阻塞 IO,這會(huì)導(dǎo)致每個(gè) TCP 連接都占用 1 個(gè)線(xiàn)程,由于線(xiàn)程資源是 JVM 虛擬機(jī)非常寶貴的資源,當(dāng) IO 讀寫(xiě)阻塞導(dǎo)致線(xiàn)程無(wú)法及時(shí)釋放時(shí),會(huì)導(dǎo)致系統(tǒng)性能急劇下降,嚴(yán)重的甚至?xí)?dǎo)致虛擬機(jī)無(wú)法創(chuàng)建新的線(xiàn)程。
高性能的三大主題
- 傳輸:用什么樣的通道將數(shù)據(jù)發(fā)送給對(duì)方,BIO、NIO 或者 AIO,IO 模型在很大程度上決定了框架的性能。
- 協(xié)議:采用什么樣的通信協(xié)議,HTTP 或者內(nèi)部私有協(xié)議。協(xié)議的選擇不同,性能模型也不同。相比于公有協(xié)議,內(nèi)部私有協(xié)議的性能通??梢员辉O(shè)計(jì)的更優(yōu)。
- 線(xiàn)程:數(shù)據(jù)報(bào)如何讀取?讀取之后的編解碼在哪個(gè)線(xiàn)程進(jìn)行,編解碼后的消息如何派發(fā),Reactor 線(xiàn)程模型的不同,對(duì)性能的影響也非常大。
Netty 高性能之道
異步非阻塞IO
在 IO 編程過(guò)程中,當(dāng)需要同時(shí)處理多個(gè)客戶(hù)端接入請(qǐng)求時(shí),可以利用多線(xiàn)程或者 IO 多路復(fù)用技術(shù)進(jìn)行處理。IO 多路復(fù)用技術(shù)通過(guò)把多個(gè) IO 的阻塞復(fù)用到同一個(gè) select 的阻塞上,從而使得系統(tǒng)在單線(xiàn)程的情況下可以同時(shí)處理多個(gè)客戶(hù)端請(qǐng)求。與傳統(tǒng)的多線(xiàn)程 / 多進(jìn)程模型比,I/O 多路復(fù)用的最大優(yōu)勢(shì)是系統(tǒng)開(kāi)銷(xiāo)小,系統(tǒng)不需要?jiǎng)?chuàng)建新的額外進(jìn)程或者線(xiàn)程,也不需要維護(hù)這些進(jìn)程和線(xiàn)程的運(yùn)行,降低了系統(tǒng)的維護(hù)工作量,節(jié)省了系統(tǒng)資源。
JDK1.4 提供了對(duì)非阻塞 IO(NIO)的支持,JDK1.5_update10 版本使用 epoll 替代了傳統(tǒng)的 select/poll,極大的提升了 NIO 通信的性能。

與 Socket 類(lèi)和 ServerSocket 類(lèi)相對(duì)應(yīng),NIO 也提供了 SocketChannel 和 ServerSocketChannel 兩種不同的套接字通道實(shí)現(xiàn)。這兩種新增的通道都支持阻塞和非阻塞兩種模式。阻塞模式使用非常簡(jiǎn)單,但是性能和可靠性都不好,非阻塞模式正好相反。開(kāi)發(fā)人員一般可以根據(jù)自己的需要來(lái)選擇合適的模式,一般來(lái)說(shuō),低負(fù)載、低并發(fā)的應(yīng)用程序可以選擇同步阻塞 IO 以降低編程復(fù)雜度。但是對(duì)于高負(fù)載、高并發(fā)的網(wǎng)絡(luò)應(yīng)用,需要使用 NIO 的非阻塞模式進(jìn)行開(kāi)發(fā)。
Netty 架構(gòu)按照 Reactor 模式設(shè)計(jì)和實(shí)現(xiàn),它的服務(wù)端通信序列圖如下:

客戶(hù)端通信序列圖如下:

總結(jié):Netty 的 IO 線(xiàn)程 NioEventLoop 由于聚合了多路復(fù)用器 Selector,可以同時(shí)并發(fā)處理成百上千個(gè)客戶(hù)端 Channel,由于讀寫(xiě)操作都是非阻塞的,這就可以充分提升 IO 線(xiàn)程的運(yùn)行效率,避免由于頻繁 IO 阻塞導(dǎo)致的線(xiàn)程掛起。另外,由于 Netty 采用了異步通信模式,一個(gè) IO 線(xiàn)程可以并發(fā)處理 N 個(gè)客戶(hù)端連接和讀寫(xiě)操作,這從根本上解決了傳統(tǒng)同步阻塞 IO 一連接一線(xiàn)程模型,架構(gòu)的性能、彈性伸縮能力和可靠性都得到了極大的提升。
零拷貝
Netty 的“零拷貝”主要體現(xiàn)在如下三個(gè)方面:
Netty 的接收和發(fā)送 ByteBuffer 采用 DIRECT BUFFERS,使用堆外直接內(nèi)存進(jìn)行 Socket 讀寫(xiě),不需要進(jìn)行字節(jié)緩沖區(qū)的二次拷貝。如果使用傳統(tǒng)的堆內(nèi)存(HEAP BUFFERS)進(jìn)行 Socket 讀寫(xiě),JVM 會(huì)將堆內(nèi)存 Buffer 拷貝一份到直接內(nèi)存中,然后才寫(xiě)入 Socket 中。相比于堆外直接內(nèi)存,消息在發(fā)送過(guò)程中多了一次緩沖區(qū)的內(nèi)存拷貝。
Netty 提供了組合 Buffer 對(duì)象,可以聚合多個(gè) ByteBuffer 對(duì)象,用戶(hù)可以像操作一個(gè) Buffer 那樣方便的對(duì)組合 Buffer 進(jìn)行操作,避免了傳統(tǒng)通過(guò)內(nèi)存拷貝的方式將幾個(gè)小 Buffer 合并成一個(gè)大的 Buffer。
Netty 的文件傳輸采用了 transferTo 方法,它可以直接將文件緩沖區(qū)的數(shù)據(jù)發(fā)送到目標(biāo) Channel,避免了傳統(tǒng)通過(guò)循環(huán) write 方式導(dǎo)致的內(nèi)存拷貝問(wèn)題。
下面,我們對(duì)上述三種“零拷貝”進(jìn)行說(shuō)明,先看 Netty 接收 Buffer 的創(chuàng)建:

每循環(huán)讀取一次消息,就通過(guò) ByteBufAllocator 的 ioBuffer 方法獲取 ByteBuf 對(duì)象,下面繼續(xù)看它的接口定義:

當(dāng)進(jìn)行 Socket IO 讀寫(xiě)的時(shí)候,為了避免從堆內(nèi)存拷貝一份副本到直接內(nèi)存,Netty 的 ByteBuf 分配器直接創(chuàng)建非堆內(nèi)存避免緩沖區(qū)的二次拷貝,通過(guò)“零拷貝”來(lái)提升讀寫(xiě)性能。(簡(jiǎn)單來(lái)說(shuō)就是使用堆外內(nèi)存進(jìn)行Socket讀寫(xiě),減少了一次堆內(nèi)存拷貝到直接內(nèi)存的操作)。
下面我們繼續(xù)看第二種“零拷貝”的實(shí)現(xiàn) CompositeByteBuf,它對(duì)外將多個(gè) ByteBuf 封裝成一個(gè) ByteBuf,對(duì)外提供統(tǒng)一封裝后的 ByteBuf 接口,它的類(lèi)定義如下:

通過(guò)繼承關(guān)系我們可以看出 CompositeByteBuf 實(shí)際就是個(gè) ByteBuf 的包裝器,它將多個(gè) ByteBuf 組合成一個(gè)集合,然后對(duì)外提供統(tǒng)一的 ByteBuf 接口,相關(guān)定義如下:

添加 ByteBuf,不需要做內(nèi)存拷貝,相關(guān)代碼如下:

最后,我們看下文件傳輸?shù)摹傲憧截悺保?/p>

Netty 文件傳輸 DefaultFileRegion 通過(guò) transferTo 方法將文件發(fā)送到目標(biāo) Channel 中,下面重點(diǎn)看 FileChannel 的 transferTo 方法,它的 API DOC 說(shuō)明如下:

對(duì)于很多操作系統(tǒng)它直接將文件緩沖區(qū)的內(nèi)容發(fā)送到目標(biāo) Channel 中,而不需要通過(guò)拷貝的方式,這是一種更加高效的傳輸方式,它實(shí)現(xiàn)了文件傳輸?shù)摹傲憧截悺薄?
內(nèi)存池
隨著 JVM 虛擬機(jī)和 JIT 即時(shí)編譯技術(shù)的發(fā)展,對(duì)象的分配和回收是個(gè)非常輕量級(jí)的工作。但是對(duì)于緩沖區(qū) Buffer,情況卻稍有不同,特別是對(duì)于堆外直接內(nèi)存的分配和回收,是一件耗時(shí)的操作。為了盡量重用緩沖區(qū),Netty 提供了基于內(nèi)存池的緩沖區(qū)重用機(jī)制。下面我們一起看下 Netty ByteBuf 的實(shí)現(xiàn):

Netty 提供了多種內(nèi)存管理策略,通過(guò)在啟動(dòng)輔助類(lèi)中配置相關(guān)參數(shù),可以實(shí)現(xiàn)差異化的定制。
下面通過(guò)性能測(cè)試,我們看下基于內(nèi)存池循環(huán)利用的 ByteBuf 和普通 ByteBuf 的性能差異。
用例一,使用內(nèi)存池分配器創(chuàng)建直接內(nèi)存緩沖區(qū):

用例二,使用非堆內(nèi)存分配器創(chuàng)建的直接內(nèi)存緩沖區(qū):

各執(zhí)行 300 萬(wàn)次,性能對(duì)比結(jié)果如下所示:性能測(cè)試表明,采用內(nèi)存池的 ByteBuf 相比于朝生夕滅的 ByteBuf,性能高 23 倍左右(性能數(shù)據(jù)與使用場(chǎng)景強(qiáng)相關(guān))。

下面我們一起簡(jiǎn)單分析下 Netty 內(nèi)存池的內(nèi)存分配:

繼續(xù)看 newDirectBuffer 方法,我們發(fā)現(xiàn)它是一個(gè)抽象方法,由 AbstractByteBufAllocator 的子類(lèi)負(fù)責(zé)具體實(shí)現(xiàn),代碼如下:

代碼跳轉(zhuǎn)到 PooledByteBufAllocator 的 newDirectBuffer 方法,從 Cache 中獲取內(nèi)存區(qū)域 PoolArena,調(diào)用它的 allocate 方法進(jìn)行內(nèi)存分配:

PoolArena 的 allocate 方法如下

我們重點(diǎn)分析 newByteBuf 的實(shí)現(xiàn),它同樣是個(gè)抽象方法,由子類(lèi) DirectArena 和 HeapArena 來(lái)實(shí)現(xiàn)不同類(lèi)型的緩沖區(qū)分配,由于測(cè)試用例使用的是堆外內(nèi)存,

因此重點(diǎn)分析 DirectArena 的實(shí)現(xiàn):如果沒(méi)有開(kāi)啟使用 sun 的 unsafe,則

執(zhí)行 PooledDirectByteBuf 的 newInstance 方法,代碼如下:

通過(guò) RECYCLER 的 get 方法循環(huán)使用 ByteBuf 對(duì)象,如果是非內(nèi)存池實(shí)現(xiàn),則直接創(chuàng)建一個(gè)新的 ByteBuf 對(duì)象。從緩沖池中獲取 ByteBuf 之后,調(diào)用 AbstractReferenceCountedByteBuf 的 setRefCnt 方法設(shè)置引用計(jì)數(shù)器,用于對(duì)象的引用計(jì)數(shù)和內(nèi)存回收(類(lèi)似 JVM 垃圾回收機(jī)制)。
高效的 Reactor 線(xiàn)程模型
常用的 Reactor 線(xiàn)程模型有三種,Reactor 單線(xiàn)程模型;Reactor 多線(xiàn)程模型;主從 Reactor 多線(xiàn)程模型。
Reactor 單線(xiàn)程模型,指的是所有的 IO 操作都在同一個(gè) NIO 線(xiàn)程上面完成,NIO 線(xiàn)程的職責(zé)如下:
作為 NIO 服務(wù)端,接收客戶(hù)端的 TCP 連接;
作為 NIO 客戶(hù)端,向服務(wù)端發(fā)起 TCP 連接;
讀取通信對(duì)端的請(qǐng)求或者應(yīng)答消息;
向通信對(duì)端發(fā)送消息請(qǐng)求或者應(yīng)答消息。
Reactor 單線(xiàn)程模型示意圖如下所示:

由于 Reactor 模式使用的是異步非阻塞 IO,所有的 IO 操作都不會(huì)導(dǎo)致阻塞,理論上一個(gè)線(xiàn)程可以獨(dú)立處理所有 IO 相關(guān)的操作。從架構(gòu)層面看,一個(gè) NIO 線(xiàn)程確實(shí)可以完成其承擔(dān)的職責(zé)。例如,通過(guò) Acceptor 接收客戶(hù)端的 TCP 連接請(qǐng)求消息,鏈路建立成功之后,通過(guò) Dispatch 將對(duì)應(yīng)的 ByteBuffer 派發(fā)到指定的 Handler 上進(jìn)行消息解碼。用戶(hù) Handler 可以通過(guò) NIO 線(xiàn)程將消息發(fā)送給客戶(hù)端。
對(duì)于一些小容量應(yīng)用場(chǎng)景,可以使用單線(xiàn)程模型。但是對(duì)于高負(fù)載、大并發(fā)的應(yīng)用卻不合適,主要原因如下:
一個(gè) NIO 線(xiàn)程同時(shí)處理成百上千的鏈路,性能上無(wú)法支撐,即便 NIO 線(xiàn)程的 CPU 負(fù)荷達(dá)到 100%,也無(wú)法滿(mǎn)足海量消息的編碼、解碼、讀取和發(fā)送;
當(dāng) NIO 線(xiàn)程負(fù)載過(guò)重之后,處理速度將變慢,這會(huì)導(dǎo)致大量客戶(hù)端連接超時(shí),超時(shí)之后往往會(huì)進(jìn)行重發(fā),這更加重了 NIO 線(xiàn)程的負(fù)載,最終會(huì)導(dǎo)致大量消息積壓和處理超時(shí),NIO 線(xiàn)程會(huì)成為系統(tǒng)的性能瓶頸;
可靠性問(wèn)題:一旦 NIO 線(xiàn)程意外跑飛,或者進(jìn)入死循環(huán),會(huì)導(dǎo)致整個(gè)系統(tǒng)通信模塊不可用,不能接收和處理外部消息,造成節(jié)點(diǎn)故障。
為了解決這些問(wèn)題,演進(jìn)出了 Reactor 多線(xiàn)程模型,下面我們一起學(xué)習(xí)下 Reactor 多線(xiàn)程模型。
Rector 多線(xiàn)程模型與單線(xiàn)程模型最大的區(qū)別就是有一組 NIO 線(xiàn)程處理 IO 操作,它的原理圖如下:

有專(zhuān)門(mén)一個(gè) NIO 線(xiàn)程 -Acceptor 線(xiàn)程用于監(jiān)聽(tīng)服務(wù)端,接收客戶(hù)端的 TCP 連接請(qǐng)求;
網(wǎng)絡(luò) IO 操作 - 讀、寫(xiě)等由一個(gè) NIO 線(xiàn)程池負(fù)責(zé),線(xiàn)程池可以采用標(biāo)準(zhǔn)的 JDK 線(xiàn)程池實(shí)現(xiàn),它包含一個(gè)任務(wù)隊(duì)列和 N 個(gè)可用的線(xiàn)程,由這些 NIO 線(xiàn)程負(fù)責(zé)消息的讀取、解碼、編碼和發(fā)送;
1 個(gè) NIO 線(xiàn)程可以同時(shí)處理 N 條鏈路,但是 1 個(gè)鏈路只對(duì)應(yīng) 1 個(gè) NIO 線(xiàn)程,防止發(fā)生并發(fā)操作問(wèn)題。
在絕大多數(shù)場(chǎng)景下,Reactor 多線(xiàn)程模型都可以滿(mǎn)足性能需求;但是,在極特殊應(yīng)用場(chǎng)景中,一個(gè) NIO 線(xiàn)程負(fù)責(zé)監(jiān)聽(tīng)和處理所有的客戶(hù)端連接可能會(huì)存在性能問(wèn)題。例如百萬(wàn)客戶(hù)端并發(fā)連接,或者服務(wù)端需要對(duì)客戶(hù)端的握手消息進(jìn)行安全認(rèn)證,認(rèn)證本身非常損耗性能。在這類(lèi)場(chǎng)景下,單獨(dú)一個(gè) Acceptor 線(xiàn)程可能會(huì)存在性能不足問(wèn)題,為了解決性能問(wèn)題,產(chǎn)生了第三種 Reactor 線(xiàn)程模型 - 主從 Reactor 多線(xiàn)程模型。
主從 Reactor 線(xiàn)程模型的特點(diǎn)是:服務(wù)端用于接收客戶(hù)端連接的不再是個(gè) 1 個(gè)單獨(dú)的 NIO 線(xiàn)程,而是一個(gè)獨(dú)立的 NIO 線(xiàn)程池。Acceptor 接收到客戶(hù)端 TCP 連接請(qǐng)求處理完成后(可能包含接入認(rèn)證等),將新創(chuàng)建的 SocketChannel 注冊(cè)到 IO 線(xiàn)程池(sub reactor 線(xiàn)程池)的某個(gè) IO 線(xiàn)程上,由它負(fù)責(zé) SocketChannel 的讀寫(xiě)和編解碼工作。Acceptor 線(xiàn)程池僅僅只用于客戶(hù)端的登陸、握手和安全認(rèn)證,一旦鏈路建立成功,就將鏈路注冊(cè)到后端 subReactor 線(xiàn)程池的 IO 線(xiàn)程上,由 IO 線(xiàn)程負(fù)責(zé)后續(xù)的 IO 操作。它的線(xiàn)程模型如下圖所示:

利用主從 NIO 線(xiàn)程模型,可以解決 1 個(gè)服務(wù)端監(jiān)聽(tīng)線(xiàn)程無(wú)法有效處理所有客戶(hù)端連接的性能不足問(wèn)題。因此,在 Netty 的官方 demo 中,推薦使用該線(xiàn)程模型。
事實(shí)上,Netty 的線(xiàn)程模型并非固定不變,通過(guò)在啟動(dòng)輔助類(lèi)中創(chuàng)建不同的 EventLoopGroup 實(shí)例并通過(guò)適當(dāng)?shù)膮?shù)配置,就可以支持上述三種 Reactor 線(xiàn)程模型。正是因?yàn)?Netty 對(duì) Reactor 線(xiàn)程模型的支持提供了靈活的定制能力,所以可以滿(mǎn)足不同業(yè)務(wù)場(chǎng)景的性能訴求。
無(wú)鎖化的串行設(shè)計(jì)理念
在大多數(shù)場(chǎng)景下,并行多線(xiàn)程處理可以提升系統(tǒng)的并發(fā)性能。但是,如果對(duì)于共享資源的并發(fā)訪(fǎng)問(wèn)處理不當(dāng),會(huì)帶來(lái)嚴(yán)重的鎖競(jìng)爭(zhēng),這最終會(huì)導(dǎo)致性能的下降。為了盡可能的避免鎖競(jìng)爭(zhēng)帶來(lái)的性能損耗,可以通過(guò)串行化設(shè)計(jì),即消息的處理盡可能在同一個(gè)線(xiàn)程內(nèi)完成,期間不進(jìn)行線(xiàn)程切換,這樣就避免了多線(xiàn)程競(jìng)爭(zhēng)和同步鎖。
為了盡可能提升性能,Netty 采用了串行無(wú)鎖化設(shè)計(jì),在 IO 線(xiàn)程內(nèi)部進(jìn)行串行操作,避免多線(xiàn)程競(jìng)爭(zhēng)導(dǎo)致的性能下降。表面上看,串行化設(shè)計(jì)似乎 CPU 利用率不高,并發(fā)程度不夠。但是,通過(guò)調(diào)整 NIO 線(xiàn)程池的線(xiàn)程參數(shù),可以同時(shí)啟動(dòng)多個(gè)串行化的線(xiàn)程并行運(yùn)行,這種局部無(wú)鎖化的串行線(xiàn)程設(shè)計(jì)相比一個(gè)隊(duì)列 - 多個(gè)工作線(xiàn)程模型性能更優(yōu)。
Netty 的串行化設(shè)計(jì)工作原理圖如下

Netty 的 NioEventLoop 讀取到消息之后,直接調(diào)用 ChannelPipeline 的 fireChannelRead(Object msg),只要用戶(hù)不主動(dòng)切換線(xiàn)程,一直會(huì)由 NioEventLoop 調(diào)用到用戶(hù)的 Handler,期間不進(jìn)行線(xiàn)程切換,這種串行化處理方式避免了多線(xiàn)程操作導(dǎo)致的鎖的競(jìng)爭(zhēng),從性能角度看是最優(yōu)的。
高效的并發(fā)編程
Netty 的高效并發(fā)編程主要體現(xiàn)在如下幾點(diǎn):
volatile 的大量、正確使用 ;
CAS 和原子類(lèi)的廣泛使用;
線(xiàn)程安全容器的使用;
通過(guò)讀寫(xiě)鎖提升并發(fā)性能。
詳見(jiàn)https://blog.csdn.net/weixin_42322850/article/details/89892596
高性能的序列化框架
影響序列化性能的關(guān)鍵因素總結(jié)如下:
序列化后的碼流大小(網(wǎng)絡(luò)帶寬的占用);
序列化 & 反序列化的性能(CPU 資源占用);
是否支持跨語(yǔ)言(異構(gòu)系統(tǒng)的對(duì)接和開(kāi)發(fā)語(yǔ)言切換)。
Netty 默認(rèn)提供了對(duì) Google Protobuf 的支持(Protobuf 序列化后的碼流只有 Java 序列化的 1/4 左右),通過(guò)擴(kuò)展 Netty 的編解碼接口,用戶(hù)可以實(shí)現(xiàn)其它的高性能序列化框架,例如 Thrift 的壓縮二進(jìn)制編解碼框架。
靈活的 TCP 參數(shù)配置能力
合理設(shè)置 TCP 參數(shù)在某些場(chǎng)景下對(duì)于性能的提升可以起到顯著的效果,例如 SO_RCVBUF 和 SO_SNDBUF。如果設(shè)置不當(dāng),對(duì)性能的影響是非常大的。下面我們總結(jié)下對(duì)性能影響比較大的幾個(gè)配置項(xiàng):
SO_RCVBUF 和 SO_SNDBUF:通常建議值為 128K 或者 256K;
SO_TCPNODELAY:NAGLE 算法通過(guò)將緩沖區(qū)內(nèi)的小封包自動(dòng)相連,組成較大的封包,阻止大量小封包的發(fā)送阻塞網(wǎng)絡(luò),從而提高網(wǎng)絡(luò)應(yīng)用效率。但是對(duì)于時(shí)延敏感的應(yīng)用場(chǎng)景需要關(guān)閉該優(yōu)化算法;
軟中斷:如果 Linux 內(nèi)核版本支持 RPS(2.6.35 以上版本),開(kāi)啟 RPS 后可以實(shí)現(xiàn)軟中斷,提升網(wǎng)絡(luò)吞吐量。RPS 根據(jù)數(shù)據(jù)包的源地址,目的地址以及目的和源端口,計(jì)算出一個(gè) hash 值,然后根據(jù)這個(gè) hash 值來(lái)選擇軟中斷運(yùn)行的 cpu,從上層來(lái)看,也就是說(shuō)將每個(gè)連接和 cpu 綁定,并通過(guò)這個(gè) hash 值,來(lái)均衡軟中斷在多個(gè) cpu 上,提升網(wǎng)絡(luò)并行處理性能。
Netty 在啟動(dòng)輔助類(lèi)中可以靈活的配置 TCP 參數(shù),滿(mǎn)足不同的用戶(hù)場(chǎng)景。相關(guān)配置接口定義如下:

Netty關(guān)鍵類(lèi)庫(kù)
Netty 的核心類(lèi)庫(kù)可以分為 5 大類(lèi):
1、ByteBuf 和相關(guān)輔助類(lèi):ByteBuf 是個(gè) Byte 數(shù)組的緩沖區(qū),它的基本功能應(yīng)該與 JDK 的 ByteBuffer 一致,提供以下幾類(lèi)基本功能:
- 7 種 Java 基礎(chǔ)類(lèi)型、byte 數(shù)組、ByteBuffer(ByteBuf)等的讀寫(xiě)。
- 緩沖區(qū)自身的 copy 和 slice 等。
- 設(shè)置網(wǎng)絡(luò)字節(jié)序。
- 構(gòu)造緩沖區(qū)實(shí)例。
- 操作位置指針等方法。
- 動(dòng)態(tài)的擴(kuò)展和收縮。
從內(nèi)存分配的角度看,ByteBuf 可以分為兩類(lèi):堆內(nèi)存(HeapByteBuf)字節(jié)緩沖區(qū):特點(diǎn)是內(nèi)存的分配和回收速度快,可以被 JVM 自動(dòng)回收;缺點(diǎn)就是如果進(jìn)行 Socket 的 I/O 讀寫(xiě),需要額外做一次內(nèi)存復(fù)制,將堆內(nèi)存對(duì)應(yīng)的緩沖區(qū)復(fù)制到內(nèi)核 Channel 中,性能會(huì)有一定程度的下降。直接內(nèi)存(DirectByteBuf)字節(jié)緩沖區(qū):非堆內(nèi)存,它在堆外進(jìn)行內(nèi)存分配,相比于堆內(nèi)存,它的分配和回收速度會(huì)慢一些,但是將它寫(xiě)入或者從 Socket Channel 中讀取時(shí),由于少了一次內(nèi)存復(fù)制,速度比堆內(nèi)存快。
2、Channel 和 Unsafe:io.netty.channel.Channel 是 Netty 網(wǎng)絡(luò)操作抽象類(lèi),它聚合了一組功能,包括但不限于網(wǎng)路的讀、寫(xiě),客戶(hù)端發(fā)起連接、主動(dòng)關(guān)閉連接,鏈路關(guān)閉,獲取通信雙方的網(wǎng)絡(luò)地址等。它也包含了 Netty 框架相關(guān)的一些功能,包括獲取該 Chanel 的 EventLoop,獲取緩沖分配器 ByteBufAllocator 和 pipeline 等。Unsafe 是個(gè)內(nèi)部接口,聚合在 Channel 中協(xié)助進(jìn)行網(wǎng)絡(luò)讀寫(xiě)相關(guān)的操作,它提供的主要功能如下表所示:

3、ChannelPipeline 和 ChannelHandler: Netty 的 ChannelPipeline 和 ChannelHandler 機(jī)制類(lèi)似于 Servlet 和 Filter 過(guò)濾器,這類(lèi)攔截器實(shí)際上是職責(zé)鏈模式的一種變形,主要是為了方便事件的攔截和用戶(hù)業(yè)務(wù)邏輯的定制。Servlet Filter 是 JEE Web 應(yīng)用程序級(jí)的 Java 代碼組件,它能夠以聲明的方式插入到 HTTP 請(qǐng)求響應(yīng)的處理過(guò)程中,用于攔截請(qǐng)求和響應(yīng),以便能夠查看、提取或以某種方式操作正在客戶(hù)端和服務(wù)器之間交換的數(shù)據(jù)。
攔截器封裝了業(yè)務(wù)定制邏輯,能夠?qū)崿F(xiàn)對(duì) Web 應(yīng)用程序的預(yù)處理和事后處理。過(guò)濾器提供了一種面向?qū)ο蟮哪K化機(jī)制,用來(lái)將公共任務(wù)封裝到可插入的組件中。
這些組件通過(guò) Web 部署配置文件(web.xml)進(jìn)行聲明,可以方便地添加和刪除過(guò)濾器,無(wú)須改動(dòng)任何應(yīng)用程序代碼或 JSP 頁(yè)面,由 Servlet 進(jìn)行動(dòng)態(tài)調(diào)用。通過(guò)在請(qǐng)求 / 響應(yīng)鏈中使用過(guò)濾器,可以對(duì)應(yīng)用程序(而不是以任何方式替代)的 Servlet 或 JSP 頁(yè)面提供的核心處理進(jìn)行補(bǔ)充,而不破壞 Servlet 或 JSP 頁(yè)面的功能。由于是純 Java 實(shí)現(xiàn),所以 Servlet 過(guò)濾器具有跨平臺(tái)的可重用性,使得它們很容易地被部署到任何符合 Servlet 規(guī)范的 JEE 環(huán)境中。
Netty 的 Channel 過(guò)濾器實(shí)現(xiàn)原理與 Servlet Filter 機(jī)制一致,它將 Channel 的數(shù)據(jù)管道抽象為 ChannelPipeline,消息在 ChannelPipeline 中流動(dòng)和傳遞。ChannelPipeline 持有 I/O 事件攔截器 ChannelHandler 的鏈表,由 ChannelHandler 對(duì) I/O 事件進(jìn)行攔截和處理,可以方便地通過(guò)新增和刪除 ChannelHandler 來(lái)實(shí)現(xiàn)不同的業(yè)務(wù)邏輯定制,不需要對(duì)已有的 ChannelHandler 進(jìn)行修改,能夠?qū)崿F(xiàn)對(duì)修改封閉和對(duì)擴(kuò)展的支持。ChannelPipeline 是 ChannelHandler 的容器,它負(fù)責(zé) ChannelHandler 的管理和事件攔截與調(diào)度:

Netty 中的事件分為 inbound 事件和 outbound 事件。inbound 事件通常由 I/O 線(xiàn)程觸發(fā),例如 TCP 鏈路建立事件、鏈路關(guān)閉事件、讀事件、異常通知事件等。
Outbound 事件通常是由用戶(hù)主動(dòng)發(fā)起的網(wǎng)絡(luò) I/O 操作,例如用戶(hù)發(fā)起的連接操作、綁定操作、消息發(fā)送等操作。ChannelHandler 類(lèi)似于 Servlet 的 Filter 過(guò)濾器,負(fù)責(zé)對(duì) I/O 事件或者 I/O 操作進(jìn)行攔截和處理,它可以選擇性地?cái)r截和處理自己感興趣的事件,也可以透?jìng)骱徒K止事件的傳遞?;?ChannelHandler 接口,用戶(hù)可以方便地進(jìn)行業(yè)務(wù)邏輯定制,例如打印日志、統(tǒng)一封裝異常信息、性能統(tǒng)計(jì)和消息編解碼等。
4、EventLoop:Netty 的 NioEventLoop 并不是一個(gè)純粹的 I/O 線(xiàn)程,它除了負(fù)責(zé) I/O 的讀寫(xiě)之外,還兼顧處理以下兩類(lèi)任務(wù):
普通 Task:通過(guò)調(diào)用 NioEventLoop 的 execute(Runnable task) 方法實(shí)現(xiàn),Netty 有很多系統(tǒng) Task,創(chuàng)建它們的主要原因是:當(dāng) I/O 線(xiàn)程和用戶(hù)線(xiàn)程同時(shí)操作網(wǎng)絡(luò)資源時(shí),為了防止并發(fā)操作導(dǎo)致的鎖競(jìng)爭(zhēng),將用戶(hù)線(xiàn)程的操作封裝成 Task 放入消息隊(duì)列中,由 I/O 線(xiàn)程負(fù)責(zé)執(zhí)行,這樣就實(shí)現(xiàn)了局部無(wú)鎖化。
定時(shí)任務(wù):通過(guò)調(diào)用 NioEventLoop 的 schedule(Runnable command, long delay, TimeUnit unit) 方法實(shí)現(xiàn)。
Netty 的線(xiàn)程模型并不是一成不變的,它實(shí)際取決于用戶(hù)的啟動(dòng)參數(shù)配置。通過(guò)設(shè)置不同的啟動(dòng)參數(shù),Netty 可以同時(shí)支持 Reactor 單線(xiàn)程模型、多線(xiàn)程模型和主從 Reactor 多線(xiàn)層模型。它的工作原理如下所示:

通過(guò)調(diào)整線(xiàn)程池的線(xiàn)程個(gè)數(shù)、是否共享線(xiàn)程池等方式,Netty 的 Reactor 線(xiàn)程模型可以在單線(xiàn)程、多線(xiàn)程和主從多線(xiàn)程間切換,這種靈活的配置方式可以最大程度地滿(mǎn)足不同用戶(hù)的個(gè)性化定制。
為了盡可能地提升性能,Netty 在很多地方進(jìn)行了無(wú)鎖化的設(shè)計(jì),例如在 I/O 線(xiàn)程內(nèi)部進(jìn)行串行操作,避免多線(xiàn)程競(jìng)爭(zhēng)導(dǎo)致的性能下降問(wèn)題。表面上看,串行化設(shè)計(jì)似乎 CPU 利用率不高,并發(fā)程度不夠。但是,通過(guò)調(diào)整 NIO 線(xiàn)程池的線(xiàn)程參數(shù),可以同時(shí)啟動(dòng)多個(gè)串行化的線(xiàn)程并行運(yùn)行,這種局部無(wú)鎖化的串行線(xiàn)程設(shè)計(jì)相比一個(gè)隊(duì)列—多個(gè)工作線(xiàn)程的模型性能更優(yōu)。它的設(shè)計(jì)原理如下圖所示:

5、Future 和 Promise:在 Netty 中,所有的 I/O 操作都是異步的,這意味著任何 I/O 調(diào)用都會(huì)立即返回,而不是像傳統(tǒng) BIO 那樣同步等待操作完成。異步操作會(huì)帶來(lái)一個(gè)問(wèn)題:調(diào)用者如何獲取異步操作的結(jié)果? ChannelFuture 就是為了解決這個(gè)問(wèn)題而專(zhuān)門(mén)設(shè)計(jì)的。下面我們一起看它的原理。ChannelFuture 有兩種狀態(tài):uncompleted 和 completed。當(dāng)開(kāi)始一個(gè) I/O 操作時(shí),一個(gè)新的 ChannelFuture 被創(chuàng)建,此時(shí)它處于 uncompleted 狀態(tài)——非失敗、非成功、非取消,因?yàn)?I/O 操作此時(shí)還沒(méi)有完成。一旦 I/O 操作完成,ChannelFuture 將會(huì)被設(shè)置成 completed,它的結(jié)果有如下三種可能:
- 操作成功。
- 操作失敗。
- 操作被取消。
ChannelFuture 的狀態(tài)遷移圖如下所示:

Promise 是可寫(xiě)的 Future,F(xiàn)uture 自身并沒(méi)有寫(xiě)操作相關(guān)的接口,Netty 通過(guò) Promise 對(duì) Future 進(jìn)行擴(kuò)展,用于設(shè)置 I/O 操作的結(jié)果,它的接口定義如下:

Netty關(guān)鍵流程
重點(diǎn)掌握 Netty 服務(wù)端和客戶(hù)端的創(chuàng)建,以及創(chuàng)建過(guò)程中使用到的核心類(lèi)庫(kù)和 API、以及消息的發(fā)送和接收、消息的編解碼。
Netty 服務(wù)端創(chuàng)建流程如下:

Netty 客戶(hù)端創(chuàng)建流程如下:

Netty項(xiàng)目實(shí)戰(zhàn)
如果項(xiàng)目中需要用到 Netty,則直接在項(xiàng)目中應(yīng)用,通過(guò)實(shí)踐來(lái)不斷提升對(duì) Netty 的理解和掌握。如果暫時(shí)使用不到,則可以通過(guò)學(xué)習(xí)一些開(kāi)源的 RPC 或者服務(wù)框架,看這些框架是怎么集成并使用 Netty 的。以 gRPC Java 版為例,我們一起看下 gRPC 是如何使用 Netty 的。
gRPC 服務(wù)端
gRPC 通過(guò)對(duì) Netty HTTP/2 的封裝,向用戶(hù)屏蔽底層 RPC 通信的協(xié)議細(xì)節(jié),Netty HTTP/2 服務(wù)端的創(chuàng)建流程如下:

服務(wù)端 HTTP/2 消息的讀寫(xiě)主要通過(guò) gRPC 的 NettyServerHandler 實(shí)現(xiàn),它的類(lèi)繼承關(guān)系如下所示:

從類(lèi)繼承關(guān)系可以看出,NettyServerHandler 主要負(fù)責(zé) HTTP/2 協(xié)議消息相關(guān)的處理,例如 HTTP/2 請(qǐng)求消息體和消息頭的讀取、Frame 消息的發(fā)送、Stream 狀態(tài)消息的處理等,相關(guān)接口定義如下:

gRPC 客戶(hù)端
gRPC 的客戶(hù)端調(diào)用主要包括基于 Netty 的 HTTP/2 客戶(hù)端創(chuàng)建、客戶(hù)端負(fù)載均衡、請(qǐng)求消息的發(fā)送和響應(yīng)接收處理四個(gè)流程,gRPC 的客戶(hù)端調(diào)用總體流程如下圖所示:

gRPC 的客戶(hù)端調(diào)用流程如下:
- 客戶(hù)端 Stub(GreeterBlockingStub) 調(diào)用 sayHello(request),發(fā)起 RPC 調(diào)用。
- 通過(guò) DnsNameResolver 進(jìn)行域名解析,獲取服務(wù)端的地址信息(列表),隨后使用默認(rèn)的 LoadBalancer 策略,選擇一個(gè)具體的 gRPC 服務(wù)端實(shí)例。
- 如果與路由選中的服務(wù)端之間沒(méi)有可用的連接,則創(chuàng)建 NettyClientTransport 和 NettyClientHandler,發(fā)起 HTTP/2 連接。
- 對(duì)請(qǐng)求消息使用 PB(Protobuf)做序列化,通過(guò) HTTP/2 Stream 發(fā)送給 gRPC 服務(wù)端。
- 接收到服務(wù)端響應(yīng)之后,使用 PB(Protobuf)做反序列化。
- 回調(diào) GrpcFuture 的 set(Response) 方法,喚醒阻塞的客戶(hù)端調(diào)用線(xiàn)程,獲取 RPC 響應(yīng)。
需要指出的是,客戶(hù)端同步阻塞 RPC 調(diào)用阻塞的是調(diào)用方線(xiàn)程(通常是業(yè)務(wù)線(xiàn)程),底層 Transport 的 I/O 線(xiàn)程(Netty 的 NioEventLoop)仍然是非阻塞的。
線(xiàn)程模型
gRPC 服務(wù)端線(xiàn)程模型整體上可以分為兩大類(lèi):
1. 網(wǎng)絡(luò)通信相關(guān)的線(xiàn)程模型,基于 Netty4.1 的線(xiàn)程模型實(shí)現(xiàn)。
2. 服務(wù)接口調(diào)用線(xiàn)程模型,基于 JDK 線(xiàn)程池實(shí)現(xiàn)。
gRPC 服務(wù)端線(xiàn)程模型和交互圖如下所示:

其中,HTTP/2 服務(wù)端創(chuàng)建、HTTP/2 請(qǐng)求消息的接入和響應(yīng)發(fā)送都由 Netty 負(fù)責(zé),gRPC 消息的序列化和反序列化、以及應(yīng)用服務(wù)接口的調(diào)用由 gRPC 的 SerializingExecutor 線(xiàn)程池負(fù)責(zé)。
gRPC 客戶(hù)端的線(xiàn)程主要分為三類(lèi):
業(yè)務(wù)調(diào)用線(xiàn)程
客戶(hù)端連接和 I/O 讀寫(xiě)線(xiàn)程
請(qǐng)求消息業(yè)務(wù)處理和響應(yīng)回調(diào)線(xiàn)程
gRPC 客戶(hù)端線(xiàn)程模型工作原理如下圖所示(同步阻塞調(diào)用為例):

客戶(hù)端調(diào)用主要涉及的線(xiàn)程包括:
應(yīng)用線(xiàn)程,負(fù)責(zé)調(diào)用 gRPC 服務(wù)端并獲取響應(yīng),其中請(qǐng)求消息的序列化由該線(xiàn)程負(fù)責(zé)。
客戶(hù)端負(fù)載均衡以及 Netty Client 創(chuàng)建,由 grpc-default-executor 線(xiàn)程池負(fù)責(zé)。
HTTP/2 客戶(hù)端鏈路創(chuàng)建、網(wǎng)絡(luò) I/O 數(shù)據(jù)的讀寫(xiě),由 Netty NioEventLoop 線(xiàn)程負(fù)責(zé)。
響應(yīng)消息的反序列化由 SerializingExecutor 負(fù)責(zé),與服務(wù)端不同的是,客戶(hù)端使用的是 ThreadlessExecutor,并非 JDK 線(xiàn)程池。
SerializingExecutor 通過(guò)調(diào)用 responseFuture 的 set(value),喚醒阻塞的應(yīng)用線(xiàn)程,完成一次 RPC 調(diào)用。
gRPC 采用的是網(wǎng)絡(luò) I/O 線(xiàn)程和業(yè)務(wù)調(diào)用線(xiàn)程分離的策略,大部分場(chǎng)景下該策略是最優(yōu)的。但是,對(duì)于那些接口邏輯非常簡(jiǎn)單,執(zhí)行時(shí)間很短,不需要與外部網(wǎng)元交互、訪(fǎng)問(wèn)數(shù)據(jù)庫(kù)和磁盤(pán),也不需要等待其它資源的,則建議接口調(diào)用直接在 Netty /O 線(xiàn)程中執(zhí)行,不需要再投遞到后端的服務(wù)線(xiàn)程池。避免線(xiàn)程上下文切換,同時(shí)也消除了線(xiàn)程并發(fā)問(wèn)題。
例如提供配置項(xiàng)或者接口,系統(tǒng)默認(rèn)將消息投遞到后端服務(wù)調(diào)度線(xiàn)程,但是也支持短路策略,直接在 Netty 的 NioEventLoop 中執(zhí)行消息的序列化和反序列化、以及服務(wù)接口調(diào)用。
減少鎖競(jìng)爭(zhēng)優(yōu)化:當(dāng)前 gRPC 的線(xiàn)程切換策略如下:

優(yōu)化之后的 gRPC 線(xiàn)程切換策略:

通過(guò)線(xiàn)程綁定技術(shù)(例如采用一致性 hash 做映射), 將 Netty 的 I/O 線(xiàn)程與后端的服務(wù)調(diào)度線(xiàn)程做綁定,1 個(gè) I/O 線(xiàn)程綁定一個(gè)或者多個(gè)服務(wù)調(diào)用線(xiàn)程,降低鎖競(jìng)爭(zhēng),提升性能。
Netty 故障定位技巧
接收不到消息
如果業(yè)務(wù)的 ChannelHandler 接收不到消息,可能的原因如下:
業(yè)務(wù)的解碼 ChannelHandler 存在 BUG,導(dǎo)致消息解碼失敗,沒(méi)有投遞到后端。
業(yè)務(wù)發(fā)送的是畸形或者錯(cuò)誤碼流(例如長(zhǎng)度錯(cuò)誤),導(dǎo)致業(yè)務(wù)解碼 ChannelHandler 無(wú)法正確解碼出業(yè)務(wù)消息。
業(yè)務(wù) ChannelHandler 執(zhí)行了一些耗時(shí)或者阻塞操作,導(dǎo)致 Netty 的 NioEventLoop 被掛住,無(wú)法讀取消息。
執(zhí)行業(yè)務(wù) ChannelHandler 的線(xiàn)程池隊(duì)列積壓,導(dǎo)致新接收的消息在排隊(duì),沒(méi)有得到及時(shí)處理。
對(duì)方確實(shí)沒(méi)有發(fā)送消息。
定位策略如下:
在業(yè)務(wù)的首個(gè) ChannelHandler 的 channelRead 方法中打斷點(diǎn)調(diào)試,看是否讀取到消息。
在 ChannelHandler 中添加 LoggingHandler,打印接口日志。
查看 NioEventLoop 線(xiàn)程狀態(tài),看是否發(fā)生了阻塞。
通過(guò) tcpdump 抓包看消息是否發(fā)送成功。
內(nèi)存泄漏
通過(guò) jmap -dump:format=b,file=xx pid 命令 Dump 內(nèi)存堆棧,然后使用 MemoryAnalyzer 工具對(duì)內(nèi)存占用進(jìn)行分析,查找內(nèi)存泄漏點(diǎn),然后結(jié)合代碼進(jìn)行分析,定位內(nèi)存泄漏的具體原因,示例如下所示:

性能問(wèn)題如果出現(xiàn)性能問(wèn)題,首先需要確認(rèn)是 Netty 問(wèn)題還是業(yè)務(wù)問(wèn)題,通過(guò) jstack 命令或者 jvisualvm 工具打印線(xiàn)程堆棧,按照線(xiàn)程 CPU 使用率進(jìn)行排序(top -Hp 命令采集),看線(xiàn)程在忙什么。通常如果采集幾次都發(fā)現(xiàn) Netty 的 NIO 線(xiàn)程堆棧停留在 select 操作上,說(shuō)明 I/O 比較空閑,性能瓶頸不在 Netty,需要繼續(xù)分析看是否是后端的業(yè)務(wù)處理線(xiàn)程存在性能瓶頸:

如果發(fā)現(xiàn)性能瓶頸在網(wǎng)絡(luò) I/O 讀寫(xiě)上,可以適當(dāng)調(diào)大 NioEventLoopGroup 中的 work I/O 線(xiàn)程數(shù),直到 I/O 處理性能能夠滿(mǎn)足業(yè)務(wù)需求。
注:本文轉(zhuǎn)載自林雪峰的《Netty 系列之 Netty 高性能之道》與《Netty 學(xué)習(xí)和進(jìn)階策略》