高性能底層怎麼運作?一文幫你吃透Netty架構原理

高性能底層怎麼運作?一文幫你吃透Netty架構原理

對於高性能的 RPC 框架,Netty 作為異步通信框架,幾乎成為必備品。例如,Dubbo 框架中通信組件,還有 RocketMQ 中生產者和消費者的通信,都使用了 Netty。今天,我們來看看 Netty 的基本架構和原理。

Netty 的特點與 NIO

Netty 是一個異步的、基於事件驅動的網絡應用框架,它可以用來開發高性能服務端和客戶端。

以前編寫網絡調用程序的時候,我們都會在客戶端創建一個 Socket,通過這個 Socket 連接到服務端。

服務端根據這個 Socket 創建一個 Thread,用來發出請求。客戶端在發起調用以後,需要等待服務端處理完成,才能繼續後面的操作。這樣線程會出現等待的狀態。

如果客戶端請求數越多,服務端創建的處理線程也會越多,JVM 如此多的線程並不是一件容易的事。

高性能底层怎么运作?一文帮你吃透Netty架构原理

使用阻塞 I/O 處理多個連接

為了解決上述的問題,推出了 NIO 的概念,也就是(Non-blocking I/O)。其中,Selector 機制就是 NIO 的核心。

當每次客戶端請求時,會創建一個 Socket Channel,並將其註冊到 Selector 上(多路複用器)。

然後,Selector 關注服務端 IO 讀寫事件,此時客戶端並不用等待 IO 事件完成,可以繼續做接下來的工作。

一旦,服務端完成了 IO 讀寫操作,Selector 會接到通知,同時告訴客戶端 IO 操作已經完成。

接到通知的客戶端,就可以通過 SocketChannel 獲取需要的數據了。

高性能底层怎么运作?一文帮你吃透Netty架构原理

NIO 機制與 Selector

上面描述的過程有點異步的意思,不過,Selector 實現的並不是真正意義上的異步操作。

因為 Selector 需要通過線程阻塞的方式監聽 IO 事件變更,只是這種方式沒有讓客戶端等待,是 Selector 在等待 IO 返回,並且通知客戶端去獲取數據。真正“異步 IO”(AIO)這裡不展開介紹,有興趣可以自行查找。

說好了 NIO 再來談談 Netty,Netty 作為 NIO 的實現,它適用於服務器/客戶端通訊的場景,以及針對於 TCP 協議下的高併發應用。

對於開發者來說,它具有以下特點:

  • 對 NIO 進行封裝,開發者不需要關注 NIO 的底層原理,只需要調用 Netty 組件就能夠完成工作。

  • 對網絡調用透明,從 Socket 建立 TCP 連接到網絡異常的處理都做了包裝。

  • 對數據處理靈活, Netty 支持多種序列化框架,通過“ChannelHandler”機制,可以自定義“編/解碼器”。

  • 對性能調優友好,Netty 提供了線程池模式以及 Buffer 的重用機制(對象池化),不需要構建複雜的多線程模型和操作隊列。

從一個簡單的例子開始

開篇講到了,為了滿足高併發下網絡請求,引入了 NIO 的概念。Netty 是針對 NIO 的實現,在 NIO 封裝,網絡調用,數據處理以及性能優化等方面都有不俗的表現。

學習架構最容易的方式就是從實例入手,從客戶端訪問服務端的代碼來看看 Netty 是如何運作的。再一次介紹代碼中調用的組件以及組件的工作原理。

假設有一個客戶端去調用一個服務端,假設服務端叫做 EchoServer,客戶端叫做 EchoClient,用 Netty 架構實現代碼如下。

服務端代碼

構建服務器端,假設服務器接受客戶端傳來的信息,然後在控制檯打印。首先,生成 EchoServer,在構造函數中傳入需要監聽的端口號。

高性能底层怎么运作?一文帮你吃透Netty架构原理

構造函數中傳入需要監聽的端口號

接下來就是服務的啟動方法:

高性能底层怎么运作?一文帮你吃透Netty架构原理

啟動 NettyServer 的 Start 方法

Server 的啟動方法涉及到了一些組件的調用,例如 EventLoopGroup,Channel。這些會在後面詳細講解。

