作者:螞蟻技術團隊;
來源:金融級分佈式架構
螞蟻通信框架實踐


前 言

互聯網領域的通信技術,有各式各樣的通信協議可以選擇,比如基於 TCP/IP 協議簇的 HTTP(1/2)、SPDY 協議、WebSocket、Google 基於 UDP 的 QUIC 協議等。這些協議,都有完整的報文格式與字段定義,對安全,序列化機制,數據壓縮機制,CRC 校驗機制等各種通信細節都有較好的設計。能夠高效、穩定、且安全地運行在公網環境。

而對於私網環境,比如一個公司的 IDC 內部,如果所有應用的節點間,全部通過標準協議來通信,會有很多問題:比如研發效率方面的影響,我們的研發框架,需要做大量業務數據轉化成標準協議的工作;再比如升級兼容性,標準協議的字段衆多,版本各異,兼容性也得不到保障;除此還有無用字段的傳輸,也會造成資源浪費,功能定製也可能不那麼靈活。而解決這些問題,比較常見的做法就是自己來設計協議,可以自己來定義字段,制定升級方式,可插拔可開關的特性需求等,我們把這樣的協議叫做私有通信協議。

在螞蟻金服的分佈式技術體系下,我們大量的技術產品(非網關類產品),都需要在內網,進行節點間通信。高吞吐、高併發的通信,數量衆多的連接管理(C10K 問題),便捷的升級機制,兼容性保障,靈活的線程池模型運用,細緻的異常處理與日誌埋點等,這些功能都需要在通信協議和實現框架上做文章。本文主要從如下幾個方面來對螞蟻通信框架實踐之路進行介紹:

  1. 私有通信協議設計
  2. 基礎通信功能設計要點分析
  3. 私有通信協議設計舉例
  4. 螞蟻自研通信框架 Bolt

私有通信協議設計

我們的分佈式架構,所需要的內部通信模塊,採用了私有協議來設計和研發。當然私有協議,也是有很多弊端的,比如在通用性上、公網傳輸的能力上相比標準協議會有劣勢。然而,我們爲了最大程度的提升性能,降低成本,提高靈活性與效率,最佳選擇還是高度定製化的私有協議:

  • 可以有效地利用協議裏的各個字段
  • 靈活滿足各種通信功能需求:比如 CRC 校驗,Server Fail-Fast 機制,自定義序列化器
  • 最大程度滿足性能需求:IO 模型與線程模型的靈活運用

比如一個典型的 Request-Response 通信場景:

  1. 在一個通信節點上,如何把一個請求對象,序列化成字節流,通過怎樣的網絡傳輸方式,傳遞到另一個節點
  2. 在對端的通信節點上,需要高效的讀取字節流,並反序列化成原始的請求對象,然後根據請求內容,做一些邏輯處理。處理完成後,響應返回。
  3. 同時,此時要考慮,如何充分利用網絡 IO、CPU 以及內存,來保證吞吐和處理效率的最優。

文章後面的內容,比較清晰地介紹了這個通信場景的設計與實現方案。


螞蟻通信框架實踐


圖1 - 私有協議與必要的功能模塊

首先協議設計上,我們需要考慮的幾個關鍵問題:

Protocol

  • 協議應該包括哪些必要字段與主要業務負載字段:協議裏設計的每個字段都應該被使用到,避免無效字段;
  • 需要考慮通信功能特性的支持:比如CRC校驗,安全校驗,數據壓縮機制等;
  • 需要考慮協議的可擴展性:充分評估現有業務的需求,設計一個通用,擴展性高的協議,避免經常對協議進行修改;
  • 需要考慮協議的升級機制:畢竟是私有協議,沒有長期的驗證,字段新增或者修改,是有可能發生的,因此升級機制是必須考慮的;

Encoder 與 Decoder

  • 協議相關的編解碼方式:私有協議需要有核心的encode與decode過程,並且針對業務負載能支持不同的序列化與反序列化機制。這部分,不同的私有協議,由於字段的差異,核心encode和decode過程是不一樣的,因此需要分開考慮

Heartbeat

  • 協議相關的心跳觸發與處理:不同的協議對心跳的需求,處理邏輯也可能是不同的。因此心跳的觸發邏輯,心跳的處理邏輯,也都需要單獨考慮。

