Netty 是如何支撐高性能網(wǎng)絡(luò)通信的?
本文選自 Doocs 開(kāi)源社區(qū)旗下“源碼獵人”項(xiàng)目,作者 AmyliaY。
項(xiàng)目將會(huì)持續(xù)更新,歡迎 Star 關(guān)注。
項(xiàng)目地址:https://github.com/doocs/source-code-hunter
作為一個(gè)高性能的 NIO 通信框架,Netty 被廣泛應(yīng)用于大數(shù)據(jù)處理、互聯(lián)網(wǎng)消息中間件、游戲和金融行業(yè)等。大多數(shù)應(yīng)用場(chǎng)景對(duì)底層的通信框架都有很高的性能要求,作為綜合性能最高的 NIO 框架 之一,Netty 可以完全滿(mǎn)足不同領(lǐng)域?qū)Ω咝阅芡ㄐ诺男枨蟆1疚奈覀儗?strong style="box-sizing: border-box;color: rgb(8, 198, 153);line-height: 1.75;">從架構(gòu)層對(duì) Netty 的高性能設(shè)計(jì)和關(guān)鍵代碼實(shí)現(xiàn)進(jìn)行剖析,看 Netty 是如何支撐高性能網(wǎng)絡(luò)通信的。
RPC 調(diào)用性能模型分析
傳統(tǒng) RPC 調(diào)用性能差的原因
網(wǎng)絡(luò)傳輸方式問(wèn)題
傳統(tǒng)的 RPC 框架或者基于 RMI 等方式的遠(yuǎn)程過(guò)程調(diào)用采用了同步阻塞 I/O,當(dāng)客戶(hù)端的并發(fā)壓力或者網(wǎng)絡(luò)時(shí)延增大之后,同步阻塞 I/O 會(huì)由于頻繁的 wait 導(dǎo)致 I/O 線程經(jīng)常性的阻塞,由于線程無(wú)法高效的工作,I/O 處理能力自然下降。
采用 BIO 通信模型的服務(wù)端,通常由一個(gè)獨(dú)立的 Acceptor 線程負(fù)責(zé)監(jiān)聽(tīng)客戶(hù)端的連接,接收到客戶(hù)端連接之后,為其創(chuàng)建一個(gè)新的線程處理請(qǐng)求消息,處理完成之后,返回應(yīng)答消息給客戶(hù)端,線程銷(xiāo)毀,這就是典型的 “?一請(qǐng)求,一應(yīng)答?” 模型。該架構(gòu)最大的問(wèn)題就是不具備彈性伸縮能力,當(dāng)并發(fā)訪問(wèn)量增加后,服務(wù)端的線程個(gè)數(shù)和并發(fā)訪問(wèn)數(shù)成線性正比,由于線程是 Java 虛擬機(jī) 非常寶貴的系統(tǒng)資源,當(dāng)線程數(shù)膨脹之后,系統(tǒng)的性能急劇下降,隨著并發(fā)量的繼續(xù)增加,可能會(huì)發(fā)生句柄溢出、線程堆棧溢出等問(wèn)題,并導(dǎo)致服務(wù)器最終宕機(jī)。
序列化性能差
Java 序列化存在如下幾個(gè)典型問(wèn)題:
1.Java 序列化機(jī)制是 Java 內(nèi)部的一 種對(duì)象編解碼技術(shù),無(wú)法跨語(yǔ)言使用。例如對(duì)于異構(gòu)系統(tǒng)之間的對(duì)接,Java 序列化后的碼流需要能夠通過(guò)其他語(yǔ)言反序列化成原始對(duì)象,這很難支持。2.相比于其他開(kāi)源的序列化框架,Java 序列化后的碼流太大,無(wú)論是網(wǎng)絡(luò)傳輸還是持久化到磁盤(pán),都會(huì)導(dǎo)致額外的資源占用。3.序列化性能差,資源占用率高?( 主要是 CPU 資源占用高 )。
線程模型問(wèn)題
由于采用同步阻塞 I/O,這會(huì)導(dǎo)致每個(gè) TCP 連接 都占用 1 個(gè)線程,由于線程資源是 JVM 虛擬機(jī) 非常寶貴的資源,當(dāng) I/O 讀寫(xiě)阻塞導(dǎo)致線程無(wú)法及時(shí)釋放時(shí),會(huì)導(dǎo)致系統(tǒng)性能急劇下降,嚴(yán)重的甚至?xí)?dǎo)致虛擬機(jī)無(wú)法創(chuàng)建新的線程。
IO 通信性能三原則
盡管影響 I/O 通信性能的因素非常多,但是從架構(gòu)層面看主要有三個(gè)要素。
1.傳輸:用什么樣的通道將數(shù)據(jù)發(fā)送給對(duì)方。可以選擇 BIO、NIO 或者 AIO,I/O 模型 在很大程度上決定了通信的性能;2.協(xié)議:采用什么樣的通信協(xié)議,HTTP 等公有協(xié)議或者內(nèi)部私有協(xié)議。協(xié)議的選擇不同,性能也不同。相比于公有協(xié)議,內(nèi)部私有協(xié)議的性能通常可以被設(shè)計(jì)得更優(yōu);3.線程模型:數(shù)據(jù)報(bào)如何讀取?讀取之后的編解碼在哪個(gè)線程進(jìn)行,編解碼后的消息如何派發(fā),Reactor 線程模型的不同,對(duì)性能的影響也非常大。
異步非阻塞通信
在 I/O 編程過(guò)程中,當(dāng)需要同時(shí)處理多個(gè)客戶(hù)端接入請(qǐng)求時(shí),可以利用多線程或者 I/O 多路復(fù)用技術(shù)進(jìn)行處理。I/O 多路復(fù)用技術(shù)通過(guò)把多個(gè) I/O 的阻塞復(fù)用到同一個(gè) select 的阻塞上,從而使得系統(tǒng)在單線程的情況下可以同時(shí)處理多個(gè)客戶(hù)端請(qǐng)求。?與傳統(tǒng)的多線程 / 多進(jìn)程模型比,I/O 多路復(fù)用的最大優(yōu)勢(shì)是系統(tǒng)開(kāi)銷(xiāo)小,系統(tǒng)不需要?jiǎng)?chuàng)建新的額外進(jìn)程或者線程,也不需要維護(hù)這些進(jìn)程和線程的運(yùn)行,降低了系統(tǒng)的維護(hù)工作量,節(jié)省了系統(tǒng)資源。
JDK1.4 提供了對(duì)非阻塞 I/O 的支持,JDK1.5 使用?epoll?替代了傳統(tǒng)的?select?/?poll,極大地提升了 NIO 通信 的性能。
與?Socket?和?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)用程序可以選擇同步阻塞 I/O 以降低編程復(fù)雜度。但是對(duì)于高負(fù)載、高并發(fā)的網(wǎng)絡(luò)應(yīng)用,需要使用 NIO 的非阻塞模式進(jìn)行開(kāi)發(fā)。
Netty 的 I/O 線程?NioEventLoop?由于聚合了多路復(fù)用器?Selector,可以同時(shí)并發(fā)處理成百上千個(gè)客戶(hù)端?SocketChannel。由于讀寫(xiě)操作都是非阻塞的,這就可以充分提升 I/O 線程 的運(yùn)行效率,避免由頻繁的 I/O 阻塞 導(dǎo)致的線程掛起。另外,由于 Netty 采用了異步通信模式,一個(gè) I/O 線程 可以并發(fā)處理 N 個(gè)客戶(hù)端連接和讀寫(xiě)操作,這從根本上解決了傳統(tǒng) 同步阻塞 I/O “ 一連接,一線程 ” 模型,架構(gòu)的性能、彈性伸縮能力和可靠性都得到了極大的提升。
高效的 Reactor 線程模型
常用的 Reactor 線程模型有三種,分別如下:
1.Reactor 單線程模型;2.Reactor 多線程模型;3.主從 Reactor 多線程模型。
Reactor 單線程模型,指的是所有的 I/O 操作都在同一個(gè) NIO 線程上面完成,NIO 線程的職責(zé)如下:
1.作為 NIO 服務(wù)端,接收客戶(hù)端的 TCP 連接;2.作為 NIO 客戶(hù)端,向服務(wù)端發(fā)起 TCP 連接;3.讀取通信對(duì)端的請(qǐng)求或者應(yīng)答消息;4.向通信對(duì)端發(fā)送消息請(qǐng)求或者應(yīng)答消息。
由于 Reactor 模式使用的是異步非阻塞 I/O,所有的 I/O 操作 都不會(huì)導(dǎo)致阻塞,理論上一個(gè)線程可以獨(dú)立處理所有 I/O 相關(guān)的操作。從架構(gòu)層面看,一個(gè) NIO 線程確實(shí)可以完成其承擔(dān)的職責(zé)。例如,通過(guò) Acceptor 接收客戶(hù)端的 TCP 連接請(qǐng)求消息,鏈路建立成功之后,通過(guò) Dispatch 將對(duì)應(yīng)的 ByteBuffer 派發(fā)到指定的 Handler 上進(jìn)行消息解碼。用戶(hù) Handler 可以通過(guò) NIO 線程 將消息發(fā)送給客戶(hù)端。
對(duì)于一些小容量應(yīng)用場(chǎng)景,可以使用單線程模型,但是對(duì)于高負(fù)載、大并發(fā)的應(yīng)用卻不合適,主要原因如下。
1.一個(gè) NIO 線程 同時(shí)處理成百上千的鏈路,性能上無(wú)法支撐。?即便 NIO 線程 的 CPU 負(fù)荷 達(dá)到 100%,也無(wú)法滿(mǎn)足海量消息的編碼,解碼、讀取和發(fā)送;2.當(dāng) NIO 線程 負(fù)載過(guò)重之后,處理速度將變慢,這會(huì)導(dǎo)致大量客戶(hù)端連接超時(shí),超時(shí)之后往往會(huì)進(jìn)行重發(fā),這更加重了 NIO 線程 的負(fù)載,最終會(huì)導(dǎo)致大量消息積壓和處理超時(shí),NIO 線程會(huì)成為系統(tǒng)的性能瓶頸;3.可靠性問(wèn)題。一旦 NIO 線程意外跑飛,或者進(jìn)入死循環(huán),會(huì)導(dǎo)致整個(gè)系統(tǒng)通信模塊不可用,不能接收和處理外部消息,造成節(jié)點(diǎn)故障。
為了解決這些問(wèn)題,演進(jìn)出了?Reactor 多線程模型,下面我們看一下 Reactor 多線程模型。
Rector 多線程模型與單線程模型最大的區(qū)別就是有一組 NIO 線程 處理 I/O 操作,它的特點(diǎn)如下。
1.有一個(gè)專(zhuān)門(mén)的 NIO 線程?—— Acceptor 線程 用于監(jiān)聽(tīng)服務(wù)端口,接收客戶(hù)端的 TCP 連接請(qǐng)求;2.網(wǎng)絡(luò) IO 操作?—— 讀、寫(xiě)等由一個(gè) NIO 線程池 負(fù)責(zé),線程池可以采用標(biāo)準(zhǔn)的 JDK 線程池 實(shí)現(xiàn),它包含一個(gè)任務(wù)隊(duì)列和 N 個(gè)可用的線程,由這些 NIO 線程 負(fù)責(zé)消息的讀取、解碼、編碼和發(fā)送;3.1 個(gè) NIO 線程 可以同時(shí)處理 N 條鏈路,但是 1 個(gè)鏈路只對(duì)應(yīng) 1 個(gè) NIO 線程,以防止發(fā)生并發(fā)操作問(wèn)題。
在絕大多數(shù)場(chǎng)景下,Reactor 多線程模型 都可以滿(mǎn)足性能需求,但是,在極特殊應(yīng)用場(chǎng)景中,一個(gè) NIO 線程負(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 線程 可能會(huì)存在性能不足問(wèn)題,為了解決性能問(wèn)題,產(chǎn)生了第三種 Reactor 線程模型 ——?主從 Reactor 多線程模型。
主從 Reactor 線程模型的特點(diǎn)是,服務(wù)端用于接收客戶(hù)端連接的不再是個(gè)單線程的連接處理 Acceptor,而是一個(gè)獨(dú)立的 Acceptor 線程池。Acceptor 接收到客戶(hù)端 TCP 連接請(qǐng)求 處理完成后 ( 可能包含接入認(rèn)證等 ),將新創(chuàng)建的 SocketChannel 注冊(cè)到 I/O 處理線程池 的某個(gè) I/O 線程 上,由它負(fù)責(zé) SocketChannel 的讀寫(xiě)和編解碼工作。Acceptor 線程池 只用于客戶(hù)端的登錄、握手和安全認(rèn)證,一旦鏈路建立成功,就將鏈路注冊(cè)到 I/O 處理線程池的 I/O 線程 上,每個(gè) I/O 線程 可以同時(shí)監(jiān)聽(tīng) N 個(gè)鏈路,對(duì)鏈路產(chǎn)生的 IO 事件 進(jìn)行相應(yīng)的 消息讀取、解碼、編碼及消息發(fā)送等操作。
利用主從 Reactor 線程模型,可以解決 1 個(gè) Acceptor 線程 無(wú)法有效處理所有客戶(hù)端連接的性能問(wèn)題。因此,Netty 官方也推薦使用該線程模型。
事實(shí)上,Netty 的線程模型并非固定不變,通過(guò)在啟動(dòng)輔助類(lèi)中創(chuàng)建不同的?EventLoopGroup?實(shí)例 并進(jìn)行適當(dāng)?shù)膮?shù)配置,就可以支持上述三種 Reactor 線程模型。可以根據(jù)業(yè)務(wù)場(chǎng)景的性能訴求,選擇不同的線程模型。
Netty 單線程模型服務(wù)端代碼示例如下:
EventLoopGroup reactor = new NioEventLoopGroup(1);ServerBootstrap bootstrap = new ServerBootstrap();bootstrap.group(reactor, reactor).channel(NioServerSocketChannel.class)......
Netty 多線程模型代碼示例如下:
EventLoopGroup acceptor = new NioEventLoopGroup(1);EventLoopGroup ioGroup = new NioEventLoopGroup();ServerBootstrap bootstrap = new ServerBootstrap();bootstrap.group(acceptor, ioGroup).channel(NioServerSocketChannel.class)......
Netty 主從多線程模型代碼示例如下:
EventLoopGroup acceptorGroup = new NioEventLoopGroup();EventLoopGroup ioGroup = new NioEventLoopGroup();ServerBootstrap bootstrap = new ServerBootstrap();bootstrap.group(acceptorGroup, ioGroup).channel(NioServerSocketChannel.class)......
無(wú)鎖化的串行設(shè)計(jì)
在大多數(shù)場(chǎng)景下,并行多線程處理可以提升系統(tǒng)的并發(fā)性能。但是,如果對(duì)于共享資源的并發(fā)訪問(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è)線程內(nèi)完成,期間不進(jìn)行線程切換,這樣就避免了多線程競(jìng)爭(zhēng)和同步鎖。
為了盡可能提升性能,Netty 對(duì)消息的處理采用了串行無(wú)鎖化設(shè)計(jì),在 I/O 線程 內(nèi)部進(jìn)行串行操作,避免多線程競(jìng)爭(zhēng)導(dǎo)致的性能下降。Netty 的串行化設(shè)計(jì)工作原理圖如下圖所示。

