背景
通過選擇合適的 NIO 框架役纹,加上高性能的壓縮二進制編解碼技術,精心的設計 Reactor 線程模型看成,達到支持10W TPS的跨節(jié)點遠程服務調(diào)用垂睬。
定義
Netty 是一個高性能、異步事件驅(qū)動的 NIO 框架究履,它提供了對 TCP滤否、UDP 和文件傳輸?shù)闹С郑鳛橐粋€異步 NIO 框架最仑,Netty 的所有 IO 操作都是異步非阻塞的藐俺,通過 Future-Listener 機制,用戶可以方便的主動獲取或者通過通知機制獲得 IO 操作結(jié)果泥彤。
NIO知識準備
緩沖區(qū) Buffer:Buffer 是一個對象欲芹,它包含一些要寫入或者要讀出的數(shù)據(jù)。在 NIO 類庫中加入 Buffer 對象吟吝,體現(xiàn)了新庫與原 I/O 的一個重要區(qū)別菱父。在面向流的 I/O 中,可以將數(shù)據(jù)直接寫入或者將數(shù)據(jù)直接讀到 Stream 對象中剑逃。在 NIO 庫中浙宜,所有數(shù)據(jù)都是用緩沖區(qū)處理的。在讀取數(shù)據(jù)時炕贵,它是直接讀到緩沖區(qū)中的梆奈;在寫入數(shù)據(jù)時,寫入到緩沖區(qū)中称开。任何時候訪問 NIO 中的數(shù)據(jù)亩钟,都是通過緩沖區(qū)進行操作。
緩沖區(qū)實質(zhì)上是一個數(shù)組鳖轰。通常它是一個字節(jié)數(shù)組(ByteBuffer)清酥,也可以使用其他種類的數(shù)組。但是一個緩沖區(qū)不僅僅是一個數(shù)組蕴侣,緩沖區(qū)提供了對數(shù)據(jù)的結(jié)構(gòu)化訪問以及維護讀寫位置(limit)等信息焰轻。
最常用的緩沖區(qū)是 ByteBuffer歇终,一個 ByteBuffer 提供了一組功能用于操作 byte 數(shù)組礼仗。比較常用的就是 get 和 put 系列方法。
通道 Channel:Channel 是一個通道箱残,可以通過它讀取和寫入數(shù)據(jù)狞膘,它就像自來水管一樣揩懒,網(wǎng)絡數(shù)據(jù)通過 Channel 讀取和寫入。通道與流的不同之處在于通道是雙向的挽封,流只是在一個方向上移動(一個流必須是 InputStream 或者 OutputStream 的子類)已球,而且通道可以用于讀、寫或者同時用于讀寫。因為 Channel 是全雙工的智亮,所以它可以比流更好地映射底層操作系統(tǒng)的 API忆某。特別是在 UNIX 網(wǎng)絡編程模型中,底層操作系統(tǒng)的通道都是全雙工的阔蛉,同時支持讀寫操作弃舒。比較常用的 Channel 是 SocketChannel 和 ServerSocketChannel。
多路復用器 Selector:Selector 是 Java NIO 編程的基礎馍忽,熟練地掌握 Selector 對于掌握 NIO 編程至關重要棒坏。多路復用器提供選擇已經(jīng)就緒的任務的能力。簡單來講遭笋,Selector 會不斷地輪詢注冊在其上的 Channel坝冕,如果某個 Channel 上面有新的 TCP 連接接入、讀和寫事件瓦呼,這個 Channel 就處于就緒狀態(tài)喂窟,會被 Selector 輪詢出來,然后通過 SelectionKey 可以獲取就緒 Channel 的集合央串,進行后續(xù)的 I/O 操作磨澡。
RPC調(diào)用的性能模型分析
傳統(tǒng) RPC 調(diào)用性能差的三宗罪
網(wǎng)絡傳輸方式問題:傳統(tǒng)的 RPC 框架或者基于 RMI 等方式的遠程服務(過程)調(diào)用采用了同步阻塞 IO,當客戶端的并發(fā)壓力或者網(wǎng)絡時延增大之后质和,同步阻塞 IO 會由于頻繁的 wait 導致 IO 線程經(jīng)常性的阻塞稳摄,由于線程無法高效的工作,IO 處理能力自然下降饲宿。
采用 BIO 通信模型的服務端厦酬,通常由一個獨立的 Acceptor 線程負責監(jiān)聽客戶端的連接,接收到客戶端連接之后為客戶端連接創(chuàng)建一個新的線程處理請求消息瘫想,處理完成之后仗阅,返回應答消息給客戶端,線程銷毀国夜,這就是典型的一請求一應答模型减噪。該架構(gòu)最大的問題就是不具備彈性伸縮能力,當并發(fā)訪問量增加后车吹,服務端的線程個數(shù)和并發(fā)訪問數(shù)成線性正比筹裕,由于線程是 JAVA 虛擬機非常寶貴的系統(tǒng)資源,當線程數(shù)膨脹之后窄驹,系統(tǒng)的性能急劇下降饶碘,隨著并發(fā)量的繼續(xù)增加,可能會發(fā)生句柄溢出馒吴、線程堆棧溢出等問題,并導致服務器最終宕機。
序列化方式問題:Java 序列化存在如下幾個典型問題:
- Java 序列化機制是 Java 內(nèi)部的一種對象編解碼技術饮戳,無法跨語言使用豪治;例如對于異構(gòu)系統(tǒng)之間的對接,Java 序列化后的碼流需要能夠通過其它語言反序列化成原始對象(副本)扯罐,目前很難支持负拟;
- 相比于其它開源的序列化框架,Java 序列化后的碼流太大歹河,無論是網(wǎng)絡傳輸還是持久化到磁盤掩浙,都會導致額外的資源占用;
- 序列化性能差(CPU 資源占用高)秸歧。
線程模型問題:由于采用同步阻塞 IO厨姚,這會導致每個 TCP 連接都占用 1 個線程,由于線程資源是 JVM 虛擬機非常寶貴的資源键菱,當 IO 讀寫阻塞導致線程無法及時釋放時谬墙,會導致系統(tǒng)性能急劇下降,嚴重的甚至會導致虛擬機無法創(chuàng)建新的線程经备。
高性能的三大主題
- 傳輸:用什么樣的通道將數(shù)據(jù)發(fā)送給對方拭抬,BIO、NIO 或者 AIO侵蒙,IO 模型在很大程度上決定了框架的性能造虎。
- 協(xié)議:采用什么樣的通信協(xié)議,HTTP 或者內(nèi)部私有協(xié)議纷闺。協(xié)議的選擇不同算凿,性能模型也不同。相比于公有協(xié)議急但,內(nèi)部私有協(xié)議的性能通撑烀剑可以被設計的更優(yōu)。
- 線程:數(shù)據(jù)報如何讀炔ㄗ戒努?讀取之后的編解碼在哪個線程進行,編解碼后的消息如何派發(fā)镐躲,Reactor 線程模型的不同储玫,對性能的影響也非常大。
Netty 高性能之道
異步非阻塞IO
在 IO 編程過程中萤皂,當需要同時處理多個客戶端接入請求時撒穷,可以利用多線程或者 IO 多路復用技術進行處理。IO 多路復用技術通過把多個 IO 的阻塞復用到同一個 select 的阻塞上裆熙,從而使得系統(tǒng)在單線程的情況下可以同時處理多個客戶端請求端礼。與傳統(tǒng)的多線程 / 多進程模型比禽笑,I/O 多路復用的最大優(yōu)勢是系統(tǒng)開銷小,系統(tǒng)不需要創(chuàng)建新的額外進程或者線程蛤奥,也不需要維護這些進程和線程的運行佳镜,降低了系統(tǒng)的維護工作量,節(jié)省了系統(tǒng)資源凡桥。
JDK1.4 提供了對非阻塞 IO(NIO)的支持蟀伸,JDK1.5_update10 版本使用 epoll 替代了傳統(tǒng)的 select/poll,極大的提升了 NIO 通信的性能缅刽。
與 Socket 類和 ServerSocket 類相對應啊掏,NIO 也提供了 SocketChannel 和 ServerSocketChannel 兩種不同的套接字通道實現(xiàn)。這兩種新增的通道都支持阻塞和非阻塞兩種模式衰猛。阻塞模式使用非常簡單迟蜜,但是性能和可靠性都不好,非阻塞模式正好相反腕侄。開發(fā)人員一般可以根據(jù)自己的需要來選擇合適的模式小泉,一般來說,低負載冕杠、低并發(fā)的應用程序可以選擇同步阻塞 IO 以降低編程復雜度微姊。但是對于高負載、高并發(fā)的網(wǎng)絡應用分预,需要使用 NIO 的非阻塞模式進行開發(fā)兢交。
Netty 架構(gòu)按照 Reactor 模式設計和實現(xiàn),它的服務端通信序列圖如下:
客戶端通信序列圖如下:
總結(jié):Netty 的 IO 線程 NioEventLoop 由于聚合了多路復用器 Selector笼痹,可以同時并發(fā)處理成百上千個客戶端 Channel配喳,由于讀寫操作都是非阻塞的,這就可以充分提升 IO 線程的運行效率凳干,避免由于頻繁 IO 阻塞導致的線程掛起晴裹。另外,由于 Netty 采用了異步通信模式救赐,一個 IO 線程可以并發(fā)處理 N 個客戶端連接和讀寫操作涧团,這從根本上解決了傳統(tǒng)同步阻塞 IO 一連接一線程模型,架構(gòu)的性能经磅、彈性伸縮能力和可靠性都得到了極大的提升泌绣。
零拷貝
Netty 的“零拷貝”主要體現(xiàn)在如下三個方面:
Netty 的接收和發(fā)送 ByteBuffer 采用 DIRECT BUFFERS,使用堆外直接內(nèi)存進行 Socket 讀寫预厌,不需要進行字節(jié)緩沖區(qū)的二次拷貝阿迈。如果使用傳統(tǒng)的堆內(nèi)存(HEAP BUFFERS)進行 Socket 讀寫,JVM 會將堆內(nèi)存 Buffer 拷貝一份到直接內(nèi)存中轧叽,然后才寫入 Socket 中苗沧。相比于堆外直接內(nèi)存刊棕,消息在發(fā)送過程中多了一次緩沖區(qū)的內(nèi)存拷貝。
Netty 提供了組合 Buffer 對象崎页,可以聚合多個 ByteBuffer 對象鞠绰,用戶可以像操作一個 Buffer 那樣方便的對組合 Buffer 進行操作,避免了傳統(tǒng)通過內(nèi)存拷貝的方式將幾個小 Buffer 合并成一個大的 Buffer飒焦。
Netty 的文件傳輸采用了 transferTo 方法,它可以直接將文件緩沖區(qū)的數(shù)據(jù)發(fā)送到目標 Channel屿笼,避免了傳統(tǒng)通過循環(huán) write 方式導致的內(nèi)存拷貝問題牺荠。
下面,我們對上述三種“零拷貝”進行說明驴一,先看 Netty 接收 Buffer 的創(chuàng)建:
每循環(huán)讀取一次消息休雌,就通過 ByteBufAllocator 的 ioBuffer 方法獲取 ByteBuf 對象,下面繼續(xù)看它的接口定義:
當進行 Socket IO 讀寫的時候肝断,為了避免從堆內(nèi)存拷貝一份副本到直接內(nèi)存杈曲,Netty 的 ByteBuf 分配器直接創(chuàng)建非堆內(nèi)存避免緩沖區(qū)的二次拷貝,通過“零拷貝”來提升讀寫性能胸懈。(簡單來說就是使用堆外內(nèi)存進行Socket讀寫担扑,減少了一次堆內(nèi)存拷貝到直接內(nèi)存的操作)。
下面我們繼續(xù)看第二種“零拷貝”的實現(xiàn) CompositeByteBuf趣钱,它對外將多個 ByteBuf 封裝成一個 ByteBuf涌献,對外提供統(tǒng)一封裝后的 ByteBuf 接口,它的類定義如下:
通過繼承關系我們可以看出 CompositeByteBuf 實際就是個 ByteBuf 的包裝器首有,它將多個 ByteBuf 組合成一個集合燕垃,然后對外提供統(tǒng)一的 ByteBuf 接口,相關定義如下:
添加 ByteBuf井联,不需要做內(nèi)存拷貝卜壕,相關代碼如下:
最后,我們看下文件傳輸?shù)摹傲憧截悺保?/p>
Netty 文件傳輸 DefaultFileRegion 通過 transferTo 方法將文件發(fā)送到目標 Channel 中烙常,下面重點看 FileChannel 的 transferTo 方法轴捎,它的 API DOC 說明如下:
對于很多操作系統(tǒng)它直接將文件緩沖區(qū)的內(nèi)容發(fā)送到目標 Channel 中,而不需要通過拷貝的方式军掂,這是一種更加高效的傳輸方式轮蜕,它實現(xiàn)了文件傳輸?shù)摹傲憧截悺薄?
內(nèi)存池
隨著 JVM 虛擬機和 JIT 即時編譯技術的發(fā)展,對象的分配和回收是個非常輕量級的工作蝗锥。但是對于緩沖區(qū) Buffer跃洛,情況卻稍有不同,特別是對于堆外直接內(nèi)存的分配和回收终议,是一件耗時的操作汇竭。為了盡量重用緩沖區(qū)葱蝗,Netty 提供了基于內(nèi)存池的緩沖區(qū)重用機制。下面我們一起看下 Netty ByteBuf 的實現(xiàn):
Netty 提供了多種內(nèi)存管理策略细燎,通過在啟動輔助類中配置相關參數(shù)两曼,可以實現(xiàn)差異化的定制。
下面通過性能測試玻驻,我們看下基于內(nèi)存池循環(huán)利用的 ByteBuf 和普通 ByteBuf 的性能差異悼凑。
用例一,使用內(nèi)存池分配器創(chuàng)建直接內(nèi)存緩沖區(qū):
用例二璧瞬,使用非堆內(nèi)存分配器創(chuàng)建的直接內(nèi)存緩沖區(qū):
各執(zhí)行 300 萬次户辫,性能對比結(jié)果如下所示:性能測試表明,采用內(nèi)存池的 ByteBuf 相比于朝生夕滅的 ByteBuf渔欢,性能高 23 倍左右(性能數(shù)據(jù)與使用場景強相關)。
下面我們一起簡單分析下 Netty 內(nèi)存池的內(nèi)存分配:
繼續(xù)看 newDirectBuffer 方法垫挨,我們發(fā)現(xiàn)它是一個抽象方法漾峡,由 AbstractByteBufAllocator 的子類負責具體實現(xiàn)遍尺,代碼如下:
代碼跳轉(zhuǎn)到 PooledByteBufAllocator 的 newDirectBuffer 方法就漾,從 Cache 中獲取內(nèi)存區(qū)域 PoolArena念搬,調(diào)用它的 allocate 方法進行內(nèi)存分配:
PoolArena 的 allocate 方法如下
我們重點分析 newByteBuf 的實現(xiàn)抑堡,它同樣是個抽象方法,由子類 DirectArena 和 HeapArena 來實現(xiàn)不同類型的緩沖區(qū)分配锁蠕,由于測試用例使用的是堆外內(nèi)存夷野,
因此重點分析 DirectArena 的實現(xiàn):如果沒有開啟使用 sun 的 unsafe,則
執(zhí)行 PooledDirectByteBuf 的 newInstance 方法荣倾,代碼如下:
通過 RECYCLER 的 get 方法循環(huán)使用 ByteBuf 對象,如果是非內(nèi)存池實現(xiàn)骑丸,則直接創(chuàng)建一個新的 ByteBuf 對象舌仍。從緩沖池中獲取 ByteBuf 之后,調(diào)用 AbstractReferenceCountedByteBuf 的 setRefCnt 方法設置引用計數(shù)器通危,用于對象的引用計數(shù)和內(nèi)存回收(類似 JVM 垃圾回收機制)铸豁。
高效的 Reactor 線程模型
常用的 Reactor 線程模型有三種,Reactor 單線程模型菊碟;Reactor 多線程模型节芥;主從 Reactor 多線程模型。
Reactor 單線程模型逆害,指的是所有的 IO 操作都在同一個 NIO 線程上面完成头镊,NIO 線程的職責如下:
作為 NIO 服務端,接收客戶端的 TCP 連接魄幕;
作為 NIO 客戶端相艇,向服務端發(fā)起 TCP 連接;
讀取通信對端的請求或者應答消息纯陨;
向通信對端發(fā)送消息請求或者應答消息坛芽。
Reactor 單線程模型示意圖如下所示:
由于 Reactor 模式使用的是異步非阻塞 IO,所有的 IO 操作都不會導致阻塞翼抠,理論上一個線程可以獨立處理所有 IO 相關的操作咙轩。從架構(gòu)層面看,一個 NIO 線程確實可以完成其承擔的職責阴颖。例如活喊,通過 Acceptor 接收客戶端的 TCP 連接請求消息,鏈路建立成功之后膘盖,通過 Dispatch 將對應的 ByteBuffer 派發(fā)到指定的 Handler 上進行消息解碼胧弛。用戶 Handler 可以通過 NIO 線程將消息發(fā)送給客戶端尤误。
對于一些小容量應用場景,可以使用單線程模型结缚。但是對于高負載损晤、大并發(fā)的應用卻不合適,主要原因如下:
一個 NIO 線程同時處理成百上千的鏈路红竭,性能上無法支撐尤勋,即便 NIO 線程的 CPU 負荷達到 100%,也無法滿足海量消息的編碼茵宪、解碼最冰、讀取和發(fā)送;
當 NIO 線程負載過重之后稀火,處理速度將變慢暖哨,這會導致大量客戶端連接超時,超時之后往往會進行重發(fā)凰狞,這更加重了 NIO 線程的負載篇裁,最終會導致大量消息積壓和處理超時,NIO 線程會成為系統(tǒng)的性能瓶頸赡若;
可靠性問題:一旦 NIO 線程意外跑飛达布,或者進入死循環(huán),會導致整個系統(tǒng)通信模塊不可用逾冬,不能接收和處理外部消息黍聂,造成節(jié)點故障。
為了解決這些問題身腻,演進出了 Reactor 多線程模型产还,下面我們一起學習下 Reactor 多線程模型。
Rector 多線程模型與單線程模型最大的區(qū)別就是有一組 NIO 線程處理 IO 操作霸株,它的原理圖如下:
有專門一個 NIO 線程 -Acceptor 線程用于監(jiān)聽服務端雕沉,接收客戶端的 TCP 連接請求;
網(wǎng)絡 IO 操作 - 讀去件、寫等由一個 NIO 線程池負責坡椒,線程池可以采用標準的 JDK 線程池實現(xiàn),它包含一個任務隊列和 N 個可用的線程尤溜,由這些 NIO 線程負責消息的讀取倔叼、解碼、編碼和發(fā)送宫莱;
1 個 NIO 線程可以同時處理 N 條鏈路丈攒,但是 1 個鏈路只對應 1 個 NIO 線程,防止發(fā)生并發(fā)操作問題。
在絕大多數(shù)場景下巡验,Reactor 多線程模型都可以滿足性能需求际插;但是,在極特殊應用場景中显设,一個 NIO 線程負責監(jiān)聽和處理所有的客戶端連接可能會存在性能問題框弛。例如百萬客戶端并發(fā)連接,或者服務端需要對客戶端的握手消息進行安全認證捕捂,認證本身非常損耗性能瑟枫。在這類場景下,單獨一個 Acceptor 線程可能會存在性能不足問題指攒,為了解決性能問題慷妙,產(chǎn)生了第三種 Reactor 線程模型 - 主從 Reactor 多線程模型。
主從 Reactor 線程模型的特點是:服務端用于接收客戶端連接的不再是個 1 個單獨的 NIO 線程允悦,而是一個獨立的 NIO 線程池膝擂。Acceptor 接收到客戶端 TCP 連接請求處理完成后(可能包含接入認證等),將新創(chuàng)建的 SocketChannel 注冊到 IO 線程池(sub reactor 線程池)的某個 IO 線程上隙弛,由它負責 SocketChannel 的讀寫和編解碼工作猿挚。Acceptor 線程池僅僅只用于客戶端的登陸、握手和安全認證驶鹉,一旦鏈路建立成功,就將鏈路注冊到后端 subReactor 線程池的 IO 線程上铣墨,由 IO 線程負責后續(xù)的 IO 操作室埋。它的線程模型如下圖所示:
利用主從 NIO 線程模型,可以解決 1 個服務端監(jiān)聽線程無法有效處理所有客戶端連接的性能不足問題伊约。因此姚淆,在 Netty 的官方 demo 中,推薦使用該線程模型屡律。
事實上腌逢,Netty 的線程模型并非固定不變,通過在啟動輔助類中創(chuàng)建不同的 EventLoopGroup 實例并通過適當?shù)膮?shù)配置超埋,就可以支持上述三種 Reactor 線程模型搏讶。正是因為 Netty 對 Reactor 線程模型的支持提供了靈活的定制能力,所以可以滿足不同業(yè)務場景的性能訴求霍殴。
無鎖化的串行設計理念
在大多數(shù)場景下媒惕,并行多線程處理可以提升系統(tǒng)的并發(fā)性能。但是来庭,如果對于共享資源的并發(fā)訪問處理不當妒蔚,會帶來嚴重的鎖競爭,這最終會導致性能的下降。為了盡可能的避免鎖競爭帶來的性能損耗肴盏,可以通過串行化設計科盛,即消息的處理盡可能在同一個線程內(nèi)完成,期間不進行線程切換菜皂,這樣就避免了多線程競爭和同步鎖贞绵。
為了盡可能提升性能,Netty 采用了串行無鎖化設計幌墓,在 IO 線程內(nèi)部進行串行操作但壮,避免多線程競爭導致的性能下降。表面上看常侣,串行化設計似乎 CPU 利用率不高蜡饵,并發(fā)程度不夠。但是胳施,通過調(diào)整 NIO 線程池的線程參數(shù)溯祸,可以同時啟動多個串行化的線程并行運行,這種局部無鎖化的串行線程設計相比一個隊列 - 多個工作線程模型性能更優(yōu)舞肆。
Netty 的串行化設計工作原理圖如下
Netty 的 NioEventLoop 讀取到消息之后焦辅,直接調(diào)用 ChannelPipeline 的 fireChannelRead(Object msg),只要用戶不主動切換線程椿胯,一直會由 NioEventLoop 調(diào)用到用戶的 Handler筷登,期間不進行線程切換,這種串行化處理方式避免了多線程操作導致的鎖的競爭哩盲,從性能角度看是最優(yōu)的前方。
高效的并發(fā)編程
Netty 的高效并發(fā)編程主要體現(xiàn)在如下幾點:
volatile 的大量、正確使用 ;
CAS 和原子類的廣泛使用廉油;
線程安全容器的使用惠险;
通過讀寫鎖提升并發(fā)性能。
詳見https://blog.csdn.net/weixin_42322850/article/details/89892596
高性能的序列化框架
影響序列化性能的關鍵因素總結(jié)如下:
序列化后的碼流大惺阆摺(網(wǎng)絡帶寬的占用)班巩;
序列化 & 反序列化的性能(CPU 資源占用);
是否支持跨語言(異構(gòu)系統(tǒng)的對接和開發(fā)語言切換)嘶炭。
Netty 默認提供了對 Google Protobuf 的支持(Protobuf 序列化后的碼流只有 Java 序列化的 1/4 左右)抱慌,通過擴展 Netty 的編解碼接口,用戶可以實現(xiàn)其它的高性能序列化框架旱物,例如 Thrift 的壓縮二進制編解碼框架遥缕。
靈活的 TCP 參數(shù)配置能力
合理設置 TCP 參數(shù)在某些場景下對于性能的提升可以起到顯著的效果,例如 SO_RCVBUF 和 SO_SNDBUF宵呛。如果設置不當弦悉,對性能的影響是非常大的。下面我們總結(jié)下對性能影響比較大的幾個配置項:
SO_RCVBUF 和 SO_SNDBUF:通常建議值為 128K 或者 256K柠偶;
SO_TCPNODELAY:NAGLE 算法通過將緩沖區(qū)內(nèi)的小封包自動相連,組成較大的封包码秉,阻止大量小封包的發(fā)送阻塞網(wǎng)絡,從而提高網(wǎng)絡應用效率鸡号。但是對于時延敏感的應用場景需要關閉該優(yōu)化算法转砖;
軟中斷:如果 Linux 內(nèi)核版本支持 RPS(2.6.35 以上版本),開啟 RPS 后可以實現(xiàn)軟中斷鲸伴,提升網(wǎng)絡吞吐量府蔗。RPS 根據(jù)數(shù)據(jù)包的源地址,目的地址以及目的和源端口汞窗,計算出一個 hash 值姓赤,然后根據(jù)這個 hash 值來選擇軟中斷運行的 cpu,從上層來看仲吏,也就是說將每個連接和 cpu 綁定不铆,并通過這個 hash 值,來均衡軟中斷在多個 cpu 上裹唆,提升網(wǎng)絡并行處理性能誓斥。
Netty 在啟動輔助類中可以靈活的配置 TCP 參數(shù),滿足不同的用戶場景许帐。相關配置接口定義如下:
Netty關鍵類庫
Netty 的核心類庫可以分為 5 大類:
1劳坑、ByteBuf 和相關輔助類:ByteBuf 是個 Byte 數(shù)組的緩沖區(qū),它的基本功能應該與 JDK 的 ByteBuffer 一致成畦,提供以下幾類基本功能:
- 7 種 Java 基礎類型泡垃、byte 數(shù)組、ByteBuffer(ByteBuf)等的讀寫羡鸥。
- 緩沖區(qū)自身的 copy 和 slice 等。
- 設置網(wǎng)絡字節(jié)序忠寻。
- 構(gòu)造緩沖區(qū)實例惧浴。
- 操作位置指針等方法。
- 動態(tài)的擴展和收縮奕剃。
從內(nèi)存分配的角度看衷旅,ByteBuf 可以分為兩類:堆內(nèi)存(HeapByteBuf)字節(jié)緩沖區(qū):特點是內(nèi)存的分配和回收速度快,可以被 JVM 自動回收纵朋;缺點就是如果進行 Socket 的 I/O 讀寫柿顶,需要額外做一次內(nèi)存復制,將堆內(nèi)存對應的緩沖區(qū)復制到內(nèi)核 Channel 中操软,性能會有一定程度的下降嘁锯。直接內(nèi)存(DirectByteBuf)字節(jié)緩沖區(qū):非堆內(nèi)存,它在堆外進行內(nèi)存分配,相比于堆內(nèi)存家乘,它的分配和回收速度會慢一些蝗羊,但是將它寫入或者從 Socket Channel 中讀取時,由于少了一次內(nèi)存復制仁锯,速度比堆內(nèi)存快耀找。
2、Channel 和 Unsafe:io.netty.channel.Channel 是 Netty 網(wǎng)絡操作抽象類业崖,它聚合了一組功能野芒,包括但不限于網(wǎng)路的讀、寫双炕,客戶端發(fā)起連接狞悲、主動關閉連接,鏈路關閉雄家,獲取通信雙方的網(wǎng)絡地址等效诅。它也包含了 Netty 框架相關的一些功能,包括獲取該 Chanel 的 EventLoop趟济,獲取緩沖分配器 ByteBufAllocator 和 pipeline 等乱投。Unsafe 是個內(nèi)部接口,聚合在 Channel 中協(xié)助進行網(wǎng)絡讀寫相關的操作顷编,它提供的主要功能如下表所示:
3戚炫、ChannelPipeline 和 ChannelHandler: Netty 的 ChannelPipeline 和 ChannelHandler 機制類似于 Servlet 和 Filter 過濾器,這類攔截器實際上是職責鏈模式的一種變形媳纬,主要是為了方便事件的攔截和用戶業(yè)務邏輯的定制双肤。Servlet Filter 是 JEE Web 應用程序級的 Java 代碼組件,它能夠以聲明的方式插入到 HTTP 請求響應的處理過程中钮惠,用于攔截請求和響應茅糜,以便能夠查看、提取或以某種方式操作正在客戶端和服務器之間交換的數(shù)據(jù)素挽。
攔截器封裝了業(yè)務定制邏輯蔑赘,能夠?qū)崿F(xiàn)對 Web 應用程序的預處理和事后處理。過濾器提供了一種面向?qū)ο蟮哪K化機制预明,用來將公共任務封裝到可插入的組件中缩赛。
這些組件通過 Web 部署配置文件(web.xml)進行聲明,可以方便地添加和刪除過濾器撰糠,無須改動任何應用程序代碼或 JSP 頁面酥馍,由 Servlet 進行動態(tài)調(diào)用。通過在請求 / 響應鏈中使用過濾器阅酪,可以對應用程序(而不是以任何方式替代)的 Servlet 或 JSP 頁面提供的核心處理進行補充旨袒,而不破壞 Servlet 或 JSP 頁面的功能汁针。由于是純 Java 實現(xiàn),所以 Servlet 過濾器具有跨平臺的可重用性峦失,使得它們很容易地被部署到任何符合 Servlet 規(guī)范的 JEE 環(huán)境中扇丛。
Netty 的 Channel 過濾器實現(xiàn)原理與 Servlet Filter 機制一致,它將 Channel 的數(shù)據(jù)管道抽象為 ChannelPipeline尉辑,消息在 ChannelPipeline 中流動和傳遞帆精。ChannelPipeline 持有 I/O 事件攔截器 ChannelHandler 的鏈表,由 ChannelHandler 對 I/O 事件進行攔截和處理隧魄,可以方便地通過新增和刪除 ChannelHandler 來實現(xiàn)不同的業(yè)務邏輯定制卓练,不需要對已有的 ChannelHandler 進行修改,能夠?qū)崿F(xiàn)對修改封閉和對擴展的支持购啄。ChannelPipeline 是 ChannelHandler 的容器襟企,它負責 ChannelHandler 的管理和事件攔截與調(diào)度:
Netty 中的事件分為 inbound 事件和 outbound 事件。inbound 事件通常由 I/O 線程觸發(fā)狮含,例如 TCP 鏈路建立事件顽悼、鏈路關閉事件、讀事件几迄、異常通知事件等蔚龙。
Outbound 事件通常是由用戶主動發(fā)起的網(wǎng)絡 I/O 操作,例如用戶發(fā)起的連接操作映胁、綁定操作木羹、消息發(fā)送等操作。ChannelHandler 類似于 Servlet 的 Filter 過濾器解孙,負責對 I/O 事件或者 I/O 操作進行攔截和處理坑填,它可以選擇性地攔截和處理自己感興趣的事件,也可以透傳和終止事件的傳遞弛姜∑旯澹基于 ChannelHandler 接口,用戶可以方便地進行業(yè)務邏輯定制廷臼,例如打印日志蚪黑、統(tǒng)一封裝異常信息、性能統(tǒng)計和消息編解碼等中剩。
4、EventLoop:Netty 的 NioEventLoop 并不是一個純粹的 I/O 線程抒寂,它除了負責 I/O 的讀寫之外结啼,還兼顧處理以下兩類任務:
普通 Task:通過調(diào)用 NioEventLoop 的 execute(Runnable task) 方法實現(xiàn),Netty 有很多系統(tǒng) Task屈芜,創(chuàng)建它們的主要原因是:當 I/O 線程和用戶線程同時操作網(wǎng)絡資源時郊愧,為了防止并發(fā)操作導致的鎖競爭朴译,將用戶線程的操作封裝成 Task 放入消息隊列中,由 I/O 線程負責執(zhí)行属铁,這樣就實現(xiàn)了局部無鎖化眠寿。
定時任務:通過調(diào)用 NioEventLoop 的 schedule(Runnable command, long delay, TimeUnit unit) 方法實現(xiàn)。
Netty 的線程模型并不是一成不變的焦蘑,它實際取決于用戶的啟動參數(shù)配置盯拱。通過設置不同的啟動參數(shù),Netty 可以同時支持 Reactor 單線程模型例嘱、多線程模型和主從 Reactor 多線層模型狡逢。它的工作原理如下所示:
通過調(diào)整線程池的線程個數(shù)、是否共享線程池等方式拼卵,Netty 的 Reactor 線程模型可以在單線程奢浑、多線程和主從多線程間切換,這種靈活的配置方式可以最大程度地滿足不同用戶的個性化定制腋腮。
為了盡可能地提升性能雀彼,Netty 在很多地方進行了無鎖化的設計,例如在 I/O 線程內(nèi)部進行串行操作即寡,避免多線程競爭導致的性能下降問題徊哑。表面上看,串行化設計似乎 CPU 利用率不高嘿悬,并發(fā)程度不夠实柠。但是,通過調(diào)整 NIO 線程池的線程參數(shù)善涨,可以同時啟動多個串行化的線程并行運行窒盐,這種局部無鎖化的串行線程設計相比一個隊列—多個工作線程的模型性能更優(yōu)。它的設計原理如下圖所示:
5钢拧、Future 和 Promise:在 Netty 中蟹漓,所有的 I/O 操作都是異步的,這意味著任何 I/O 調(diào)用都會立即返回源内,而不是像傳統(tǒng) BIO 那樣同步等待操作完成葡粒。異步操作會帶來一個問題:調(diào)用者如何獲取異步操作的結(jié)果? ChannelFuture 就是為了解決這個問題而專門設計的膜钓。下面我們一起看它的原理嗽交。ChannelFuture 有兩種狀態(tài):uncompleted 和 completed。當開始一個 I/O 操作時颂斜,一個新的 ChannelFuture 被創(chuàng)建夫壁,此時它處于 uncompleted 狀態(tài)——非失敗、非成功沃疮、非取消盒让,因為 I/O 操作此時還沒有完成梅肤。一旦 I/O 操作完成,ChannelFuture 將會被設置成 completed邑茄,它的結(jié)果有如下三種可能:
- 操作成功姨蝴。
- 操作失敗。
- 操作被取消肺缕。
ChannelFuture 的狀態(tài)遷移圖如下所示:
Promise 是可寫的 Future左医,F(xiàn)uture 自身并沒有寫操作相關的接口,Netty 通過 Promise 對 Future 進行擴展搓谆,用于設置 I/O 操作的結(jié)果炒辉,它的接口定義如下:
Netty關鍵流程
重點掌握 Netty 服務端和客戶端的創(chuàng)建,以及創(chuàng)建過程中使用到的核心類庫和 API泉手、以及消息的發(fā)送和接收黔寇、消息的編解碼。
Netty 服務端創(chuàng)建流程如下:
Netty 客戶端創(chuàng)建流程如下:
Netty項目實戰(zhàn)
如果項目中需要用到 Netty斩萌,則直接在項目中應用缝裤,通過實踐來不斷提升對 Netty 的理解和掌握。如果暫時使用不到颊郎,則可以通過學習一些開源的 RPC 或者服務框架憋飞,看這些框架是怎么集成并使用 Netty 的。以 gRPC Java 版為例姆吭,我們一起看下 gRPC 是如何使用 Netty 的榛做。
gRPC 服務端
gRPC 通過對 Netty HTTP/2 的封裝,向用戶屏蔽底層 RPC 通信的協(xié)議細節(jié)内狸,Netty HTTP/2 服務端的創(chuàng)建流程如下:
服務端 HTTP/2 消息的讀寫主要通過 gRPC 的 NettyServerHandler 實現(xiàn)检眯,它的類繼承關系如下所示:
從類繼承關系可以看出,NettyServerHandler 主要負責 HTTP/2 協(xié)議消息相關的處理昆淡,例如 HTTP/2 請求消息體和消息頭的讀取锰瘸、Frame 消息的發(fā)送、Stream 狀態(tài)消息的處理等昂灵,相關接口定義如下:
gRPC 客戶端
gRPC 的客戶端調(diào)用主要包括基于 Netty 的 HTTP/2 客戶端創(chuàng)建避凝、客戶端負載均衡、請求消息的發(fā)送和響應接收處理四個流程,gRPC 的客戶端調(diào)用總體流程如下圖所示:
gRPC 的客戶端調(diào)用流程如下:
- 客戶端 Stub(GreeterBlockingStub) 調(diào)用 sayHello(request)眨补,發(fā)起 RPC 調(diào)用管削。
- 通過 DnsNameResolver 進行域名解析,獲取服務端的地址信息(列表)撑螺,隨后使用默認的 LoadBalancer 策略含思,選擇一個具體的 gRPC 服務端實例。
- 如果與路由選中的服務端之間沒有可用的連接实蓬,則創(chuàng)建 NettyClientTransport 和 NettyClientHandler茸俭,發(fā)起 HTTP/2 連接。
- 對請求消息使用 PB(Protobuf)做序列化安皱,通過 HTTP/2 Stream 發(fā)送給 gRPC 服務端调鬓。
- 接收到服務端響應之后,使用 PB(Protobuf)做反序列化酌伊。
- 回調(diào) GrpcFuture 的 set(Response) 方法腾窝,喚醒阻塞的客戶端調(diào)用線程,獲取 RPC 響應居砖。
需要指出的是虹脯,客戶端同步阻塞 RPC 調(diào)用阻塞的是調(diào)用方線程(通常是業(yè)務線程),底層 Transport 的 I/O 線程(Netty 的 NioEventLoop)仍然是非阻塞的奏候。
線程模型
gRPC 服務端線程模型整體上可以分為兩大類:
1. 網(wǎng)絡通信相關的線程模型循集,基于 Netty4.1 的線程模型實現(xiàn)。
2. 服務接口調(diào)用線程模型蔗草,基于 JDK 線程池實現(xiàn)咒彤。
gRPC 服務端線程模型和交互圖如下所示:
其中,HTTP/2 服務端創(chuàng)建咒精、HTTP/2 請求消息的接入和響應發(fā)送都由 Netty 負責镶柱,gRPC 消息的序列化和反序列化、以及應用服務接口的調(diào)用由 gRPC 的 SerializingExecutor 線程池負責模叙。
gRPC 客戶端的線程主要分為三類:
業(yè)務調(diào)用線程
客戶端連接和 I/O 讀寫線程
請求消息業(yè)務處理和響應回調(diào)線程
gRPC 客戶端線程模型工作原理如下圖所示(同步阻塞調(diào)用為例):
客戶端調(diào)用主要涉及的線程包括:
應用線程歇拆,負責調(diào)用 gRPC 服務端并獲取響應,其中請求消息的序列化由該線程負責范咨。
客戶端負載均衡以及 Netty Client 創(chuàng)建故觅,由 grpc-default-executor 線程池負責。
HTTP/2 客戶端鏈路創(chuàng)建湖蜕、網(wǎng)絡 I/O 數(shù)據(jù)的讀寫逻卖,由 Netty NioEventLoop 線程負責。
響應消息的反序列化由 SerializingExecutor 負責昭抒,與服務端不同的是评也,客戶端使用的是 ThreadlessExecutor,并非 JDK 線程池灭返。
SerializingExecutor 通過調(diào)用 responseFuture 的 set(value)盗迟,喚醒阻塞的應用線程,完成一次 RPC 調(diào)用熙含。
gRPC 采用的是網(wǎng)絡 I/O 線程和業(yè)務調(diào)用線程分離的策略罚缕,大部分場景下該策略是最優(yōu)的。但是怎静,對于那些接口邏輯非常簡單邮弹,執(zhí)行時間很短黔衡,不需要與外部網(wǎng)元交互、訪問數(shù)據(jù)庫和磁盤腌乡,也不需要等待其它資源的盟劫,則建議接口調(diào)用直接在 Netty /O 線程中執(zhí)行,不需要再投遞到后端的服務線程池与纽。避免線程上下文切換侣签,同時也消除了線程并發(fā)問題。
例如提供配置項或者接口急迂,系統(tǒng)默認將消息投遞到后端服務調(diào)度線程影所,但是也支持短路策略,直接在 Netty 的 NioEventLoop 中執(zhí)行消息的序列化和反序列化僚碎、以及服務接口調(diào)用猴娩。
減少鎖競爭優(yōu)化:當前 gRPC 的線程切換策略如下:
優(yōu)化之后的 gRPC 線程切換策略:
通過線程綁定技術(例如采用一致性 hash 做映射), 將 Netty 的 I/O 線程與后端的服務調(diào)度線程做綁定,1 個 I/O 線程綁定一個或者多個服務調(diào)用線程听盖,降低鎖競爭胀溺,提升性能。
Netty 故障定位技巧
接收不到消息
如果業(yè)務的 ChannelHandler 接收不到消息皆看,可能的原因如下:
業(yè)務的解碼 ChannelHandler 存在 BUG仓坞,導致消息解碼失敗,沒有投遞到后端腰吟。
業(yè)務發(fā)送的是畸形或者錯誤碼流(例如長度錯誤)无埃,導致業(yè)務解碼 ChannelHandler 無法正確解碼出業(yè)務消息。
業(yè)務 ChannelHandler 執(zhí)行了一些耗時或者阻塞操作毛雇,導致 Netty 的 NioEventLoop 被掛住嫉称,無法讀取消息。
執(zhí)行業(yè)務 ChannelHandler 的線程池隊列積壓灵疮,導致新接收的消息在排隊织阅,沒有得到及時處理。
對方確實沒有發(fā)送消息震捣。
定位策略如下:
在業(yè)務的首個 ChannelHandler 的 channelRead 方法中打斷點調(diào)試荔棉,看是否讀取到消息。
在 ChannelHandler 中添加 LoggingHandler蒿赢,打印接口日志润樱。
查看 NioEventLoop 線程狀態(tài),看是否發(fā)生了阻塞羡棵。
通過 tcpdump 抓包看消息是否發(fā)送成功壹若。
內(nèi)存泄漏
通過 jmap -dump:format=b,file=xx pid 命令 Dump 內(nèi)存堆棧,然后使用 MemoryAnalyzer 工具對內(nèi)存占用進行分析,查找內(nèi)存泄漏點店展,然后結(jié)合代碼進行分析养篓,定位內(nèi)存泄漏的具體原因,示例如下所示:
性能問題如果出現(xiàn)性能問題赂蕴,首先需要確認是 Netty 問題還是業(yè)務問題觉至,通過 jstack 命令或者 jvisualvm 工具打印線程堆棧,按照線程 CPU 使用率進行排序(top -Hp 命令采集)睡腿,看線程在忙什么。通常如果采集幾次都發(fā)現(xiàn) Netty 的 NIO 線程堆棧停留在 select 操作上峻贮,說明 I/O 比較空閑席怪,性能瓶頸不在 Netty,需要繼續(xù)分析看是否是后端的業(yè)務處理線程存在性能瓶頸:
如果發(fā)現(xiàn)性能瓶頸在網(wǎng)絡 I/O 讀寫上纤控,可以適當調(diào)大 NioEventLoopGroup 中的 work I/O 線程數(shù)挂捻,直到 I/O 處理性能能夠滿足業(yè)務需求。
注:本文轉(zhuǎn)載自林雪峰的《Netty 系列之 Netty 高性能之道》與《Netty 學習和進階策略》