鍍金池/ 教程/ Java/ Netty
Struts2
Java 泛型
排序算法
Java 內(nèi)存管理
Webservice
Spring
輸入輸出流
Socket
字符串與數(shù)組
面向?qū)ο缶幊?/span>
海量數(shù)據(jù)處理
Hibernate
Netty
基本類型與運(yùn)算符
常見(jiàn)設(shè)計(jì)模式
Java 虛擬機(jī)
Java 多線程
JDBC
搭建 Java 開(kāi)發(fā)環(huán)境
Java 數(shù)據(jù)庫(kù)操作
異常處理
集合類
Servlet 與 JSP

Netty

Netty 系列之 Netty 高性能之道

1. 背景

1.1. 驚人的性能數(shù)據(jù)

最近一個(gè)圈內(nèi)朋友通過(guò)私信告訴我,通過(guò)使用 Netty4 + Thrift 壓縮二進(jìn)制編解碼技術(shù),他們實(shí)現(xiàn)了 10 W TPS(1 K 的復(fù)雜 POJO 對(duì)象)的跨節(jié)點(diǎn)遠(yuǎn)程服務(wù)調(diào)用。相比于傳統(tǒng)基于 Java 序列化 +BIO(同步阻塞 IO)的通信框架,性能提升了8倍多。

事實(shí)上,我對(duì)這個(gè)數(shù)據(jù)并不感到驚訝,根據(jù)我5年多的 NIO 編程經(jīng)驗(yàn),通過(guò)選擇合適的 NIO 框架,加上高性能的壓縮二進(jìn)制編解碼技術(shù),精心的設(shè)計(jì) Reactor 線程模型,達(dá)到上述性能指標(biāo)是完全有可能的。

下面我們就一起來(lái)看下 Netty 是如何支持 10 W TPS的跨節(jié)點(diǎn)遠(yuǎn)程服務(wù)調(diào)用的,在正式開(kāi)始講解之前,我們先簡(jiǎn)單介紹下 Netty。

1.2. Netty 基礎(chǔ)入門

Netty 是一個(gè)高性能、異步事件驅(qū)動(dòng)的 NIO 框架,它提供了對(duì) TCP、UDP 和文件傳輸?shù)闹С郑鳛橐粋€(gè)異步 NIO 框架,Netty 的所有 IO 操作都是異步非阻塞的,通過(guò) Future-Listener 機(jī)制,用戶可以方便的主動(dòng)獲取或者通過(guò)通知機(jī)制獲得 IO 操作結(jié)果。

作為當(dāng)前最流行的 NIO 框架,Netty 在互聯(lián)網(wǎng)領(lǐng)域、大數(shù)據(jù)分布式計(jì)算領(lǐng)域、游戲行業(yè)、通信行業(yè)等獲得了廣泛的應(yīng)用,一些業(yè)界著名的開(kāi)源組件也基于 Netty 的 NIO 框架構(gòu)建。

2. Netty 高性能之道

2.1. RPC 調(diào)用的性能模型分析

2.1.1. 傳統(tǒng) RPC 調(diào)用性能差的三宗罪

網(wǎng)絡(luò)傳輸方式問(wèn)題:傳統(tǒng)的 RPC 框架或者基于 RMI 等方式的遠(yuǎn)程服務(wù)(過(guò)程)調(diào)用采用了同步阻塞 IO,當(dāng)客戶端的并發(fā)壓力或者網(wǎng)絡(luò)時(shí)延增大之后,同步阻塞 IO 會(huì)由于頻繁的 wait 導(dǎo)致 IO 線程經(jīng)常性的阻塞,由于線程無(wú)法高效的工作,IO 處理能力自然下降。

下面,我們通過(guò) BIO 通信模型圖看下 BIO 通信的弊端:

http://wiki.jikexueyuan.com/project/java-special-topic/images/37.png" alt="" />

圖2-1 BIO 通信模型圖

采用 BIO 通信模型的服務(wù)端,通常由一個(gè)獨(dú)立的 Acceptor 線程負(fù)責(zé)監(jiān)聽(tīng)客戶端的連接,接收到客戶端連接之后為客戶端連接創(chuàng)建一個(gè)新的線程處理請(qǐng)求消息,處理完成之后,返回應(yīng)答消息給客戶端,線程銷毀,這就是典型的一請(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ī)。

