1. 程式人生 > >深入解析netty的高效能、高併發之道

深入解析netty的高效能、高併發之道

一丶 Netty基礎入門

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

作為當前最流行的NIO框架,Netty在網際網路領域、大資料分散式計算領域、遊戲行業、通訊行業等獲得了廣泛的應用,一些業界著名的開源元件也基於Netty的NIO框架構建。

二丶 Netty高效能之道

RPC呼叫的效能模型分析

RPC 的全稱是 Remote Procedure Call 是一種程序間通訊方式。 它允許程式呼叫另一個地址空間(通常是共享網路的另一臺機器上)的過程或函式,而不用程式設計師顯式編碼這個遠端呼叫的細節。即程式設計師無論是呼叫本地的還是遠端的函式,本質上編寫的呼叫程式碼基本相同。

我們追溯下當初開發 RPC 的原動機是什麼?在 Nelson 的論文 Implementing Remote Procedure Calls(參考[2]) 中他提到了幾點:

簡單:RPC 概念的語義十分清晰和簡單,這樣建立分散式計算就更容易。

高效:過程呼叫看起來十分簡單而且高效。

通用:在單機計算中「過程」往往是不同演算法部分間最重要的通訊機制。

通俗一點說,就是一般程式設計師對於本地的過程呼叫很熟悉,那麼我們把 RPC 做成和本地呼叫完全類似,那麼就更容易被接受,使用起來毫無障礙。 Nelson 的論文發表於 30 年前,其觀點今天看來確實高瞻遠矚,今天我們使用的 RPC 框架基本就是按這個目標來實現的。

傳統RPC呼叫效能差的三大誤區

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

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

BIO通訊模型圖

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

高效能的三大要素

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

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

3) 執行緒:資料報如何讀取?讀取之後的編解碼在哪個執行緒進行,編解碼後的訊息如何派發,Reactor執行緒模型的不同,對效能的影響也非常大。

非同步非阻塞通訊

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

NIO的多路複用模型圖

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

零拷貝

零拷貝是Netty的重要特性之一,而究竟什麼是零拷貝呢?

"Zero-copy" describes computer operations in which the CPU does not perform the task of copying data from one memory area to another.

從WIKI的定義中,我們看到“零拷貝”是指計算機操作的過程中,CPU不需要為資料在記憶體之間的拷貝消耗資源。而它通常是指計算機在網路上傳送檔案時,不需要將檔案內容拷貝到使用者空間(User Space)而直接在核心空間(Kernel Space)中傳輸到網路的方式。

Non-Zero Copy方式:

Non-Zero Copy方式

Zero Copy方式:

從上圖中可以清楚的看到,Zero Copy的模式中,避免了資料在使用者空間和記憶體空間之間的拷貝,從而提高了系統的整體效能。Linux中的sendfile()以及Java NIO中的FileChannel.transferTo()方法都實現了零拷貝的功能,而在Netty中也通過在FileRegion中包裝了NIO的FileChannel.transferTo()方法實現了零拷貝。

而在Netty中還有另一種形式的零拷貝,即Netty允許我們將多段資料合併為一整段虛擬資料供使用者使用,而過程中不需要對資料進行拷貝操作,這也是我們今天要講的重點。我們都知道在stream-based transport(如TCP/IP)的傳輸過程中,資料包有可能會被重新封裝在不同的資料包中,例如當你傳送如下資料時:

有可能實際收到的資料如下:

因此在實際應用中,很有可能一條完整的訊息被分割為多個數據包進行網路傳輸,而單個的資料包對你而言是沒有意義的,只有當這些資料包組成一條完整的訊息時你才能做出正確的處理,而Netty可以通過零拷貝的方式將這些資料包組合成一條完整的訊息供你來使用。而此時,零拷貝的作用範圍僅在使用者空間中。

記憶體池

為什麼要使用記憶體池?

隨著JVM虛擬機器和JIT即時編譯技術的發展,物件的分配和回收是個非常輕量級的工作。但是對於緩衝區Buffer,情況卻稍有不同,特別是對於堆外直接記憶體的分配和回收,是一件耗時的操作。而且這些例項隨著訊息的處理朝生夕滅,這就會給伺服器帶來沉重的GC壓力,同時消耗大量的記憶體。為了儘量重用緩衝區,Netty提供了基於記憶體池的緩衝區重用機制。效能測試表明,採用記憶體池的ByteBuf相比於朝生夕滅的ByteBuf,效能高23倍左右(效能資料與使用場景強相關)。

