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

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

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

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

    你可能說,這還不簡單,用 socket 唄。服務之間分調用方和被調用方,我們就建立一個 TCP 或者 UDP 連接進行通信就好了。

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

    我們就拿最簡單的場景:

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

    如果放在本地調用,那是簡單的不能再簡單,但是一旦變成了遠程調用,門檻一下子就上去了。

    首先,你要會 socket 編程,至少要先了解咱們這個系列的所有協議 ,然後再看 N 本磚頭厚的 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;
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章