標籤:好的 java pcc 相容性 地方 用戶端 表達 維護 解決
本文轉載自這裡是原文
《深入篇》我們主要圍繞 RPC 的功能目標和實現考量去展開,一個基本的 RPC 架構應該提供什麼功能,滿足什麼要求以及如何去實現它?
RPC 功能目標
RPC的主要功能呢個目標是讓構建分散式運算更加容易,在提供強大的遠程調用能力時不損失本地調用的語義簡潔性。為實現該目標,RPC架構提供一種透明的調用機制讓使用者不必顯式的區分本地調用和遠程調用,在前文《淺出篇》給出了一種時間結構,基於stub的是結構來實現,下面我們將細化stub結構的實現。
RPC調用分類
RPC調用分以下兩種:
RPC結構拆解
《淺出篇》給出了一個比較粗粒度的RPC實現概念結構,這裡我們進一步細化它該由哪些組件構成,如所示:
RPC服務方通過RpcServer匯出遠程介面方法,而客戶方通過RpcClient去引入遠程介面方法。客戶方向調用本地方法一樣去調用遠程方法,RPC架構提供介面的代理實現,實際的調用委託給RpcProxy。代理封裝調用信用資訊將調用轉交給RpcInvoker去實際執行,在用戶端的RpcInvoker通過連接器RpcConnector去維持伺服器端的通道RpcChannel,並使用RpcProtocol執行協議編碼(encode)並將編碼後的訊息通過通道發給服務方。
RPC服務方的RpcAcceptor去接受客戶方的調用請求,同樣使用RPCProtocol執行協議解碼(decode)。解碼後的資訊傳遞給RPCProcessor去控制處理調用過程,最後在委託調用給RPCInvoker去實際執行並返回調用結果。
RPC組件職責
上面我們進一步拆解了RPC實現結構的哥哥組件部分,下面我們詳細說明每個部分的職責:
-
- RPCServer > 負責匯出介面
-
- RpcClient > 負責匯入介面
-
- RpcProxy > 遠程介面代理實現
-
- RpcInvoker > 客戶方實現:負責編碼調用資訊,並發送調用資訊並等待調用返回。 > 服務方實現:負責調用服務端介面的具體實現並返回調用結果。
-
- RpcProtocol > 負責協議編解碼
-
- RpcConnector > 負責維持客戶方和服務方的諒解通道和發送資料到服務方
-
- RpcAcceptor > 負責接收客戶方的請求並返回處理結果
-
- RpcProcessor > 負責在服務方管理調用過程,包括線程池和調用的逾時時間等。
-
- RpcChannel > 資料轉送通道
RPC實現分析
在進一步拆解了組件並劃分了職責以後,這裡以在Java平台實現該RPC架構概念性模型為例,詳細分析下實現中需要考慮的因素:
匯出遠程介面
匯出遠程介面的意思指的只有匯出的介面可以供遠程調用,而未匯出的介面則不能用,在java中匯出介面的程式碼片段可能如下:
DemoServer demo = new ...; RpcSerber server = new ...; server.export(Demoserver.class, demo, options)
我們可以匯出整個介面也可以更細粒度一點只匯出介面中的某些方法,如:
//只匯出Demoserver中籤名為hi(string s) 的方法 server.export(DemoServer.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 ...; DemoServer demo = client.refer(DemoServer.class); demo.hi("how are you");
在 java 中 import 是關鍵字,所以程式碼片段中我們用 refer 來表達匯入介面的意思。 這裡的匯入方式本質也是一種代碼產生技術,只不過是在運行時產生,比靜態編譯期的代碼產生看起來更簡潔些。 java 裡至少提供了兩種技術來提供動態代碼產生,一種是 jdk 動態代理,另外一種是位元組碼產生。 動態代理相比位元組碼產生使用起來更方便,但動態代理方式在效能上是要遜色於直接的位元組碼產生的,而位元組碼產生在代碼可讀性上要差很多。 兩者權衡起來,個人認為犧牲一些效能來獲得代碼可讀性和可維護性顯得更重要。
協議編解碼
用戶端代理在發起調用前需要對調用資訊進行編碼,這就要考慮需要編碼些什麼資訊並以什麼格式傳輸到服務端才能讓服務端完成調用。 出於效率考慮,編碼的資訊越少越好(傳輸資料少),編碼的規則越簡單越好(執行效率高)。 我們先看下需要編碼些什麼資訊:
調用編碼
- 介面方法
包括介面名、方法名
- 方法參數
包括參數類型、參數值
- 調用屬性
包括調用屬性資訊,例如調用附件隱式參數、調用逾時時間等
返回編碼
- 返回結果
介面方法中定義的傳回值
- 返回碼
異常返回碼
- 返回異常資訊
調用異常資訊
除了以上這些必須的調用資訊,我們可能還需要一些元資訊以方便程式編解碼以及未來可能的擴充。 這樣我們的編碼訊息裡面就分成了兩部分,一部分是元資訊、另一部分是調用的必要資訊。 如果設計一種 RPC 協議訊息的話,元資訊我們把它放在協議訊息頭中,而必要資訊放在協議訊息體中。 下面給出一種概念上的 RPC 協議訊息設計格式:
訊息頭
- magic:協議魔數,為解碼設計
- header size: 協議頭長度,為擴充設計
- version: 協議版本,為擴充時使用
- st: 訊息體序列化類別型
- hb: 心跳資訊標記,為長串連傳輸層心跳設計
- ow: 單向訊息標記
- rp: 相應訊息標記。不置位預設是請求訊息
- status code: 響應訊息狀態代碼
- reserved: 為位元組對齊保留
- message id:訊息id
- body size:訊息體長度
訊息體
採用序列化編碼,常見有以下格式:
- XML:如webservice(SOAP)
- json: 如json-RPC
- binary: 如thrift,hession,kryo等
格式確定後編解碼就簡單了,由於頭長度一定所以我們比較關心的就是訊息體的序列化方式。 序列化我們關心三個方面:
- 序列化和還原序列化的效率,越快越好。
- 序列化後的位元組長度,越小越好。
- 序列化和還原序列化的相容性,介面參數對象若增加了欄位,是否相容。
上面這三點有時是魚與熊掌不可兼得,這裡面涉及到具體的序列化庫實現細節,就不在本文進一步展開分析了。
傳輸服務
協議編碼之後,自然就是需要將編碼後的 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 需要提供什麼樣地調用控制服務呢? 下面提出幾點以啟發思考:
每個請求應該儘快被執行,因此我們不能每請求來再建立線程去執行,需要提供線程池服務.
當我們匯出多個遠程介面時,如何避免單一介面調用佔據所有線程資源,而引發其他介面執行阻塞。
當某個介面執行緩慢,而 client 端已經逾時放棄等待後,server 端的線程繼續執行此時顯得毫無意義。
RPC 異常處理
無論 RPC 怎樣努力把遠程調用偽裝的像本地調用,但它們依然有很大的不同點,而且有一些異常情況是在本地調用時絕對不會碰到的。 在說異常處理之前,我們先比較下本地調用和 RPC 調用的一些差異:
- 本地調用一定會執行,而遠程調用則不一定,調用訊息可能因為網路原因並未發送到服務方。
- 本地調用只會拋出介面聲明的異常,而遠程調用還會跑出 RPC 架構運行時的其他異常。
- 本地調用和遠程調用的效能可能差距很大,這取決於 RPC 固有消耗所佔的比重。
正是這些區別決定了使用 RPC 時需要更多考量。 當調用遠程介面拋出異常時,異常可能是一個業務異常, 也可能是 RPC 架構拋出的運行時異常(如:網路中斷等)。 業務異常表明服務方已經執行了調用,可能因為某些原因導致未能正常執行, 而 RPC 運行時異常則有可能服務方根本沒有執行,對調用方而言的異常處理策略自然需要區分。
由於 RPC 固有的消耗相對本地調用高出幾個數量級,本地調用的固有消耗是納秒級,而 RPC 的固有消耗是在毫秒級。 那麼對於過於輕量的計算任務就並不合適匯出遠程介面由獨立的進程提供服務, 只有花在計算任務上時間遠遠高於 RPC 的固有消耗才值得匯出為遠程介面提供服務。
總結
至此我們提出了一個 RPC 實現的概念架構,並詳細分析了需要考慮的一些實現細節。 無論 RPC 的概念是如何優雅,但是“草叢中依然有幾條蛇隱藏著”,只有深刻理解了 RPC 的本質,才能更好地應用。
http://daodaoliang.com/blog/2015/08/27/%E6%B7%B1%E5%85%A5%E6%B5%85%E5%87%BARPC(%E6%B7%B1%E5%85%A5%E7%AF%87).html
深入淺出RPC——深入篇(轉載)