序列化方式問(wèn)題: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ò)傳輸還是持久化到磁盤,都會(huì)導(dǎo)致額外的資源占用;

3) 序列化性能差(CPU 資源占用高)。

線程模型問(wèn)題:由于采用同步阻塞 IO,這會(huì)導(dǎo)致每個(gè) TCP 連接都占用1個(gè)線程,由于線程資源是 JVM 虛擬機(jī)非常寶貴的資源,當(dāng) IO 讀寫阻塞導(dǎo)致線程無(wú)法及時(shí)釋放時(shí),會(huì)導(dǎo)致系統(tǒng)性能急劇下降,嚴(yán)重的甚至?xí)?dǎo)致虛擬機(jī)無(wú)法創(chuàng)建新的線程。

2.1.2. 高性能的三個(gè)主題

1) 傳輸:用什么樣的通道將數(shù)據(jù)發(fā)送給對(duì)方,BIO、NIO 或者 AIO,IO 模型在很大程度上決定了框架的性能。

2) 協(xié)議:采用什么樣的通信協(xié)議,HTTP 或者內(nèi)部私有協(xié)議。協(xié)議的選擇不同,性能模型也不同。相比于公有協(xié)議,內(nèi)部私有協(xié)議的性能通??梢员辉O(shè)計(jì)的更優(yōu)。

3) 線程:數(shù)據(jù)報(bào)如何讀取?讀取之后的編解碼在哪個(gè)線程進(jìn)行,編解碼后的消息如何派發(fā), Reactor 線程模型的不同,對(duì)性能的影響也非常大。

http://wiki.jikexueyuan.com/project/java-special-topic/images/38.png" alt="" />

圖2-2 RPC 調(diào)用性能三要素

2.2. Netty 高性能之道

2.2.1. 異步非阻塞通信

在 IO 編程過(guò)程中,當(dāng)需要同時(shí)處理多個(gè)客戶端接入請(qǐng)求時(shí),可以利用多線程或者 IO 多路復(fù)用技術(shù)進(jìn)行處理。IO 多路復(fù)用技術(shù)通過(guò)把多個(gè) IO 的阻塞復(fù)用到同一個(gè) select 的阻塞上,從而使得系統(tǒng)在單線程的情況下可以同時(shí)處理多個(gè)客戶端請(qǐng)求。與傳統(tǒng)的多線程/多進(jìn)程模型比,I/O 多路復(fù)用的最大優(yōu)勢(shì)是系統(tǒng)開(kāi)銷小,系統(tǒng)不需要?jiǎng)?chuàng)建新的額外進(jìn)程或者線程,也不需要維護(hù)這些進(jìn)程和線程的運(yùn)行,降低了系統(tǒng)的維護(hù)工作量,節(jié)省了系統(tǒng)資源。

JDK1.4 提供了對(duì)非阻塞 IO(NIO)的支持,JDK1.5_update10 版本使用 epoll 替代了傳統(tǒng)的 select/poll,極大的提升了 NIO 通信的性能。

JDK NIO 通信模型如下所示:

http://wiki.jikexueyuan.com/project/java-special-topic/images/39.png" alt="" />

圖2-3 NIO 的多路復(fù)用模型圖

與 Socket 類和 ServerSocket 類相對(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ù)端通信序列圖如下:

http://wiki.jikexueyuan.com/project/java-special-topic/images/40.png" alt="" />

圖2-3 NIO 服務(wù)端通信序列圖

客戶端通信序列圖如下:

http://wiki.jikexueyuan.com/project/java-special-topic/images/41.png" alt="" />

圖2-4 NIO 客戶端通信序列圖

Netty 的 IO 線程 NioEventLoop 由于聚合了多路復(fù)用器 Selector,可以同時(shí)并發(fā)處理成百上千個(gè)客戶端 Channel,由于讀寫操作都是非阻塞的,這就可以充分提升 IO 線程的運(yùn)行效率,避免由于頻繁 IO 阻塞導(dǎo)致的線程掛起。另外,由于 Netty 采用了異步通信模式,一個(gè) IO 線程可以并發(fā)處理 N 個(gè)客戶端連接和讀寫操作,這從根本上解決了傳統(tǒng)同步阻塞 IO 一連接一線程模型,架構(gòu)的性能、彈性伸縮能力和可靠性都得到了極大的提升。

2.2.2. 零拷貝

