1. 程式人生 > >網路協議 19 - RPC協議綜述:遠在天邊近在眼前

網路協議 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;