如何啟動並初始化記憶體池?

在Netty4或Netty5中實現了一個新的ByteBuf記憶體池,它是一個純Java版本的 jemalloc (Facebook也在用)。現在,Netty不會再因為用零填充緩衝區而浪費記憶體帶寬了。 不過,由於它不依賴於GC,開發人員需要小心記憶體洩漏。如果忘記在處理程式中釋放緩衝區,那麼記憶體使用率會無限地增長。 Netty預設不使用記憶體池,需要在建立客戶端或者服務端的時候在引導輔助類中進行配置:

work執行緒配置

如何在自己的業務程式碼中使用記憶體池?

首先,介紹一下Netty的ByteBuf緩衝區的種類:ByteBuf支援堆緩衝區和堆外直接緩衝區,根據經驗來說,底層IO處理執行緒的緩衝區使用堆外直接緩衝區,減少一次IO複製。業務訊息的編解碼使用堆緩衝區,分配效率更高,而且不涉及到核心緩衝區的複製問題。

ByteBuf的堆緩衝區又分為記憶體池緩衝區PooledByteBuf和普通記憶體緩衝區UnpooledHeapByteBuf。PooledByteBuf採用二叉樹來實現一個記憶體池,集中管理記憶體的分配和釋放,不用每次使用都新建一個緩衝區物件。UnpooledHeapByteBuf每次都會新建一個緩衝區物件。在高併發的情況下推薦使用PooledByteBuf,可以節約記憶體的分配。在效能能夠保證的情況下,可以使用UnpooledHeapByteBuf,實現比較簡單。

在此說明這是當我們在業務程式碼中要使用池化的ByteBuf時的方法:

第一種情況:若我們的業務程式碼只是為了將資料寫入ByteBuf中併發送出去,那麼我們應該使用堆外直接緩衝區DirectBuffer.使用方式如下:

高效的Reactor執行緒模型

Reactor模式是事件驅動的,有一個或多個併發輸入源,有一個Service Handler,有多個Request Handlers;這個Service Handler會同步的將輸入的請求(Event)多路複用的分發給相應的Request Handler

從結構上,這有點類似生產者消費者模式,即有一個或多個生產者將事件放入一個Queue中,而一個或多個消費者主動的從這個Queue中Poll事件來處理;而Reactor模式則並沒有Queue來做緩衝,每當一個Event輸入到Service Handler之後,該Service Handler會立刻的根據不同的Event型別將其分發給對應的Request Handler來處理。

這個做的好處有很多,首先我們可以將處理event的Request handler實現一個單獨的執行緒,即:

Request handler執行緒

這樣Service Handler 和request Handler實現了非同步,加快了service Handler處理event的速度,那麼每一個request同樣也可以以多執行緒的形式來處理自己的event,即Thread1 擴充套件成Thread pool 1,

Netty的Reactor執行緒模型1 Reactor單執行緒模型 Reactor機制中保證每次讀寫能非阻塞讀寫

Reactor單執行緒模型

一個執行緒(單執行緒)來處理CONNECT事件(Acceptor),一個執行緒池(多執行緒)來處理read,一個執行緒池(多執行緒)來處理write,那麼從Reactor Thread到handler都是非同步的,從而IO操作也多執行緒化。

到這裡跟BIO對比已經提升了很大的效能,但是還可以繼續提升,由於Reactor Thread依然為單執行緒,從效能上考慮依然有所限制

Reactor多執行緒模型

Reactor多執行緒模型

這樣通過Reactor Thread Pool來提高event的分發能力

3 Reactor主從模型

Netty的高效併發程式設計主要體現在如下幾點:

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

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

3) 執行緒安全容器的使用;

4) 通過讀寫鎖提升併發效能。

Netty除了使用reactor來提升效能,當然還有

1、零拷貝,IO效能優化

2、通訊上的粘包拆包

2、同步的設計

3、高效能的序列



作者:CatherinePlans
連結:https://www.jianshu.com/p/ac7fb5c2640f
來源:簡書
簡書著作權歸作者所有,任何形式的轉載都請聯絡作者獲得授權並註明出處。