很多用戶都聽(tīng)說(shuō)過(guò) Netty 具有“零拷貝”功能,但是具體體現(xiàn)在哪里又說(shuō)不清楚,本小節(jié)就詳細(xì)對(duì) Netty 的“零拷貝”功能進(jìn)行講解。

Netty 的“零拷貝”主要體現(xiàn)在如下三個(gè)方面:

1) Netty 的接收和發(fā)送 ByteBuffer 采用 DIRECT BUFFERS,使用堆外直接內(nèi)存進(jìn)行 Socket 讀寫,不需要進(jìn)行字節(jié)緩沖區(qū)的二次拷貝。如果使用傳統(tǒng)的堆內(nèi)存(HEAP BUFFERS)進(jìn)行 Socket 讀寫,JVM 會(huì)將堆內(nèi)存 Buffer 拷貝一份到直接內(nèi)存中,然后才寫入 Socket 中。相比于堆外直接內(nèi)存,消息在發(fā)送過(guò)程中多了一次緩沖區(qū)的內(nèi)存拷貝。

2) Netty 提供了組合 Buffer 對(duì)象,可以聚合多個(gè) ByteBuffer 對(duì)象,用戶可以像操作一個(gè) Buffer 那樣方便的對(duì)組合 Buffer 進(jìn)行操作,避免了傳統(tǒng)通過(guò)內(nèi)存拷貝的方式將幾個(gè)小 Buffer 合并成一個(gè)大的 Buffer。

3) 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)建:

http://wiki.jikexueyuan.com/project/java-special-topic/images/42.png" alt="" />

圖2-5 異步消息讀取“零拷貝”

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

http://wiki.jikexueyuan.com/project/java-special-topic/images/43.png" alt="" />

圖2-6 ByteBufAllocator 通過(guò) ioBuffer 分配堆外內(nèi)存

當(dāng)進(jìn)行 Socket IO 讀寫的時(shí)候,為了避免從堆內(nèi)存拷貝一份副本到直接內(nèi)存,Netty 的 ByteBuf 分配器直接創(chuàng)建非堆內(nèi)存避免緩沖區(qū)的二次拷貝,通過(guò)“零拷貝”來(lái)提升讀寫性能。

下面我們繼續(xù)看第二種“零拷貝”的實(shí)現(xiàn) CompositeByteBuf,它對(duì)外將多個(gè) ByteBuf 封裝成一個(gè) ByteBuf,對(duì)外提供統(tǒng)一封裝后的 ByteBuf 接口,它的類定義如下:

http://wiki.jikexueyuan.com/project/java-special-topic/images/44.png" alt="" />

圖2-7 CompositeByteBuf 類繼承關(guān)系

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

http://wiki.jikexueyuan.com/project/java-special-topic/images/45.png" alt="" />

圖2-8 CompositeByteBuf 類定義

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

http://wiki.jikexueyuan.com/project/java-special-topic/images/46.png" alt="" />

圖2-9 新增 ByteBuf 的“零拷貝”

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

http://wiki.jikexueyuan.com/project/java-special-topic/images/47.png" alt="" />

圖2-10 文件傳輸“零拷貝”

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

http://wiki.jikexueyuan.com/project/java-special-topic/images/48.png" alt="" />

圖2-11 文件傳輸 “零拷貝”

對(duì)于很多操作系統(tǒng)它直接將文件緩沖區(qū)的內(nèi)容發(fā)送到目標(biāo) Channel 中,而不需要通過(guò)拷貝的方式,這是一種更加高效的傳輸方式,它實(shí)現(xiàn)了文件傳輸?shù)摹傲憧截悺薄?/p>

2.2.3. 內(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):

http://wiki.jikexueyuan.com/project/java-special-topic/images/49.png" alt="" />

圖2-12 內(nèi)存池 ByteBuf

Netty 提供了多種內(nèi)存管理策略,通過(guò)在啟動(dòng)輔助類中配置相關(guān)參數(shù),可以實(shí)現(xiàn)差異化的定制。

下面通過(guò)性能測(cè)試,我們看下基于內(nèi)存池循環(huán)利用的 ByteBuf 和普通 ByteBuf 的性能差異。

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

http://wiki.jikexueyuan.com/project/java-special-topic/images/50.png" alt="" />

圖2-13 基于內(nèi)存池的非堆內(nèi)存緩沖區(qū)測(cè)試用例

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

