程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> JAVA綜合教程 >> Netty系列之Netty高性能之道,netty高性能之道

Netty系列之Netty高性能之道,netty高性能之道

編輯:JAVA綜合教程

Netty系列之Netty高性能之道,netty高性能之道


轉載自http://www.infoq.com/cn/articles/netty-high-performance

1. 背景

1.1. 驚人的性能數據

最近一個圈內朋友通過私信告訴我,通過使用Netty4 + Thrift壓縮二進制編解碼技術,他們實現了10W TPS(1K的復雜POJO對象)的跨節點遠程服務調用。相比於傳統基於Java序列化+BIO(同步阻塞IO)的通信框架,性能提升了8倍多。

 

事實上,我對這個數據並不感到驚訝,根據我5年多的NIO編程經驗,通過選擇合適的NIO框架,加上高性能的壓縮二進制編解碼技術,精心的設計Reactor線程模型,達到上述性能指標是完全有可能的。

 

下面我們就一起來看下Netty是如何支持10W TPS的跨節點遠程服務調用的,在正式開始講解之前,我們先簡單介紹下Netty。

 

1.2. Netty基礎入門

Netty是一個高性能、異步事件驅動的NIO框架,它提供了對TCP、UDP和文件傳輸的支持,作為一個異步NIO框架,Netty的所有IO操作都是異步非阻塞的,通過Future-Listener機制,用戶可以方便的主動獲取或者通過通知機制獲得IO操作結果。

 

作為當前最流行的NIO框架,Netty在互聯網領域、大數據分布式計算領域、游戲行業、通信行業等獲得了廣泛的應用,一些業界著名的開源組件也基於Netty的NIO框架構建。

 

2. Netty高性能之道

2.1. RPC調用的性能模型分析

2.1.1. 傳統RPC調用性能差的三宗罪

網絡傳輸方式問題:傳統的RPC框架或者基於RMI等方式的遠程服務(過程)調用采用了同步阻塞IO,當客戶端的並發壓力或者網絡時延增大之後,同步阻塞IO會由於頻繁的wait導致IO線程經常性的阻塞,由於線程無法高效的工作,IO處理能力自然下降。

 

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

圖2-1 BIO通信模型圖

 

采用BIO通信模型的服務端,通常由一個獨立的Acceptor線程負責監聽客戶端的連接,接收到客戶端連接之後為客戶端連接創建一個新的線程處理請求消息,處理完成之後,返回應答消息給客戶端,線程銷毀,這就是典型的一請求一應答模型。該架構最大的問題就是不具備彈性伸縮能力,當並發訪問量增加後,服務端的線程個數和並發訪問數成線性正比,由於線程是JAVA虛擬機非常寶貴的系統資源,當線程數膨脹之後,系統的性能急劇下降,隨著並發量的繼續增加,可能會發生句柄溢出、線程堆棧溢出等問題,並導致服務器最終宕機。

 

序列化方式問題:Java序列化存在如下幾個典型問題:

 

1) Java序列化機制是Java內部的一種對象編解碼技術,無法跨語言使用;例如對於異構系統之間的對接,Java序列化後的碼流需要能夠通過其它語言反序列化成原始對象(副本),目前很難支持;

 

2) 相比於其它開源的序列化框架,Java序列化後的碼流太大,無論是網絡傳輸還是持久化到磁盤,都會導致額外的資源占用;

 

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

 

線程模型問題:由於采用同步阻塞IO,這會導致每個TCP連接都占用1個線程,由於線程資源是JVM虛擬機非常寶貴的資源,當IO讀寫阻塞導致線程無法及時釋放時,會導致系統性能急劇下降,嚴重的甚至會導致虛擬機無法創建新的線程。

 

2.1.2. 高性能的三個主題

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

 

2) 協議:采用什麼樣的通信協議,HTTP或者內部私有協議。協議的選擇不同,性能模型也不同。相比於公有協議,內部私有協議的性能通常可以被設計的更優。

 

3) 線程:數據報如何讀取?讀取之後的編解碼在哪個線程進行,編解碼後的消息如何派發,Reactor線程模型的不同,對性能的影響也非常大。

圖2-2 RPC調用性能三要素

 

2.2. Netty高性能之道

2.2.1. 異步非阻塞通信

在IO編程過程中,當需要同時處理多個客戶端接入請求時,可以利用多線程或者IO多路復用技術進行處理。IO多路復用技術通過把多個IO的阻塞復用到同一個select的阻塞上,從而使得系統在單線程的情況下可以同時處理多個客戶端請求。與傳統的多線程/多進程模型比,I/O多路復用的最大優勢是系統開銷小,系統不需要創建新的額外進程或者線程,也不需要維護這些進程和線程的運行,降低了系統的維護工作量,節省了系統資源。

 

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

 