Command 與 Command Handler

  • 可擴展的命令與命令處理器管理

螞蟻通信框架實踐


  • 圖2 - 通信命令設計舉例


  • 負載命令:一般傳輸的業務的具體數據,比如帶着請求參數,響應結果的命令;
  • 控制命令:一些功能管理命令,心跳命令等,它們通常完成一些複雜的分佈式跨節點的協調功能,以此來保證負載命令通信過程的穩定,是必不可少的一部分。
  • 協議的通信過程,會有各種命令定義,邏輯上,我們把傳輸業務具體負載的請求對象,叫做負載命令(Payload Command),另一種叫做控制命令(Control Command),比如一些功能管理命令,或者心跳命令。
  • 定義了通信命令,我們還需要定義命令處理器,用來編寫各個命令對應的業務處理邏輯。同時,我們需要保存命令與命令處理器的映射關係,以便在處理階段,走到正確的處理器。

有了私有協議的設計要點,我們接下來分兩部分來介紹下實現:基礎通信模塊與私有協議設計舉例。

首先是基礎通信功能模塊的實現,這部分沉澱了我們的一些優化和最佳實踐,可以被不同的私有協議複用。

基礎通信功能設計要點分析

螞蟻的中間件產品,主要是 Java 語言開發,如果通信產品直接用原生的 Java NIO 接口開發,工作量相當龐大。通常我們會選擇一些基礎網絡編程框架,而在基礎網絡通信框架上,我們也經歷了自研(比如伯巖的 Gecko)、基於 Apache Mina 實現。最終,由於 Netty 在網絡編程領域的出色表現,我們逐步切換到了 Netty 上。

Netty 在 2008 年就發佈了3.0.0 版本,到現在已經經歷了 10 年多的發展。而且從 4.x之後的版本,把無鎖化的設計理念放在第一位,然後針對內存分配,高效的 Queue 隊列,高吞吐的超時機制等,做了各種細節優化。同時 Netty 的核心 Committer 與社區非常活躍,如果發現了缺陷能夠及時得到修復。所有這些,使得 Netty 性能非常的出色和穩定,成爲當下 Java 領域最優秀的網絡通信組件。接下來主要介紹我們對 Netty 的學習經驗,內部使用上的一些最佳實踐。

1

網絡 IO 模型與線程模型


螞蟻通信框架實踐


圖3 - Netty與Reactor

