Netty系列之Netty高效能之道

來源:互聯網
上載者:User

標籤:blog   http   io   ar   os   使用   java   sp   strong   

  • 1. 背景
  • 1.1. 驚人的效能資料
  • 1.2. Netty基礎入門
  • 2. Netty高效能之道
  • 2.1. RPC調用的效能模型分析
  • 2.1.1. 傳統RPC調用效能差的三宗罪
  • 2.1.2. 高效能的三個主題
  • 2.2. Netty高效能之道
  • 2.2.1. 非同步非阻塞通訊
  • 2.2.2. 零拷貝
  • 2.2.3. 記憶體池
  • 2.2.4. 高效的Reactor執行緒模式
  • 2.2.5. 無鎖化的串列設計理念
  • 2.2.6. 高效的並發編程
  • 2.2.7. 高效能的序列化架構
  • 2.2.8. 靈活的TCP參數配置能力
  • 2.3. 總結
  • 3. 作者簡介
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線程負責監聽用戶端的串連,接收到用戶端串連之後為用戶端串連建立一個新的線程處理請求訊息,處理完成之後,返回應答訊息給用戶端,線程銷毀,這就是典型的一請求一應答模型。該架構最大的問題就是不具備Auto Scaling能力,當並發訪問量增加後,服務端的線程個數和並發訪問數成線性正比,由於線程是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一串連一執行緒模式,架構的效能、Auto Scaling能力和可靠性都得到了極大的提升。

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 基於記憶體池的非堆記憶體緩衝區測試案例

用例二,使用非堆記憶體 Clerk建立的直接記憶體緩衝區:

圖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

Netty系列之Netty高效能之道

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.