這裡有個大致的印象就好:

  • 創建 EventLoopGroup。

  • 創建 ServerBootstrap。

  • 指定所使用的 NIO 傳輸 Channel。

  • 使用指定的端口設置套接字地址。

  • 添加一個 ServerHandler 到 Channel 的 ChannelPipeline。

  • 異步地綁定服務器;調用 sync 方法阻塞等待直到綁定完成。

  • 獲取 Channel 的 CloseFuture,並且阻塞當前線程直到它完成。

  • 關閉 EventLoopGroup,釋放所有的資源。

NettyServer 啟動以後會監聽某個端口的請求,當接受到了請求就需要處理了。在 Netty 中客戶端請求服務端,被稱為“入站”操作。

可以通過 ChannelInboundHandlerAdapter 實現,具體內容如下:

高性能底层怎么运作?一文帮你吃透Netty架构原理

處理來自客戶端的請求

從上面的代碼可以看出,服務端處理的代碼包含了三個方法。這三個方法都是根據事件觸發的。

他們分別是:

  • 當接收到消息時的操作,channelRead。

  • 消息讀取完成時的方法,channelReadComplete。

  • 出現異常時的方法,exceptionCaught。

客戶端代碼

客戶端和服務端的代碼基本相似,在初始化時需要輸入服務端的 IP 和 Port。

高性能底层怎么运作?一文帮你吃透Netty架构原理

同樣在客戶端啟動函數中包括以下內容:

高性能底层怎么运作?一文帮你吃透Netty架构原理

客戶端啟動程序的順序:

  • 創建 Bootstrap。

  • 指定 EventLoopGroup 用來監聽事件。

  • 定義 Channel 的傳輸模式為 NIO(Non-BlockingInputOutput)。

  • 設置服務器的 InetSocketAddress。

  • 在創建 Channel 時,向 ChannelPipeline 中添加一個 EchoClientHandler 實例。

  • 連接到遠程節點,阻塞等待直到連接完成。

  • 阻塞,直到 Channel 關閉。

  • 關閉線程池並且釋放所有的資源。

客戶端在完成以上操作以後,會與服務端建立連接從而傳輸數據。同樣在接受到 Channel 中觸發的事件時,客戶端會觸發對應事件的操作。

高性能底层怎么运作?一文帮你吃透Netty架构原理

例如 Channel 激活,客戶端接受到服務端的消息,或者發生異常的捕獲。

從代碼結構上看還是比較簡單的。服務端和客戶端分別初始化創建監聽和連接。然後分別定義各自的 Handler 處理對方的請求。

高性能底层怎么运作?一文帮你吃透Netty架构原理

服務端/客戶端初始化和事件處理

Netty 核心組件

通過上面的簡單例子,發現有些 Netty 組件在服務初始化以及通訊時被用到,下面就來介紹一下這些組件的用途和關係。

①Channel

通過上面例子可以看出,當客戶端和服務端連接的時候會建立一個 Channel。

這個 Channel 我們可以理解為 Socket 連接,它負責基本的 IO 操作,例如:bind,connect,read,write 等等。

簡單的說,Channel 就是代表連接,實體之間的連接,程序之間的連接,文件之間的連接,設備之間的連接。同時它也是數據入站和出站的載體。

②EventLoop 和 EventLoopGroup

既然有了 Channel 連接服務,讓信息之間可以流動。如果服務發出的消息稱作“出站”消息,服務接受的消息稱作“入站”消息。那麼消息的“出站”/“入站”就會產生事件(Event)。

例如:連接已激活;數據讀取;用戶事件;異常事件;打開鏈接;關閉鏈接等等。

順著這個思路往下想,有了數據,數據的流動產生事件,那麼就有一個機制去監控和協調事件。

這個機制(組件)就是 EventLoop。在 Netty 中每個 Channel 都會被分配到一個 EventLoop。一個 EventLoop 可以服務於多個 Channel。

每個 EventLoop 會佔用一個 Thread,同時這個 Thread 會處理 EventLoop 上面發生的所有 IO 操作和事件(Netty 4.0)。

高性能底层怎么运作?一文帮你吃透Netty架构原理

EventLoop 與 Channel 關係