如果你對 Java 網絡 IO 這個話題感興趣的話,肯定看過 Doug Lea 的《Scalable IO in Java》,在這個 PPT 裏詳細介紹瞭如何使用 Java NIO 的技術來實現 Douglas C. Schmidt 發表的 Reactor 論文裏所描述的 IO 模型。針對這個高效的通信模型,Netty 做了非常友好的支持:

  • Reactor模型
  • 我們只需要在初始化 ServerBootstrap 時,提供兩個不同的 EventLoopGroup實例,就實現了 Reactor 的主從模型。我們通常把處理建連事件的線程,叫做 BossGroup,對應 ServerBootstrap 構造方法裏的 parentGroup 參數,即我們常說的 Acceptor 線程;處理已創建好的 channel 相關連 IO 事件的線程,叫做 WorkerGroup,對應 ServerBootstrap 構造方法裏的 childGroup 參數,即我們常說的 IO 線程。
  • 最佳實踐:通常 bossGroup 只需要設置爲 1 即可,因爲 ServerSocketChannel 在初始化階段,只會註冊到某一個 eventLoop 上,而這個 eventLoop 只會有一個線程在運行,所以沒有必要設置爲多線程(什麼時候需要多線程呢,可以參考 Norman Maurer 在 StackOverflow 上的這個回答);而 IO 線程,爲了充分利用 CPU,同時考慮減少線上下文切換的開銷,通常設置爲 CPU 核數的兩倍,這也是 Netty 提供的默認值。
  • 串行化設計理念
  • Netty 從 4.x 的版本之後,所推崇的設計理念是串行化處理一個 Channel 所對應的所有 IO 事件和異步任務,單線程處理來規避併發問題。Netty 裏的 Channel在創建後,會通過 EventLoopGroup 註冊到某一個 EventLoop 上,之後該 Channel 所有讀寫事件,以及經由 ChannelPipeline 裏各個 Handler 的處理,都是在這一個線程裏。一個 Channel 只會註冊到一個 EventLoop 上,而一個 EventLoop 可以註冊多個 Channel 。所以我們在使用時,也需要儘可能避免使用帶鎖的實現,能無鎖化就無鎖。
  • 最佳實踐:Channel 的實現是線程安全的,因此我們通常在運行時,會保存一個 Channel 的引用,同時爲了保持 Netty 的無鎖化理念,也應該儘可能避免使用帶鎖的實現,尤其是在 Handler 裏的處理邏輯。舉個例子:這裏會有一個比較特殊的容易死鎖的場景,比如在業務線程提交異步任務前需要先搶佔某個鎖,Handler裏某個異步任務的處理也需要獲取同一把鎖。如果某一個時刻業務線程先拿到鎖 lock1,同時 Handler 裏由於事件機制觸發了一個異步任務 A,並在業務線程提交異步任務之前,提交到了 EventLoop 的隊列裏。之後,業務線程提交任務 B,等待 B 執行完成後才能釋放鎖 lock1;而任務 A 在隊列裏排在 B 之前,先被執行,執行過程需要獲取鎖 lock1 才能完成。這樣死鎖就發生了,與常見的資源競爭不同,而是任務執行權導致的死鎖。要規避這類問題,最好的辦法就是不要加鎖;如果實在需要用鎖,需要格外注意 Netty 的線程模型與任務處理機制。
  • 業務處理
  • IO 密集型的輕計算業務:此時線程的上下文切換消耗,會比 IO 線程的佔用消耗更爲突出,所以我們通常會建議在 IO 線程來處理請求;
  • CPU 密集型的計算業務:比如需要做遠程調用,操作 DB 的業務,此時 IO 線程的佔用遠遠超過線程上下文切換的消耗,所以我們就會建議在單獨的業務線程池裏來處理請求,以此來釋放 IO 線程的佔用。該模式,也是我們螞蟻微服務,消息通信等最常使用的模型。該模式在後面的 RPC 協議實現舉例部分會詳細介紹。
  • 如文章開頭所描述的場景,我們需要合理設計,來將硬件的 IO 能力,CPU 計算能力與內存結合起來,發揮最佳的效果。針對不同的業務類型,我們會選擇不同的處理方式
  • 最佳實踐:“Never block the event loop, reduce context-swtiching”,引自Netty committer Norman Maurer,另外阿里 HSF 的作者畢玄也有類似的總結。
  • 其他實踐建議
  • 最小化線程池,能複用 EventLoopGroup 的地方儘量複用。比如螞蟻因爲歷史原因,有過兩版 RPC 協議,在兩個協議升級過渡期間,我們會複用 Acceptor 線程與 IO 線程在同一個端口處理不同協議的請求;除此,針對多應用合併部署的場景,我們也會複用 IO 線程防止一個進程開過多的 IO 線程。
  • 對於無狀態的 ChannelHandler ,設置成共享模式。比如我們的事件處理器,RPC 處理器都可以設置爲共享,減少不同的 Channel 對應的 ChannelPipeline 裏生成的對象個數。
  • 正確使用 ChannelHandlerContext 的 ctx.write() 與 ctx.channel().write() 方法。前者是從當前 Handler 的下一個 Handler開始處理,而後者會從 tail 開始處理。大多情況下使用 ctx.write() 即可。
  • 在使用 Channel 寫數據之前,建議使用 isWritable() 方法來判斷一下當前 ChannelOutboundBuffer 裏的寫緩存水位,防止 OOM 發生。不過實踐下來,正常的通信過程不太會 OOM,但當網絡環境不好,同時傳輸報文很大時,確實會出現限流的情況。

2

連接管理

爲了提高通信效率,我們需要考慮複用連接,減少 TCP 三次握手的次數,因此需要有連接管理的機制。而在業務的通信場景中,我們還識別到一些不得不走硬負載(比如 LVS VIP)的場景,此時如果只建立單鏈接,可能會出現負載不均衡的問題,此時需要建立多個連接,來緩解負載不均的問題。我們需要設計一個針對某個連接地址(IP 與 Port 唯一確定的地址)建立特定數目連接的實現,同時保存在一個連接池裏。該連接池設計了一個通用的 PoolKey不限定 Key 的類型。

