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

高性能的三大要素

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

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

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

異步非阻塞通信

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

與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默認不使用內存池,需要在創建客戶端或者服務端的時候在引導輔助類中進行配置:

如何在自己的業務代碼中使用內存池?

首先,介紹一下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實現一個單獨的線程,即:

這樣Service Handler 和request Handler實現了異步,加快了service Handler處理event的速度,那麼每一個request同樣也可以以多線程的形式來處理自己的event,即Thread1 擴展成Thread pool 1,

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

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

到這裏跟BIO對比已經提升了很大的性能,但是還可以繼續提升,由於Reactor Thread依然爲單線程,從性能上考慮依然有所限制

Reactor多線程模型

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

Reactor主從模型

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

  1. volatile的大量、正確使用;
  2. CAS和原子類的廣泛使用;
  3. 線程安全容器的使用;
  4. 通過讀寫鎖提升併發性能。

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

1、零拷貝,IO性能優化
2、通信上的粘包拆包
3、同步的設計
4、高性能的序列

寫在最後:

歡迎大家關注我新開通的公衆號【風平浪靜如碼】,海量Java相關文章,學習資料都會在裏面更新,整理的資料也會放在裏面。

覺得寫的還不錯的就點個贊,加個關注唄!點關注,不迷路,持續更新!!!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章