理解了 EventLoop,再來說 EventLoopGroup 就容易了,EventLoopGroup 是用來生成 EventLoop 的,還記得例子代碼中第一行就 new 了 EventLoopGroup 對象。

一個 EventLoopGroup 中包含了多個 EventLoop 對象。

高性能底层怎么运作?一文帮你吃透Netty架构原理

創建 EventLoopGroup

EventLoopGroup 要做的就是創建一個新的 Channel,並且給它分配一個 EventLoop。

高性能底层怎么运作?一文帮你吃透Netty架构原理

EventLoopGroup,EventLoop 和 Channel 的關係

在異步傳輸的情況下,一個 EventLoop 是可以處理多個 Channel 中產生的事件的,它主要的工作就是事件的發現以及通知。

相對於以前一個 Channel 就佔用一個 Thread 的情況。Netty 的方式就要合理多了。

客戶端發送消息到服務端,EventLoop 發現以後會告訴服務端:“你去獲取消息”,同時客戶端進行其他的工作。

當 EventLoop 檢測到服務端返回的消息,也會通知客戶端:“消息返回了,你去取吧“。客戶端再去獲取消息。整個過程 EventLoop 就是監視器+傳聲筒。

③ChannelHandler,ChannelPipeline 和 ChannelHandlerContext

如果說 EventLoop 是事件的通知者,那麼 ChannelHandler 就是事件的處理者。

在 ChannelHandler 中可以添加一些業務代碼,例如數據轉換,邏輯運算等等。

正如上面例子中展示的,Server 和 Client 分別都有一個 ChannelHandler 來處理,讀取信息,網絡可用,網絡異常之類的信息。

並且,針對出站和入站的事件,有不同的 ChannelHandler,分別是:

  • ChannelInBoundHandler(入站事件處理器)

  • ChannelOutBoundHandler(出站事件處理器)

高性能底层怎么运作?一文帮你吃透Netty架构原理

假設每次請求都會觸發事件,而由 ChannelHandler 來處理這些事件,這個事件的處理順序是由 ChannelPipeline 來決定的。

高性能底层怎么运作?一文帮你吃透Netty架构原理

ChannelHanlder 處理,出站/入站的事件

ChannelPipeline 為 ChannelHandler 鏈提供了容器。到 Channel 被創建的時候,會被 Netty 框架自動分配到 ChannelPipeline 上。

ChannelPipeline 保證 ChannelHandler 按照一定順序處理事件,當事件觸發以後,會將數據通過 ChannelPipeline 按照一定的順序通過 ChannelHandler。

說白了,ChannelPipeline 是負責“排隊”的。這裡的“排隊”是處理事件的順序。

同時,ChannelPipeline 也可以添加或者刪除 ChannelHandler,管理整個隊列。

高性能底层怎么运作?一文帮你吃透Netty架构原理

如上圖,ChannelPipeline 使 ChannelHandler 按照先後順序排列,信息按照箭頭所示方向流動並且被 ChannelHandler 處理。

說完了 ChannelPipeline 和 ChannelHandler,前者管理後者的排列順序。那麼它們之間的關聯就由 ChannelHandlerContext 來表示了。

每當有 ChannelHandler 添加到 ChannelPipeline 時,同時會創建 ChannelHandlerContext 。

ChannelHandlerContext 的主要功能是管理 ChannelHandler 和 ChannelPipeline 的交互。

不知道大家注意到沒有,開始的例子中 ChannelHandler 中處理事件函數,傳入的參數就是 ChannelHandlerContext。

高性能底层怎么运作?一文帮你吃透Netty架构原理

ChannelHandlerContext 參數貫穿 ChannelPipeline,將信息傳遞給每個 ChannelHandler,是個合格的“通訊員”。

高性能底层怎么运作?一文帮你吃透Netty架构原理

ChannelHandlerContext 負責傳遞消息

把上面提到的幾個核心組件歸納一下,用下圖表示方便記憶他們之間的關係。

高性能底层怎么运作?一文帮你吃透Netty架构原理

Netty 核心組件關係圖

Netty 的數據容器

前面介紹了 Netty 的幾個核心組件,服務器在數據傳輸的時候,產生事件,並且對事件進行監控和處理。

