歡迎光臨
每天分享高質量文章

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

SOFA

Scalable Open Financial Architecture

是螞蟻金服自主研發的金融級分佈式中間件,包含了構建金融級雲原生架構所需的各個組件,是在金融場景里錘煉出來的最佳實踐。

 

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

 

目前已經完成的原始碼系列《剖析 | SOFARPC 框架》系列可在文末獲取

 

  基礎介紹

SOFABolt 是螞蟻金服開發的一套基於 Netty 實現的網絡通信框架。

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

Bolt 名字取自迪士尼動畫-閃電狗,是一個基於 Netty 最佳實踐的輕量、易用、高性能、易擴展的通信框架。 

這些年我們在微服務與訊息中間件在網絡通信上解決過很多問題,積累了很多經驗,並持續的進行著優化和完善,我們希望能把總結出的解決方案沉澱到 SOFABolt 這個基礎組件里,讓更多的使用網絡通信的場景能夠統一受益。 目前該產品已經運用在了螞蟻中間件的微服務 (SOFARPC)、訊息中心、分佈式事務、分佈式開關、以及配置中心等眾多產品上。

  前言

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

  編解碼介紹

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

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

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

  TCP 粘包拆包問題

如上圖所示,三種拆包原因見黃色標簽說明;兩種粘包原因見藍色標簽說明。TCP 本身是面向流的,它無法從源源不斷涌來的資料流中拆分出或者合併出有意義的信息,通常可以通過以下幾種方式來解決:
  • 基於分隔符協議:使用定義的字符來標記一個訊息的結尾,在編碼的時候我們在訊息尾部添加指定的分隔符,在解碼的時候根據分隔符來拆分或者合併訊息。Netty 提供了兩種基於分隔符協議的解碼器 LineBasedFrameDecoder 和 DelimiterBasedFrameDecoder。LineBasedFrameDecoder 指定以 \n 或者 \r\n 作為訊息的分隔符;DelimiterBasedFrameDecoder 使用用戶自定義的分隔符來標記訊息的結尾。

  • 基於定長訊息協議:每一個訊息在編碼的時候都使用固定的長度,在解碼的時候根據這個長度進行訊息的拆分和合併。Netty 提供了 FixedLengthFrameDecoder 解碼器來實現定長訊息解碼。

  • 基於變長訊息協議:每一個訊息分為訊息頭和訊息體兩部分,在編碼時,將訊息體的長度設置到訊息頭部,在解碼的時候,首先解析出訊息頭部的長度信息,之後拆分或合併出該長度的訊息體。Netty 提供了 LengthFieldBasedFrameDecoder 來實現變長訊息協議解碼。

  • 基於私有通信協議:Netty 提供了 MessageToByteEncoder 和 ByteToMessageDecoder 兩個抽象類,這兩個抽象類提供了基本的編解碼模板。用戶可以通過繼承這兩個類來實現自定義的編解碼器。SOFABolt 通過繼承 MessageToByteEncoder 實現了自定義的編碼器,通過繼承修改版的 ByteToMessageDecoder 來實現瞭解碼器。對於處理 TCP 粘包拆包問題,SOFABolt 實際上也是使用變長訊息協議,SOFABolt 的私有通信協議將訊息體分為三部分 className、essay-header、body,在訊息頭對應的提供了 classLen、essay-headerLen、bodyContent 分別標識三部分的長度,之後就可以基於這三個長度信息進行訊息的拆分和合併。

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

  SOFABolt 私有通信協議的設計

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

RpcProtocol 協議定義

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

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

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

RpcProtocolV2 協議定義

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

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

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

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

  SOFABolt 編解碼原理

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

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

1. 編碼原理

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

上圖只列出了部分核心代碼,詳細代碼見 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

2. 解碼原理

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

上圖只列出了部分核心代碼

  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 就可以以如下的方式進行訊息的處理。

     

 

  1. 回收 RecyclableArrayList 實體

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

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

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

 

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

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

  參考文件

 

相關鏈接

SOFA 文件: http://www.sofastack.tech/

SOFA: https://github.com/alipay

SOFARPC: https://github.com/alipay/sofa-rpc

SOFABolt: https://github.com/alipay/sofa-bolt

 

  《剖析 | SOFARPC 框架》系列歷史文章

長按關註,獲取分佈式架構乾貨

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

 

點擊閱讀原文,加入我們

    赞(0)

    分享創造快樂