需要注意的是,這裏的建連過程,有一個併發問題要解,比如客戶端在高併發的調用建連接口時,如何保證建立的連接剛好是所設定的個數呢?爲了配合 Netty 的無鎖理念,我們也採用一個無鎖化的建連過程來實現,利用 ConcurrentHashMap 的 putIfAbsent 接口:


螞蟻通信框架實踐


代碼1 - 無鎖建連代碼

除此,我們的連接管理,還要具備定時斷連功能,自動重連功能,自定義連接選擇算法功能來適用不同的連接場景。

  • 最佳實踐:在 Netty 的 4.0.28.Final#3218 裏,提供了一種 ChannelPool 的接口類與默認實現,其中 FixedChannelPool 與我們實現的連接池做的事情一樣。而 Netty 採用了更巧妙的方式來規避併發問題,即在初始化 FixedChannelPool 時,就將其關聯到某一個 eventLoop 上,後續的建連動作,採用經典的 inEventLoop() 方法來判斷,如果不在 eventLoop 線程,則入隊等待下次調度。如此規避了併發問題。這個功能,我們目前還沒有實踐過,後續計劃採用這個官方實現重構一版。

3

基礎通信模型


螞蟻通信框架實踐


圖4 - 幾種通信模型

如圖所示,我們實現了多種通信接口 oneway ,sync ,future ,callback 。圖中都是ping/pong模式的通信,藍色部分表示線程正在執行任務

  • 可以看到 oneway 不關心響應,請求線程不會被阻塞,但使用時需要注意控制調用節奏,防止壓垮接收方;
  • sync 調用會阻塞請求線程,待響應返回後才能進行下一個請求。這是最常用的一種通信模型;
  • future 調用,在調用過程不會阻塞線程,但獲取結果的過程會阻塞線程;
  • callback 是真正的異步調用,永遠不會阻塞線程,結果處理是在異步線程裏執行。

4

超時控制


螞蟻通信框架實踐


圖5 - 超時控制模型

除了 oneway 模式,其他三種通信模型都需要進行超時控制,我們同樣採用 Netty 裏針對超時機制,所設計的高效方案 HashedWheelTimer 。如圖所示,其原理是首先在發起調用前,我們會新增一個超時任務 timeoutTask 到 MpscQueue (Netty 實現的一種高效的無鎖隊列)裏,然後在循環裏,會不斷的遍歷 Queue 裏的這些超時任務(每次最多10萬),針對每個任務,會根據其設置的超時時間,來計算該任務所屬於的 bucket 位置與剩餘輪數 remainingRounds ,然後加入到對應 bucket 的鏈表結構裏。隨着 tick++ 的進行,時間在不斷的增長,每 tick 8 次,就是 1 個時間輪 round。當對應超時任務的remainingRounds減到 0 時,就是觸發這個超時任務的時候,此時再執行其 run() 方法,做超時邏輯處理。

  • 最佳實踐:通常一個進程使用一個HashedWheelTimer實例,採用單例模型即可。

5

批量解包與批量提交


螞蟻通信框架實踐


圖6 - 批量解包與批量提交

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

  • 最佳實踐:ByteToMessageDecoder 因爲內部的實現有成員變量,不是無狀態的,所以一定不能被設置爲 @Sharable

6

其他有用的功能

  • 事件觸發與監聽機制
  • Netty 的 ChannelHandler 完美實現了攔截器模式。在 ChannelHandler 裏 hook 了各個IO事件與IO操作的方法,我們可以方便的覆寫這些方法,來加一些自定義的邏輯。比如爲了把建連,斷連事件觸發給上層業務,方便做一些準備或者優雅關閉的處理,我們實現一個繼承了 ChannelInBoundHandler 與 ChannelOutboundHandler 的處理器,覆蓋這些事件所對應的建連與斷連方法,然後設計一套業務的 event 感知邏輯即可。
  • 雙工通信
  • 我們知道 TCP 是可以提供全雙工的通信能力的。因此,當客戶端與服務端建立連接後,我們是可以由服務端發起通信請求,客戶端來處理的。而爲了支持這個功能,我們只需要把可以複用的 inboundHandler 與 outboundHandler 在 客戶端的 Bootstrap 與服務端的 ServerBootstrap 裏都註冊一遍即可