接下來看看數據是如何存放以及是如何讀寫的。Netty 將 ByteBuf 作為數據容器,來存放數據。

ByteBuf 工作原理

從結構上來說,ByteBuf 由一串字節數組構成。數組中每個字節用來存放信息。

ByteBuf 提供了兩個索引,一個用於讀取數據,一個用於寫入數據。這兩個索引通過在字節數組中移動,來定位需要讀或者寫信息的位置。

當從 ByteBuf 讀取時,它的 readerIndex(讀索引)將會根據讀取的字節數遞增。

同樣,當寫 ByteBuf 時,它的 writerIndex 也會根據寫入的字節數進行遞增。

高性能底层怎么运作?一文帮你吃透Netty架构原理

ByteBuf 讀寫索引圖例

需要注意的是極限的情況是 readerIndex 剛好讀到了 writerIndex 寫入的地方。

如果 readerIndex 超過了 writerIndex 的時候,Netty 會拋出 IndexOutOf-BoundsException 異常。

ByteBuf 使用模式

談了 ByteBuf 的工作原理以後,再來看看它的使用模式。

根據存放緩衝區的不同分為三類:

  • 堆緩衝區,ByteBuf 將數據存儲在 JVM 的堆中,通過數組實現,可以做到快速分配。

    由於在堆上被 JVM 管理,在不被使用時可以快速釋放。可以通過 ByteBuf.array 來獲取 byte 數據。

  • 直接緩衝區,在 JVM 的堆之外直接分配內存,用來存儲數據。其不佔用堆空間,使用時需要考慮內存容量。

    它在使用 Socket 傳遞時性能較好,因為間接從緩衝區發送數據,在發送之前 JVM 會先將數據複製到直接緩衝區再進行發送。

    由於,直接緩衝區的數據分配在堆之外,通過 JVM 進行垃圾回收,並且分配時也需要做複製的操作,因此使用成本較高。

  • 複合緩衝區,顧名思義就是將上述兩類緩衝區聚合在一起。Netty 提供了一個 CompsiteByteBuf,可以將堆緩衝區和直接緩衝區的數據放在一起,讓使用更加方便。

ByteBuf 的分配

聊完了結構和使用模式,再來看看 ByteBuf 是如何分配緩衝區的數據的。

Netty 提供了兩種 ByteBufAllocator 的實現,他們分別是:

  • PooledByteBufAllocator,實現了 ByteBuf 的對象的池化,提高性能減少內存碎片。

  • Unpooled-ByteBufAllocator,沒有實現對象的池化,每次會生成新的對象實例。

對象池化的技術和線程池,比較相似,主要目的是提高內存的使用率。池化的簡單實現思路,是在 JVM 堆內存上構建一層內存池,通過 allocate 方法獲取內存池中的空間,通過 release 方法將空間歸還給內存池。

對象的生成和銷燬,會大量地調用 allocate 和 release 方法,因此內存池面臨碎片空間回收的問題,在頻繁申請和釋放空間後,內存池需要保證連續的內存空間,用於對象的分配。

基於這個需求,有兩種算法用於優化這一塊的內存分配:夥伴系統和 slab 系統。

夥伴系統,用完全二叉樹管理內存區域,左右節點互為夥伴,每個節點代表一個內存塊。內存分配將大塊內存不斷二分,直到找到滿足所需的最小內存分片。

內存釋放會判斷釋放內存分片的夥伴(左右節點)是否空閒,如果空閒則將左右節點合成更大塊內存。

slab 系統,主要解決內存碎片問題,將大塊內存按照一定內存大小進行等分,形成相等大小的內存片構成的內存集。

按照內存申請空間的大小,申請儘量小塊內存或者其整數倍的內存,釋放內存時,也是將內存分片歸還給內存集。

Netty 內存池管理以 Allocate 對象的形式出現。一個 Allocate 對象由多個 Arena 組成,每個 Arena 能執行內存塊的分配和回收。

Arena 內有三類內存塊管理單元:

  • TinySubPage

  • SmallSubPage

  • ChunkList

Tiny 和 Small 符合 Slab 系統的管理策略,ChunkList 符合夥伴系統的管理策略。

