RPC 核心,萬變不離其宗

微信搜 「yes的練級攻略」乾貨滿滿,否則來掐我,回覆【123】一份20W字的算法刷題筆記等你來領。 我的文章彙總:https://github.com/yessimida/yes 歡迎 star !java

Hola,我是 yes。git

應讀者要求,就下來會更新 Dubbo 相關的文章,還有位同窗想在春招以前看到,安排!github

這裏要爲官網辯護一下,其實官網仍是挺詳細的哈,就是最近 UI 改了看着有點不太爽。算法

好了回到今天的文章,在瞭解 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的練級攻略】,更多硬核文章等你來讀。

更多文章可看個人文章彙總:https://github.com/yessimida/yes 歡迎 star !


我是 yes,從一點點到億點點,歡迎在看、轉發、留言,咱們下篇見。

本文分享自微信公衆號 - yes的練級攻略(yes_java)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。