開發與維運

螞蟻集團網絡通信框架 SOFABolt 功能介紹及協議框架解析 | 開源

,有趣實用的分佈式架構頻道。
回顧視頻以及 PPT 查看地址見文末。歡迎加入直播互動釘釘群 : 30315793,不錯過每場直播。

SOFAChannel#17

大家好,我是本期 SOFAChannel 的分享講師丞一,來自螞蟻集團,是 SOFABolt 的開源負責人。今天我們來聊一下螞蟻集團開源的網絡通信框架 SOFABolt 的框架解析以及功能介紹。本期分享將從以下四個方面展開:

  • SOFABolt 簡介;
  • 基礎通信能力解析;
  • 協議框架解析;
  • 私有協議實現解析;

SOFABolt 是什麼

SOFABolt 產生背景

相信大家都知道 SOFAStack,SOFAStack(Scalable Open Financial Architecture Stack)是一套用於快速構建金融級雲原生架構的中間件,也是在金融場景裡錘鍊出來的最佳實踐。

SOFABolt 則是 SOFAStack 中的網絡通信框架,是一個基於 Netty 最佳實踐的輕量、易用、高性能、易擴展的通信框架,他的名字 Bolt 取自迪士尼動畫《閃電狗》。他一開始是怎麼在螞蟻集團內部產生的,我們可以類比一下 Netty 的產生原因:

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

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

同時,已有數家企業在生產環境中使用了 SOFABolt,感謝大家的肯定,也希望 SOFABolt 可以給更多的企業帶來實踐價值。

SOFABolt 企業用戶

以上企業信息根據企業用戶 Github 上反饋統計 — 截止 2020.06。

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

SOFABolt 框架組成

SOFABolt 整體可以分為三個部分:

  • 基礎通信能力(基於 Netty 高效的網絡 IO 與線程模型、連接管理、超時控制);
  • 協議框架(命令與命令處理器、編解碼處理器);
  • 私有協議實現(私有 RPC 通信協議的實現);

下面,我們分別介紹一下 SOFABolt 每個部分的具體能力。

基礎通信能力

基礎通信模型

基礎通信模型

如上圖所示,SOFABolt 有多種通信模型,分別為:oneway、sync、future、callback。下面,我們介紹一下每個通信模型以及他們的使用場景。

  • oneway:不關注結果,即客戶端發起調用後不關注服務端返回的結果,適用於發起調用的一方不需要拿到請求的處理結果,或者說請求或處理結果可以丟失的場景;
  • sync:同步調用,調用線程會被阻塞,直到拿到響應結果或者超時,是最常用的方式,適用於發起調用方需要同步等待響應的場景;
  • future:異步調用,調用線程不會被阻塞,通過 future 獲取調用結果時才會被阻塞,適用於需要併發調用的場景,比如調用多個服務端並等待所有結果返回後執行特定邏輯的場景;
  • callback:異步調用,調用線程不會被阻塞,調用結果在 callback 線程中被處理,適用於高併發要求的場景;

oneway 調用的場景非常明確,當調用方不需要拿到調用結果的時候就可以使用這種模式,但是當需要處理調用結果的時候,選擇使用同步的 sync 還是使用異步的 future 和 callback?都是異步調用,又如何在 future、callback 兩種模式中選擇?

顯然同步能做的事情異步也能做,但是異步調用會涉及到線程上下文的切換、異步線程池的設置等等,較為複雜。如果你的場景比較簡單,比如整個流程就一個調用並處理結果,那麼建議使用同步的方式處理;如果整個過程需要分幾個步驟執行,可以拆分不同的步驟異步執行,給耗時的操作分配更多的資源來提升系統整體的吞吐。

在 future 和 callback 的選擇中,callback 是更徹底的異步調用,future 適用於需要協調多個異步調用的場景。比如需要調用多個服務,並且根據多個服務端響應結果執行邏輯時,可以採用 future 的模式給多個服務發送請求,在統一對所有的 future 進行處理完成協同操作。