http://wiki.jikexueyuan.com/project/java-special-topic/images/51.png" alt="" />

圖2-14 基于非內(nèi)存池創(chuàng)建的非堆內(nèi)存緩沖區(qū)測(cè)試用例

各執(zhí)行300萬(wàn)次,性能對(duì)比結(jié)果如下所示:

http://wiki.jikexueyuan.com/project/java-special-topic/images/52.png" alt="" />

圖2-15 內(nèi)存池和非內(nèi)存池緩沖區(qū)寫入性能對(duì)比

性能測(cè)試表明,采用內(nèi)存池的 ByteBuf 相比于朝生夕滅的 ByteBuf,性能高23倍左右(性能數(shù)據(jù)與使用場(chǎng)景強(qiáng)相關(guān))。

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

http://wiki.jikexueyuan.com/project/java-special-topic/images/53.png" alt="" />

圖2-16 AbstractByteBufAllocator 的緩沖區(qū)分配

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

http://wiki.jikexueyuan.com/project/java-special-topic/images/54.png" alt="" />

圖2-17 newDirectBuffer 的不同實(shí)現(xiàn)

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

http://wiki.jikexueyuan.com/project/java-special-topic/images/55.png" alt="" />

圖2-18 PooledByteBufAllocator 的內(nèi)存分配

PoolArena 的 allocate 方法如下:

http://wiki.jikexueyuan.com/project/java-special-topic/images/56.png" alt="" />

圖2-18 PoolArena 的緩沖區(qū)分配

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

http://wiki.jikexueyuan.com/project/java-special-topic/images/57.png" alt="" />

圖2-19 PoolArena 的 newByteBuf 抽象方法

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

http://wiki.jikexueyuan.com/project/java-special-topic/images/58.png" alt="" />

圖2-20 DirectArena 的 newByteBuf 方法實(shí)現(xiàn)

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

http://wiki.jikexueyuan.com/project/java-special-topic/images/59.png" alt="" />

圖2-21 PooledDirectByteBuf 的 newInstance 方法實(shí)現(xiàn)

通過(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)存回收(類似 JVM 垃圾回收機(jī)制)。

2.2.4. 高效的 Reactor 線程模型

常用的 Reactor 線程模型有三種,分別如下:

1) Reactor 單線程模型;

2) Reactor 多線程模型;

3) 主從 Reactor 多線程模型

Reactor 單線程模型,指的是所有的 IO 操作都在同一個(gè) NIO 線程上面完成,NIO 線程的職責(zé)如下:

1) 作為 NIO 服務(wù)端,接收客戶端的 TCP 連接;

2) 作為 NIO 客戶端,向服務(wù)端發(fā)起 TCP 連接;

3) 讀取通信對(duì)端的請(qǐng)求或者應(yīng)答消息;

4) 向通信對(duì)端發(fā)送消息請(qǐng)求或者應(yīng)答消息。

Reactor 單線程模型示意圖如下所示:

http://wiki.jikexueyuan.com/project/java-special-topic/images/60.png" alt="" />

圖2-22 Reactor 單線程模型

由于 Reactor 模式使用的是異步非阻塞 IO,所有的 IO 操作都不會(huì)導(dǎo)致阻塞,理論上一個(gè)線程可以獨(dú)立處理所有 IO 相關(guān)的操作。從架構(gòu)層面看,一個(gè) NIO 線程確實(shí)可以完成其承擔(dān)的職責(zé)。例如,通過(guò) Acceptor 接收客戶端的 TCP 連接請(qǐng)求消息,鏈路建立成功之后,通過(guò) Dispatch 將對(duì)應(yīng)的 ByteBuffer 派發(fā)到指定的 Handler 上進(jìn)行消息解碼。用戶 Handler 可以通過(guò) NIO 線程將消息發(fā)送給客戶端。

對(duì)于一些小容量應(yīng)用場(chǎng)景,可以使用單線程模型。但是對(duì)于高負(fù)載、大并發(fā)的應(yīng)用卻不合適,主要原因如下:

1) 一個(gè) NIO 線程同時(shí)處理成百上千的鏈路,性能上無(wú)法支撐,即便 NIO 線程的 CPU 負(fù)荷達(dá)到100%,也無(wú)法滿足海量消息的編碼、解碼、讀取和發(fā)送;

