螞蟻金服通訊框架SOFABolt解析|編解碼機制

花肉醬發表於2018-12-11

SOFA

Scalable Open Financial Architecture

是螞蟻金服自主研發的金融級分散式中介軟體,包含了構建金融級雲原生架構所需的各個元件,是在金融場景裡錘鍊出來的最佳實踐。

本文為《螞蟻金服通訊框架SOFABolt解析》系列第一篇,作者水寒,就職於網易考《螞蟻金服通訊框架SOFABolt解析》系列由 SOFA 團隊和原始碼愛好者們出品。

基礎介紹

SOFABolt 是螞蟻金融服務集團開發的一套基於 Netty 實現的網路通訊框架。

  • 為了讓 Java 程式設計師能將更多的精力放在基於網路通訊的業務邏輯實現上,而不是過多的糾結於網路底層 NIO 的實現以及處理難以除錯的網路問題,Netty 應運而生。
  • 為了讓中介軟體開發者能將更多的精力放在產品功能特性實現上,而不是重複地一遍遍製造通訊框架的輪子,SOFABolt 應運而生。

Bolt 名字取自迪士尼動畫-閃電狗,是一個基於 Netty 最佳實踐的輕量、易用、高效能、易擴充套件的通訊框架。 這些年我們在微服務與訊息中介軟體在網路通訊上解決過很多問題,積累了很多經驗,並持續的進行著優化和完善,我們希望能把總結出的解決方案沉澱到 SOFABolt 這個基礎元件裡,讓更多的使用網路通訊的場景能夠統一受益。 目前該產品已經運用在了螞蟻中介軟體的微服務 (SOFARPC)、訊息中心、分散式事務、分散式開關、以及配置中心等眾多產品上。

前言

SOFABolt 提供了設計良好、使用便捷的編解碼功能。本篇我們會依次介紹編解碼的概念, TCP 粘包拆包問題,SOFABolt 私有通訊協議的設計,以及SOFABolt 編解碼原理,最後還會介紹一下相較於 Netty,我們做出的優化。歡迎大家與我們討論交流。

編解碼介紹

每個網路應用程式都必須定義如何解析在兩個節點之間來回傳輸的原始位元組,以及如何將其和目標應用程式的資料格式做相互轉換。在一個成熟的通訊框架中,我們通常都會通過私有通訊協議來描述這種定義,通過編解碼技術將理論上的私有通訊協議轉化為實踐。

通過編解碼技術,我們可以方便的做一些邏輯,例如雙方可以方便的統一序列化與反序列化方式、解決 TCP 拆包粘包問題等。

下面,我們先來看一下 TCP 粘包拆包問題的產生,然後分析 Netty 是如何解決粘包拆包問題的,最後分析 SOFABolt 是如何解決粘包拆包問題的。

TCP 粘包拆包問題

image.png | left | 827x373

如上圖所示,三種拆包原因見黃色標籤說明;兩種粘包原因見藍色標籤說明。TCP 本身是面向流的,它無法從源源不斷湧來的資料流中拆分出或者合併出有意義的資訊,通常可以通過以下幾種方式來解決:

  • 基於分隔符協議:使用定義的字元來標記一個訊息的結尾,在編碼的時候我們在訊息尾部新增指定的分隔符,在解碼的時候根據分隔符來拆分或者合併訊息。Netty 提供了兩種基於分隔符協議的解碼器 LineBasedFrameDecoder 和 DelimiterBasedFrameDecoder。LineBasedFrameDecoder 指定以 n 或者 rn 作為訊息的分隔符;DelimiterBasedFrameDecoder 使用使用者自定義的分隔符來標記訊息的結尾。
  • 基於定長訊息協議:每一個訊息在編碼的時候都使用固定的長度,在解碼的時候根據這個長度進行訊息的拆分和合並。Netty 提供了 FixedLengthFrameDecoder 解碼器來實現定長訊息解碼。
  • 基於變長訊息協議:每一個訊息分為訊息頭和訊息體兩部分,在編碼時,將訊息體的長度設定到訊息頭部,在解碼的時候,首先解析出訊息頭部的長度資訊,之後拆分或合併出該長度的訊息體。Netty 提供了 LengthFieldBasedFrameDecoder 來實現變長訊息協議解碼。
  • 基於私有通訊協議:Netty 提供了 MessageToByteEncoder 和 ByteToMessageDecoder 兩個抽象類,這兩個抽象類提供了基本的編解碼模板。使用者可以通過繼承這兩個類來實現自定義的編解碼器。SOFABolt 通過繼承 MessageToByteEncoder 實現了自定義的編碼器,通過繼承修改版的 ByteToMessageDecoder 來實現瞭解碼器。對於處理 TCP 粘包拆包問題,SOFABolt 實際上也是使用變長訊息協議,SOFABolt 的私有通訊協議將訊息體分為三部分 className、header、body,在訊息頭對應的提供了 classLen、headerLen、bodyContent 分別標識三部分的長度,之後就可以基於這三個長度資訊進行訊息的拆分和合並。