超時控制機制

在上一部分的通信模型中,除了 oneway 之後,其他三種(sync、future、callback)都需要進行超時控制,因為用戶需要在預期的時間內拿到結果。超時控制簡單來說就是在用戶發起調用後,在預期的時間內如果沒有拿到服務端響應的結果,那麼這次調用就超時了,需要讓用戶感知到超時,避免一直阻塞調用線程或者 callback 永遠得不到執行。

在通信框架中,超時控制必須要滿足高效、準確的要求,因為通信框架是分佈式系統的基礎組件,一旦通信框架出現性能問題,那麼上層系統的性能顯然是無法提升的。超時控制的準確性也非常重要,比如用戶預期一次調用最多隻能執行3秒,因為超時控制不準確導致用戶調用時線程被阻塞了4秒,這顯然是不能接受的。

超時控制

SOFABolt 的超時控制採用了 Netty 中的 HashedWheelTimer,其原理如上圖。假設一次 tick 表示100毫秒,那麼上面的時間輪 tick 一輪表示800毫秒,如果需要在300毫秒後觸發超時,那麼這個超時任務會被放到'2'的 bucket 中,等到 tick 到'2'時則被觸發。如果一個超時任務需要在900毫秒後觸發,那麼它會被放到如'0'的 bucket 中,並標記 task 的 remainingRounds=1,當第一次 tick 到'0'時發現 remainingRounds 不等於0,會對 remainingRounds 進行減1操作,當第二次 tick 到'0',發現這個任務的 remainingRounds 是0,則觸發這個任務。

如果將時間輪的一次 tick 設置為1秒,ticksPerWheel 設置為60,那麼就是現實時鐘的秒針,走完一圈代表一分鐘。如果一個任務需要再1分15秒後執行,就是標記為秒針走一輪之後指向第15格時觸發。關於時間輪的原理推薦閱讀下面這篇論文:
《Hashed and Hierarchical Timing Wheels: data structures to efficiently implement a timer facility》。

快速失敗機制

超時控制機制可以保證客戶端的調用在一個預期時間之後一定會拿到一個響應,無論這個響應是由服務端返回的真實響應,還是觸發了超時。如果因為某些原因導致客戶端的調用超時了,而服務端在超時之後實際將響應結果返回給客戶端了會怎麼樣?

這個響應結果在客戶端會被丟棄,因為對應的請求已經因為超時被釋放掉,服務端的這個響應會因為找不到對應的請求而被丟棄。既然響應在請求超時之後返回給客戶端會被丟棄,那麼在確定請求已經超時的情況下服務端是否可以不處理這個請求而直接返回超時的響應給客戶端?——這就是 SOFABolt 的快速失敗機制。

快速失敗機制

快速失敗機制可以減輕服務端的負擔,使服務端儘快恢復服務。比如因為某些外部依賴的因素導致服務端處理一批請求產生了阻塞,而此時客戶端還在將更多的請求發送到服務端堆積在 Buffer 中等待處理。當外部依賴恢復時,服務端因為要處理已經在 Buffer 中的請求(實際這些請求已經超時,處理這些請求將沒有業務意義),而導致後續正常的請求排隊阻塞。加入快速失敗機制後,在這種情況下可以將 Buffer 中的請求進行丟棄而開始服務當前新增的未超時的請求,使的服務能快速的恢復。

快速失敗機制-2

快速失敗機制的前提條件是能判斷出一個請求已經超時,而判斷超時需要依賴時間,依賴時間則需要統一的時間參照。在分佈式系統中是無法依賴不同的機器上的時間的,因為網絡會有延遲、機器時間的時間會有偏差。為了避免參照時間的不一致(機器之間的時鐘不一致),SOFABolt 的快速失敗機制只依賴於服務端機器自身的時鐘(統一的時間參照),判斷請求已經超時的條件為:

