rpc的正確開啟方式|讀懂Go原生net/rpc包

白澤來了 發表於 2022-06-11
Go

前言

最近在閱讀位元組跳動開源RPC框架Kitex的原始碼,分析了如何藉助命令列,由一個IDL檔案,生成clientserver的腳手架程式碼,也分析了Kitex的日誌元件klog。當然Kitex還有許多其他元件:服務註冊、發現、負載均衡、熔斷、限流等等,後續我也會繼續分析。

我希望藉助這篇文章,用盡可能少的語言,配合分析Go原生net/rpc包的部分核心程式碼,幫助你貫通RPC的知識,梳理RPC的運作流程,讓你對RPC有一個比較全面的認識。

以此為基礎,將有助於你在閱讀其他開源RPC框架原始碼時,對比發掘開源RPC框架具體做了哪些提高。

RPC的流程

遠端過程呼叫 (Remote Procedure Call,RPC) 是一種計算機通訊協議。允許執行在一臺計算機的程式呼叫另一個地址空間的子程式(一般是開放網路中的一臺計算機),而程式設計師就像呼叫呼叫本地程式一樣,無需額外做互動程式設計。

rpc的正確開啟方式|讀懂Go原生net/rpc包

假設你要呼叫一個Add(a int, b int) int方法,實現求和功能,但是這個方法部署在另一臺機器上,該如何呼叫?

rpc的正確開啟方式|讀懂Go原生net/rpc包

這就是一次RPC的流程,甚至和HTTP請求/響應流程很像,眼下我先側重於介紹RPC的概念,以後會介紹其與HTTP的區別。

並且這裡暫時沒有涉及所謂的服務註冊、發現、負載均衡、熔斷、限流等字眼,這些都是一個成熟的RPC框架應該具備的功能元件,用於確保一個RPC框架的高可用,但是卻不是一個RPC框架所必需的。

RPC協議本質上定義了一種通訊的流程,而具體的實現技術是沒有約束的,每一種RPC框架都有自己的實現方式,比如你可以規定自己的RPC請求/響應包含訊息頭和訊息體,使用gob/json/pb/thrift來序列化/反序列化訊息內容,使用socket/http2進行網路通訊,只要clientserver訊息的傳送和解析能對應即可。希望讀者仔細體會——“約定”這個概念,這將貫穿始終。

分析net/rpc

先講解一下流程圖中的序列化和網路傳輸部分,這是RPC的核心。

訊息編碼/解碼(序列化)

rpc的正確開啟方式|讀懂Go原生net/rpc包

上面的RPC通訊流程圖,其中很重要的一環就是訊息的編解碼,訊息只有序列化之後,才能高效地參與網路傳輸。通過實現上圖net/rpc包定義的介面,可以指定使用的編解碼方式,比如net/rpc包預設使用了gob二進位制編碼:

rpc的正確開啟方式|讀懂Go原生net/rpc包

服務端負責序列化的結構gobServerCodec的實現了ServerCodec介面,服務端需要編解碼訊息的地方,都會呼叫gobServerCodec的對應方法(客戶端也是類似的實現,也是一樣使用gob編解碼)。

訊息的網路傳輸

訊息序列化之後,是需要用於網路傳輸的,涉及到客戶端與服務端的通訊方式。

rpc的正確開啟方式|讀懂Go原生net/rpc包

這是服務端的接受連結的邏輯,和大部分網路應用相同,server監聽了一個ip:port

,然後accept一個連線之後,會開啟一個go協程處理請求與響應。

rpc的正確開啟方式|讀懂Go原生net/rpc包

這是客戶端發起請求的方式,也印證了socket網路程式設計的通訊模型。

理解了RPC的各個流程之後,就能梳理清楚RPC框架的各種元件是作用在哪個層面的,例如Kitex的網路庫netpoll,雖然我未曾看過其原始碼實現,但是有理由猜測其是在網路通訊/傳輸部分做了提高。

Server端的設計

rpc的正確開啟方式|讀懂Go原生net/rpc包

這是service的結構,可以看到一個服務通過Map可以繫結多個名稱的方法,提供呼叫,且對應service需要提前註冊到服務端,這樣在客戶端請求達到時才能準確呼叫。

rpc的正確開啟方式|讀懂Go原生net/rpc包

服務註冊主要引數是serviceNameservice實體。

  • reflect.xxx():主要的工作就是通過反射的機制,解析所繫結的服務的名稱、型別等。
  • suitableMethods():解析一個service繫結的所有method
  • serviceMap.LoadOrStore():將service註冊到服務端serverMap,如下是Server的結構:
rpc的正確開啟方式|讀懂Go原生net/rpc包

Client端的設計

rpc的正確開啟方式|讀懂Go原生net/rpc包

這是Client的結構:

  • codec:編解碼的具體實現。
  • seqRPC的序列號,每發起一個就計數增加,加入Map,且完成或失敗後從Map中移除。
  • pending:配合seq工作的Map
rpc的正確開啟方式|讀懂Go原生net/rpc包

這是客戶端具體發起一次RPC請求的過程,當然一次具體的RPC請求可以是同步的,也可以是非同步的:

rpc的正確開啟方式|讀懂Go原生net/rpc包
  • client.Go()是非同步的。
  • client.Call()是同步的,且其內部就是呼叫了client.Go(),但是因為其呼叫之後,在呼叫完成之前,會被阻塞在chan上,因此後續的RPC請求必須等待傳送。

小結

到此為止我們粗淺的分析了net/rpc的一些核心原始碼,藉此梳理了RPC的工作流程,主要包括:

  • RPC的編解碼(序列化)協議選擇
  • RPC的網路通訊/傳輸模型(Socket程式設計
  • RPC的請求發起/響應接受(同步/非同步)

RPC的功能元件

rpc的正確開啟方式|讀懂Go原生net/rpc包

一個成熟的RPC框架只實現基本的通訊功能是不夠的,否則它將十分的脆弱,沒有任何應對服務當機的能力,在高併發場景下也難堪重任,因此需要增加很多的功能元件來提高服務的可靠性:

  • 超時控制|請求重試|負載均衡|熔斷器|限流器|日誌|監控|鏈路追蹤|...

Go原生net/rpc包也有很多提高可靠性的設計,本文沒有過多展開)

結束語

這篇文章,我藉助Go原生net/rpc包的部分核心原始碼,梳理了RPC的工作流程,試圖幫助你建立RPC的全域性觀念,希望你明白,RPC框架是對RPC通訊流程的具體實現,每一個框架為提高自身的可靠性,又延伸出了多種功能元件。

後續的文章我也將繼續分析位元組跳動開源RPC框架Kitex的核心元件原始碼,共勉。

關注公眾號【程式設計師白澤】,我會同步分享部落格文章。