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