對於一個成熟的 rpc 框架或者通訊框架來講,編解碼器不僅僅是要處理粘包拆包問題,還要實現一些特有的需求,所以必須制定一些私有通訊協議,下面來看一下 SOFABolt 的私有通訊協議的設計。

SOFABolt 私有通訊協議的設計

以下分析以 SOFABolt 1.5.1 版本為例。SOFABolt 定義了兩種協議 RpcProtocol 和 RpcProtocolV2。針對這兩種協議,提供了兩組不同的編解碼器。

RpcProtocol 協議定義

請求命令(協議頭長度:22 byte)

image.png | left | 747x226

  • ProtocolCode :這個欄位是必須的。因為需要根據 ProtocolCode 來進入不同的核心編解碼器。該欄位可以在想換協議的時候,方便的進行更換。
  • RequestType :請求型別,request / response / oneway 三者之一。oneway 之所以需要單獨設定,是因為在處理響應時,需要做特殊判斷,來控制響應是否回傳。
  • CommandCode :請求命令型別,request / response / heartbeat 三者之一。
  • CommandVersion :請求命令版本號。該欄位用來區分請求命令的不同版本。如果修改 Command 版本,不修改協議,那麼就是純粹程式碼重構的需求;除此情況,Command 的版本升級,往往會同步做協議的升級。
  • RequestId :請求 ID,該欄位主要用於非同步請求時,保留請求存根使用,便於響應回來時觸發回撥。另外,在日誌列印與問題除錯時,也需要該欄位。
  • Codec :序列化器。該欄位用於儲存在做業務的序列化時,使用的是哪種序列化器。通訊框架不限定序列化方式,可以方便的擴充套件。
  • Timeout :超時欄位,客戶端發起請求時,所設定的超時時間。
  • ClassLen :業務請求類名長度
  • HeaderLen :業務請求頭長度
  • ContentLen :業務請求體長度
  • ClassName :業務請求類名。需要注意類名傳輸的時候,務必指定字符集,不要依賴系統的預設字符集。曾經線上的機器,因為運維誤操作,預設的字符集被修改,導致字元的傳輸出現編解碼問題。而我們的通訊框架指定了預設字符集,因此躲過一劫。
  • HeaderContent :業務請求頭
  • BodyContent :業務請求體

響應命令(協議頭長度:20 byte)

image.png | left | 747x223

  • ResponseStatus :響應碼。從欄位精簡的角度,我們不可能每次響應都帶上完整的異常棧給客戶端排查問題,因此,我們會定義一些響應碼,通過編號進行網路傳輸,方便客戶端定位問題。

RpcProtocolV2 協議定義

請求命令(協議頭長度:24 byte)

image.png | left | 747x223

  • ProtocolVersion :確定了某一種通訊協議後,我們還需要考慮協議的微小調整需求,因此需要增加一個 version 的欄位,方便在協議上追加新的欄位
  • Switch :協議開關,用於一些協議級別的開關控制,比如 CRC 校驗,安全校驗等。
  • CRC32 :CRC校驗碼,這也是通訊場景裡必不可少的一部分,而我們金融業務屬性的特徵,這個顯得尤為重要。

響應命令(協議頭長度:22 byte)

image.png | left | 747x254

SOFABolt 針對 RpcProtocol 和 RpcProtocolV2 這兩種協議,提供了兩組不同的編解碼器。下面我們來看一下編解碼器的設計原理。

SOFABolt 編解碼原理

image.png | left | 827x490

上圖僅列出編解碼中最主要的類。

  • RpcCodec 是工廠類,建立 ProtocolCodeBasedEncoder 和 ProtocolCodeBasedDecoder(實際上是其子類),二者被設定為 netty 的編解碼器 handler – 工廠模式
  • MessageToByteEncoder 提供了編碼模板,該類由 netty 本身提供;AbstractBatchDecoder 提供瞭解碼模板,由 SOFABolt 提供,該類是 ByteToMessageDecoder 的 hack 版本,相較於 netty 提供了批量提交的功能 – 模板模式
  • ProtocolCodeBasedEncoder 和 ProtocolCodeBasedDecoder 分別是 CommandEncoder 和 CommandDecoder 的代理類,通過不同的 protocol 協議,指定使用不同的編解碼器 – 代理模式和策略模式
  • 最下層的四個編解碼器:Xxx 是 RpcProtocol 協議資料的編解碼器;XxxV2 是RpcProtocolV2 協議資料的編解碼器

編碼原理