JDK NIO通信模型如下所示:

圖2-3 NIO的多路復用模型圖

 

與Socket類和ServerSocket類相對應,NIO也提供了SocketChannel和ServerSocketChannel兩種不同的套接字通道實現。這兩種新增的通道都支持阻塞和非阻塞兩種模式。阻塞模式使用非常簡單,但是性能和可靠性都不好,非阻塞模式正好相反。開發人員一般可以根據自己的需要來選擇合適的模式,一般來說,低負載、低並發的應用程序可以選擇同步阻塞IO以降低編程復雜度。但是對於高負載、高並發的網絡應用,需要使用NIO的非阻塞模式進行開發。

 

Netty架構按照Reactor模式設計和實現,它的服務端通信序列圖如下:

圖2-3 NIO服務端通信序列圖

 

客戶端通信序列圖如下:

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

 

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

 

2.2.2. 零拷貝

很多用戶都聽說過Netty具有“零拷貝”功能,但是具體體現在哪裡又說不清楚,本小節就詳細對Netty的“零拷貝”功能進行講解。

 

Netty的“零拷貝”主要體現在如下三個方面:

 

1) Netty的接收和發送ByteBuffer采用DIRECT BUFFERS,使用堆外直接內存進行Socket讀寫,不需要進行字節緩沖區的二次拷貝。如果使用傳統的堆內存(HEAP BUFFERS)進行Socket讀寫,JVM會將堆內存Buffer拷貝一份到直接內存中,然後才寫入Socket中。相比於堆外直接內存,消息在發送過程中多了一次緩沖區的內存拷貝。

 

2) Netty提供了組合Buffer對象,可以聚合多個ByteBuffer對象,用戶可以像操作一個Buffer那樣方便的對組合Buffer進行操作,避免了傳統通過內存拷貝的方式將幾個小Buffer合並成一個大的Buffer。

 

3) Netty的文件傳輸采用了transferTo方法,它可以直接將文件緩沖區的數據發送到目標Channel,避免了傳統通過循環write方式導致的內存拷貝問題。

 

下面,我們對上述三種“零拷貝”進行說明,先看Netty 接收Buffer的創建:

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

 

每循環讀取一次消息,就通過ByteBufAllocator的ioBuffer方法獲取ByteBuf對象,下面繼續看它的接口定義:

圖2-6 ByteBufAllocator 通過ioBuffer分配堆外內存

 

當進行Socket IO讀寫的時候,為了避免從堆內存拷貝一份副本到直接內存,Netty的ByteBuf分配器直接創建非堆內存避免緩沖區的二次拷貝,通過“零拷貝”來提升讀寫性能。

 

下面我們繼續看第二種“零拷貝”的實現CompositeByteBuf,它對外將多個ByteBuf封裝成一個ByteBuf,對外提供統一封裝後的ByteBuf接口,它的類定義如下:

圖2-7 CompositeByteBuf類繼承關系

 

通過繼承關系我們可以看出CompositeByteBuf實際就是個ByteBuf的包裝器,它將多個ByteBuf組合成一個集合,然後對外提供統一的ByteBuf接口,相關定義如下:

圖2-8 CompositeByteBuf類定義

 

添加ByteBuf,不需要做內存拷貝,相關代碼如下:

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

 

最後,我們看下文件傳輸的“零拷貝”:

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

 

Netty文件傳輸DefaultFileRegion通過transferTo方法將文件發送到目標Channel中,下面重點看FileChannel的transferTo方法,它的API DOC說明如下:

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

 

對於很多操作系統它直接將文件緩沖區的內容發送到目標Channel中,而不需要通過拷貝的方式,這是一種更加高效的傳輸方式,它實現了文件傳輸的“零拷貝”。

 

2.2.3. 內存池

隨著JVM虛擬機和JIT即時編譯技術的發展,對象的分配和回收是個非常輕量級的工作。但是對於緩沖區Buffer,情況卻稍有不同,特別是對於堆外直接內存的分配和回收,是一件耗時的操作。為了盡量重用緩沖區,Netty提供了基於內存池的緩沖區重用機制。下面我們一起看下Netty ByteBuf的實現:

圖2-12 內存池ByteBuf

 

Netty提供了多種內存管理策略,通過在啟動輔助類中配置相關參數,可以實現差異化的定制。

 

下面通過性能測試,我們看下基於內存池循環利用的ByteBuf和普通ByteBuf的性能差異。

 

用例一,使用內存池分配器創建直接內存緩沖區:

圖2-13 基於內存池的非堆內存緩沖區測試用例

 

用例二,使用非堆內存分配器創建的直接內存緩沖區:

圖2-14 基於非內存池創建的非堆內存緩沖區測試用例

 

