螞蟻金服近期開源了研發(fā)多年的SOFA一籃子框架,其中就有一個(gè)非常核心的RPC框架,它叫SOFA-BOLT。小編今天花了近一天的時(shí)間仔細(xì)閱讀研究它的源碼,閱讀過程中遇到了不少問題,螞蟻金服的相關(guān)技術(shù)人員都非常耐心的及時(shí)解答了我的疑難。這里將我從中學(xué)到的知識(shí)點(diǎn)一并分享給大家。
SOFA-BOLT基于開源的Netty框架,同時(shí)提供了服務(wù)器和客戶端的實(shí)現(xiàn)。它的源碼非常值得一讀,結(jié)構(gòu)簡(jiǎn)單,考慮周全,絕不是一個(gè)普通的玩具。它沒有濫用設(shè)計(jì)模式,源碼閱讀起來比較直接,沒有太多繞來繞去的復(fù)雜結(jié)構(gòu)。
一個(gè)節(jié)點(diǎn)既可以同時(shí)既是RPC服務(wù)器又是客戶端,作為客戶端該節(jié)點(diǎn)需要其它節(jié)點(diǎn)提供服務(wù),作為服務(wù)器它可以為其它節(jié)點(diǎn)提供服務(wù)。不過上面這張圖并不是合理的結(jié)構(gòu),因?yàn)閮蓚€(gè)服務(wù)相互耦合了,我需要你,你也需要我,就成了雞蛋問題。比較合理的結(jié)構(gòu)一般如下圖所示,它們之間不構(gòu)成環(huán)。
通訊協(xié)議
通訊協(xié)議是客戶端和服務(wù)器之間交流的語言,SOFA定義了自己的一套通訊協(xié)議,它的編碼解碼分為二層,第一層是消息體對(duì)象的二進(jìn)制序列化,這部分默認(rèn)由開源的Hession協(xié)議庫序列化完成,第二層是負(fù)責(zé)給序列化的消息體增加一系列包裝字段,形成一個(gè)完整的消息。包括請(qǐng)求ID、消息體的長(zhǎng)度、協(xié)議版本號(hào)和CRC32校驗(yàn)位等等
如果希望進(jìn)一步優(yōu)化網(wǎng)絡(luò)性能,SOFA還提供了Snappy壓縮協(xié)議,可以在現(xiàn)有的兩層協(xié)議基礎(chǔ)上增加第三層,能顯著降低網(wǎng)絡(luò)傳輸負(fù)擔(dān)。壓縮是時(shí)間換空間,提升網(wǎng)絡(luò)性能的同時(shí),它也會(huì)加重CPU計(jì)算,所以在使用時(shí)需要適當(dāng)進(jìn)行權(quán)衡。
連接池
客戶端和服務(wù)器之間一般需要建立多個(gè)連接,但是也不能每個(gè)請(qǐng)求都建立一個(gè)連接。一般是通過維護(hù)一個(gè)連接池,限定最大連接數(shù)??蛻舳送ㄟ^有限的連接來和服務(wù)器進(jìn)行通信。
我們?cè)谑褂肑edis客戶端和Redis服務(wù)器進(jìn)行通信時(shí),也是通過連接池來獲取連接的。Jedis的連接必須是線程獨(dú)占的,因?yàn)樗皇蔷€程安全的。從連接池中獲取連接時(shí),其它線程就暫時(shí)拿不到這個(gè)連接了,待當(dāng)前線程處理完畢后,要將連接歸還給線程池,這樣其它線程才可以繼續(xù)使用這個(gè)連接。
Redis的客戶端請(qǐng)求和應(yīng)答是順序性的,一問一答,所以請(qǐng)求和應(yīng)答不需要唯一ID就可以建立起關(guān)聯(lián)。
Bolt不一樣,它的問答是亂序的,問和答之間是必須通過請(qǐng)求的唯一ID來建立起關(guān)聯(lián)。Bolt的客戶端是線程安全的,它可以同時(shí)傳遞多個(gè)請(qǐng)求,連接對(duì)象會(huì)維護(hù)一個(gè)正在處理的RPC請(qǐng)求對(duì)象字典。當(dāng)客戶端想要發(fā)起RPC請(qǐng)求時(shí),它不是從連接池中摘出一個(gè)獨(dú)占連接,而是隨意選擇一個(gè)連接來傳遞自己的請(qǐng)求,這個(gè)連接也可以被其它線程同時(shí)使用。
負(fù)載均衡
客戶端提供了多種復(fù)雜均衡的實(shí)現(xiàn),阿里默認(rèn)使用帶權(quán)重的隨機(jī)算法(RandomLoadBalancer),此外還有
- ConsistentHashLoaderBalancer 一致性hash,客戶端和服務(wù)器之間的連接關(guān)系(誰跟誰連)比較穩(wěn)定
- LocalPreferenceLoadBalancer 本地環(huán)回地址優(yōu)先,提升本機(jī)調(diào)用性能
- RoundRobinLoadBalancer 循環(huán)依次來
- WeightedRoundRobinLoadBalancer 帶權(quán)重的循環(huán)依次來
- RandomLoadBalancer 這個(gè)是帶權(quán)重的隨機(jī),阿里的默認(rèn)使用
服務(wù)器線程模型
服務(wù)器采用傳統(tǒng)netty多線程模型,一個(gè)acceptor線程專門用來接收連接,然后扔給io線程處理讀消息并解碼成請(qǐng)求對(duì)象,最后扔給業(yè)務(wù)線程池進(jìn)行處理。
心跳
客戶端和服務(wù)器之間會(huì)有定時(shí)心跳檢測(cè)連接的存活,默認(rèn)30s來一次。tcp的關(guān)閉是通過FIN包來通知對(duì)方的,如果因?yàn)榫W(wǎng)絡(luò)問題,對(duì)方連FIN包都收不到,那么即使一邊關(guān)閉了套接字,另一邊可能還以為連接正常。所以心跳檢測(cè)存活機(jī)制在長(zhǎng)連接應(yīng)用里非常普遍。如果客戶端連續(xù)發(fā)了三次心跳都沒有收到服務(wù)器的回復(fù),那么就認(rèn)為連接已經(jīng)關(guān)閉。服務(wù)器也會(huì)有連接存活檢測(cè),如果一個(gè)客戶端連接90s內(nèi)沒有任何消息進(jìn)來,那么也認(rèn)為該連接已經(jīng)斷開。服務(wù)器不會(huì)主動(dòng)發(fā)送心跳消息。
雙工通信
RPC一般是由客戶端向服務(wù)器發(fā)起一個(gè)請(qǐng)求,然后收到服務(wù)器的應(yīng)答。Bolt的RPC是雙工通信,服務(wù)器也可以向客戶端主動(dòng)發(fā)起請(qǐng)求,它們共享一個(gè)TCP連接。TCP連接本身就是雙工的,所以這也不算什么奇跡。只是服務(wù)器在什么業(yè)務(wù)場(chǎng)景需要向客戶端主動(dòng)發(fā)起請(qǐng)求,這個(gè)螞蟻并沒有進(jìn)行詳細(xì)說明。
客戶端作為主動(dòng)連接方,它要負(fù)責(zé)重連和發(fā)起心跳消息。服務(wù)器作為被動(dòng)方,它不需要處理重連,如果連接斷開,它就直接將連接從集合中移除就行,不需要做特殊的處理,但是它會(huì)檢測(cè)心跳消息,如果在一定時(shí)間內(nèi)連接通道沒有任何消息到來,它就會(huì)主動(dòng)關(guān)閉。
重連
客戶端的重連策略是一個(gè)單獨(dú)的模塊,有兩個(gè)地方會(huì)成為重連的入口。一個(gè)是正常連接斷開觸發(fā)channelInActive回調(diào),另一個(gè)就是重連連接不能建立成功時(shí)需要進(jìn)行重試。Bolt有一個(gè)單獨(dú)的重連線程,所有需要重連的連接會(huì)被包裝成一個(gè)任務(wù)塞進(jìn)這個(gè)線程的任務(wù)隊(duì)列,該線程不斷地從隊(duì)列里拿任務(wù)進(jìn)行重連處理,如果重連失敗會(huì)嘗試再將任務(wù)重新包裝進(jìn)隊(duì)列延后繼續(xù)處理。默認(rèn)是1s鐘處理一個(gè)重連任務(wù)。
RPC連接是延遲建立的,它在第一次客戶端發(fā)送RPC請(qǐng)求時(shí)嘗試進(jìn)行連接,如果連接失敗,它會(huì)立即繼續(xù)重連最多默認(rèn)兩次。如果三次嘗試連接后還是沒有建立成功,就向上層爆出異常。它不需要包裝一個(gè)重連任務(wù)塞進(jìn)ReconnectManager,因?yàn)楹罄m(xù)客戶端請(qǐng)求會(huì)繼續(xù)觸發(fā)連接。
單向消息
RPC通常是一應(yīng)一答,客戶端可以同步等待響應(yīng),也可以提供回調(diào)接口等待結(jié)果通知。Bolt除了提供應(yīng)答模式之外,還提供了oneway單向消息,這種消息服務(wù)器收到后不用回復(fù),客戶端發(fā)送請(qǐng)求之后就立即返回了也不需要等待結(jié)果。
oneway消息一般用于不那么重要的日志類消息,它不能保證服務(wù)器一定能收到,所以此種業(yè)務(wù)消息應(yīng)該是那種允許丟失的消息,形式上類似于UDP,它在犧牲可靠性的前提下能大幅提升消息的吞吐量。
消息追蹤
Bolt提供了回調(diào)接口,方便監(jiān)控系統(tǒng)可以對(duì)請(qǐng)求的調(diào)用狀況進(jìn)行分析。監(jiān)控的客戶端可以通過實(shí)現(xiàn)該接口,注冊(cè)進(jìn)RPC的客戶端和服務(wù)器進(jìn)行打點(diǎn)收集日志,然后發(fā)送到日志分析系統(tǒng)。
- 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);
- ...
- }
總結(jié)
Bolt是一個(gè)成熟的比較復(fù)雜的RPC系統(tǒng),這篇小文章只講解了其中一部分,內(nèi)部還有大量的實(shí)現(xiàn)細(xì)節(jié)有待去挖掘。