有了私有協議的設計要點,與基礎通信模塊的實現,我們來看一個私有協議設計的舉例,一種典型的 RPC 特徵的通信實現。

私有通信協議舉例

1

通信協議的設計


螞蟻通信框架實踐


圖7 - 協議字段舉例

  • ProtocolCode :如果一個端口,需要處理多種協議的請求,那麼這個字段是必須的。因爲需要根據 ProtocolCode 來進入不同的核心編解碼器。比如在支付寶,因爲曾經使用過基於mina開發的通信框架,當時設計了一版協議。因此,我們在設計新版協議時,需要預留該字段,來適配不同的協議類型。該字段可以在想換協議的時候,方便的進行更換。
  • ProtocolVersion :確定了某一種通信協議後,我們還需要考慮協議的微小調整需求,因此需要增加一個 version 的字段,方便在協議上追加新的字段

螞蟻通信框架實踐


圖8 - 協議號與版本號的關係

  • RequestType :請求類型, 比如request response oneway
  • CommandCode :請求命令類型,比如 request 可以分爲:負載請求,或者心跳請求。oneway 之所以需要單獨設置,是因爲在處理響應時,需要做特殊判斷,來控制響應是否回傳。
  • CommandVersion :請求命令版本號。該字段用來區分請求命令的不同版本。如果修改 Command 版本,不修改協議,那麼就是純粹代碼重構的需求;除此情況,Command 的版本升級,往往會同步做協議的升級。
  • RequestId :請求 ID,該字段主要用於異步請求時,保留請求存根使用,便於響應回來時觸發回調。另外,在日誌打印與問題調試時,也需要該字段。
  • Codec :序列化器。該字段用於保存在做業務的序列化時,使用的是哪種序列化器。通信框架不限定序列化方式,可以方便的擴展。
  • Switch :協議開關,用於一些協議級別的開關控制,比如 CRC 校驗,安全校驗等。
  • Timeout :超時字段,客戶端發起請求時,所設置的超時時間。該字段非常有用,在後面會詳細講解用法。
  • ResponseStatus :響應碼。從字段精簡的角度,我們不可能每次響應都帶上完整的異常棧給客戶端排查問題,因此,我們會定義一些響應碼,通過編號進行網絡傳輸,方便客戶端定位問題。
  • ClassLen :業務請求類名長度
  • HeaderLen :業務請求頭長度
  • ContentLen :業務請求體長度
  • ClassName :業務請求類名。需要注意類名傳輸的時候,務必指定字符集,不要依賴系統的默認字符集。曾經線上的機器,因爲運維誤操作,默認的字符集被修改,導致字符的傳輸出現編解碼問題。而我們的通信框架指定了默認字符集,因此躲過一劫。
  • HeaderContent :業務請求頭
  • BodyContent :業務請求體
  • CRC32 :CRC校驗碼,這也是通信場景裏必不可少的一部分,而我們金融業務屬性的特徵,這個顯得尤爲重要。

2

靈活的反序列化時機控制

從上面的協議介紹,可以看到協議的基本字段所佔用空間是比較小的,目前只有24個字節。協議上的主要負載就是 ClassName ,HeaderContent , BodyContent 這三部分。這三部分的序列化和反序列化是整個請求響應裏最耗時的部分。在請求發送階段,在調用 Netty 的寫接口之前,會在業務線程先做好序列化,這裏沒有什麼疑問。而在請求接收階段,反序列化的時機就需要考慮一下了。結合上面提到的最佳實踐的網絡 IO 模型,請求接收階段,我們有 IO 線程,業務線程兩種線程池。爲了最大程度的配合業務特性,保證整體吞吐我們設計了精細的開關來控制反序列化時機:


螞蟻通信框架實踐


圖9 - 反序列化與業務處理時序圖

螞蟻通信框架實踐


表格1 - 反序列化場景具體介紹

3

Server Fail-Fast 機制


螞蟻通信框架實踐


圖10 - Server Fail-Fast機制

