我們一起聊聊 RPC 的底層原理
當(dāng)你在構(gòu)建一個分布式系統(tǒng)時,勢必需要考慮的一個問題是:如何實現(xiàn)服務(wù)與服務(wù)之間的調(diào)用?當(dāng)然,你可以使用 Dubbo 或 Spring Cloud 等分布式服務(wù)框架來封裝技術(shù)實現(xiàn)的復(fù)雜性,以此完成這個目標(biāo)。不過,假如現(xiàn)在沒有這些框架,需要你自己來實現(xiàn)遠(yuǎn)程調(diào)用,你會怎么做呢?
很多人會選擇實現(xiàn)一套 RPC 框架來調(diào)用遠(yuǎn)程服務(wù)。
那么你了解 RPC 架構(gòu)的基本結(jié)構(gòu)嗎?如果你想要自己實現(xiàn) RPC 框架來完成遠(yuǎn)程調(diào)用,又該構(gòu)建怎么樣的技術(shù)體系呢?接下來,我就給你具體介紹一下。
RPC 架構(gòu)的基本結(jié)構(gòu)
想要構(gòu)建一套完整的 RPC 架構(gòu),就需要明確該架構(gòu)所具備的基本結(jié)構(gòu),而 RPC 架構(gòu)的基本結(jié)構(gòu)中又存在很多組件。因此接下來,我就通過 RPC 基本結(jié)構(gòu)演進的過程,來給你一一講解下。
首先,我們通常把發(fā)生調(diào)用關(guān)系的兩個服務(wù)分別稱為服務(wù)的提供者(Provider)和消費者(Consumer)。所以,簡單來說,RPC 就是服務(wù)的消費者向提供者發(fā)起遠(yuǎn)程調(diào)用并獲取結(jié)果的過程,這是 RPC 最簡單的一種表現(xiàn)形式。
圖片
如果想要實現(xiàn)服務(wù)提供者和消費者之間的有效交互,那么兩者之間就需要確立與網(wǎng)絡(luò)通信相關(guān)的網(wǎng)絡(luò)協(xié)議以及通信通道。同時,服務(wù)的提供者需要把自己的服務(wù)調(diào)用入口暴露出來,并時刻準(zhǔn)備接收來自消費者的請求。
這里,我們把通信通道和網(wǎng)絡(luò)協(xié)議分別命名為 RpcChannel 和 RpcProtocol,而把服務(wù)提供者接收請求的組件稱為 RpcAcceptor,把消費者發(fā)起請求的組件稱為 RpcConnector。這樣,RPC 架構(gòu)就演變成了這個樣子:
圖片
然后,對于服務(wù)提供者和消費者而言,為了雙方能夠正常識別所發(fā)送的請求和所接收到的響應(yīng)結(jié)果,需要定義統(tǒng)一的契約。我們把這種契約稱為遠(yuǎn)程 API(Remote API),以便與本地 API 加以區(qū)別。如此一來,基于同一套遠(yuǎn)程 API 的定義,RPC 架構(gòu)就具備了根據(jù)業(yè)務(wù)來定義通信契約的能力。
圖片
類似地,為了更好地區(qū)分 RPC 架構(gòu)中的角色,我們把真正提供業(yè)務(wù)服務(wù)的組件稱為 RpcServer,而把發(fā)起真實客戶端請求的組件稱為 RpcClient。這樣,RpcServer 負(fù)責(zé)實現(xiàn)遠(yuǎn)程 API,而 RpcClient 負(fù)責(zé)調(diào)用遠(yuǎn)程 API。
圖片
當(dāng)然,對于遠(yuǎn)程 API 而言,服務(wù)提供者和消費者的處理方式顯然是不一樣的。提供者需要根據(jù)消費者的請求來調(diào)用 RpcServer 的具體實現(xiàn)并返回結(jié)果,這部分的工作由 RpcInvoker 來執(zhí)行,而消費者通過 RpcCaller 組件對請求進行編碼之后,發(fā)送給服務(wù)方并等待結(jié)果。
圖片
最后,為了降低開發(fā)人員的開發(fā)難度,讓遠(yuǎn)程調(diào)用的執(zhí)行過程看上去就像在執(zhí)行本地方法一樣,在主流的 RPC 實現(xiàn)機制中,通常都會在客戶端添加代理機制,以此提供遠(yuǎn)程服務(wù)本地化訪問的入口,我們把這個代理組件稱為 RpcProxy。另外,在服務(wù)器端,為了更好地控制業(yè)務(wù)方法執(zhí)行過程,通常也會引入具備線程管理、超時控制等機制的 RpcProcessor 組件。
圖片
以上就是整個 RPC 架構(gòu)的演進過程了。從中你可以發(fā)現(xiàn),RPC 架構(gòu)中的客戶端組件和服務(wù)器端組件形成了一種對稱結(jié)構(gòu),它們各司其職,但又共同構(gòu)成一個整體。為了幫你加深理解,這里我再總結(jié)下前面提到的各個組件。
客戶端組件與職責(zé)包括:
- RpcClient,負(fù)責(zé)調(diào)用遠(yuǎn)程 API,這個過程會依賴于 RpcProxy 提供的代理實現(xiàn)
- RpcProxy,遠(yuǎn)程 API 的代理實現(xiàn),提供遠(yuǎn)程服務(wù)本地化訪問的入口
- RpcCaller,負(fù)責(zé)編碼和發(fā)送調(diào)用請求到服務(wù)方并等待結(jié)果
- RpcConnector,負(fù)責(zé)與服務(wù)端建立通信通道并發(fā)送請求到服務(wù)端
服務(wù)端組件與職責(zé)包括:
- RpcServer,負(fù)責(zé)實現(xiàn)遠(yuǎn)程 API
- RpcInvoker,負(fù)責(zé)調(diào)用服務(wù)端的具體實現(xiàn)并返回結(jié)果
- RpcProcessor,負(fù)責(zé)對請求進行處理,高效控制調(diào)用過程
- RpcAcceptor,負(fù)責(zé)接收客戶方請求并返回請求結(jié)果
而客戶端和服務(wù)器端所共有的組件包括:
- RpcProtocol,負(fù)責(zé)網(wǎng)絡(luò)傳輸協(xié)議的編碼和解碼
- RpcChannel,負(fù)責(zé)建立和維護網(wǎng)絡(luò)數(shù)據(jù)傳輸通道
這樣,我們對一個典型 RPC 架構(gòu)中的基本結(jié)構(gòu)和組件就有了完整的了解。那么,如果我們想要實現(xiàn)這個架構(gòu),需要構(gòu)建怎樣的技術(shù)體系呢?
RPC 架構(gòu)的技術(shù)體系
我們都知道,架構(gòu)是一種設(shè)計上的思想和方法,明白了它的基本結(jié)構(gòu)和組成部分之后,我們就可以進一步梳理想要實現(xiàn) RPC 架構(gòu)的技術(shù)體系,包括網(wǎng)絡(luò)通信、序列化、傳輸協(xié)議和遠(yuǎn)程調(diào)用。
網(wǎng)絡(luò)通信
我們先來看網(wǎng)絡(luò)通信。網(wǎng)絡(luò)通信的涉及面很廣,對于 RPC 架構(gòu)而言,一方面我們會重點關(guān)注性能,所以勢必要考慮基于 TCP 等特定協(xié)議的網(wǎng)絡(luò)連接方式和 IO 模型;另一方面,我們也需要考慮可靠性,因為這樣才能確保遠(yuǎn)程調(diào)用過程的穩(wěn)定。
好,下面我們就具體來看看。
首先是性能問題。一般來說,基于 TCP 協(xié)議的網(wǎng)絡(luò)連接有兩種基本方式:長連接和短連接。長連接和短連接的本質(zhì)區(qū)別是連接的創(chuàng)建和關(guān)閉策略,長連接可以復(fù)用現(xiàn)有連接,而短連接則能夠更快地釋放資源。這兩者本身各有利弊,而在 RPC 框架的實現(xiàn)過程中,考慮到性能和服務(wù)治理等因素,我們通常是使用長連接進行通信,典型的實現(xiàn)框架就是 Dubbo。
而對于 IO 模型,最簡單、最基礎(chǔ)的網(wǎng)絡(luò) IO 模型就是阻塞式 IO,即 BIO(Blocking IO)。BIO 要求客戶端請求數(shù)與服務(wù)端線程數(shù)一一對應(yīng),但是顯然,由于線程的創(chuàng)建需要消耗系統(tǒng)資源,在分布式系統(tǒng)中,服務(wù)端可以創(chuàng)建的線程數(shù)將會成為系統(tǒng)的瓶頸。因此,在 RPC 架構(gòu)中,我們通常都會使用非阻塞 IO,即 NIO(Non-blocking IO)技術(shù)來提供性能?;?NIO 模式下的多路復(fù)用機制,創(chuàng)建少數(shù)的線程就能對大量請求進行高效的響應(yīng)。
然后是針對可靠性問題,由于存在網(wǎng)絡(luò)閃斷、超時等與網(wǎng)絡(luò)狀態(tài)相關(guān)的不穩(wěn)定性因素,以及業(yè)務(wù)系統(tǒng)本身的故障,網(wǎng)絡(luò)之間的通信就必須在發(fā)生上述問題時能夠快速感知并修復(fù)。常見的網(wǎng)絡(luò)通信保障手段,包括鏈路有效性檢測及斷線之后的重連處理等。這些機制都比較常見,也不是我們討論的重點,這里就不做具體展開了。
序列化
而如果我們想要在網(wǎng)絡(luò)上傳輸數(shù)據(jù),就需要用到數(shù)據(jù)序列化技術(shù)了。
目前業(yè)界成熟的序列化工具已經(jīng)有很多,常見的 XML 和 JSON 就是文本類序列化方式的代表,它們可以讓數(shù)據(jù)以開發(fā)人員可讀的方式進行傳輸。還有一種基于二進制實現(xiàn)的方案,包括 Google 的 Protocol Buffer 和 Facebook 的 Thrift。
那么,我們在選擇序列化工具時,應(yīng)該考慮什么呢?一個關(guān)鍵指標(biāo)就是性能。
性能指標(biāo)主要包括空間復(fù)雜度、時間復(fù)雜度以及 CPU/內(nèi)存資源占用等。我在下表列舉了目前主流的一些序列化技術(shù),供你參考:
可以看到,在時間維度上,Alibaba 的 fastjson 具有一定優(yōu)勢;而從空間維度上看,相較其他技術(shù),你可以優(yōu)先選擇 Protocol Buffer。
傳輸協(xié)議
我們知道,但凡涉及通過網(wǎng)絡(luò)來傳輸數(shù)據(jù),就一定要采用某種傳輸協(xié)議。在 ISO/OSI 的 7 層網(wǎng)絡(luò)模型中,RPC 架構(gòu)的設(shè)計和實現(xiàn)通常會涉及傳輸層及以上各個層次的相關(guān)協(xié)議,我們所熟悉的 TCP 協(xié)議就屬于傳輸層,而 HTTP 協(xié)議則位于應(yīng)用層。
無論是采用 7 層網(wǎng)絡(luò)模型中的哪一層,在網(wǎng)絡(luò)請求過程中,數(shù)據(jù)都是以消息的形式進行傳遞。而消息的組成是有一定結(jié)構(gòu)的,消息頭和消息體構(gòu)成了所傳輸消息的主體,其中消息體表示需要傳輸?shù)臉I(yè)務(wù)數(shù)據(jù),而消息頭用于進行傳輸控制。
圖片
可以看到,每個層次都從上層取得數(shù)據(jù),加上消息頭信息形成新的消息體,并將新的消息傳遞給下一層次。通過對消息頭和消息體進行擴展,我們就可以實現(xiàn)私有化的傳輸協(xié)議。
這也是大部分 RPC 框架內(nèi)部所采用的實現(xiàn)方式,這樣做的主要目的是對公有協(xié)議進行精簡,從而提升性能。另外,出于擴展性的考慮,具備高度定制化的私有協(xié)議也比公共協(xié)議更加容易實現(xiàn)擴展。這方面的典型示例還是 Dubbo 框架,它提供了完全自定義的 Dubbo 協(xié)議。
遠(yuǎn)程調(diào)用
明確了網(wǎng)絡(luò)通信的基本方式、序列化手段以及所采用的傳輸協(xié)議之后,我們就可以發(fā)起真正的遠(yuǎn)程調(diào)用了。RPC 本質(zhì)也是一種服務(wù)調(diào)用,而服務(wù)調(diào)用存在兩種基本方式,即單向(One Way)模式和請求應(yīng)答(Request-Response)模式,前者體現(xiàn)為異步操作,后者一般執(zhí)行同步操作。
首先我們要知道,同步調(diào)用會造成業(yè)務(wù)線程阻塞,但開發(fā)和管理會相對簡單。這是為什么呢?我們先來看一下同步調(diào)用的時序圖:
圖片
從中可以看到,服務(wù)線程發(fā)送請求到 IO 線程之后,就一直處于等待階段,直到 IO 線程完成與網(wǎng)絡(luò)的讀寫操作之后,才會被主動喚醒。
而使用異步調(diào)用的目的就在于獲取高性能。在實現(xiàn)異步調(diào)用過程中,我們通常都會使用到 Java 中所提供的 Future 機制。Future 調(diào)用可以進一步細(xì)分成兩種模式,F(xiàn)uture-Get 模式和 Future-Listener 模式。Future-Get 模式參考下圖:
圖片
可以看到在這種模式下,服務(wù)線程通過主動 get 結(jié)果的方式獲取 Future 結(jié)果,而這個 get 過程是串行的,會造成執(zhí)行 get 方法的線程形成阻塞。
Future-Listener 模式則不同,在 Future-Listener 模式中需要創(chuàng)建 Listener,當(dāng) Future 結(jié)果生成時會喚醒注冊到該 Future 上的 Listener 對象,從而形成異步回調(diào)機制。
除了同步和異步調(diào)用之外,還存在并行(Parallel)調(diào)用和泛化(Generic)調(diào)用等調(diào)用方法,雖然也有其特定的應(yīng)用場景,但對于 RPC 架構(gòu)而言并不是主流的調(diào)用方式,這里就不具體展開了。
總結(jié)
可以說,RPC 是分布式系統(tǒng)中一項基礎(chǔ)設(shè)施類的技術(shù)體系,但凡涉及服務(wù)與服務(wù)之間的交互就需要使用到 RPC 架構(gòu)。當(dāng)你在使用一個分布式框架時,可以嘗試用今天介紹的 RPC 架構(gòu)的基本結(jié)構(gòu)和技術(shù)體系進行分析,從而加深對這項技術(shù)體系的理解。