2) 當(dāng) NIO 線程負(fù)載過(guò)重之后,處理速度將變慢,這會(huì)導(dǎo)致大量客戶端連接超時(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 多線程模型,下面我們一起學(xué)習(xí)下 Reactor 多線程模型。

Rector 多線程模型與單線程模型最大的區(qū)別就是有一組 NIO 線程處理 IO 操作,它的原理圖如下:

http://wiki.jikexueyuan.com/project/java-special-topic/images/61.png" alt="" />

圖2-23 Reactor 多線程模型

Reactor 多線程模型的特點(diǎn):

1) 有專門一個(gè) NIO 線程-Acceptor 線程用于監(jiān)聽(tīng)服務(wù)端,接收客戶端的 TCP 連接請(qǐng)求;

2) 網(wǎng)絡(luò) IO 操作-讀、寫等由一個(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 多線程模型都可以滿足性能需求;但是,在極特殊應(yīng)用場(chǎng)景中,一個(gè) NIO 線程負(fù)責(zé)監(jiān)聽(tīng)和處理所有的客戶端連接可能會(huì)存在性能問(wèn)題。例如百萬(wàn)客戶端并發(fā)連接,或者服務(wù)端需要對(duì)客戶端的握手消息進(jìn)行安全認(rèn)證,認(rèn)證本身非常損耗性能。在這類場(chǎng)景下,單獨(dú)一個(gè) Acceptor 線程可能會(huì)存在性能不足問(wèn)題,為了解決性能問(wèn)題,產(chǎn)生了第三種 Reactor 線程模型-主從 Reactor 多線程模型。

主從 Reactor 線程模型的特點(diǎn)是:服務(wù)端用于接收客戶端連接的不再是個(gè)1個(gè)單獨(dú)的 NIO 線程,而是一個(gè)獨(dú)立的 NIO 線程池。Acceptor 接收到客戶端 TCP 連接請(qǐng)求處理完成后(可能包含接入認(rèn)證等),將新創(chuàng)建的 SocketChannel 注冊(cè)到 IO 線程池(sub reactor 線程池)的某個(gè) IO 線程上,由它負(fù)責(zé) SocketChannel 的讀寫和編解碼工作。Acceptor 線程池僅僅只用于客戶端的登陸、握手和安全認(rèn)證,一旦鏈路建立成功,就將鏈路注冊(cè)到后端 subReactor 線程池的 IO 線程上,由 IO 線程負(fù)責(zé)后續(xù)的 IO 操作。

它的線程模型如下圖所示:

http://wiki.jikexueyuan.com/project/java-special-topic/images/62.png" alt="" />

圖2-24 Reactor 主從多線程模型

利用主從 NIO 線程模型,可以解決1個(gè)服務(wù)端監(jiān)聽(tīng)線程無(wú)法有效處理所有客戶端連接的性能不足問(wèn)題。因此,在 Netty 的官方 demo 中,推薦使用該線程模型。

事實(shí)上,Netty 的線程模型并非固定不變,通過(guò)在啟動(dòng)輔助類中創(chuàng)建不同的 EventLoopGroup 實(shí)例并通過(guò)適當(dāng)?shù)膮?shù)配置,就可以支持上述三種 Reactor 線程模型。正是因?yàn)?Netty 對(duì) Reactor 線程模型的支持提供了靈活的定制能力,所以可以滿足不同業(yè)務(wù)場(chǎng)景的性能訴求。

2.2.5. 無(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 采用了串行無(wú)鎖化設(shè)計(jì),在 IO 線程內(nèi)部進(jìn)行串行操作,避免多線程競(jìng)爭(zhēng)導(dǎo)致的性能下降。表面上看,串行化設(shè)計(jì)似乎 CPU 利用率不高,并發(fā)程度不夠。但是,通過(guò)調(diào)整 NIO 線程池的線程參數(shù),可以同時(shí)啟動(dòng)多個(gè)串行化的線程并行運(yùn)行,這種局部無(wú)鎖化的串行線程設(shè)計(jì)相比一個(gè)隊(duì)列-多個(gè)工作線程模型性能更優(yōu)。

Netty 的串行化設(shè)計(jì)工作原理圖如下:

http://wiki.jikexueyuan.com/project/java-special-topic/images/63.png" alt="" />

圖2-25 Netty 串行化工作原理圖

Netty 的 NioEventLoop 讀取到消息之后,直接調(diào)用 ChannelPipeline 的 fireChannelRead(Object msg),只要用戶不主動(dòng)切換線程,一直會(huì)由 NioEventLoop 調(diào)用到用戶的 Handler,期間不進(jìn)行線程切換,這種串行化處理方式避免了多線程操作導(dǎo)致的鎖的競(jìng)爭(zhēng),從性能角度看是最優(yōu)的。

2.2.6. 高效的并發(fā)編程

Netty 的高效并發(fā)編程主要體現(xiàn)在如下幾點(diǎn):

1) volatile 的大量、正確使用;