在協議裏,留意到我們有timeout這個字段,這個是把客戶端發起調用時,所設置的超時時間通過協議傳到了 Server 端。有了這個,我們就可以實現 Fail-Fast 快速失敗的機制。比如當客戶端設置超時時間 1s,當請求到達 Server 開始計時 arriveTimeStamp ,到任務被線程調度到開始處理時,記錄 startToProcessTimestamp ,二者的差值即請求反序列化與線程池排隊的時延,如果這個時間間隔已經超過了 1s,那麼請求就沒有必要被處理了。這個機制,在服務端出現處理抖動時,對於快速恢復會很有用。

  • 最佳實踐:不要依賴跨系統的時鐘,因爲時鐘可能會不一致,跨系統就會出現誤差,因此是從請求到達 Server 的那一刻,在 Server 的進程裏開始計時。

4

用戶請求處理器(UserProcessor)

在通用設計部分,我們提到了命令處理器。而爲了方便開發者使用,我們還提供了一個用戶請求處理器,即在 RPC 的命令處理器中,再增加一層映射關係,保存的是 業務傳輸對象的 className 與 UserProcessor 的對應關係。此時服務端只需要簡單註冊一個 className 對應的processor,並提供一個獨立的 executor ,就可以實現在業務線程處理請求了。


螞蟻通信框架實踐


圖11 - 命令處理器與用戶請求處理器的關係

除此,我們還設計了一個 RemotingContext 用於保存請求處理階段的一些通信層的關鍵輔助類或者信息,方便通信框架開發者使用;同時還提供了一個 BizContext ,有選擇把通信層的信息暴露給框架使用者,方便框架使用者使用。有了用戶請求處理器,以及上下文的傳遞機制,我們就可以方便的把通信層處理邏輯與業務處理邏輯聯動起來,比如一些開關的控制,字段的傳遞等定製功能:

  • 請求超時處理開關:用於開關 Server Fail-Fast 機制。
  • IO 線程業務處理開關:用戶可以選擇在 IO 線程處理業務請求;或者在業務線程來處理。
  • 線程池選擇器 ExecutorSelector :用戶可以提供多個業務線程池,使用 ExecutorSelector 來實現選擇邏輯
  • 泛化調用的支持:序列化請求與反序列化響應階段,針對泛化調用,使用特殊的序列化器。而是否開啓該功能,需要依賴上下文來傳遞一些標識。

5

其他實現細節

  • 可擴展的序列化機制
  • 針對業務對象裏的 HeaderContent 與 BodyContent ,我們提供了用戶自定義邏輯:用戶可以結合自身的請求內容做定製的序列化和反序列化動作;如果用戶沒有自定義,那麼會默認使用 Bolt 框架當前集成的序列化器,比如 Hessian(默認使用)、FastJson 等。
  • 埋點與異常處理
  • 爲了精細化請求處理過程,我們會記錄請求發送階段的建連耗時,客戶端超時時間,請求到達時間,線程調度等待時間等,然後通過上下文傳遞機制,連通業務與通信層;同時還會細化各個異常場景,比如請求超時異常,服務端線程池繁忙,序列化異常(請求與響應),反序列化異常(請求與響應)等。有了這些就能方便進行問題排查和快速定位。
  • 日誌打印

螞蟻通信框架實踐


  • 圖12 - 日誌模板
  • 作爲通信框架,必要的日誌打印也是很重要的。比如可以打印建連與斷連的日誌,便於排查連接問題;一些關鍵的異常場景也可以打印出來,方便定位問題;還可以打印一些關鍵字,來表示程序 BUG,便於框架開發者定位和分析。而打印日誌的方式,我們選擇依賴日誌門面 SLF4J ,然後提供不同的日誌實現所需要的配置文件。運行時,根據業務所依賴的日誌實現(比如 log4j , log4j2 , logback 來動態加載日誌配置)。同時默認使用異步 logger 來打印日誌。

螞蟻通信框架-BOLT

  • 爲了讓 Java 程序員,花更多的時間在一些 Productive 的事情上,而不是糾結底層 NIO 的實現,處理難以調試的網絡問題,Netty 應運而生
  • 爲了讓中間件的開發者,花更多的時間在中間件的特性實現上,而不是重複地一遍遍製造通信框架的輪子,Bolt 應運而生。

Bolt 即爲本文所描述的方法論的一個實踐實現,名字取自迪士尼動畫,閃電狗。定位是一個基於 Netty 最佳實踐過的,通用、高效、穩定的通信框架。我們希望能把這些年,在 RPC,MSG 在網絡通信上碰到的問題與解決方案沉澱到這個基礎組件裏,不斷的優化和完善它。讓更多的需要網絡通信的場景能夠統一受益。目前已經運用在了螞蟻中間件的微服務,消息中心,分佈式事務,分佈式開關,配置中心等衆多產品上。

