RPC 核心,萬變不離其宗
微信搜 「yes的練級攻略」乾貨滿滿,不然來掐我,回復【123】一份20W字的演算法刷題筆記等你來領。 個人文章匯總://github.com/yessimida/yes 歡迎 star !
Hola,我是 yes。
在了解 Dubbo 之前有必要先來剖析一波 RPC ,先搞清 RPC 原理再去深入了解 Dubbo 會起到事半功倍的效果。
理解核心原理很重要,市面上所有 RPC 框架都逃不過這些核心。
搞清原理之後再看 Dubbo 就會有我說的那個熟悉感和「認可感」。
其實 RPC 不僅僅用在我們平日微服務的調用中,在很多和網路通訊相關的場景都能用到 RPC。
比如消息隊列客戶端和 Broker 之間的交互,還有和一些其他中間件的交互都會用到 RPC。
你可能會說沒啊?哪有 RPC 調用?
嘿嘿,你看這就是 RPC 的作用,讓你無感知地完成了遠程通訊。
其實在這篇我被噴的上「熱門」的文章中已經提過一次 RPC,也提到了 HTTP 和 RPC 的區別,不過那次的主角是 HTTP 。
這次咱們深入剖析一波 RPC,從根上來理解一波。
來,上車!
正文
RPC 全稱是 Remote Procedure Call ,即遠程過程調用,其對應的是我們的本地調用。
遠程其實指的就是需要網路通訊,可以理解為調用遠程機器上的方法。
那可能有人說:我用 HTTP 調用不就是遠程調用了,那不也叫 RPC 了?
不是的,RPC 的目的是:讓我們調用遠程方法像調用本地方法一樣無差別。
來看下程式碼就很清晰,比如本來沒有拆分服務都是本地調用的時候方法是這樣寫的:
public String getSth(String str) {
return yesService.get(str);
}
如果 yesSerivce 被拆分出去,此時需要遠程調用了,如果用 HTTP 方式,可能就是:
public String getSth(String str) {
RequestParam param = new RequestParam();
......
return HttpClient.get(url, param,.....);
}
此時需要關心遠程服務的地址,還需要組裝請求等等,而如果採用 RPC 調用那就是:
public String getSth(String str) {
// 看起來和之前調用沒差?哈哈沒唬你,
// 具體的實現已經搬到另一個服務上了,這裡只有介面。
// 看完下面就知道了。
return yesService.get(str);
}
所以說 RPC 其實就是用來屏蔽遠程調用網路相關的細節,使得遠程調用和本地調用使用一致,讓開發的效率更高。
在了解了 RPC 的作用之後,我們來看看 RPC 調用需要經歷哪些步驟。
RPC 調用基本流程
按上面的例子來說,yesService 服務實現被移到了遠程服務上,本地沒有具體的實現只有一個介面。
那這時候我們需要調用 yesService.get(str)
,該怎麼辦呢?
我們所要做的就是把傳入的參數和調用的介面全限定名通過網路通訊告知到遠程服務那裡。
然後遠程服務接收到參數和介面全限定名就能選中具體的實現並進行調用。
業務處理完之後再通過網路返回結果,這就搞定了!
上面的操作這些就是由yesService.get(str)
觸發的。
不過我們知道 yesService 就是一個介面,沒有實現的,所以這些操作是怎麼來的?
是通過動態代理來的。
RPC 會給介面生成一個代理類,所以我們調用這個介面實際調用的是動態生成的代理類,由代理類來觸發遠程調用,這樣我們調用遠程介面就無感知了。
動態代理想必大家都比較熟悉,最常見的就是 Spring 的 AOP 了,涉及的有 JDK 動態代理和 cglib。
在 Dubbo 中用的是 Javassist,至於為什麼用這個其實梁飛大佬已經寫了部落格說明了。
他當時對比了 JDK 自帶的、ASM、CGLIB(基於ASM包裝)、Javassist。
經過測試最終選用了 Javassist。
梁飛:最終決定使用JAVAASSIST的位元組碼生成代理方式。雖然ASM稍快,但並沒有快一個數量級,而JAVAASSIST的位元組碼生成方式比ASM方便,JAVAASSIST只需用字元串拼接出Java源碼,便可生成相應位元組碼,而ASM需要手工寫位元組碼。
可以看到選擇一個框架的時候性能是一方面,易用性也很關鍵。
說回 RPC 。
現在我們知道動態代理屏蔽了 RPC 調用的細節,使得用戶無感知的調用遠程服務,那調用的細節有哪些呢?
序列化
像我們的請求參數都是對象,有時候是定義的 DTO ,有時候是 Map ,這些對象是無法直接在網路中傳輸的。
你可以理解為對象是「立體」的,而網路傳輸的數據是「扁平」的,最終需要轉化成「扁平」的二進位數據在網路中傳輸。
你想想,各對象分配在記憶體不同位置,各種引用,這看起來是不是有種立體的感覺?
最終都是要變成一段01組成的數字傳輸給對方,這種就01組成的數字看起來是不是很「扁平」?
把對象轉化成二進位數據的過程稱為序列化,把二進位數據轉化成對象的過程稱為反序列化。
當然如何選擇序列化格式也很重要。
比如採用二進位的序列化格式數據更加緊湊,採用 JSON 等文本型序列化格式可讀性更佳,排查問題比較方便。
還有很多序列化選擇,一般需要綜合考慮通用性、性能、可讀性和兼容性。
具體本文就不分析了,之後再專門寫一篇分析各種序列化協議的。
RPC 協議
剛才也提到了只有二進位數據才能在網路中傳輸,那一堆二進位在底層看來是連起來的,它可不會管你哪些數據是哪個請求的。
但接收方得知道呀,不然就不能順利的把二進位數據還原成對應的一個個請求了。
於是就需要定義一個協議,來約定一些規範,制定一些邊界使得二進位數據可以被還原。
比如下面一串數字按照不同位數來識別得到的結果是不同的。
所以協議其實就定義了到底如何構造和解析這些二進位數據。
我們的參數肯定比上面的複雜,因為參數值長度是不定的,而且協議常常伴隨著升級而擴展,畢竟有時候需要加一些新特性,那麼協議就得變了。
一般 RPC 協議都是採用協議頭+協議體的方式。
協議頭放一些元數據,包括:魔法位、協議的版本、消息的類型、序列化方式、整體長度、頭長度、擴展位等。
協議體就是放請求的數據了。
通過魔法位可以得知這是不是咱們約定的協議,比如魔法位固定叫 233 ,一看我們就知道這是 233 協議。
然後協議的版本是為了之後協議的升級。
從整體長度和頭長度我們就能知道這個請求到底有多少位,前面多少位是頭,剩下的都是協議體,這樣就能識別出來,擴展位就是留著日後擴展備用。
貼一下 Dubbo 協議:
可以看到有 Magic 位,請求 ID, 數據長度等等。
網路傳輸
組裝好數據就等著發送了,這時候就涉及網路傳輸了。
網路通訊那就離不開網路 IO 模型了。
網路 IO 分為這四種模型,具體以後單獨寫文章分析,這篇就不展開了。
一般而言我們用的都是 IO 多路復用,因為大部分 RPC 調用場景都是高並發調用,IO 復用可以利用較少的執行緒 hold 住很多請求。
一般 RPC 框架會使用已經造好的輪子來作為底層通訊框架。
例如 Java 語言的都會用 Netty ,人家已經封裝的很好了,也做了很多優化,拿來即用,便捷高效。
小結
RPC 通訊的基礎流程已經講完了,看下圖:
響應返回就沒畫了,反正就是倒著來。
我再用一段話來總結一下:
服務調用方,面向介面編程,利用動態代理屏蔽底層調用細節將請求參數、介面等數據組合起來並通過序列化轉化為二進位數據,再通過 RPC 協議的封裝利用網路傳輸到服務提供方。
服務提供方根據約定的協議解析出請求數據,然後反序列化得到參數,找到具體調用的介面,然後執行具體實現,再返回結果。
這裡面還有很多細節。
比如請求都是非同步的,所以每個請求會有唯一 ID,返回結果會帶上對應的 ID, 這樣調用方就能通過 ID 找到對應的請求塞入相應的結果。
有人會問為什麼要非同步,那是為了提高吞吐。
當然還有很多細節,會在之後剖析 Dubbo 的時候提到,結合實際中間件體會才會更深。
真正工業級別的 RPC
以上提到的只是 RPC 的基礎流程,這對於工業級別的使用是遠遠不夠的。
生產環境中的服務提供者都是集群部署的,所以有多個提供者,而且還會隨著大促等流量情況動態增減機器。
因此需要註冊中心,作為服務的發現。
調用者可以通過註冊中心得知服務提供者們的 IP 地址等元資訊,進行調用。
調用者也能通過註冊中心得知服務提供者下線。
還需要有路由分組策略,調用者根據下發的路由資訊選擇對應的服務提供者,能實現分組調用、灰度發布、流量隔離等功能。
還需要有負載均衡策略,一般經過路由過濾之後還是有多個服務提供者可以選擇,通過負載均衡策略來達到流量均衡。
當然還需要有異常重試,畢竟網路是不穩定的,而且有時候某個服務提供者也可能出點問題,所以一次調用出錯進行重試,較少業務的損耗。
還需要限流熔斷,限流是因為服務提供者不知道會接入多少調用者,也不清楚每個調用者的調用量,所以需要衡量一下自身服務的承受值來進行限流,防止服務崩潰。
而熔斷是為了防止下游服務故障導致自身服務調用超時阻塞堆積而崩潰,特別是調用鏈很長的那種,影響很大。
比如A=>B=>C=>D=>E,然後 E 出了故障,你看ABCD四個服務就傻等著,慢慢的資源就佔滿了就崩了,全崩。
大致就是以上提到的幾點,不過還能細化,比如負載均衡的各種策略、限流到底是限制總流量還是根據每個調用者指定限流量,還是上自適應限流等等。
這個在之後分析 Dubbo 的時候都會提到,等著哈。
最後
我之前面過一個同學,兩年經驗,簡歷寫著熟悉 Spring Cloud Alibaba 然後了解 Dubbo 。
我問他 RPC 的調用原理,他問我什麼是 RPC,沒聽過這個名詞。
這就太浮在表面了。
理解原理還是很重要的,像我上面提到的動態代理也不是一定是要的,像 C++ 就沒有動態代理, gRPC 框架用的是程式碼生成。
反正最終只要能屏蔽調用細節,不需要使用者關心即可,至於用什麼方式達到這個目的,影響不大。
還有上面提到面向介面,其實有時候就是沒介面,例如一些服務網關,暴露出 HTTP 調用的方式給調用者來調用後端 RPC 服務。
網關是要接入很多後端服務的,所以不可能依賴後端的介面,不然就不靈活了。
這裡就有個泛化調用的概念。
其實只要你理解了請求方無非就是告知服務提供方我要調哪個方法,參數都是哪些,你就能很容易的理解什麼叫泛化調用。
也就能理解其實不需要介面我們也能進行 RPC 調用。
具體泛化調用是什麼之後寫 Dubbo 會提到。
所以聽起來好像很高級的玩意,如果你理解了本質,其實也就這麼點東西。
萬變不離其宗。
歡迎關注我的公眾號【yes的練級攻略】,更多硬核文章等你來讀。
更多文章可看我的文章匯總://github.com/yessimida/yes 歡迎 star !
我是 yes,從一點點到億點點,歡迎在看、轉發、留言,我們下篇見。