各執行300萬次,性能對比結果如下所示:

圖2-15 內存池和非內存池緩沖區寫入性能對比

 

性能測試表明,采用內存池的ByteBuf相比於朝生夕滅的ByteBuf,性能高23倍左右(性能數據與使用場景強相關)。

 

下面我們一起簡單分析下Netty內存池的內存分配:

圖2-16 AbstractByteBufAllocator的緩沖區分配

 

繼續看newDirectBuffer方法,我們發現它是一個抽象方法,由AbstractByteBufAllocator的子類負責具體實現,代碼如下:

圖2-17 newDirectBuffer的不同實現

 

代碼跳轉到PooledByteBufAllocator的newDirectBuffer方法,從Cache中獲取內存區域PoolArena,調用它的allocate方法進行內存分配:

圖2-18 PooledByteBufAllocator的內存分配

 

PoolArena的allocate方法如下:

圖2-18 PoolArena的緩沖區分配

 

我們重點分析newByteBuf的實現,它同樣是個抽象方法,由子類DirectArena和HeapArena來實現不同類型的緩沖區分配,由於測試用例使用的是堆外內存,

圖2-19 PoolArena的newByteBuf抽象方法

 

因此重點分析DirectArena的實現:如果沒有開啟使用sun的unsafe,則

圖2-20 DirectArena的newByteBuf方法實現

 

執行PooledDirectByteBuf的newInstance方法,代碼如下:

圖2-21 PooledDirectByteBuf的newInstance方法實現

 

通過RECYCLER的get方法循環使用ByteBuf對象,如果是非內存池實現,則直接創建一個新的ByteBuf對象。從緩沖池中獲取ByteBuf之後,調用AbstractReferenceCountedByteBuf的setRefCnt方法設置引用計數器,用於對象的引用計數和內存回收(類似JVM垃圾回收機制)。

 

2.2.4. 高效的Reactor線程模型

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

1) Reactor單線程模型;

2) Reactor多線程模型;

3) 主從Reactor多線程模型

 

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

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

2) 作為NIO客戶端,向服務端發起TCP連接;

3) 讀取通信對端的請求或者應答消息;

4) 向通信對端發送消息請求或者應答消息。

 

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

圖2-22 Reactor單線程模型

 

由於Reactor模式使用的是異步非阻塞IO,所有的IO操作都不會導致阻塞,理論上一個線程可以獨立處理所有IO相關的操作。從架構層面看,一個NIO線程確實可以完成其承擔的職責。例如,通過Acceptor接收客戶端的TCP連接請求消息,鏈路建立成功之後,通過Dispatch將對應的ByteBuffer派發到指定的Handler上進行消息解碼。用戶Handler可以通過NIO線程將消息發送給客戶端。

 

對於一些小容量應用場景,可以使用單線程模型。但是對於高負載、大並發的應用卻不合適,主要原因如下:

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

2) 當NIO線程負載過重之後,處理速度將變慢,這會導致大量客戶端連接超時,超時之後往往會進行重發,這更加重了NIO線程的負載,最終會導致大量消息積壓和處理超時,NIO線程會成為系統的性能瓶頸;

3) 可靠性問題:一旦NIO線程意外跑飛,或者進入死循環,會導致整個系統通信模塊不可用,不能接收和處理外部消息,造成節點故障。

 

為了解決這些問題,演進出了Reactor多線程模型,下面我們一起學習下Reactor多線程模型。

 

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

圖2-23 Reactor多線程模型

 

Reactor多線程模型的特點:

1) 有專門一個NIO線程-Acceptor線程用於監聽服務端,接收客戶端的TCP連接請求;

2) 網絡IO操作-讀、寫等由一個NIO線程池負責,線程池可以采用標准的JDK線程池實現,它包含一個任務隊列和N個可用的線程,由這些NIO線程負責消息的讀取、解碼、編碼和發送;

3) 1個NIO線程可以同時處理N條鏈路,但是1個鏈路只對應1個NIO線程,防止發生並發操作問題。

 

在絕大多數場景下,Reactor多線程模型都可以滿足性能需求;但是,在極特殊應用場景中,一個NIO線程負責監聽和處理所有的客戶端連接可能會存在性能問題。例如百萬客戶端並發連接,或者服務端需要對客戶端的握手消息進行安全認證,認證本身非常損耗性能。在這類場景下,單獨一個Acceptor線程可能會存在性能不足問題,為了解決性能問題,產生了第三種Reactor線程模型-主從Reactor多線程模型。

 

