螞蟻金服近期開源了研發多年的SOFA一籃子框架,其中就有一個非常核心的RPC框架,它叫SOFA-BOLT。小編今天花了近一天的時間仔細閱讀研究它的原始碼,閱讀過程中遇到了不少問題,螞蟻金服的相關技術人員都非常耐心的及時解答了我的疑難。這裡將我從中學到的知識點一併分享給大家。
SOFA-BOLT基於開源的Netty框架,同時提供了伺服器和客戶端的實現。它的原始碼非常值得一讀,結構簡單,考慮周全,絕不是一個普通的玩具。它沒有濫用設計模式,原始碼閱讀起來比較直接,沒有太多繞來繞去的複雜結構。
一個節點既可以同時既是RPC伺服器又是客戶端,作為客戶端該節點需要其它節點提供服務,作為伺服器它可以為其它節點提供服務。不過上面這張圖並不是合理的結構,因為兩個服務相互耦合了,我需要你,你也需要我,就成了雞蛋問題。比較合理的結構一般如下圖所示,它們之間不構成環。
通訊協議
通訊協議是客戶端和伺服器之間交流的語言,SOFA定義了自己的一套通訊協議,它的編碼解碼分為二層,第一層是訊息體物件的二進位制序列化,這部分預設由開源的Hession協議庫序列化完成,第二層是負責給序列化的訊息體增加一系列包裝欄位,形成一個完整的訊息。包括請求ID、訊息體的長度、協議版本號和CRC32校驗位等等
如果希望進一步優化網路效能,SOFA還提供了Snappy壓縮協議,可以在現有的兩層協議基礎上增加第三層,能顯著降低網路傳輸負擔。壓縮是時間換空間,提升網路效能的同時,它也會加重CPU計算,所以在使用時需要適當進行權衡。
連線池
客戶端和伺服器之間一般需要建立多個連線,但是也不能每個請求都建立一個連線。一般是通過維護一個連線池,限定最大連線數。客戶端通過有限的連線來和伺服器進行通訊。
我們在使用Jedis客戶端和Redis伺服器進行通訊時,也是通過連線池來獲取連線的。Jedis的連線必須是執行緒獨佔的,因為它不是執行緒安全的。從連線池中獲取連線時,其它執行緒就暫時拿不到這個連線了,待當前執行緒處理完畢後,要將連線歸還給執行緒池,這樣其它執行緒才可以繼續使用這個連線。
Redis的客戶端請求和應答是順序性的,一問一答,所以請求和應答不需要唯一ID就可以建立起關聯。
Bolt不一樣,它的問答是亂序的,問和答之間是必須通過請求的唯一ID來建立起關聯。Bolt的客戶端是執行緒安全的,它可以同時傳遞多個請求,連線物件會維護一個正在處理的RPC請求物件字典。當客戶端想要發起RPC請求時,它不是從連線池中摘出一個獨佔連線,而是隨意選擇一個連線來傳遞自己的請求,這個連線也可以被其它執行緒同時使用。
負載均衡
客戶端提供了多種複雜均衡的實現,阿里預設使用帶權重的隨機演算法(RandomLoadBalancer),此外還有
- ConsistentHashLoaderBalancer 一致性hash,客戶端和伺服器之間的連線關係(誰跟誰連)比較穩定
- LocalPreferenceLoadBalancer 本地環回地址優先,提升本機呼叫效能
- RoundRobinLoadBalancer 迴圈依次來
- WeightedRoundRobinLoadBalancer 帶權重的迴圈依次來
- RandomLoadBalancer 這個是帶權重的隨機,阿里的預設使用
伺服器執行緒模型
伺服器採用傳統netty多執行緒模型,一個acceptor執行緒專門用來接收連線,然後扔給io執行緒處理讀訊息並解碼成請求物件,最後扔給業務執行緒池進行處理。
心跳
客戶端和伺服器之間會有定時心跳檢測連線的存活,預設30s來一次。tcp的關閉是通過FIN包來通知對方的,如果因為網路問題,對方連FIN包都收不到,那麼即使一邊關閉了套接字,另一邊可能還以為連線正常。所以心跳檢測存活機制在長連線應用裡非常普遍。如果客戶端連續發了三次心跳都沒有收到伺服器的回覆,那麼就認為連線已經關閉。伺服器也會有連線存活檢測,如果一個客戶端連線90s內沒有任何訊息進來,那麼也認為該連線已經斷開。伺服器不會主動傳送心跳訊息。
雙工通訊
RPC一般是由客戶端向伺服器發起一個請求,然後收到伺服器的應答。Bolt的RPC是雙工通訊,伺服器也可以向客戶端主動發起請求,它們共享一個TCP連線。TCP連線本身就是雙工的,所以這也不算什麼奇蹟。只是伺服器在什麼業務場景需要向客戶端主動發起請求,這個螞蟻並沒有進行詳細說明。
客戶端作為主動連線方,它要負責重連和發起心跳訊息。伺服器作為被動方,它不需要處理重連,如果連線斷開,它就直接將連線從集合中移除就行,不需要做特殊的處理,但是它會檢測心跳訊息,如果在一定時間內連線通道沒有任何訊息到來,它就會主動關閉。
重連
客戶端的重連策略是一個單獨的模組,有兩個地方會成為重連的入口。一個是正常連線斷開觸發channelInActive回撥,另一個就是重連連線不能建立成功時需要進行重試。Bolt有一個單獨的重連執行緒,所有需要重連的連線會被包裝成一個任務塞進這個執行緒的任務佇列,該執行緒不斷地從佇列裡拿任務進行重連處理,如果重連失敗會嘗試再將任務重新包裝進佇列延後繼續處理。預設是1s鍾處理一個重連任務。
RPC連線是延遲建立的,它在第一次客戶端傳送RPC請求時嘗試進行連線,如果連線失敗,它會立即繼續重連最多預設兩次。如果三次嘗試連線後還是沒有建立成功,就向上層爆出異常。它不需要包裝一個重連任務塞進ReconnectManager,因為後續客戶端請求會繼續觸發連線。
單向訊息
RPC通常是一應一答,客戶端可以同步等待響應,也可以提供回撥介面等待結果通知。Bolt除了提供應答模式之外,還提供了oneway單向訊息,這種訊息伺服器收到後不用回覆,客戶端傳送請求之後就立即返回了也不需要等待結果。
oneway訊息一般用於不那麼重要的日誌類訊息,它不能保證伺服器一定能收到,所以此種業務訊息應該是那種允許丟失的訊息,形式上類似於UDP,它在犧牲可靠性的前提下能大幅提升訊息的吞吐量。
訊息追蹤
Bolt提供了回撥介面,方便監控系統可以對請求的呼叫狀況進行分析。監控的客戶端可以通過實現該介面,註冊進RPC的客戶端和伺服器進行打點收集日誌,然後傳送到日誌分析系統。
interface Tracer {
void startRpc(SofaRequest request);
void serverReceived(SofaRequest request);
void serverSend(SofaRequest request, SofaResponse response, Throwable throwable);
void clientReceived(SofaRequest request, SofaResponse response, Throwable throwable);
...
}
複製程式碼
總結
Bolt是一個成熟的比較複雜的RPC系統,這篇小文章只講解了其中一部分,內部還有大量的實現細節有待去挖掘。
閱讀相關文章,請關注公眾號「碼洞」,SOFA開源系列程式碼是Java程式設計師的大寶藏,它有太多的專案和程式碼會讓很多使用者感到望而生畏,後續我會陸續放出SOFA開源系列其它模組的分析文章,幫助大家輕鬆理解SOFA專案。