天天看點

360四面:說說Spring Boot程式啟動中Netty異步架構的原理!前言Netty 的特點與 NIO從一個簡單的例子開始Netty 核心元件Netty 的資料容器Netty 的 Bootstrap總結

前言

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

Spring Boot實戰學習筆記

Netty 的特點與 NIO

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

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

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

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

360四面:說說Spring Boot程式啟動中Netty異步架構的原理!前言Netty 的特點與 NIO從一個簡單的例子開始Netty 核心元件Netty 的資料容器Netty 的 Bootstrap總結

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

當每次用戶端請求時,會建立一個 Socket Channel,并将其注冊到 Selector 上(多路複用器)。

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

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

接到通知的用戶端,就可以通過 SocketChannel 擷取需要的資料了。

360四面:說說Spring Boot程式啟動中Netty異步架構的原理!前言Netty 的特點與 NIO從一個簡單的例子開始Netty 核心元件Netty 的資料容器Netty 的 Bootstrap總結

上面描述的過程有點異步的意思,不過,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,在構造函數中傳入需要監聽的端口号。

360四面:說說Spring Boot程式啟動中Netty異步架構的原理!前言Netty 的特點與 NIO從一個簡單的例子開始Netty 核心元件Netty 的資料容器Netty 的 Bootstrap總結

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

360四面:說說Spring Boot程式啟動中Netty異步架構的原理!前言Netty 的特點與 NIO從一個簡單的例子開始Netty 核心元件Netty 的資料容器Netty 的 Bootstrap總結

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

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

  • 建立 EventLoopGroup。
  • 建立 ServerBootstrap。
  • 指定所使用的 NIO 傳輸 Channel。
  • 使用指定的端口設定套接字位址。
  • 添加一個 ServerHandler 到 Channel 的 ChannelPipeline。
  • 異步地綁定伺服器;調用 sync() 方法阻塞等待直到綁定完成。
  • 擷取 Channel 的 CloseFuture,并且阻塞目前線程直到它完成。
  • 關閉 EventLoopGroup,釋放所有的資源。

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

可以通過

ChannelInboundHandlerAdapter 實作,具體内容如下:

360四面:說說Spring Boot程式啟動中Netty異步架構的原理!前言Netty 的特點與 NIO從一個簡單的例子開始Netty 核心元件Netty 的資料容器Netty 的 Bootstrap總結

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

他們分别是:

  • 當接收到消息時的操作,channelRead。
  • 消息讀取完成時的方法,channelReadComplete。
  • 出現異常時的方法,exceptionCaught。

用戶端代碼

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

360四面:說說Spring Boot程式啟動中Netty異步架構的原理!前言Netty 的特點與 NIO從一個簡單的例子開始Netty 核心元件Netty 的資料容器Netty 的 Bootstrap總結

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

360四面:說說Spring Boot程式啟動中Netty異步架構的原理!前言Netty 的特點與 NIO從一個簡單的例子開始Netty 核心元件Netty 的資料容器Netty 的 Bootstrap總結

用戶端啟動程式的順序:

  • 建立 Bootstrap。
  • 指定 EventLoopGroup 用來監聽事件。
  • 定義 Channel 的傳輸模式為 NIO(Non-BlockingInputOutput)。
  • 設定伺服器的 InetSocketAddress。
  • 在建立 Channel 時,向 ChannelPipeline 中添加一個 EchoClientHandler 執行個體。
  • 連接配接到遠端節點,阻塞等待直到連接配接完成。
  • 阻塞,直到 Channel 關閉。
  • 關閉線程池并且釋放所有的資源。

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

360四面:說說Spring Boot程式啟動中Netty異步架構的原理!前言Netty 的特點與 NIO從一個簡單的例子開始Netty 核心元件Netty 的資料容器Netty 的 Bootstrap總結

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

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

360四面:說說Spring Boot程式啟動中Netty異步架構的原理!前言Netty 的特點與 NIO從一個簡單的例子開始Netty 核心元件Netty 的資料容器Netty 的 Bootstrap總結

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)。

360四面:說說Spring Boot程式啟動中Netty異步架構的原理!前言Netty 的特點與 NIO從一個簡單的例子開始Netty 核心元件Netty 的資料容器Netty 的 Bootstrap總結

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

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

360四面:說說Spring Boot程式啟動中Netty異步架構的原理!前言Netty 的特點與 NIO從一個簡單的例子開始Netty 核心元件Netty 的資料容器Netty 的 Bootstrap總結

EventLoopGroup 要做的就是建立一個新的 Channel,并且給它配置設定一個 EventLoop。

360四面:說說Spring Boot程式啟動中Netty異步架構的原理!前言Netty 的特點與 NIO從一個簡單的例子開始Netty 核心元件Netty 的資料容器Netty 的 Bootstrap總結

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

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

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

當 EventLoop 檢測到服務端傳回的消息,也會通知用戶端:“消息傳回了,你去取吧“。用戶端再去擷取消息。整個過程 EventLoop 就是螢幕+傳聲筒。

③ChannelHandler,ChannelPipeline 和 ChannelHandlerContext

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

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

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

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

  • ChannelInBoundHandler(入站事件處理器)
  • ChannelOutBoundHandler(出站事件處理器)
360四面:說說Spring Boot程式啟動中Netty異步架構的原理!前言Netty 的特點與 NIO從一個簡單的例子開始Netty 核心元件Netty 的資料容器Netty 的 Bootstrap總結

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

360四面:說說Spring Boot程式啟動中Netty異步架構的原理!前言Netty 的特點與 NIO從一個簡單的例子開始Netty 核心元件Netty 的資料容器Netty 的 Bootstrap總結

ChannelPipeline 為 ChannelHandler 鍊提供了容器。到 Channel 被建立的時候,會被 Netty 架構自動配置設定到 ChannelPipeline 上。

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

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

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

360四面:說說Spring Boot程式啟動中Netty異步架構的原理!前言Netty 的特點與 NIO從一個簡單的例子開始Netty 核心元件Netty 的資料容器Netty 的 Bootstrap總結

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

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

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

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

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

360四面:說說Spring Boot程式啟動中Netty異步架構的原理!前言Netty 的特點與 NIO從一個簡單的例子開始Netty 核心元件Netty 的資料容器Netty 的 Bootstrap總結

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

360四面:說說Spring Boot程式啟動中Netty異步架構的原理!前言Netty 的特點與 NIO從一個簡單的例子開始Netty 核心元件Netty 的資料容器Netty 的 Bootstrap總結

把上面提到的幾個核心元件歸納一下,用下圖表示友善記憶他們之間的關系。

360四面:說說Spring Boot程式啟動中Netty異步架構的原理!前言Netty 的特點與 NIO從一個簡單的例子開始Netty 核心元件Netty 的資料容器Netty 的 Bootstrap總結

Netty 的資料容器

前面介紹了 Netty 的幾個核心元件,伺服器在資料傳輸的時候,産生事件,并且對事件進行監控和處理。

接下來看看資料是如何存放以及是如何讀寫的。Netty 将 ByteBuf 作為資料容器,來存放資料。

ByteBuf 工作原理

從結構上來說,ByteBuf 由一串位元組數組構成。數組中每個位元組用來存放資訊。

ByteBuf 提供了兩個索引,一個用于讀取資料,一個用于寫入資料。這兩個索引通過在位元組數組中移動,來定位需要讀或者寫資訊的位置。

當從 ByteBuf 讀取,它的 readerIndex(讀索引)将會根據讀取的位元組數遞增。

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

360四面:說說Spring Boot程式啟動中Netty異步架構的原理!前言Netty 的特點與 NIO從一個簡單的例子開始Netty 核心元件Netty 的資料容器Netty 的 Bootstrap總結

需要注意的是極限的情況是 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 對象,後面所有的配置都是基于這個對象展開的。

360四面:說說Spring Boot程式啟動中Netty異步架構的原理!前言Netty 的特點與 NIO從一個簡單的例子開始Netty 核心元件Netty 的資料容器Netty 的 Bootstrap總結

Bootstrap 的作用就是将 Netty 核心元件配置到程式中,并且讓他們運作起來。

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

360四面:說說Spring Boot程式啟動中Netty異步架構的原理!前言Netty 的特點與 NIO從一個簡單的例子開始Netty 核心元件Netty 的資料容器Netty 的 Bootstrap總結

用戶端引導 Bootstrap,主要有兩個方法 bind() 和 connect()。Bootstrap 通過 bind() 方法建立一個 Channel。

在 bind() 之後,通過調用 connect() 方法來建立 Channel 連接配接。

360四面:說說Spring Boot程式啟動中Netty異步架構的原理!前言Netty 的特點與 NIO從一個簡單的例子開始Netty 核心元件Netty 的資料容器Netty 的 Bootstrap總結

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

360四面:說說Spring Boot程式啟動中Netty異步架構的原理!前言Netty 的特點與 NIO從一個簡單的例子開始Netty 核心元件Netty 的資料容器Netty 的 Bootstrap總結

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

  • ServerBootstrap(服務端引導)綁定一個端口,用來監聽用戶端的連接配接請求。而 Bootstrap(用戶端引導)隻要知道服務端 IP 和 Port 建立連接配接就可以了。
  • Bootstrap(用戶端引導)需要一個 EventLoopGroup,但是 ServerBootstrap(服務端引導)則需要兩個 EventLoopGroup。因為伺服器需要兩組不同的 Channel。第一組 ServerChannel 自身監聽本地端口的套接字。第二組用來監聽用戶端請求的套接字。

總結