前面我們講了容器網路如何實作跨主機互通,以及微服務之間的相互呼叫,

網路是打通了,那服務之間的互相呼叫,該怎么實作呢?你可能說,咱不是學過 Socket 嗎,服務之間分呼叫方和被呼叫方,我們就建立一個 TCP 或者 UDP 的連接,不就可以通信了?

你仔細想一下,這事兒沒這么簡單,我們就拿最簡單的場景,客戶端呼叫一個加法函式,將兩個整數加起來,回傳它們的和,
如果放在本地呼叫,那是簡單的不能再簡單了,只要稍微學過一種編程語言,三下五除二就搞定了,但是一旦變成了遠程呼叫,門檻一下子就上去了,
首先你要會 Socket 編程,至少先要把咱們這門網路協議課學一下,然后再看 N 本磚頭厚的 Socket 程式設計的書,學會咱們學過的幾種 Socket 程式設計的模型,這就使得本來大學畢業就能干的一項作業,變成了一件五年作業經驗都不一定干好的作業,而且,搞定了 Socket 程式設計,才是萬里長征的第一步,后面還有很多問題呢!
如何解決這五個問題?
問題一:如何規定遠程呼叫的語法?
客戶端如何告訴服務端,我是一個加法,而另一個是乘法,我是用字串“add”傳給你,還是傳給你一個整數,比如 1 表示加法,2 表示乘法?服務端該如何告訴客戶端,我的這個加法,目前只能加整數,不能加小數,不能加字串;而另一個加法“add1”,它能實作小數和整數的混合加法,那回傳值是什么?正確的時候回傳什么,錯誤的時候又回傳什么?
問題二:如果傳遞引數?
我是先傳兩個整數,后傳一個運算子“add”,還是先傳運算子,再傳兩個整數?是不是像咱們資料結構里一樣,如果都是 UDP,想要實作一個逆波蘭運算式,放在一個報文里面還好,如果是 TCP,是一個流,在這個流里面,如何將兩次呼叫進行分界?什么時候是頭,什么時候是尾?把這次的引數和上次的引數混了起來,TCP 一端發送出去的資料,另外一端不一定能一下子全部讀取出來,所以,怎么才算讀完呢?
問題三:如何表示資料?
在這個簡單的例子中,傳遞的就是一個固定長度的 int 值,這種情況還好,如果是變長的型別,是一個結構體,甚至是一個類,應該怎么辦呢?如果是 int,不同的平臺上長度也不同,該怎么辦呢?
在網路上傳輸超過一個 Byte 的型別,還有大端 Big Endian 和小端 Little Endian 的問題,
假設我們要在 32 位四個 Byte 的一個空間存放整數 1,很顯然只要一個 Byte 放 1,其他三個 Byte 放 0 就可以了,那問題是,最后一個 Byte 放 1 呢,還是第一個 Byte 放 1 呢?或者說 1 作為最低位,應該是放在 32 位的最后一個位置呢,還是放在第一個位置呢?
最低位放在最后一個位置,叫作 Little Endian,最低位放在第一個位置,叫作 Big Endian,TCP/IP 協議堆疊是按照 Big Endian 來設計的,而 X86 機器多按照 Little Endian 來設計的,因而發出去的時候需要做一個轉換,
問題四:如何知道一個服務端都實作了哪些遠程呼叫?從哪個埠可以訪問這個遠程呼叫?
假設服務端實作了多個遠程呼叫,每個可能實作在不同的行程中,監聽的埠也不一樣,而且由于服務端都是自己實作的,不可能使用一個大家都公認的埠,而且有可能多個行程部署在一臺機器上,大家需要搶占埠,為了防止沖突,往往使用隨機埠,那客戶端如何找到這些監聽的埠呢?
問題五:發生了錯誤、重傳、丟包、性能等問題怎么辦?
本地呼叫沒有這個問題,但是一旦到網路上,這些問題都需要處理,因為網路是不可靠的,雖然在同一個連接中,我們還可通過 TCP 協議保證丟包、重傳的問題,但是如果服務器崩潰了又重啟,當前連接斷開了,TCP 就保證不了了,需要應用自己進行重新呼叫,重新傳輸會不會同樣的操作做兩遍,遠程呼叫性能會不會受影響呢?

協議約定問題
看到這么多問題,你是不是想起了我第一節講過的這張圖,

本地呼叫函式里有很多問題,比如詞法分析、語法分析、語意分析等等,這些編譯器本來都能幫你做了,但是在遠程呼叫中,這些問題你都需要重新操心,
很多公司的解決方法是,弄一個核心通信組,里面都是 Socket 編程的大牛,實作一個統一的庫,讓其他業務組的人來呼叫,業務的人不需要知道中間傳輸的細節,通信雙方的語法、語意、格式、埠、錯誤處理等,都需要呼叫方和被呼叫方開會協商,雙方達成一致,一旦有一方改變,要及時通知對方,否則通信就會有問題,
可是不是每一個公司都有這種大牛團隊,往往只有大公司才配得起,那有沒有已經實作好的框架可以使用呢?
當然有,一個大牛 Bruce Jay Nelson 寫了一篇論文Implementing Remote Procedure Calls,定義了 RPC 的呼叫標準,后面所有 RPC 框架,都是按照這個標準模式來的,

當客戶端的應用想發起一個遠程呼叫時,它實際是通過本地呼叫本地呼叫方的 Stub,它負責將呼叫的介面、方法和引數,通過約定的協議規范進行編碼,并通過本地的 RPCRuntime 進行傳輸,將呼叫網路包發送到服務器,
服務器端的 RPCRuntime 收到請求后,交給提供方 Stub 進行解碼,然后呼叫服務端的方法,服務端執行方法,回傳結果,提供方 Stub 將回傳結果編碼后,發送給客戶端,客戶端的 RPCRuntime 收到結果,發給呼叫方 Stub 解碼得到結果,回傳給客戶端,
這里面分了三個層次,對于用戶層和服務端,都像是本地呼叫一樣,專注于業務邏輯的處理就可以了,對于 Stub 層,處理雙方約定好的語法、語意、封裝、解封裝,對于 RPCRuntime,主要處理高性能的傳輸,以及網路的錯誤和例外,
最早的 RPC 的一種實作方式稱為 Sun RPC 或 ONC 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,
- 一個程式可以有多個方法,方法也有編號,如果找不到方法,就會回傳 PROC_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 編程就可以了,其實是很復雜的,要解決協議約定問題、傳輸問題和服務發現問題,
- 大牛 Bruce Jay Nelson 的論文、早期 ONC RPC 框架,以及 NFS 的實作,給出了解決這三大問題的示范性實作,也即協議約定要公用協議描述檔案,并通過這個檔案生成 Stub 程式;RPC 的傳輸一般需要一個狀態機,需要另外一個行程專門做服務發現,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/208632.html
標籤:其他
上一篇:程序(函式)引數傳遞告警