System.currentTimestamp - request.arriveTimestamp > request.timeout

request.arriveTimestamp 為請求達到服務端時的時間,request.timeout 為請求設置的超時時間,因為請求從客戶端發出到服務端需要時間,所以當以到達時間來計算時,如果這個請求已經超時,那麼這個請求在客戶端側必然已經超時,可以安全的將這個請求丟棄。

具體分佈式系統中時間和順序等相關的文件推薦閱讀《Time, Clocks, and the Ordering of Events in a Distributed System》,Lamport 在此文中透徹的分析了分佈式系統中的順序問題。

協議框架

協議框架

SOFABolt 中包含的協議命令如上圖所示。在 RPC 版本的協議命令中只包含兩類:RPC 請求/響應、心跳的請求/響應。RPC 的請求/響應負責攜帶用戶的請求數據和響應數據,心跳請求用於連接的保活,只攜帶少量的信息(一般只包含請求 ID 之類的必要信息即可)。

有了命令之後,還需要有命令的編解碼器和命令處理器,以實現命令的編解碼和處理。RemotingCommand 的處理模型如下:

emotingCommand 的處理模型

整個請求和響應的過程設計的核心組件如上圖所示,其中:

  • 客戶端側:

    • Connection 連接對象的封裝,封裝對底層網絡的操作;
    • CommandEncoder 負責編碼 RemotingCommand,將 RemotingCommand 按照私有協議編碼成 byte 數據;
    • RpcResponseProcessor 負責處理服務端的響應;
  • 服務端側:

    • CommandDecoder 分別負責解碼 byte 數據,按照私有協議將 byte 數據解析成 RemotingCommand 對象;
    • RpcHandler 按照協議碼將 RemotingCommand 轉發到對應的 CommandHandler 處理;
    • CommandHandler 按照 CommandCode 將 RemotingCommand 轉發到對應的 RpcRequestProcessor 處理;
    • RpcRequestProcessor 按照 RemotingCommand 攜帶對象的 Class 將請求轉發到用戶的 UserProcessor 執行業務邏輯,並將結果通過 CommandDecoder 編碼後返回給客戶端;

私有協議實現

內置私有協議實現

SOFABolt 除了提供基礎通信能力外,內置了私有協議的實現,可以做到開箱即用。內置的私有協議實現是經過實踐打磨的,具備擴展性的私有協議實現。

內置私有協議

  • proto:預留的協議碼字段,當協議發生較大變更時,可以通過協議碼進行區分;
  • ver1:確定協議之後,通過協議版本來兼容未來協議的小調整,比如追加字段;
  • type:標識 Command 類型:oneway、request、response;
  • cmdcode:命令碼,比如之前介紹的 RpcRequestCommand、HeartbeatCommand 就需要用不同的命令碼進行區分;
  • ver2:Command 的版本,用於標識同一個命令的不同版本;
  • requestId:請求的 ID,用於唯一標識一個請求,在異步操作中通過此 ID 來映射請求和響應;
  • codec:序列化碼,用於標識使用哪種方式來進行業務數據的序列化;
  • switch:協議開關,用於標識是否開啟某些協議層面的能力,比如開啟 CRC 校驗;
  • timeout:客戶端進行請求時設置的超時時間,快速失敗機制所依賴的超時時間;
  • classLen:業務請求類的的類名長度;
  • headerLen:業務請求頭的長度;
  • contentLen:業務請求體的長度;
  • className:業務請求類的類名;
  • header:業務請求頭;
  • content:業務請求體;
  • CRC32:CRC校驗碼;

實現自定義協議

在 SOFABolt 中實現私有協議的關鍵是實現編解碼器(CommandEncoder/CommandDecoder)及命令處理器(CommandHandler)。

編解碼器

