專注于Java領域優質技術,歡迎關注
作者: 老錢 碼洞
螞蟻金服近期開源了研發多年的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系統,這篇小文章隻講解了其中一部分,内部還有大量的實作細節有待去挖掘。