2) CAS 和原子類的廣泛使用;

3) 線程安全容器的使用;

4) 通過(guò)讀寫鎖提升并發(fā)性能。

如果大家想了解 Netty 高效并發(fā)編程的細(xì)節(jié),可以閱讀之前我在微博分享的《多線程并發(fā)編程在 Netty 中的應(yīng)用分析》,在這篇文章中對(duì) Netty 的多線程技巧和應(yīng)用進(jìn)行了詳細(xì)的介紹和分析。

2.2.7. 高性能的序列化框架

影響序列化性能的關(guān)鍵因素總結(jié)如下:

1) 序列化后的碼流大?。ňW(wǎng)絡(luò)帶寬的占用);

2) 序列化&反序列化的性能(CPU 資源占用);

3) 是否支持跨語(yǔ)言(異構(gòu)系統(tǒng)的對(duì)接和開(kāi)發(fā)語(yǔ)言切換)。

Netty 默認(rèn)提供了對(duì) Google Protobuf 的支持,通過(guò)擴(kuò)展 Netty 的編解碼接口,用戶可以實(shí)現(xiàn)其它的高性能序列化框架,例如 Thrift 的壓縮二進(jìn)制編解碼框架。

下面我們一起看下不同序列化&反序列化框架序列化后的字節(jié)數(shù)組對(duì)比:

http://wiki.jikexueyuan.com/project/java-special-topic/images/64.png" alt="" />

圖2-26 各序列化框架序列化碼流大小對(duì)比

從上圖可以看出,Protobuf 序列化后的碼流只有 Java 序列化的1/4左右。正是由于 Java 原生序列化性能表現(xiàn)太差,才催生出了各種高性能的開(kāi)源序列化技術(shù)和框架(性能差只是其中的一個(gè)原因,還有跨語(yǔ)言、IDL 定義等其它因素)。

2.2.8. 靈活的 TCP 參數(shù)配置能力

合理設(shè)置 TCP 參數(shù)在某些場(chǎng)景下對(duì)于性能的提升可以起到顯著的效果,例如 SO_RCVBUF 和 SO_SNDBUF。如果設(shè)置不當(dāng),對(duì)性能的影響是非常大的。下面我們總結(jié)下對(duì)性能影響比較大的幾個(gè)配置項(xiàng):

1) SO_RCVBUF 和 SO_SNDBUF:通常建議值為 128 K 或者 256 K;

2) 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)化算法;

3) 軟中斷:如果 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)輔助類中可以靈活的配置 TCP 參數(shù),滿足不同的用戶場(chǎng)景。相關(guān)配置接口定義如下:

http://wiki.jikexueyuan.com/project/java-special-topic/images/65.png" alt="" />

圖2-27 Netty 的 TCP 參數(shù)配置定義

2.3. 總結(jié)

通過(guò)對(duì) Netty 的架構(gòu)和性能模型進(jìn)行分析,我們發(fā)現(xiàn) Netty 架構(gòu)的高性能是被精心設(shè)計(jì)和實(shí)現(xiàn)的,得益于高質(zhì)量的架構(gòu)和代碼,Netty 支持 10W TPS 的跨節(jié)點(diǎn)服務(wù)調(diào)用并不是件十分困難的事情。

  1. 作者簡(jiǎn)介

李林鋒,2007年畢業(yè)于東北大學(xué),2008年進(jìn)入華為公司從事高性能通信軟件的設(shè)計(jì)和開(kāi)發(fā)工作,有6年 NIO 設(shè)計(jì)和開(kāi)發(fā)經(jīng)驗(yàn),精通 Netty、Mina 等 NIO 框架。Netty 中國(guó)社區(qū)創(chuàng)始人,《Netty 權(quán)威指南》作者。

聯(lián)系方式:新浪微博 Nettying 微信:Nettying

上一篇:Webservice下一篇:Servlet 與 JSP