上面是為了在 SOFABolt 中實現自定義私有協議鎖需要編寫的類。SOFABolt 將編解碼器及命令處理器都綁定到 Protocol 對象上,每個 Protocol 實現都有一組自己的編解碼器和命令處理器。

Protocol 對象

在編解碼器中實現自定義的私有協議。在設計私有協議時一定要考慮好協議的可拓展性,以便在未來進行功能增強時不會出現協議無法兼容的情況。

可拓展性

完成編解碼之後剩餘工作就是實現處理器。處理器分為兩塊:命令處理入口 CommandHandler 及具體的業務邏輯執行器 RemotingProcessor。

命令處理入口 CommandHandler

具體的業務邏輯執行器 RemotingProcessor

完成以上工作後,使用 SOFABolt 實現自定義私有協議通信的開發工作基本完成了,但是在實際編寫這部分代碼時會遇到種種困難及限制,主要體現在以下一些方面:

  • 擴展性不足:比如在 RpcClient 中默認使用了內置的編解碼器,且沒有預留接口進行設置,當使用自定義協議時只能繼承 RpcClient 進行覆蓋;
  • 框架和協議耦合:比如默認提供了 CommandHandler->RemotingProcessor->UserProcessor 這樣的處理模型,但是這個模型和協議耦合嚴重(依賴於 CommandCode 和 RequestCode),導致使用自定義協議時只能自己實現 CommandHandler,然後自己在實現請求的分發邏輯等,相當於要重寫 CommandHandler->RemotingProcessor->UserProcessor 這個模型;
  • 協議限制:雖然可以通過自定義 Encoder 和 Decoder 實現自定義協議,但是框架內部組織時都依賴 ProtocolCode,導致需要將 ProtocolCode 加入到協議中,限制了用戶設計私有協議的自由;

總體而言,當前 SOFABolt 提供了非常強大的通信能力和多年沉澱的協議設計。如果用戶需要去適配自己當前已經在運行的私有協議還有可以完善的地方,根本原因還是在於設計之初是貼合這 RPC 框架來設計的(從很多代碼的命名上也能看出來),所以在協議和框架的分離上可以做的更好。

總結

本次分享從 SOFABolt 整體框架的實現開始,介紹了 SOFABolt 的基礎通信模型、超時控制以及快速失敗機制,著重分析了私有協議實現的示例,總結而言 SOFABolt 提供了:

  • 基於 Netty 的最佳實踐;
  • 基礎的通信模型和高效的超時控制機制、快速失敗機制;
  • 內置的私有協議實現,開箱即用;

歡迎 Star SOFABolt:https://github.com/sofastack/sofa-bolt

以上就是本期分享的主要內容。因為直播時間有限,關於 SOFABolt 更詳細的介紹,可以閱讀「剖析 SOFABolt 框架」系列文章,由 SOFABolt 團隊以及開源社區同學共同出品:

「剖析 SOFABolt 框架」解析:https://www.sofastack.tech/blog/ 點擊 tag 「剖析 | SOFABolt 框架」

one more thing

SOFABolt 目前也存在可以提升完善的地方,在嘗試實現完全自定義的私有協議時是相對困難的,需要對代碼做一些繼承改造。

針對這個現狀,我們在“阿里巴巴編程之夏”活動中提交了一個 SOFABolt 的課題:“拆分 SOFABolt 的框架和協議”,希望先通過拆分框架和協議,之後再進行模塊化的處理,使 SOFABolt 成為一個靈活的、可拓展的通信框架最佳實踐!

歡迎大家一起共建來解決這個問題,讓 SOFABolt 變得更好:
https://github.com/sofastack/sofa-bolt/issues/224

SOFAStack 也歡迎更多開源愛好者加入社區共建,成為社區 Contributor、Committer(emoji 表情)

SOFACommunity:https://www.sofastack.tech/community/

本期視頻回顧以及 PPT 查看地址

https://tech.antfin.com/community/live/1265

Leave a Reply

Your email address will not be published. Required fields are marked *