螞蟻通訊框架實踐
前言
網際網路領域的通訊技術,有各式各樣的通訊協議可以選擇,比如基於 TCP/IP 協議簇的 HTTP(1/2)、SPDY 協議、Socket/">WebSocket、Google 基於 UDP 的 QUIC 協議等。這些協議,都有完整的報文格式與欄位定義,對安全,序列化機制,資料壓縮機制,CRC 校驗機制等各種通訊細節都有較好的設計。能夠高效、穩定、且安全地執行在公網環境。
而對於私網環境,比如一個公司的 IDC 內部,如果所有應用的節點間,全部通過標準協議來通訊,會有很多問題:比如研發效率方面的影響,我們的研發框架,需要做大量業務資料轉化成標準協議的工作;再比如升級相容性,標準協議的欄位眾多,版本各異,相容性也得不到保障;除此還有無用欄位的傳輸,也會造成資源浪費,功能定製也可能不那麼靈活。而解決這些問題,比較常見的做法就是自己來設計協議,可以自己來定義欄位,制定升級方式,可插拔可開關的特性需求等,我們把這樣的協議叫做私有通訊協議。
在螞蟻金服的分散式技術體系下,我們大量的技術產品(非閘道器類產品),都需要在內網,進行節點間通訊。高吞吐、高併發的通訊,數量眾多的連線管理(C10K 問題),便捷的升級機制,相容性保障,靈活的執行緒池模型運用,細緻的異常處理與日誌埋點等,這些功能都需要在通訊協議和實現框架上做文章。本文主要從如下幾個方面來對螞蟻通訊框架實踐之路進行介紹:
- 私有通訊協議設計
- 基礎通訊功能設計要點分析
- 私有通訊協議設計舉例
- 螞蟻自研通訊框架 Bolt
私有通訊協議設計
我們的分散式架構,所需要的內部通訊模組,採用了私有協議來設計和研發。當然私有協議,也是有很多弊端的,比如在通用性上、公網傳輸的能力上相比標準協議會有劣勢。然而,我們為了最大程度的提升效能,降低成本,提高靈活性與效率,最佳選擇還是高度定製化的私有協議:
- 可以有效地利用協議裡的各個欄位
- 靈活滿足各種通訊功能需求:比如 CRC 校驗,Server Fail-Fast 機制,自定義序列化器
- 最大程度滿足效能需求:IO 模型與執行緒模型的靈活運用
比如一個典型的 Request-Response 通訊場景:
- 在一個通訊節點上,如何把一個請求物件,序列化成位元組流,通過怎樣的網路傳輸方式,傳遞到另一個節點
- 在對端的通訊節點上,需要高效的讀取位元組流,並反序列化成原始的請求物件,然後根據請求內容,做一些邏輯處理。處理完成後,響應返回。
- 同時,此時要考慮,如何充分利用網路 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 的學習經驗,內部使用上的一些最佳實踐。
網路 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
只會有一個執行緒在執行,所以沒有必要設定為多執行緒;而 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 的執行緒模型與任務處理機制。
- 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,但當網路環境不好,同時傳輸報文很大時,確實會出現限流的情況。
- 最小化執行緒池,能複用
連線管理
為了提高通信效率,我們需要考慮複用連線,減少 TCP 三次握手的次數,因此需要有連線管理的機制。而在業務的通訊場景中,我們還識別到一些不得不走硬負載(比如 LVS VIP)的場景,此時如果只建立單鏈接,可能會出現負載不均衡的問題,此時需要建立多個連線,來緩解負載不均的問題。我們需要設計一個針對某個連線地址(IP 與 Port 唯一確定的地址)建立特定數目連線的實現,同時儲存在一個連線池裡。該連線池設計了一個通用的 PoolKey
不限定 Key 的型別。
需要注意的是,這裡的建連過程,有一個併發問題要解,比如客戶端在高併發的呼叫建連線口時,如何保證建立的連線剛好是所設定的個數呢?為了配合 Netty 的無鎖理念,我們也採用一個無鎖化的建連過程來實現,利用 ConcurrentHashMap
的 putIfAbsent
介面:
程式碼1 - 無鎖建連程式碼
除此,我們的連線管理,還要具備定時斷連功能,自動重連功能,自定義連線選擇演算法功能來適用不同的連線場景。
- 最佳實踐:在 Netty 的 4.0.28.Final#3218 裡,提供了一種
ChannelPool
的介面類與預設實現,其中FixedChannelPool
與我們實現的連線池做的事情一樣。而 Netty 採用了更巧妙的方式來規避併發問題,即在初始化FixedChannelPool
時,就將其關聯到某一個eventLoop
上,後續的建連動作,採用經典的inEventLoop()
方法來判斷,如果不在eventLoop
執行緒,則入隊等待下次排程。如此規避了併發問題。這個功能,我們目前還沒有實踐過,後續計劃採用這個官方實現重構一版。
基礎通訊模型
圖4 - 幾種通訊模型
如圖所示,我們實現了多種通訊介面 oneway
, sync
, future
, callback
。圖中都是ping/pong模式的通訊,藍色部分表示執行緒正在執行任務
oneway sync future callback
超時控制
圖5 - 超時控制模型
除了 oneway
模式,其他三種通訊模型都需要進行超時控制,我們同樣採用 Netty 裡針對超時機制,所設計的高效方案 HashedWheelTimer
。如圖所示,其原理是首先在發起呼叫前,我們會新增一個超時任務 timeoutTask
到 MpscQueue
(Netty 實現的一種高效的無鎖佇列)裡,然後在迴圈裡,會不斷的遍歷 Queue 裡的這些超時任務(每次最多10萬),針對每個任務,會根據其設定的超時時間,來計算該任務所屬於的 bucket
位置與剩餘輪數 remainingRounds
,然後加入到對應 bucket
的連結串列結構裡。隨著 tick++
的進行,時間在不斷的增長,每 tick
8 次,就是 1 個時間輪 round
。當對應超時任務的 remainingRounds
減到 0
時,就是觸發這個超時任務的時候,此時再執行其 run()
方法,做超時邏輯處理。
- 最佳實踐:通常一個程序使用一個
HashedWheelTimer
例項,採用單例模型即可。
批量解包與批量提交
圖6 - 批量解包與批量提交
Netty 提供了一個方便的解碼工具類 ByteToMessageDecoder
,如圖上半部分所示,這個類具備 accumulate
批量解包能力,可以儘可能的從 socket
裡讀取位元組,然後同步呼叫 decode
方法,解碼出業務物件,並組成一個 List
。最後再迴圈遍歷該 List
,依次提交到 ChannelPipeline
進行處理。此處我們做了一個細小的改動,如圖下半部分所示,即將提交的內容從單個 command
,改為整個 List
一起提交,如此能減少 pipeline
的執行次數,同時提升吞吐量。這個模式在低併發場景,並沒有什麼優勢,而在高併發場景下對提升吞吐量有不小的效能提升。
- 最佳實踐:
ByteToMessageDecoder
因為內部的實現有成員變數,不是無狀態的,所以一定不能被設定為@Sharable
其他有用的功能
-
事件觸發與監聽機制
-
- Netty 的
ChannelHandler
完美實現了攔截器模式。在ChannelHandler
裡hook
了各個IO事件與IO操作的方法,我們可以方便的覆寫這些方法,來加一些自定義的邏輯。比如為了把建連,斷連事件觸發給上層業務,方便做一些準備或者優雅關閉的處理,我們實現一個繼承了ChannelInBoundHandler
與ChannelOutboundHandler
的處理器,覆蓋這些事件所對應的建連與斷連方法,然後設計一套業務的event
感知邏輯即可。
- Netty 的
-
雙工通訊
-
- 我們知道 TCP 是可以提供全雙工的通訊能力的。因此,當客戶端與服務端建立連線後,我們是可以由服務端發起通訊請求,客戶端來處理的。而為了支援這個功能,我們只需要把可以複用的
inboundHandler
與outboundHandler
在 客戶端的Bootstrap
與服務端的ServerBootstrap
裡都註冊一遍即可
- 我們知道 TCP 是可以提供全雙工的通訊能力的。因此,當客戶端與服務端建立連線後,我們是可以由服務端發起通訊請求,客戶端來處理的。而為了支援這個功能,我們只需要把可以複用的
有了私有協議的設計要點,與基礎通訊模組的實現,我們來看一個私有協議設計的舉例,一種典型的 RPC 特徵的通訊實現。
私有通訊協議舉例
通訊協議的設計
圖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校驗碼,這也是通訊場景裡必不可少的一部分,而我們金融業務屬性的特徵,這個顯得尤為重要。
靈活的反序列化時機控制
從上面的協議介紹,可以看到協議的基本欄位所佔用空間是比較小的,目前只有24個位元組。協議上的主要負載就是 ClassName
, HeaderContent
, BodyContent
這三部分。這三部分的序列化和反序列化是整個請求響應裡最耗時的部分。在請求傳送階段,在呼叫 Netty 的寫介面之前,會在業務執行緒先做好序列化,這裡沒有什麼疑問。而在請求接收階段,反序列化的時機就需要考慮一下了。結合上面提到的最佳實踐的網路 IO 模型,請求接收階段,我們有 IO 執行緒,業務執行緒兩種執行緒池。為了最大程度的配合業務特性,保證整體吞吐我們設計了精細的開關來控制反序列化時機:
圖9 - 反序列化與業務處理時序圖
表格1 - 反序列化場景具體介紹
Server Fail-Fast 機制
圖10 - Server Fail-Fast機制
在協議裡,留意到我們有timeout這個欄位,這個是把客戶端發起呼叫時,所設定的超時時間通過協議傳到了 Server 端。有了這個,我們就可以實現 Fail-Fast 快速失敗的機制。比如當客戶端設定超時時間 1s,當請求到達 Server 開始計時 arriveTimeStamp
,到任務被執行緒排程到開始處理時,記錄 startToProcessTimestamp
,二者的差值即請求反序列化與執行緒池排隊的時延,如果這個時間間隔已經超過了 1s,那麼請求就沒有必要被處理了。這個機制,在服務端出現處理抖動時,對於快速恢復會很有用。
- 最佳實踐:不要依賴跨系統的時鐘,因為時鐘可能會不一致,跨系統就會出現誤差,因此是從請求到達 Server 的那一刻,在 Server 的程序裡開始計時。
使用者請求處理器(UserProcessor)
在通用設計部分,我們提到了命令處理器。而為了方便開發者使用,我們還提供了一個使用者請求處理器,即在 RPC 的命令處理器中,再增加一層對映關係,儲存的是 業務傳輸物件的 className
與 UserProcessor
的對應關係。此時服務端只需要簡單註冊一個 className
對應的processor,並提供一個獨立的 executor
,就可以實現在業務執行緒處理請求了。
圖11 - 命令處理器與使用者請求處理器的關係
除此,我們還設計了一個 RemotingContext
用於儲存請求處理階段的一些通訊層的關鍵輔助類或者資訊,方便通訊框架開發者使用;同時還提供了一個 BizContext
,有選擇把通訊層的資訊暴露給框架使用者,方便框架使用者使用。有了使用者請求處理器,以及上下文的傳遞機制,我們就可以方便的把通訊層處理邏輯與業務處理邏輯聯動起來,比如一些開關的控制,欄位的傳遞等定製功能:
- 請求超時處理開關:用於開關 Server Fail-Fast 機制。
- IO 執行緒業務處理開關:使用者可以選擇在 IO 執行緒處理業務請求;或者在業務執行緒來處理。
- 執行緒池選擇器
ExecutorSelector
:使用者可以提供多個業務執行緒池,使用ExecutorSelector
來實現選擇邏輯 - 泛化呼叫的支援:序列化請求與反序列化響應階段,針對泛化呼叫,使用特殊的序列化器。而是否開啟該功能,需要依賴上下文來傳遞一些標識。
其他實現細節
-
可擴充套件的序列化機制
針對業務物件裡的
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 通訊框架,敬請期待!
參考
- Scalable IO in Java, Slides, by Doug Lea, http://gee.cs.oswego.edu/dl/cpjslides/nio.pdf
- Reactor, Thesis, by Douglas C. Schmidt, http://www.dre.vanderbilt.edu/~schmidt/PDF/reactor-siemens.pdf
- Hashed and Hierarchical Timing Wheels, Thesis, by George Varghese and Anthony Lauck, http://www.cs.columbia.edu/~nahum/w6998/papers/ton97-timing-wheels.pdf
- Netty Best Practices, Slides, by Norman Maurer, http://normanmaurer.me/presentations/2014-facebook-eng-netty/slides.html
- NSF-RPC的優化過程,部落格文章,來自畢玄,http://bluedavy.me/?p=384
- Netty 原始碼分析系列 ,部落格文章,來自永順,https://segmentfault.com/a/1190000007282628
- 《Netty權威指南》,書籍,來自李林鋒
- 《Netty實戰》,書籍,來自Norman Maurer等著,何品翻譯
附文中提到的一些連結地址資訊
- Gecko: https://github.com/killme2008/gecko
- Mina: http://mina.apache.org/
- Netty: http://netty.io/
- 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
- [#3218] Add ChannelPool abstraction and implementations:https://github.com/netty/netty/pull/3607
作者:螞蟻技術團隊
連結:https://mp.weixin.qq.com/s/JRsbK1Un2av9GKmJ8DK7IQ
版權歸作者所有,轉載請註明出處