當用戶申請內存介於 tinySize 和 smallSize 之間時,從 tinySubPage 中獲取內存塊。

申請內存介於 smallSize 和 pageSize 之間時,從 smallSubPage 中獲取內存塊;介於 pageSize 和 chunkSize 之間時,從 ChunkList 中獲取內存;大於 ChunkSize(不知道分配內存的大小)的內存塊不通過池化分配。

Netty 的 Bootstrap

說完了 Netty 的核心組件以及數據存儲。再回到最開始的例子程序,在程序最開始的時候會 new 一個 Bootstrap 對象,後面所有的配置都是基於這個對象展開的。

高性能底层怎么运作?一文帮你吃透Netty架构原理

生成 Bootstrap 對象

Bootstrap 的作用就是將 Netty 核心組件配置到程序中,並且讓他們運行起來。

從 Bootstrap 的繼承結構來看,分為兩類分別是 Bootstrap 和 ServerBootstrap,一個對應客戶端的引導,另一個對應服務端的引導。

高性能底层怎么运作?一文帮你吃透Netty架构原理

支持客戶端和服務端的程序引導

客戶端引導 Bootstrap,主要有兩個方法 bind 和 connect。Bootstrap 通過 bind 方法創建一個 Channel。

在 bind 之後,通過調用 connect 方法來創建 Channel 連接。

高性能底层怎么运作?一文帮你吃透Netty架构原理

Bootstrap 通過 bind 和 connect 方法創建連接

服務端引導 ServerBootstrap,與客戶端不同的是在 Bind 方法之後會創建一個 ServerChannel,它不僅會創建新的 Channel 還會管理已經存在的 Channel。

高性能底层怎么运作?一文帮你吃透Netty架构原理

ServerBootstrap 通過 bind 方法創建/管理連接

通過上面的描述,服務端和客戶端的引導存在兩個區別:

  • ServerBootstrap(服務端引導)綁定一個端口,用來監聽客戶端的連接請求。而 Bootstrap(客戶端引導)只要知道服務端 IP 和 Port 建立連接就可以了。

  • Bootstrap(客戶端引導)需要一個 EventLoopGroup,但是 ServerBootstrap(服務端引導)則需要兩個 EventLoopGroup。

    因為服務器需要兩組不同的 Channel。第一組 ServerChannel 自身監聽本地端口的套接字。第二組用來監聽客戶端請求的套接字。

高性能底层怎么运作?一文帮你吃透Netty架构原理

ServerBootstrap 有兩組 EventLoopGroup

總結

我們從 NIO 入手,談到了 Selector 的核心機制。然後通過介紹 Netty 客戶端和服務端源代碼運行流程,讓大家對 Netty 編寫代碼有基本的認識。

在 Netty 的核心組件中,Channel 提供 Socket 的連接通道,EventLoop 會對應 Channel 監聽其產生的事件,並且通知執行者。EventloopGroup 的容器,負責生成和管理 EventLoop。

ChannelPipeline 作為 ChannelHandler 的容器會綁定到 Channel 上,然後由 ChannelHandler 提供具體事件處理。另外,ChannelHandlerContext 為 ChannelHandler 和 ChannelPipeline 提供信息共享。

ByteBuf 作為 Netty 的數據容器,通過字節數組的方式存儲數據,並且通過讀索引和寫索引來引導讀寫操作。

上述的核心組件都是通過 Bootstrap 來配置並且引導啟動的,Bootstrap 啟動方式雖然一致,但是針對客戶端和服務端有些許的區別。

簡介:十六年開發和架構經驗,曾擔任過惠普武漢交付中心技術專家,需求分析師,項目經理,後在創業公司擔任技術/產品經理。善於學習,樂於分享。目前專注於技術架構與研發管理。

  • 分享一個很多團隊不知道的方法:回收不健康JVM提升可用性

  • 你所不知道的 ChaosBlade 那些事

  • 里程碑式 Dubbo 2.7.5 版本發佈,性能提升30%,支持 HTTP/2、TLS、Protobuf等特性

  • 美圖每天億級消息存儲演進——從Redis到Titan,完美解決擴容問題


高可用架構

改變互聯網的構建方式


分享到:


相關文章: