網絡協議 19 - RPC 協議:遠在天邊近在眼前

【前五篇】系列文章傳送門:html

  1. 網絡協議 14 - 流媒體協議:要說愛你不容易
  2. 網絡協議 15 - P2P 協議:小種子大學問
  3. 網絡協議 16 - DNS 協議:網絡世界的地址簿
  4. 網絡協議 17 - HTTPDNS:私人定製的 DNS 服務
  5. 網絡協議 18 - CDN:家門口的小賣鋪

    這幾年微服務很火,想必各位博友或多或少的都接觸過。微服務概念中,
各服務間的相互調用是不可或缺的一環。你知道微服務之間是經過什麼方式相互調用的嗎?編程

    你可能說,這還不簡單,用 socket 唄。服務之間分調用方和被調用方,咱們就創建一個 TCP 或者 UDP 鏈接進行通訊就行了。服務器

    說着說着,你可能就會發現,這事兒沒那麼簡單。網絡

    咱們就拿最簡單的場景:app

客戶端調用一個加法函數,將兩個整數加起來,返回它們的和。

    若是放在本地調用,那是簡單的不能再簡單,可是一旦變成了遠程調用,門檻一會兒就上去了。框架

    首先,你要會 socket 編程,至少要先了解我們這個系列的全部協議 ,而後再看 N 本磚頭厚的 socket 程序設計的書,學會我們瞭解過的幾種 socket 程序設計的模型。異步

    這就使得原本大學畢業就能幹的一項工做,變成了一件五年工做經驗都不必定幹好的工做,並且,搞定了 socket 程序設計,纔是萬里長征的第一步,後面還有不少問題呢。socket

存在問題

問題一:如何規定遠程調用的語法?
    客戶端如何告訴服務端,我是一個加法,而另外一個是減法。是用字符串 「add」 傳給你,仍是傳給你一個整數,好比 1 表示加法,2 表示減法?函數

    服務端又該若是告訴客戶端,我這個是加法,目前只能加整數,不能加小數和字符串。而另外一個加法 「add1」,它能實現小數和整數的混合加法,那返回值是什麼?正確的時候返回什麼,錯誤的時候又返回什麼?微服務

問題二:如何傳遞參數?
    是先傳兩個整數,後傳一個操做數 「add」,仍是先傳操做符,再傳兩個整數?

    另外,若是咱們是用 UDP 傳輸,把參數放在一個報文裏還好,但若是是 TCP,是一個流,在這個流裏面如何區分先後兩次調用?

問題三:如何表示數據?
    在咱們的加法例子中,傳遞的就是一個固定長度的 int 值,這種狀況還好,若是是變長的類型,是一個結構體,甚至是一個類,應該怎麼辦呢?即便是 int,在不一樣的平臺上長度也不一樣,該怎麼辦呢?

問題四:如何知道一個服務端都實現了哪些遠程調用?從哪一個端口能夠訪問這個遠程調用?
    假設服務端實現了多個遠程調用,每一個實現可能都不在一個進程中,監聽的端口也不同,並且因爲服務端都是本身實現的,不可能使用一個你們都公認的端口,並且有可能多個進程部署在一臺機器上,你們須要搶佔端口,爲了防止衝突,每每使用隨機端口,那客戶端如何找到這些監聽的端口呢?

問題五:發生了錯誤、重傳、丟包、性能等問題怎麼辦?
    本地調用沒有這個問題,可是一旦到網絡上,這些問題都須要處理,由於網絡是不可靠的,雖然在同一個鏈接中,咱們還能夠經過 TCP 協議保證丟包、重傳的問題,可是若是服務器崩潰了又重啓,當前鏈接斷開了,TCP 就保證不了了,須要應用本身進行從新調用,從新傳輸會不會一樣的操做作兩遍,遠程調用性能會不會受影響呢?

解決問題

    看到這麼多問題,是否是很頭疼?還記得我們瞭解 http 的時候,認識的協議三要素嗎?

    本地調用函數裏不少問題,好比詞法分析、語法分析、語義分析等待,這些問題編譯器基本上都幫咱們解決了,可是在遠程調用中,這些問題咱們都要本身考慮。

