本文原封不動的來至于csdn MindWind,原文請見
RPC:RPC 的全稱是 Remote Procedure Call 是一種程序間通信方式。它允許程式調用另一個位址空間(通常是共享網絡的另一台機器上)的過程或函數,而不用程式員顯式編碼這個遠端調用的細節。即程式員無論是調用本地的還是遠端的,本質上編寫的調用代碼基本相同(程序間通訊:匿名管道,命名管道,消息隊列,信号,共享記憶體,socket).
RPC結構:
1. User
2. User-stub
3. RPCRuntime
4. Server-stub
5. Server
這裡 user 就是 client 端,當 user 想發起一個遠端調用時,它實際是通過本地調用 user-stub。user-stub 負責将調用的接口、方法和參數通過約定的協定規範進行編碼并通過本地的 RPCRuntime 執行個體傳輸到遠端的執行個體。遠端 RPCRuntime 執行個體收到請求後交給 server-stub 進行解碼後發起本地端調用,調用結果再傳回給 user 端。
CORBAR 為了解決異構平台的 RPC,使用了 IDL(Interface Definition Language)來定義遠端接口,并将其映射到特定的平台語言中。後來大部分的跨語言平台 RPC 基本都采用了此類方式,比如我們熟悉的 Web Service(SOAP),近年開源的 Thrift 等。他們大部分都通過 IDL 定義,并提供工具來映射生成不同語言平台的 user-stub 和 server-stub,并通過架構庫來提供 RPCRuntime 的支援。不過貌似每個不同的 RPC 架構都定義了各自不同的 IDL 格式,導緻程式員的學習成本進一步上升(苦逼啊),Web Service 嘗試建立業界标準,無賴标準規範複雜而效率偏低,否則 Thrift 等更高效的 RPC 架構就沒必要出現了。
IDL 是為了跨平台語言實作 RPC 不得已的選擇,要解決更廣泛的問題自然導緻了更複雜的方案。而對于同一平台内的 RPC 而言顯然沒必要搞個中間語言出來,例如 java 原生的 RMI,這樣對于 java 程式員而言顯得更直接簡單,降低使用的學習成本。
PRC調用分類:
1. 同步調用
02. 客戶方等待調用執行完成并傳回結果。
03. 異步調用
04. 客戶方調用後不用等待執行結果傳回,但依然可以通過回調通知等方式擷取傳回結果。
05. 若客戶方不關心調用傳回結果,則變成單向異步調用,單向調用不用傳回結果。
異步和同步的區分在于是否等待服務端執行完成并傳回結果。
RPC結構解析:
RPC 服務方通過
RpcServer
去導出(export)遠端接口方法,而客戶方通過
RpcClient
去引入(import)遠端接口方法。客戶方像調用本地方法一樣去調用遠端接口方法,RPC 架構提供接口的代理實作,實際的調用将委托給代理
RpcProxy
。代理封裝調用資訊并将調用轉交給
RpcInvoker
去實際執行。在用戶端的
RpcInvoker
通過連接配接器
RpcConnector
去維持與服務端的通道
RpcChannel
,并使用
RpcProtocol
執行協定編碼(encode)并将編碼後的請求消息通過通道發送給服務方。
RPC 服務端接收器
RpcAcceptor
接收用戶端的調用請求,同樣使用
RpcProtocol
執行協定解碼(decode)。解碼後的調用資訊傳遞給
RpcProcessor
去控制處理調用過程,最後再委托調用給
RpcInvoker
去實際執行并傳回調用結果
RPC元件職責:
01. RpcServer
02. 負責導出(export)遠端接口
03. RpcClient
04. 負責導入(import)遠端接口的代理實作 refer
05. RpcProxy
06. 遠端接口的代理實作
07. RpcInvoker
08. 客戶方實作:負責編碼調用資訊和發送調用請求到服務方并等待調用結果傳回
09. 服務方實作:負責調用服務端接口的具體實作并傳回調用結果
10. RpcProtocol
11. 負責協定編/解碼
12. RpcConnector
13. 負責維持客戶方和服務方的連接配接通道和發送資料到服務方
14.RpcAcceptor
15. 負責接收客戶方請求并傳回請求結果
16. RpcProcessor
17. 負責在服務方控制調用過程,包括管理調用線程池、逾時時間等
18. RpcChannel
19. 資料傳輸通道
RPC實作分析:
遠端接口的導出:導出遠端接口的意思是指隻有導出的接口可以供遠端調用,而未導出的接口則不能。在 java 中導出接口的代碼片段可能如下:
DemoService demo = new ...;
RpcServer server = new ...;
server.export(DemoService.class, demo, options);
我們可以導出整個接口,也可以更細粒度一點隻導出接口中的某些方法,如:
// 隻導出 DemoService 中簽名為 hi(String s) 的方法
server.export(DemoService.class, demo, "hi", new Class<?>[] { String.class }, options);
java 中還有一種比較特殊的調用就是多态,也就是一個接口可能有多個實作,那麼遠端調用時到底調用哪個?這個本地調用的語義是通過 jvm 提供的引用多态性隐式實作的,那麼對于 RPC 來說跨程序的調用就沒法隐式實作了。如果前面
DemoService
接口有 2 個實作,那麼在導出接口時就需要特殊标記不同的實作,如:
DemoService demo = new ...;
DemoService demo2 = new ...;
RpcServer server = new ...;
server.export(DemoService.class, demo, options);
server.export("demo2", DemoService.class, demo2, options);
上面 demo2 是另一個實作,我們标記為 "demo2" 來導出,那麼遠端調用時也需要傳遞該标記才能調用到正确的實作類,這樣就解決了多态調用的語義。
導入遠端接口和用戶端代理:
導入相對于導出遠端接口,用戶端代碼為了能夠發起調用必須要獲得遠端接口的方法或過程定義。目前,大部分跨語言平台 RPC 架構采用根據 IDL 定義通過 code generator 去生成 stub(存根) 代碼,這種方式下實際導入的過程就是通過代碼生成器在編譯期完成的。我所使用過的一些跨語言平台 RPC 架構如 CORBAR、WebService、ICE、Thrift 均是此類方式。
代碼生成的方式對跨語言平台 RPC 架構而言是必然的選擇,而對于同一語言平台的 RPC 則可以通過共享接口定義來實作。在 java 中導入接口的代碼片段可能如下:
RpcClient client = new ...;
DemoService demo = client.refer(DemoService.class); //導入或refer接口,獲得接口實作的proxy(stub)
demo.hi("how are you?");
在 java 中 'import' 是關鍵字,是以代碼片段中我們用 refer 來表達導入接口的意思。這裡的導入方式本質也是一種代碼生成技術,隻不過是在運作時生成,比靜态編譯期的代碼生成看起來更簡潔些。java 裡至少提供了兩種技術來提供動态代碼生成,一種是 jdk 動态代理,另外一種是位元組碼生成(asm bcel,javasist等,老祖宗還是javac)。動态代理相比位元組碼生成使用起來更友善,但動态代理方式在性能上是要遜色于直接的位元組碼生成的,而位元組碼生成在代碼可讀性上要差很多。
協定編解碼:
用戶端代理在發起調用前需要對調用資訊進行編碼,這就要考慮需要編碼些什麼資訊并以什麼格式傳輸到服務端才能讓服務端完成調用。出于效率考慮,編碼的資訊越少越好(傳輸資料少),編碼的規則越簡單越好(執行效率高)。我們先看下需要編碼些什麼資訊
-- 調用編碼 --
接口方法
包括接口名、方法名
方法參數
包括參數類型、參數值
調用屬性
包括調用屬性資訊,例如調用附件隐式參數、調用逾時時間等
-- 傳回編碼 --
傳回結果
接口方法中定義的傳回值
傳回碼
異常傳回碼
傳回異常資訊
調用異常資訊
除了以上這些必須的調用資訊,我們可能還需要一些元資訊以友善程式編解碼以及未來可能的擴充。這樣我們的編碼消息裡面就分成了兩部分,一部分是元資訊、另一部分是調用的必要資訊。如果設計一種 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 等
格式确定後編解碼就簡單了,由于頭長度一定是以我們比較關心的就是消息體的序列化方式。序列化我們關心三個方面:
1. 序列化和反序列化的效率,越快越好。
2. 序列化後的位元組長度,越小越好。
3. 序列化和反序列化的相容性,接口參數對象若增加了字段,是否相容。
傳輸服務:
協定編碼之後,自然就是需要将編碼後的 RPC 請求消息傳輸到服務方,服務方執行後傳回結果消息或确認消息給客戶方。RPC 的應用場景實質是一種可靠的請求應答消息流,和 HTTP 類似。是以選擇長連接配接方式的 TCP 協定會更高效,與 HTTP 不同的是在協定層面我們定義了每個消息的唯一 id,是以可以更容易的複用連接配接。
既然使用長連接配接,那麼第一個問題是到底 client 和 server 之間需要多少根連接配接?實際上單連接配接和多連接配接在使用上沒有差別,對于資料傳輸量較小的應用類型,單連接配接基本足夠。單連接配接和多連接配接最大的差別在于,每根連接配接都有自己私有的發送和接收緩沖區,是以大資料量傳輸時分散在不同的連接配接緩沖區會得到更好的吞吐效率。是以,如果你的資料傳輸量不足以讓單連接配接的緩沖區一直處于飽和狀态的話,那麼使用多連接配接并不會産生任何明顯的提升,反而會增加連接配接管理的開銷。
連接配接是由 client 端發起建立并維持。如果 client 和 server 之間是直連的,那麼連接配接一般不會中斷(當然實體鍊路故障除外)。如果 client 和 server 連接配接經過一些負載中轉裝置,有可能連接配接一段時間不活躍時會被這些中間裝置中斷。為了保持連接配接有必要定時為每個連接配接發送心跳資料以維持連接配接不中斷。心跳消息是 RPC 架構庫使用的内部消息,在前文協定頭結構中也有一個專門的心跳位,就是用來标記心跳消息的,它對業務應用透明。
執行調用:
client stub 所做的事情僅僅是編碼消息并傳輸給服務方,而真正調用過程發生在服務方。server stub 從前文的結構拆解中我們細分了
RpcProcessor
和
RpcInvoker
兩個元件,一個負責控制調用過程,一個負責真正調用。這裡我們還是以 java 中實作這兩個元件為例來分析下它們到底需要做什麼?
java 中實作代碼的動态接口調用目前一般通過反射調用。除了原生的 jdk 自帶的反射,一些第三方庫也提供了性能更優的反射調用,是以
RpcInvoker
就是封裝了反射調用的實作細節。
調用過程的控制需要考慮哪些因素,
RpcProcessor
需要提供什麼樣地調用控制服務呢?下面提出幾點以啟發思考:
1. 效率提升
每個請求應該盡快被執行,是以我們不能每請求來再建立線程去執行,需要提供線程池服務。
2. 資源隔離
當我們導出多個遠端接口時,如何避免單一接口調用占據所有線程資源,而引發其他接口執行阻塞。
3. 逾時控制
當某個接口執行緩慢,而 client 端已經逾時放棄等待後,server 端的線程繼續執行此時顯得毫無意義。
RPC異常處理:
無論 RPC 怎樣努力把遠端調用僞裝的像本地調用,但它們依然有很大的不同點,而且有一些異常情況是在本地調用時絕對不會碰到的。在說異常處理之前,我們先比較下本地調用和 RPC 調用的一些差異:
1. 本地調用一定會執行,而遠端調用則不一定,調用消息可能因為網絡原因并未發送到服務方。
2. 本地調用隻會抛出接口聲明的異常,而遠端調用還會跑出 RPC 架構運作時的其他異常。
3. 本地調用和遠端調用的性能可能差距很大,這取決于 RPC 固有消耗所占的比重。
正是這些差別決定了使用 RPC 時需要更多考量。當調用遠端接口抛出異常時,異常可能是一個業務異常,也可能是 RPC 架構抛出的運作時異常(如:網絡中斷等)。業務異常表明服務方已經執行了調用,可能因為某些原因導緻未能正常執行,而 RPC 運作時異常則有可能服務方根本沒有執行,對調用方而言的異常處理政策自然需要區分。
由于 RPC 固有的消耗相對本地調用高出幾個數量級,本地調用的固有消耗是納秒級,而 RPC 的固有消耗是在毫秒級。那麼對于過于輕量的計算任務就并不合适導出遠端接口由獨立的程序提供服務,隻有花在計算任務上時間遠遠高于 RPC 的固有消耗才值得導出為遠端接口提供服務。
總結:
至此我們提出了一個 RPC 實作的概念架構,并詳細分析了需要考慮的一些實作細節。無論 RPC 的概念是如何優雅,但是“草叢中依然有幾條蛇隐藏着”,隻有深刻了解了 RPC 的本質,才能更好地應用。
轉載于:https://www.cnblogs.com/onlysun/p/4520163.html