除了 Bolt 提供的高效通信能力外,還可以方便的進行協議適配的工作。比如螞蟻內部之前使用的 RPC 協議是 Tr 協議,是基於 Apache Mina 開發的老版本通信框架,由於年久失修,同時性能逐步落伍,我們重新設計了 Bolt 協議,精簡以及新增了一些協議字段,同時切換到了 Netty 上。在新老 RPC 協議的切換期間,我們利用 Bolt 進行了協議適配,開發了 BoltTrAdaptor,最大程度的複用基礎通信能力,僅僅把協議相關的部分單獨實現,以此來保證新老協議調用的兼容性。

針對螞蟻內部的新老通信框架,我們進行了細緻的壓測,如下圖所示。我們的壓測環境是,4 核10G 的虛擬機,千兆網卡,請求與響應包大小 1024 字節,分別壓測了四種場景。由壓測結果能看出 Bolt -> Bolt 的場景,整體吞吐量最大,平均RT最小,同時對比了 IO ,CPU 使用率等情況,資源整體利用率上也提升很多。


螞蟻通信框架實踐


圖13 - 壓測TPS數據

螞蟻通信框架實踐


圖14 - 壓測平均RT數據

Bolt 在實驗室裏的極限性能壓測,採用的是 32 核物理機,萬兆網卡的環境,請求和響應 100 字節負載,服務端收到請求後馬上返回響應,瓶頸基本就是業務線程池所使用的 ArrayBlockingQueue LinkedBlockingQueue 的性能瓶頸,壓力到了十多萬,就會出現較大幅度的毛刺和抖動。純粹爲了壓測場景,改成使用 SynchronousQueue 後,毛刺減少了很多,基本能穩定在 30W TPS 的處理能力。

寫在最後

近期我們也在準備開源螞蟻 Bolt 通信框架,主要是吸取 Netty 的開源精神,回饋社區,與社區共建與完善。如果你也有製造通信框架輪子的需求,或者想適配內部的自有或者開源通信協議(比如 Dubbo 等),可以試一下螞蟻 Bolt 通信框架,敬請期待!我們有很多想法還在實驗室裏醞釀,還沒有落地到生產環境使用。非常歡迎一起來探討網絡通信問題,參與共建。

最後附上螞蟻中間件的招聘鏈接,通信是分佈式架構體系的基礎設施,歡迎有志之士加盟,打造高效、穩定的通信技術。點擊左下角的【閱讀原文】獲取螞蟻中間件通信組的招聘信息。

參考

  1. Scalable IO in Java, Slides, by Doug Lea, http://gee.cs.oswego.edu/dl/cpjslides/nio.pdf
  2. Reactor, Thesis, by Douglas C. Schmidt, http://www.dre.vanderbilt.edu/~schmidt/PDF/reactor-siemens.pdf
  3. Hashed and Hierarchical Timing Wheels, Thesis, by George Varghese and Anthony Lauck, http://www.cs.columbia.edu/~nahum/w6998/papers/ton97-timing-wheels.pdf
  4. Netty Best Practices, Slides, by Norman Maurer, http://normanmaurer.me/presentations/2014-facebook-eng-netty/slides.html
  5. NSF-RPC的優化過程,博客文章,來自畢玄,http://bluedavy.me/?p=384
  6. Netty 源碼分析系列 ,博客文章,來自永順,https://segmentfault.com/a/1190000007282628
  7. 《Netty權威指南》,書籍,來自李林鋒
  8. 《Netty實戰》,書籍,來自Norman Maurer等著,何品翻譯

附文中提到的一些鏈接地址信息

  1. Gecko: https://github.com/killme2008/gecko
  2. Mina: http://mina.apache.org/
  3. Netty: http://netty.io/
  4. Stackoverflow - Do we need more than a single thread for boss group?:https://stackoverflow.com/questions/22280916/do-we-need-more-than-a-single-thread-for-boss-group
  5. [#3218] Add ChannelPool abstraction and implementations:https://github.com/netty/netty/pull/3607
相关文章