RPC入門--定義與原理

一、RPC

1. RPC是什麼

RPC(Remote Procedure Call Protocol)——遠程過程調用協議,它是一種通過網絡從遠程計算機程序上請求服務,而不需要了解底層網絡技術的協議。RPC協議假定某些傳輸協議的存在,如TCP或UDP,爲通信程序之間攜帶信息數據。在OSI網絡通信模型中,RPC跨越了傳輸層應用層。RPC使得開發包括網絡分佈式多程序在內的應用程序更加容易。

RPC採用客戶機/服務器模式。請求程序就是一個客戶機,而服務提供程序就是一個服務器。首先,客戶機調用進程發送一個有進程參數的調用信息到服務進程,然後等待應答信息。在服務器端,進程保持睡眠狀態直到調用信息到達爲止。當一個調用信息到達,服務器獲得進程參數,計算結果,發送答覆信息,然後等待下一個調用信息,最後,客戶端調用進程接收答覆信息,獲得進程結果,然後調用執行繼續進行。

 

2. 爲什麼要用RPC? 

其實這是應用開發到一定的階段的強烈需求驅動的。

 

1. 如果我們開發簡單的單一應用,邏輯簡單、用戶不多、流量不大,那我們用不着;

2. 當我們的系統訪問量增大、業務增多時,我們會發現一臺單機運行此係統已經無法承受。此時,我們可以將業務拆分成幾個互不關聯的應用,分別部署在各自機器上,以劃清邏輯並減小壓力。此時,我們也可以不需要RPC,因爲應用之間是互不關聯的。
3. 當我們的業務越來越多、應用也越來越多時,自然的,我們會發現有些功能已經不能簡單劃分開來或者劃分不出來。此時,可以將公共業務邏輯抽離出來,將之組成獨立的服務Service應用 。而原有的、新增的應用都可以與那些獨立的Service應用 交互,以此來完成完整的業務功能。所以此時,我們急需一種高效的應用程序之間的通訊手段來完成這種需求,所以你看,RPC大顯身手的時候來了!
其實3描述的場景也是服務化 、微服務 分佈式系統架構 的基礎場景。即RPC框架就是實現以上結構的有力方式。

二、RPC的原理和框架

Nelson 的論文中指出實現 RPC 的程序包括 5 個部分:

1. User

2. User-stub

3. RPCRuntime

4. Server-stub

5. Server

這 5 個部分的關係如下圖所示

這裏 user 就是 client 端,當 user 想發起一個遠程調用時,它實際是通過本地調用user-stub。user-stub 負責將調用的接口、方法和參數通過約定的協議規範進行編碼並通過本地的 RPCRuntime 實例傳輸到遠端的實例。遠端 RPCRuntime 實例收到請求後交給 server-stub 進行解碼後發起本地端調用,調用結果再返回給 user 端。

粗粒度的 RPC 實現概念結構,這裏我們進一步細化它應該由哪些組件構成,如下圖所示。

RPC 服務方通過 RpcServer 去導出(export)遠程接口方法,而客戶方通過 RpcClient 去引入(import)遠程接口方法。客戶方像調用本地方法一樣去調用遠程接口方法,RPC 框架提供接口的代理實現,實際的調用將委託給代理RpcProxy 。代理封裝調用信息並將調用轉交給RpcInvoker 去實際執行。在客戶端的RpcInvoker 通過連接器RpcConnector 去維持與服務端的通道RpcChannel,並使用RpcProtocol 執行協議編碼(encode)並將編碼後的請求消息通過通道發送給服務方。
RPC 服務端接收器 RpcAcceptor 接收客戶端的調用請求,同樣使用RpcProtocol 執行協議解碼(decode)。解碼後的調用信息傳遞給RpcProcessor 去控制處理調用過程,最後再委託調用給RpcInvoker 去實際執行並返回調用結果。如下是各個部分的詳細職責:

1. RpcServer  

   負責導出(export)遠程接口  

2. RpcClient  

   負責導入(import)遠程接口的代理實現  

3. RpcProxy  

   遠程接口的代理實現  

4. RpcInvoker  

   客戶方實現:負責編碼調用信息和發送調用請求到服務方並等待調用結果返回  

   服務方實現:負責調用服務端接口的具體實現並返回調用結果  

5. RpcProtocol  

   負責協議編/解碼  

6. RpcConnector  

   負責維持客戶方和服務方的連接通道和發送數據到服務方  

7. RpcAcceptor  

   負責接收客戶方請求並返回請求結果  

8. RpcProcessor  

   負責在服務方控制調用過程,包括管理調用線程池、超時時間等  

9. RpcChannel  

   數據傳輸通道  

三、Java中常用的RPC框架

目前常用的RPC框架如下:

1. Thrift:thrift是一個軟件框架,用來進行可擴展且跨語言的服務的開發。它結合了功能強大的軟件堆棧和代碼生成引擎,以構建在 C++, Java, Python, PHP, Ruby, Erlang, Perl, Haskell, C#, Cocoa, JavaScript, Node.js, Smalltalk, and OCaml 這些編程語言間無縫結合的、高效的服務。

2. Dubbo:Dubbo是一個分佈式服務框架,以及SOA治理方案。其功能主要包括:高性能NIO通訊及多協議集成,服務動態尋址與路由,軟負載均衡與容錯,依賴分析與降級等。 Dubbo是阿里巴巴內部的SOA服務化治理方案的核心框架,Dubbo自2011年開源後,已被許多非阿里系公司使用。 

3. Spring Cloud:Spring Cloud由衆多子項目組成,如Spring Cloud Config、Spring Cloud Netflix、Spring Cloud Consul 等,提供了搭建分佈式系統及微服務常用的工具,如配置管理、服務發現、斷路器、智能路由、微代理、控制總線、一次性token、全局鎖、選主、分佈式會話和集羣狀態等,滿足了構建微服務所需的所有解決方案。Spring Cloud基於Spring Boot, 使得開發部署極其簡單。

四、RPC和消息隊列的差異

1. 功能差異

在架構上,RPC和Message的差異點是,Message有一箇中間結點Message Queue,可以把消息存儲。
消息的特點
1. Message Queue把請求的壓力保存一下,逐漸釋放出來,讓處理者按照自己的節奏來處理。
2. Message Queue引入一下新的結點,系統的可靠性會受Message Queue結點的影響。
3. Message Queue是異步單向的消息。發送消息設計成是不需要等待消息處理的完成。
所以對於有同步返回需求,用Message Queue則變得麻煩了。
RPC的特點
同步調用,對於要等待返回結果/處理結果的場景,RPC是可以非常自然直覺的使用方式(RPC也可以是異步調用)。
由於等待結果,Consumer(Client)會有線程消耗。如果以異步RPC的方式使用,Consumer(Client)線程消耗可以去掉。但不能做到像消息一樣暫存消息/請求,壓力會直接傳導到服務Provider。
2. 適用場合差異
1. 希望同步得到結果的場合,RPC合適。
2. 希望使用簡單,則RPC;RPC操作基於接口,使用簡單,使用方式模擬本地調用。異步的方式編程比較複雜。
3. 不希望發送端(RPC Consumer、Message Sender)受限於處理端(RPC Provider、Message Receiver)的速度時,使用Message Queue。
隨着業務增長,有的處理端處理量會成爲瓶頸,會進行同步調用到異步消息的改造。這樣的改造實際上有調整業務的使用方式。比如原來一個操作頁面提交後就下一個頁面會看到處理結果;改造後異步消息後,下一個頁面就會變成“操作已提交,完成後會得到通知”。
3. 不適用場合說明
1. RPC同步調用使用Message Queue來傳輸調用信息。 上面分析可以知道,這樣的做法,發送端是在等待,同時佔用一箇中間點的資源。變得複雜了,但沒有對等的收益。
2. 對於返回值是void的調用,可以這樣做,因爲實際上這個調用業務上往往不需要同步得到處理結果的,只要保證會處理即可。(RPC的方式可以保證調用返回即處理完成,使用消息方式後這一點不能保證了。)
3. 返回值是void的調用,使用消息,效果上是把消息的使用方式Wrap成了服務調用(服務調用使用方式成簡單,基於業務接口)。

五、RPC框架的核心技術點

RPC框架實現的幾個核心技術點:

(1)服務暴露:

遠程提供者需要以某種形式提供服務調用相關的信息,包括但不限於服務接口定義數據結構、或者中間態的服務定義文件。例如Facebook的Thrift的IDL文件,Web service的WSDL文件;服務的調用者需要通過一定的途徑獲取遠程服務調用相關的信息。

目前,大部分跨語言平臺 RPC 框架採用根據 IDL 定義通過 code generator 去生成 stub 代碼,這種方式下實際導入的過程就是通過代碼生成器在編譯期完成的。代碼生成的方式對跨語言平臺 RPC 框架而言是必然的選擇,而對於同一語言平臺的 RPC 則可以通過共享接口定義來實現。這裏的導入方式本質也是一種代碼生成技術,只不過是在運行時生成,比靜態編譯期的代碼生成看起來更簡潔些。

java 中還有一種比較特殊的調用就是多態,也就是一個接口可能有多個實現,那麼遠程調用時到底調用哪個?這個本地調用的語義是通過 jvm 提供的引用多態性隱式實現的,那麼對於 RPC 來說跨進程的調用就沒法隱式實現了。如果前面DemoService 接口有 2 個實現,那麼在導出接口時就需要特殊標記不同的實現需要,那麼遠程調用時也需要傳遞該標記才能調用到正確的實現類,這樣就解決了多態調用的語義問題。

(2)遠程代理對象:

服務調用者用的服務實際是遠程服務的本地代理。說白了就是通過動態代理來實現。