主從Reactor線程模型的特點是:服務端用於接收客戶端連接的不再是個1個單獨的NIO線程,而是一個獨立的NIO線程池。Acceptor接收到客戶端TCP連接請求處理完成後(可能包含接入認證等),將新創建的SocketChannel注冊到IO線程池(sub reactor線程池)的某個IO線程上,由它負責SocketChannel的讀寫和編解碼工作。Acceptor線程池僅僅只用於客戶端的登陸、握手和安全認證,一旦鏈路建立成功,就將鏈路注冊到後端subReactor線程池的IO線程上,由IO線程負責後續的IO操作。

 

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

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

 

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

 

事實上,Netty的線程模型並非固定不變,通過在啟動輔助類中創建不同的EventLoopGroup實例並通過適當的參數配置,就可以支持上述三種Reactor線程模型。正是因為Netty 對Reactor線程模型的支持提供了靈活的定制能力,所以可以滿足不同業務場景的性能訴求。

 

2.2.5. 無鎖化的串行設計理念

在大多數場景下,並行多線程處理可以提升系統的並發性能。但是,如果對於共享資源的並發訪問處理不當,會帶來嚴重的鎖競爭,這最終會導致性能的下降。為了盡可能的避免鎖競爭帶來的性能損耗,可以通過串行化設計,即消息的處理盡可能在同一個線程內完成,期間不進行線程切換,這樣就避免了多線程競爭和同步鎖。

 

為了盡可能提升性能,Netty采用了串行無鎖化設計,在IO線程內部進行串行操作,避免多線程競爭導致的性能下降。表面上看,串行化設計似乎CPU利用率不高,並發程度不夠。但是,通過調整NIO線程池的線程參數,可以同時啟動多個串行化的線程並行運行,這種局部無鎖化的串行線程設計相比一個隊列-多個工作線程模型性能更優。

Netty的串行化設計工作原理圖如下:

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

 

Netty的NioEventLoop讀取到消息之後,直接調用ChannelPipeline的fireChannelRead(Object msg),只要用戶不主動切換線程,一直會由NioEventLoop調用到用戶的Handler,期間不進行線程切換,這種串行化處理方式避免了多線程操作導致的鎖的競爭,從性能角度看是最優的。

 

2.2.6. 高效的並發編程

Netty的高效並發編程主要體現在如下幾點:

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

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

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

4) 通過讀寫鎖提升並發性能。

 

如果大家想了解Netty高效並發編程的細節,可以閱讀之前我在微博分享的《多線程並發編程在 Netty 中的應用分析》,在這篇文章中對Netty的多線程技巧和應用進行了詳細的介紹和分析。

 

2.2.7. 高性能的序列化框架

影響序列化性能的關鍵因素總結如下:

1) 序列化後的碼流大小(網絡帶寬的占用);

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

3) 是否支持跨語言(異構系統的對接和開發語言切換)。

 

Netty默認提供了對Google Protobuf的支持,通過擴展Netty的編解碼接口,用戶可以實現其它的高性能序列化框架,例如Thrift的壓縮二進制編解碼框架。

 

下面我們一起看下不同序列化&反序列化框架序列化後的字節數組對比:

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

 

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

 

2.2.8. 靈活的TCP參數配置能力

合理設置TCP參數在某些場景下對於性能的提升可以起到顯著的效果,例如SO_RCVBUF和SO_SNDBUF。如果設置不當,對性能的影響是非常大的。下面我們總結下對性能影響比較大的幾個配置項:

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

2) SO_TCPNODELAY:NAGLE算法通過將緩沖區內的小封包自動相連,組成較大的封包,阻止大量小封包的發送阻塞網絡,從而提高網絡應用效率。但是對於時延敏感的應用場景需要關閉該優化算法;

3) 軟中斷:如果Linux內核版本支持RPS(2.6.35以上版本),開啟RPS後可以實現軟中斷,提升網絡吞吐量。RPS根據數據包的源地址,目的地址以及目的和源端口,計算出一個hash值,然後根據這個hash值來選擇軟中斷運行的cpu,從上層來看,也就是說將每個連接和cpu綁定,並通過這個hash值,來均衡軟中斷在多個cpu上,提升網絡並行處理性能。

 

Netty在啟動輔助類中可以靈活的配置TCP參數,滿足不同的用戶場景。相關配置接口定義如下:

圖2-27 Netty的TCP參數配置定義

 

2.3. 總結

通過對Netty的架構和性能模型進行分析,我們發現Netty架構的高性能是被精心設計和實現的,得益於高質量的架構和代碼,Netty支持10W TPS的跨節點服務調用並不是件十分困難的事情。

 

3. 作者簡介

李林鋒,2007年畢業於東北大學,2008年進入華為公司從事高性能通信軟件的設計和開發工作,有6年NIO設計和開發經驗,精通Netty、Mina等NIO框架。Netty中國社區創始人,《Netty權威指南》作者。

聯系方式:新浪微博 Nettying 微信:Nettying

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved