Seata 高效能 RPC 通訊的實現基石-Netty篇
來源:架構染色
一、Netty 簡述
Netty
是一個非同步的、基於事件驅動的網路應用框架,用以快速開發高效能、高可靠性的網路 IO 程式。從下方所列舉的特性中不難發現 Netty 優點很多。
學習 Netty
需要從瞭解與 Netty
相關的幾個關鍵類開始,如Bootstrap
、ServerBootstrap
、Channel
、Selector
、ChannelFuture
、EventLoop
、EventLoopGroup
、ChannelHandler
和 Pipeline
等。這些類是 Netty
對網路程式設計抽象的代表,也是 Netty
的精髓。
二、Bootstrap 和 ServerBootstrap
Bootstrap
和 ServerBootstrap
作為 Netty
的引導類,提供配置 Netty 元件的介面,開發者透過這些介面來定製搭配 Netty 的各個元件,組裝出一個健壯、高效能的網路通訊模組。
Bootstrap
是 Netty
的客戶端引導類,引導客戶端程式連線到另一個執行在某個指定主機的指定埠上的服務端程式後進行網路通訊。
ServerBootstrap
是 Netty
的服務端引導類,引導一個服務端程式繫結到某個指定的埠,接收來自客戶端的網路連線後進行網路通訊。
三、Channel
Channel
是 Java NIO 的一個基本構造,從網路程式設計視角看可把Channel
理解成是對 Socket
操作的封裝,所提供的如埠繫結、建立連線、資料讀寫等 API 降低了直接使用 Socket
的複雜度;Channel
具備以下特性:
可獲得當前網路連線的通道狀態 可獲得網路連線的配置引數(緩衝區大小等) 提供非同步的⽹絡 I/O 操作,⽐如建⽴連線、繫結端⼝、資料讀寫等 獲得 ChannelFuture
例項,並在其上註冊監聽器⽤於監聽 I/O 操作成功、失敗、取消時的事件回撥。不同協議、不同 I/O 型別的連線都有不同的 Channel
型別與之對應
四、Selector
java.nio.channels.Selector
是 Java 非阻塞 I/O 實現的關鍵。Selector
管理一組非阻塞 socket
,當這些 socket
中有已就緒可進行 I/O 相關操作的時候,會進行事件通知。使用非阻塞 I/O 比用阻塞 I/O 來處理大量事件相比,處理更快速、更經濟。
Selector
被稱作多路復⽤器,正是因為藉助它可以實現用一個執行緒監視多個檔案控制程式碼,在網路場景中即是一個執行緒監視多個 socket
控制程式碼。
在 Netty
中即是一個 Selector
可以監視多個 Channel
,監聽 I/O 事件,如 OP_ACCEPT(接收連線事件)、OP_CONNECT(連線事件)、OP_READ(讀事件)、OP_WRITE(寫事件),還可以不斷的查詢已註冊 Channel
是否處於就緒狀態,透過一個執行緒中管理一個Selector
,一個Selector
監視多個 Channel
,繼而達到用少量的執行緒管理大量的 Channel
。
五、ChannelFuture
Netty
中所有的 I/O 操作都是非同步的。非同步操作會立即返回,但操作結果可能不會立即返回,獲取結果有同步和非同步兩種方式:
非同步方式,即需要一種在操作執行之後的某個時間點通知使用者其結果的方法。
ChannelFuture
可透過addListener()
方法註冊了一個或多個ChannelFutureListener
,當操作完成時(無論是否成功)監聽器的operationComplete(ChannelFuture channelFuture)
方法會被回撥執行,若是異常可透過channelFuture.cause()
來獲得對應的Throwable
物件。同步方式,需藉助
ChannelFuture#sync()
⽅法達到同步執⾏的效果。
六、EventLoop 和 EventLoopGroup
Netty
具有用少量的執行緒管理大量的 Channel
的能力的基礎是一個執行緒可管理一個可監聽多個 Channel
中 I/O 事件 的 Selector
,那從開發者視角出發,如何提供執行緒,如何關注事件並提供對應的處理邏輯,並儘量少的關注執行緒安全問題?Netty 是瞭解開發者的,提供的這個元件就是EventLoop
。
EventLoop
內建立一個執行緒並管理一個 Selector
,每個 Channel
被建立後就會被分配給一個 Selector
,Selector
會監聽註冊在其上的多個 Channel
的 I/O 事件,EventLoop
會在這個內部執行緒中透過Selector
檢測到多個 Channel
裡發生的 I/O 事件,並將 I/O 事件派發給對應Channel
的 ChannelHandler
。所以一個 Channel
的所有 I/O 事件都在EventLoop
內的這個執行緒中被處理。EventLoop
並不獨立存在,在 Netty
中是被池化管理的,這個管理者就是 EventLoopGroup
,因為每個EventLoop
內都有一個執行緒,所以通常也把EventLoopGroup
類比為執行緒池,參考下圖:
透過上邊的介紹不難看出 EventLoop
的能力封裝將 Selector
透明化了,因此通常 Netty 的資料常常僅介紹 Channel
、EventLoop
和 EventLoopGroup
之間的關係:
一個 EventLoopGroup
包含 n 個EventLoop
;EventLoopGroup
負責為每個新建立的Channel
分配一個EventLoop
,在當前實現中,使用round-robin
(順序迴圈)的方式進行分配以獲取一個均衡的分佈一個 EventLoop
在它的生命週期內只和一個執行緒繫結;且執行緒是按需建立所有由 EventLoop
處理的 I/O 事件都將在它專有的執行緒上被處理;一個 Channel
在它的生命週期內只註冊於一個EventLoop
;多個 Channel
會被分配給同一個EventLoop
。
從執行機制來說EventLoop
是一種事件等待和處理的程式模型,如 Node.js 就是採用 EventLoop
的執行機制,這種機制可以解決多執行緒資源消耗高的問題。每當事件發生時,應用程式都會將產生的事件放入事件佇列當中,然後會輪詢從佇列中取出事件執行或者將事件分發給相應的事件監聽者執行。事件執行的方式通常分為立即執行、延後執行、定期執行幾種,Netty 中的事件執行方式也是這樣,只是事件名稱上稍有差異。
Netty 中 EventLoop
的實現大概是這樣,當EventLoop
首次收到任務後,在其內部例項化一個執行緒,這個執行緒run()
方法的主體邏輯是 for 迴圈來處理事件(Selector
上監聽到的 I/O 任務)和 非同步任務(⾮ I/O 任務,每個 EventLoop
都擁有它自已的非同步任務佇列):
事件(I/O 任務):如 OP_ACCEPT(接收連線事件)、OP_CONNECT(連線事件)、OP_READ(讀事件)、OP_WRITE(寫事件)等,由 processSelectedKeys()
⽅法觸發。非同步任務(⾮ I/O 任務):如 register0、bind0 等任務,以及其他顯式提交的排程任務最終將會被新增到 taskQueue 任務佇列中,由 runAllTasks
⽅法觸發。
事件和非同步任務是以先進先出(FIFO)的順序執行的。這樣可以透過保證位元組內容總是按正確的順序被處理。一個非同步任務提交的小細節,當呼叫 execute()
或者submit()
方法提交非同步任務的執行緒剛好是EventLoop
中的執行緒,則任務會被立即執行,而無需再投入到taskQueue
中。
EventLoop
中除了可以提交這種普通非同步任務,還可以提交定時任務(也算一種特殊的非同步任務),定時任務是排程一個任務在稍後(延遲)執行或者週期性地執行。例如,定時傳送心跳訊息到服務端,以檢查連線是否仍然還活著。如果沒有響應,你便知道可以關閉該 Channe
l 了。定時任務和普通非同步任務在EventLoop
中的執行時機基本類似,其他區別之處在於:
提交⽅法 :
定時非同步任務使⽤ scheduleAtFixedRate()
或者scheduleWithFixedDelay()
⽅法提交任務普通非同步任務使⽤ execute()
或者submit()
方法提交任務
任務佇列 :
定時非同步任務提交到 ScheduleTaskQueue
任務佇列中普通非同步任務提交到 TaskQueue
任務佇列中
EventLoop
中 I/O 事件的處理優先順序是高於taskQueue
中的非同步任務,優先順序的管控可透過ioRatio
微調(請讀者老師自省查閱)。優先順序的控制方法是限制runAllTasks(xxx)
這個方法中非同步任務的處理時長。在runAllTasks(xxx)
,會提取定時任務佇列ScheduleTaskQueue
中到時間點需要被執行的任務,轉移到taskQueue
排隊,之後從taskQueue
裡面逐個取出任務並執行,當本次runAllTasks()
中處理耗時超過限定時間後終止,轉去繼續處理 I/O 事件,如此形成迴圈。
NioEventLoop#run()
的核心邏輯是處理完輪詢到的 key 之後, 首先記錄下耗時, 然後透過 runAllTasks(ioTime \* (100 - ioRatio) / ioRatio)
,限時執行taskQueue
中的任務
protected void run() {
for (;;) {
try {
switch (selectStrategy.calculateStrategy(selectNowSupplier, hasTasks())) {
case SelectStrategy.CONTINUE:
continue;
case SelectStrategy.SELECT:
//輪詢io事件(1)
select(wakenUp.getAndSet(false));
if (wakenUp.get()) {
selector.wakeup();
}
default:
}
cancelledKeys = 0;
needsToSelectAgain = false;
//預設是50
final int ioRatio = this.ioRatio;
if (ioRatio == 100) {
try {
processSelectedKeys();
} finally {
runAllTasks();
}
} else {
//記錄下開始時間
final long ioStartTime = System.nanoTime();
try {
//處理輪詢到的key(2)
processSelectedKeys();
} finally {
//計算耗時
final long ioTime = System.nanoTime() - ioStartTime;
//執行task(3)
runAllTasks(ioTime * (100 - ioRatio) / ioRatio);
}
}
} catch (Throwable t) {
handleLoopException(t);
}
//程式碼省略
}
}
runAllTasks(xxx)
非同步任務的限時處理環節,會提取定時任務佇列ScheduleTaskQueue
中到時間點需要被執行的任務,轉移到taskQueue
排隊,之後從taskQueue
裡面逐個取出任務並執行,處理耗時超過限定時間後終止任務處理,退出方法。
protected boolean runAllTasks(long timeoutNanos) {
//定時任務佇列中提到點取需執行任務
fetchFromScheduledTaskQueue();
//從普通taskQ裡面拿一個任務
Runnable task = pollTask();
//task為空, 則直接返回
if (task == null) {
//跑完所有的任務執行收尾的操作
afterRunningAllTasks();
return false;
}
//如果佇列不為空
//首先算一個截止時間(+50毫秒, 因為執行任務, 不要超過這個時間)
final long deadline = ScheduledFutureTask.nanoTime() + timeoutNanos;
long runTasks = 0;
long lastExecutionTime;
//執行每一個任務
for (;;) {
safeExecute(task);
//標記當前跑完的任務
runTasks ++;
//當跑完64個任務的時候, 會計算一下當前時間
if ((runTasks & 0x3F) == 0) {
//定時任務初始化到當前的時間
lastExecutionTime = ScheduledFutureTask.nanoTime();
//如果超過截止時間則不執行(nanoTime()是耗時的)
if (lastExecutionTime >= deadline) {
break;
}
}
//如果沒有超過這個時間, 則繼續從普通任務佇列拿任務
task = pollTask();
//直到沒有任務執行
if (task == null) {
//記錄下最後執行時間
lastExecutionTime = ScheduledFutureTask.nanoTime();
break;
}
}
//收尾工作
afterRunningAllTasks();
this.lastExecutionTime = lastExecutionTime;
return true;
}
fetchFromScheduledTaskQueue()
這個方法將定時任務中提取到點需執行的定時任務新增到 taskQueue
中
private boolean fetchFromScheduledTaskQueue() {
long nanoTime = AbstractScheduledEventExecutor.nanoTime();
//從定時任務佇列中抓取第一個定時任務
//尋找截止時間為nanoTime的任務
Runnable scheduledTask = pollScheduledTask(nanoTime);
//如果該定時任務佇列不為空, 則塞到普通任務佇列裡面
while (scheduledTask != null) {
//如果新增到普通任務佇列過程中失敗
if (!taskQueue.offer(scheduledTask)) {
//則重新新增到定時任務佇列中
scheduledTaskQueue().add((ScheduledFutureTask<?>) scheduledTask);
return false;
}
//繼續從定時任務佇列中拉取任務
//方法執行完成之後, 所有符合執行條件的定時任務佇列, 都新增到了普通任務佇列中
scheduledTask = pollScheduledTask(nanoTime);
}
return true;
}
EventLoop
將負責在同一個執行緒中處理一個或多個 Channel
的整個生命週期內的所有事件。這個情況也有弊端:
若事件處理耗時很長,將導致 I/O 流量下降。所以需考慮任務處理對系統效能的影響,選擇合適的 Netty 執行緒模型,配置合理的執行緒數
EventLoop
被多個Channel
複用,那麼這些Channel
的ThreadLocal
都將是一樣的。
七、ChannelPipeline
和ChannelHandler
上文提到Eventloop
中將Channel
中的 I/O 事件派發給 ChannelHandler
處理,開發人員在ChannelHandler
中新增對應事件的處理邏輯,從 Netty
對ChannelHandler
的組織管理來說,開發者的視角是用ChannelPipeline
將ChannelHandler
以連結串列的方式串聯起來。如果一個完整的 I/O 處理流程是由 解碼構建訊息->接收處理訊息->回執傳送訊息->訊息編碼傳送 這四個步驟組成,那就用 4 個ChannelHandler
分別實現這 4 步驟的邏輯,這種鏈式處理層次分明、程式碼清晰。
但從原始碼實現的角度是這樣的,Channel
中有個ChannelPipeline
屬性,建立Channel
時,同時例項化這個ChannelPipeline
屬性。ChannelPipeline
串起的其實是ChannelHandlerContext
,為什麼資料常說ChannelPipeline
將ChannelHandler
以連結串列的方式串聯起來呢?原因是被串起的 ChannelHandlerContext 中有個屬性是 ChannelHandler
。ChannelHandlerContext
使得ChannelHandler
能夠和它的ChannelPipeline
以及其他的ChannelHandler
互動。
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70024923/viewspace-2941449/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- Seata 無侵入式分散式事務服務的實現基石-JDBC篇分散式JDBC
- netty 實現簡單的rpc呼叫NettyRPC
- 從零開始實現簡單 RPC 框架 6:網路通訊之 NettyRPC框架Netty
- Java使用Netty實現簡單的RPCJavaNettyRPC
- 簡單的Java實現Netty進行通訊JavaNetty
- 自己用 Netty 實現一個簡單的 RPCNettyRPC
- RPC基本原理以及如何用Netty來實現RPCRPCNetty
- 使用 httputils + protostuff 實現高效能 rpcHTTPRPC
- 微服務8:通訊之RPC實踐篇(附原始碼)微服務RPC原始碼
- 如何利用 Netty 實現自定義協議通訊?Netty協議
- netty通訊Netty
- 訊號處理技術:現代通訊技術的基石
- Netty實現Http高效能伺服器NettyHTTP伺服器
- 造個輪子之基於 Netty 實現自己的 RPC 框架NettyRPC框架
- 使用Netty和動態代理實現一個簡單的RPCNettyRPC
- Netty實現高效能IOT伺服器(Groza)之精盡程式碼篇中Netty伺服器
- Netty實現高效能IOT伺服器(Groza)之手撕MQTT協議篇上Netty伺服器MQQT協議
- 結合RPC框架通訊談 netty如何解決TCP粘包問題RPC框架NettyTCP
- Seata RPC 模組的重構之路RPC
- Thrift RPC 通訊搭建RPC
- RocketMQ(二):RPC通訊MQRPC
- 聊聊 Netty 那些事兒之 Reactor 在 Netty 中的實現(建立篇)NettyReact
- HarmonyOS跨裝置通訊:多端協同的RPC資料傳輸實現RPC
- lms微服務的rpc通訊框架微服務RPC框架
- 大廚小鮮——基於Netty自己動手實現RPC框架NettyRPC框架
- 訊息中介軟體—RocketMQ的RPC通訊(一)MQRPC
- 微服務7:通訊之RPC微服務RPC
- 一個簡單的netty通訊的例子Netty
- 基於Netty實現自定義訊息通訊協議(協議設計及解析應用實戰)Netty協議
- 分散式架構基石-TCP通訊協議分散式架構TCP協議
- 手把手教你基於Netty實現一個基礎的RPC框架(通俗易懂)NettyRPC框架
- 網路通訊2:TCP通訊實現TCP
- go語言實現自己的RPC:go rpc codecGoRPC
- 如何優雅的實現訊息通訊?
- 從零開始實現簡單 RPC 框架 5:網路通訊之序列化RPC框架
- HTTPS通訊的C++實現HTTPC++
- 基於netty手寫RPC框架NettyRPC框架
- 手寫RPC框架(六)整合NettyRPC框架Netty