java 裏至少提供了兩種技術來提供動態代碼生成,一種是 jdk 動態代理,另外一種是字節碼生成。動態代理相比字節碼生成使用起來更方便,但動態代理方式在性能上是要遜色於直接的字節碼生成的,而字節碼生成在代碼可讀性上要差很多。兩者權衡起來,個人認爲犧牲一些性能來獲得代碼可讀性和可維護性顯得更重要。

(3)通信:

RPC框架與具體的協議無關。RPC 可基於 HTTP 或 TCP 協議,Web Service 就是基於 HTTP 協議的 RPC,它具有良好的跨平臺性,但其性能卻不如基於 TCP 協議的 RPC。

1. TCP/HTTP:衆所周知,TCP 是傳輸層協議,HTTP 是應用層協議,而傳輸層較應用層更加底層,在數據傳輸方面,越底層越快,因此,在一般情況下,TCP 一定比 HTTP 快。

2. 消息ID:RPC 的應用場景實質是一種可靠的請求應答消息流,和 HTTP 類似。因此選擇長連接方式的 TCP 協議會更高效,與 HTTP 不同的是在協議層面我們定義了每個消息的唯一 id,因此可以更容易的複用連接。

3. IO方式:爲了支持高併發,傳統的阻塞式 IO 顯然不太合適,因此我們需要異步的 IO,即 NIO。Java 提供了 NIO 的解決方案,Java 7 也提供了更優秀的 NIO.2 支持。

4. 多連接:既然使用長連接,那麼第一個問題是到底 client 和 server 之間需要多少根連接?實際上單連接和多連接在使用上沒有區別,對於數據傳輸量較小的應用類型,單連接基本足夠。單連接和多連接最大的區別在於,每根連接都有自己私有的發送和接收緩衝區,因此大數據量傳輸時分散在不同的連接緩衝區會得到更好的吞吐效率。所以,如果你的數據傳輸量不足以讓單連接的緩衝區一直處於飽和狀態的話,那麼使用多連接並不會產生任何明顯的提升,反而會增加連接管理的開銷。
5. 心跳:連接是由 client 端發起建立並維持。如果 client 和 server 之間是直連的,那麼連接一般不會中斷(當然物理鏈路故障除外)。如果 client 和 server 連接經過一些負載中轉設備,有可能連接一段時間不活躍時會被這些中間設備中斷。爲了保持連接有必要定時爲每個連接發送心跳數據以維持連接不中斷。心跳消息是 RPC 框架庫使用的內部消息,在前文協議頭結構中也有一個專門的心跳位,就是用來標記心跳消息的,它對業務應用透明。

(4)序列化:

兩方面會直接影響 RPC 的性能,一是傳輸方式,二是序列化。

1. 序列化方式:畢竟是遠程通信,需要將對象轉化成二進制流進行傳輸。不同的RPC框架應用的場景不同,在序列化上也會採取不同的技術。 就序列化而言,Java 提供了默認的序列化方式,但在高併發的情況下,這種方式將會帶來一些性能上的瓶頸,於是市面上出現了一系列優秀的序列化框架,比如:Protobuf、Kryo、Hessian、Jackson 等,它們可以取代 Java 默認的序列化,從而提供更高效的性能。

2. 編碼內容:出於效率考慮,編碼的信息越少越好(傳輸數據少),編碼的規則越簡單越好(執行效率高)。如下是編碼需要具備的信息:

-- 調用編碼 --  
1. 接口方法  
   包括接口名、方法名  
2. 方法參數  
   包括參數類型、參數值  
3. 調用屬性  
   包括調用屬性信息,例如調用附件隱式參數、調用超時時間等  
  
-- 返回編碼 --  
1. 返回結果  
   接口方法中定義的返回值  
2. 返回碼  
   異常返回碼  
3. 返回異常信息  
   調用異常信息 

除了以上這些必須的調用信息,我們可能還需要一些元信息以方便程序編解碼以及未來可能的擴展。這樣我們的編碼消息裏面就分成了兩部分,一部分是元信息、另一部分是調用的必要信息。如果設計一種 RPC 協議消息的話,元信息我們把它放在協議消息頭中,而必要信息放在協議消息體中。下面給出一種概念上的 RPC 協議消息設計格式:

-- 消息頭 --  
magic      : 協議魔數,爲解碼設計  
header size: 協議頭長度,爲擴展設計  
version    : 協議版本,爲兼容設計  
st         : 消息體序列化類型  
hb         : 心跳消息標記,爲長連接傳輸層心跳設計  
ow         : 單向消息標記,  
rp         : 響應消息標記,不置位默認是請求消息  
status code: 響應消息狀態碼  
reserved   : 爲字節對齊保留  
message id : 消息 id  
body size  : 消息體長度  
  
-- 消息體 --  
採用序列化編碼,常見有以下格式  
xml   : 如 webservie soap  
json  : 如 JSON-RPC  
binary: 如 thrift; hession; kryo 等

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章