Netty 的 NioEventLoop 讀取到消息之后,直接調(diào)用 ChannelPipeline 的 fireChannelRead(Object msg),只要用戶(hù)不主動(dòng)切換線程,一直會(huì)由 NioEventLoop 調(diào)用到 用戶(hù)的 Handler,期間不進(jìn)行線程切換。這種串行化處理方式避免了多線程操作導(dǎo)致的鎖的競(jìng)爭(zhēng),從性能角度看是最優(yōu)的。
零拷貝
Netty 的“ 零拷貝 ”主要體現(xiàn)在如下三個(gè)方面。
第一種情況。Netty 的接收和發(fā)送 ByteBuffer 采用堆外直接內(nèi)存 (DIRECT BUFFERS) 進(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)存拷貝。
下面我們繼續(xù)看第二種“ 零拷貝 ” 的實(shí)現(xiàn)?CompositeByteBuf,它對(duì)外將多個(gè) ByteBuf 封裝成一個(gè) ByteBuf,對(duì)外提供統(tǒng)一封裝后的 ByteBuf 接口。CompositeByteBuf 實(shí)際就是個(gè) ByteBuf 的裝飾器,它將多個(gè) ByteBuf 組合成一個(gè)集合,然后對(duì)外提供統(tǒng)一的 ByteBuf 接口,添加 ByteBuf,不需要做內(nèi)存拷貝。
第三種?“ 零拷貝 ” 就是文件傳輸,Netty 文件傳輸類(lèi) DefaultFileRegion 通過(guò)?transferTo()?方法 將文件發(fā)送到目標(biāo) Channel 中。很多操作系統(tǒng)直接將文件緩沖區(qū)的內(nèi)容發(fā)送到目標(biāo) Channel 中,而不需要通過(guò)循環(huán)拷貝的方式,這是一種更加高效的傳輸方式,提升了傳輸性能,降低了 CPU 和內(nèi)存占用,實(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ī)制。?ByteBuf 的子類(lèi)中提供了多種 PooledByteBuf 的實(shí)現(xiàn),基于這些實(shí)現(xiàn) Netty 提供了多種內(nèi)存管理策略,通過(guò)在啟動(dòng)輔助類(lèi)中配置相關(guān)參數(shù),可以實(shí)現(xiàn)差異化的定制。
全文完!
希望本文對(duì)大家有所幫助。如果感覺(jué)本文有幫助,有勞轉(zhuǎn)發(fā)或“點(diǎn)贊”、“在看”!讓更多人收獲知識(shí)!
長(zhǎng)按識(shí)別下圖二維碼,關(guān)注公眾號(hào)「Doocs 開(kāi)源社區(qū)」,第一時(shí)間跟你們分享好玩、實(shí)用的技術(shù)文章與業(yè)內(nèi)最新資訊。