如上述類圖所示,SOFABolt 的編碼器 ProtocolCodeBasedEncoder 是繼承 MessageToByteEncoder 的,MessageToByteEncoder 為 ProtocolCodeBasedEncoder 提供了編碼模板。在 MessageToByteEncoder 中呼叫了子類 ProtocolCodeBasedEncoder 的實際編碼程式碼,大致流程如下所示:

encode2.png | center | 747x920

上圖只列出了部分核心程式碼,詳細程式碼見 SOFABolt 原始碼與 Netty 原始碼。

  1. 判斷傳入的資料是否是 Serializable 型別(該型別由 MessageToByteEncoder 的泛型指定),如果不是,直接傳播給 pipeline 中的下一個 handler;否則
  2. 建立一個 ByteBuf 例項,用於儲存最終的編碼資料
  3. 從 channel 的附加屬性中獲取協議標識 protocolCode,之後從協議管理器中獲取相應的協議物件
  4. 再從協議物件中獲取相應的 CommandEncoder 實現類例項,使用 CommandEncoder 實現類例項按照上文所介紹的協議規則將資料寫入到第二步建立好的 ByteBuf 例項中
  5. 如果原始資料是 ReferenceCounted 實現類,則釋放原始資料
  6. 如果 ByteBuf 中有資料了,則傳播給 pipeline 中的下一個 handler;否則,釋放該 ByteBuf 物件,傳遞一個空的 ByteBuf 給下一個 handler

注意:

  • 由第一步可知,在 SOFABolt 中,資料要想經過編碼器的處理,必須實現 Serializable 介面。
  • 編碼器是無狀態的,可以標註註解 @ChannelHandler.Sharable

解碼原理

SOFABolt 的解碼器 ProtocolCodeBasedDecoder 是繼承 AbstractBatchDecoder 的,AbstractBatchDecoder 為 ProtocolCodeBasedDecoder 提供瞭解碼模板。在 AbstractBatchDecoder 中呼叫了子類 ProtocolCodeBasedDecoder 的實際解碼程式碼,如下所示:

decode3.png | center | 747x1149

上圖只列出了部分核心程式碼

  1. 建立或者從 netty 的回收池中獲取一個 RecyclableArrayList 例項,用於儲存最終的解碼資料
  2. 將傳入的 ByteBuf 新增到 Cumulator 累加器例項中
  3. 之後不斷的從 ByteBuf 中讀取資料:首先解碼出protocolCode,之後從協議管理器中獲取相應的協議物件,再從協議物件中獲取相應的 CommandDecoder 實現類例項
  4. 使用 CommandDecoder 實現類例項按照上文所介紹的協議規則進行解碼,將解碼好的資料放到 RecyclableArrayList 例項中,需要注意的是在解碼之前必須先記錄當前 ByteBuf 的 readerIndex,如果發現資料不夠一個整包長度(發生了拆包粘包問題),則將當前 ByteBuf 的 readerIndex 復原到解碼之前,然後直接返回,等待讀取更多的資料
  5. 為了防止傳送端傳送資料太快導致OOM,會清理 Cumulator 累加器例項或者其空間,將已經讀取的位元組刪除,向左壓縮 ByteBuf 空間
  6. 判斷 RecyclableArrayList 中的元素個數,如果是1個,則將這個元素單個傳送給 pipeline 的下一個 handler;如果元素大於1個,則將整個 RecyclableArrayList 以 List 形式傳送給 pipeline 的下一個 handler。後續的 handler 就可以以如下的方式進行訊息的處理。

image.png | center | 507x295

  1. 回收 RecyclableArrayList 例項

注意:解碼器是有狀態的,不可標註註解 @ChannelHandler.Sharable

最後我們介紹一下 SOFABolt 解碼器相較於 Netty 作出的優化。

SOFABolt 解碼器相較於 Netty 作出的優化

image.png | left | 673x353

(圖片來自 螞蟻通訊框架實踐

Netty 提供了一個方便的解碼工具類 ByteToMessageDecoder ,如圖上半部分所示,這個類具備 accumulate 批量解包能力,可以儘可能的從 socket 裡讀取位元組,然後同步呼叫 decode 方法,解碼出業務物件,並組成一個 List 。最後再迴圈遍歷該 List ,依次提交到 ChannelPipeline 進行處理。此處我們做了一個細小的改動,如圖下半部分所示,即將提交的內容從單個 command ,改為整個 List 一起提交,如此能減少 pipeline 的執行次數,同時提升吞吐量。這個模式在低併發場景,並沒有什麼優勢,而在高併發場景下對提升吞吐量有不小的效能提升。

參考文件

image | left | 216x216

長按關注,獲取分散式架構乾貨

歡迎大家共同打造 SOFAStack https://github.com/alipay


相關文章