協議約定問題

    不少公司對於這個問題,是弄一個核心通訊組,裏面都是 socket 編程的大牛,實現一個統一的庫,讓其餘業務組的人來調用,業務的人不須要知道中間傳輸的細節。

    通訊雙方的語法、語義、格式、端口、錯誤處理等,都須要調用方和被調用方開會商量,雙方達成一致。一旦有一方改變,要及時通知對方,不然就會出現問題。

    可是,不是每一個公司都能經過這種大牛團隊解決問題的,而是使用已經實現好的框架。

    有一個大牛(Bruce Jay Nelson)經過一篇論文,定義了 RPC 的調用標準。後面全部 RPC 框架都是按照這個標準模式來的。

整個過程以下:

  1. 客戶端的應用想發起一個遠程調用時,它其實是經過本地調用方的 Stub。它負責將調用的接口、方法和參數,經過約定的協議規範進行編碼,並經過本地 RPCRuntime 進行傳輸,將調用網絡包發送到服務器;
  2. 服務端的 RPCRuntime 收到請求後,交給提供方 Stub 進行編碼,而後調用服務端的方法,獲取結果,並將結果編碼後,發送給客戶端;
  3. 客戶端的 RPCRuntime 收到結果,發給調用方 Stub 解碼獲得結果,返回給客戶端。

    上面過程當中分了三個層次:客戶端、Stub 層、服務端。

    對於客戶端和服務端,都像是本地調用同樣,專一於業務邏輯的處理就能夠了。對於 Stub 層,處理雙方約定好的語法、語義、封裝、解封裝。對於 RPCRuntime,主要處理高性能的傳輸,以及網絡的錯誤和異常。

    最先的 RPC 的一種實現方式稱爲 Sun RPCONC RPC。Sun 公司是第一個提供商業化 RPC 庫和 RPC 編譯器的公司。這個 RPC 框架是在 NFS 協議中使用的。

    NFS(Network File System)就是網絡文件系統。要使 NFS 成功運行,就要啓動兩個服務端,一個 mountd,用來掛載文件路徑。另外一個是 nfsd,用來讀寫文件。NFS 能夠在本地 mount 一個遠程的目錄到本地目錄,從而實現讓本地用戶在本地目錄裏面讀寫文件時,操做是是遠程另外一臺機器上的文件。

    遠程操做和遠程調用的思路是同樣的,就像本地操做同樣,因此 NFS 協議就是基於 RPC 實現的。固然,不管是什麼 RPC,底層都是 socket 編程。

    XDR(External Data Representation,外部數據表示法)是有一個標準的數據壓縮格式,能夠表示基本的數據類型,也能夠表示結構體。

    這裏有幾種基本的數據類型。

    在 RPC 的調用過程當中,全部的數據類型都要封裝成相似的格式,並且 RPC 的調用和結果返回也有嚴格的格式。

  • XID 惟一標識請求和回覆。請求是 0,回覆是 1;
  • RPC 有版本號,兩端要匹配 RPC 協議的版本號。若是不匹配,就會返回 Deny,緣由是 RPC_MISMATCH;
  • 程序有編號。若是服務端找不到這個程序,就會返回 PROG_UNAVAIL;
  • 程序有版本號。若是程序的版本號不匹配,就會返回 PROG_MISMATCH;
  • 一個程序能夠有多個方法,方法也有編號,若是找不到方法,就會返回 PROG_UNAVAIL;
  • 調用須要認證鑑權,若是不經過,返回 Deny;
  • 最後是參數列表,若是參數沒法解析,返回 GABAGE_ARGS;

    爲了能夠成功調用 RPC,在客戶端和服務端實現 RPC 的時候,首先要定義一個雙方都承認的程序、版本、方法、參數等。

    對於上面的加法而言,雙方約定爲一個協議定義文件,同理,若是是 NFS、mount 和讀寫,也會有相似的定義。

    有了協議定義文件,ONC RPC 會提供一個工具,根據這個文件生成客戶端和服務器端的 Stub 程序。

    最下層的是 XDR 文件,用於編碼和解碼參數。這個文件是客戶端和服務端共享的,由於只有雙方一致才能成功通訊。

    在客戶端,會調用 clnt_create 建立一個鏈接,而後調用 add_1,這是一個 Stub 函數,感受是在調用本地函數同樣。實際上是這個函數發起了一個 RPC 調用,經過調用 clnt_call 來調用 ONC RPC 的類庫,來真正發送請求。調用的過程較爲複雜,後續再進行專門的說明。

    固然,服務端也有一個 Stub 程序,監聽客戶端的請求,當調用到達的時候,判斷若是是 add,則調用真正的服務端邏輯,也就是將兩個數加起來。

    服務端將結果返回服務端的 Stub,Stub 程序發送結果給客戶端 Stub,客戶端 Stub 收到結果後就返回給客戶端的應用程序,從而完成這個調用過。

    有了這個 RPC 框架,前面五個問題中的 「如何規定遠程調用的語法?」、「如何傳遞參數?」 以及 「如何表示數據?」 基本解決了,這三個問題咱們統稱爲協議約定問題

傳輸問題

    前三個問題解決了,可是錯誤、重傳、丟包以及性能問題尚未解決,這些問題咱們統稱爲傳輸問題。這個 Stub 層就無能爲力了,而是由 ONC RPC 的類庫來實現。

    在這個類庫中,爲了解決傳輸問題,對於每個客戶端,都會建立一個傳輸管理層,而每一次 RPC 調用,都會是一個任務,在傳輸管理層,你能夠看到熟悉的隊列機制、擁塞窗口機制等。

    因爲在網絡傳輸的時候,常常須要等待,而同步的方式每每效率比較低,於是也就有 socket 的異步模型。

    爲了可以異步處理,對於遠程調用的處理,每每是經過狀態機來實現的。只有當知足某個狀態的時候,才進行下一步,若是不知足狀態,不是在那裏等待,而是將資源留出來,用來處理其餘的 RPC 調用。

    如上圖,從圖也能夠看出,這個狀態轉換圖仍是很複雜的。

    首先,進入起始狀態,查看 RPC 的傳輸層隊列中有沒有空閒的位置,能夠處理新的 RPC 任務,若是沒有,說明太忙了,直接結束或重試。若是申請成功,就能夠分配內存,獲取服務端的端口號,而後鏈接服務器。

    鏈接的過程要有一段時間,於是要等待鏈接結果,若是鏈接失敗,直接結束或重試。若是鏈接成功,則開始發送 RPC 請,而後等待獲取 RPC 結果。一樣的,這個過程也須要時間,若是發送出錯,就從新發送,若是鏈接斷開,要從新鏈接,若是超時,要從新傳輸。若是獲取到結果,就能夠解碼,正常結束。

    這裏處理了鏈接失敗、重試、發送失敗、超時、重試等場景,於是實現一個 RPC 框架,其實頗有難度。

服務發現問題

    傳輸問題解決了,咱們還遺留了一個 「如何找到 RPC 服務端的那個隨機端口」,這個問題咱們稱爲服務發現問題,在 ONC RPC 中,服務發現是經過 portmapper 實現的。

    portmapper 會啓動在一個衆所周知的端口上,RPC 程序因爲是用戶本身寫的,會監聽在一個隨機端口上,可是 RPC 程序啓動的時候,會向 portmapper 註冊。

    客戶端要訪問 RPC 服務端這個程序的時候,首先查詢 portmapper,獲取 RPC 服務端程序的隨機端口,而後向這個隨機端口創建鏈接,開始 RPC 調用。

從下圖中能夠看出,mount 命令的 RPC 調用就是這樣實現的。

小結

  • 遠程調用看起來用 socket 編程就能夠了,實際上是很複雜的,要解決協議約定問題、傳輸問題和服務發現問題;
  • ONC RPC 框架以及 NFS 的實現,給出瞭解決上述三大問題的示範性實現,也就是公用協議描述文件,並經過這個文件生成 Stub 程序。RPC 的傳輸通常須要一個狀態機,須要另一個進程專門作服務發現。

參考:

  1. 劉超-趣談網絡協議系列課;
  2. 如何給老婆解釋什麼是RPC;
相關文章
相關標籤/搜索