gRPC-go原始碼(1):連線管理

紅雞菌發表於2021-01-24

1 寫在前面

在這個系列的文章中,我們將會從原始碼的層面學習和理解gRPC

整個系列的文章的計劃大概是這樣的:我們會先從客戶端開始,沿著呼叫路徑逐步分析到服務端,以模組為粒度進行學習,考慮這個模組是為了解決什麼問題,然後思考gRPC應該怎麼去解決這個問題。在分析完這部分的架構設計後,我們會在接下來的一篇文章中研究具體的程式碼實現。

因此,這個系列的文章不會像之前的原始碼分析那樣貼一大段的程式碼,然後加上註釋。這樣做不但使得閱讀成本很高,而且很難學到除了程式碼實現以外的東西。

我們會先從客戶端開始,沿著呼叫路徑,逐步分析到服務端。

2 什麼是RPC

在閱讀gRPC的原始碼之前,我們先思考實現一個RPC框架,應該提供什麼樣的功能?

在我們上一篇文章的內容中,我們已經知道了gRPC的使用方式。簡單的來講,就是對於同一個方法,在服務端實現具體的邏輯,在客戶端發起呼叫,就能夠實現“遠端過程呼叫”。

那麼,我們要怎麼實現這個過程呢?

那麼我們很容易可以推測,無論是客戶端還是服務端,在我們呼叫的方法背後肯定還封裝了一套複雜的邏輯,負責把客戶端的呼叫“傳送”到服務端中,而服務端中也封裝了一套複雜的邏輯,負責接收客戶端傳送過來的請求,並根據接收到的資料選擇對應的方法,執行完後把結果“返回”給客戶端。

於是我們會接著推測,這部分複雜的邏輯有什麼呢?

我們以客戶度為例:首先,我們需要跟服務端建立連線。當我們呼叫某個遠端方法的時候,我們需要令服務端得知客戶端呼叫的是哪個方法、有哪些引數等,這意味著我們需要設計一種協議,這個協議承載了以上的資訊。最後,把我們的資料塞進這個協議中,編碼成二進位制的格式,塞進網路中。

而對於服務端來說也是一樣的,從網路IO中接收到二進位制的資料之後需要進行解碼,然後根據解碼後的資料得知需要呼叫的方法名、引數,在執行完相應的方法後將結果傳送回客戶端中。

這樣就足夠了嗎?

還不夠,我們還需要通過一種方式,將以上的邏輯封裝起來,避免每次呼叫的時候都寫這麼一大堆的重複程式碼。也就是說,我們的開發人員不需要知道底層呼叫細節,他只需要定義方法和呼叫方法,剩下的都交給框架。

至此,我們就實現了一個最基本RPC框架。

但是你可能會有一個問題,如果RPC框架只是提供了一個通訊的功能,那麼他存在的意義是什麼呢?

如果只是為了解決通訊的問題,我們不需要費盡心思來開發這麼一個新的框架,我們可以用RESTful API,甚至你也可以直接把資料塞進TCP報文中。

答案是這樣的,雖然我們稱RPC為遠端過程呼叫,但是RPC框架不僅僅是能夠實現服務間的通訊,它還提供了一些服務治理、負載均衡、流量控制等方面的功能。

因此,當我們談到了RPC框架這個話題的時候,通常我們說是提供了以遠端過程呼叫為核心的一整套解決方案。

3 如何實現gRPC

上一節中,我們聊了聊一個RPC框架應該提供哪些功能。在這一節中,我們來聊聊gRPC實現了哪些功能。

3.1 連線管理

為了讓連線變得更可靠和高效,gRPC需要對連線進行管理。

考慮這樣的一種情景,由於公司規模的擴大、流量的增加,gRPC的服務端由單機擴充套件成了一個叢集。這個時候,我們的客戶端需要呼叫服務端中的某一個方法,那麼這個客戶端需要向哪臺機器建立連線,傳送資料呢?

如果我們把這個問題劃分的更具體,那麼可以需要解決的問題如下:

  • 假設現在這個叢集裡面有很多臺機器,那麼我們該怎麼告知客戶端每臺服務端機器的ip:port呢?

  • 假設我們新增或減少了一些gRPC的服務端,客戶端該怎麼更新它所維護的ip:port列表呢?

  • 假設客戶端當前請求的服務端,存在了多個ip:port,那麼這個客戶端該向哪個連線傳送資料呢?

這幾個問題可以歸結為,gRPC如何解決服務註冊服務發現負載均衡的問題。

然而,gRPC並沒有提供諸如Spring CloudDubbo等框架的服務註冊、服務發現的功能。

我想gRPC這麼做的原因大概是為了能夠提供更靈活的服務發現和負載均衡功能。

3.2 Resolver

Resolver稱為解析器,能夠將客戶端傳入的“符合某種規則的名稱”解析為IP地址列表。

假設你定義了一種地址格式:aaa:///bbb-project/ccc-srv

然後Resolver會將這個地址解析成好幾個ip:port,代表了提供ccc-srv服務叢集的所有機器地址。

這就是Resolver的作用。

那麼,Resolver是怎麼進行解析的呢?換句話說,Resolver是如何做到輸入某種地址,輸出一串IP地址呢?

這部分的工作需要由使用者自己實現。

gRPC提供的是外掛式Resolver功能,他會根據使用者傳入的aaa:///bbb-project/ccc-srv,選擇一個能夠解析aaaResolver,並進行解析,得到ip:port列表。

3.3 Balancer

Balancer稱為負載均衡器,負責在Resolver解析出的一串地址中,選擇其中的一個建立連線。

至於如何選擇,也是由使用者自己編寫LB的邏輯。

也就是說,gRPC實現了基礎的邏輯,但是也提供了很強大的外掛式程式設計的能力,將很多操作都留給開發人員自己去做選擇。

不過,很大的靈活度對應的是很複雜的程式碼結構,直接去看原始碼可能會讓人摸不著頭腦。所以在這一篇的文章中我們先來介紹整體的一個設計邏輯,在下一篇文章中我們再來聊具體的細節。

3.4 Wrapper

我們在上面聊到,gRPCResolverBalancer都是支援自定義的。我們可以自己定義各種不同的ResolverBalancer,來應對不同場景的需求。

這麼做雖然增加了程式碼複雜度,但是卻能夠讓gRPC變得更靈活,能夠對各種複雜情景提供支援。

那麼,要怎麼才能夠實現外掛式的程式設計呢?

答案是使用裝飾器模式

裝飾模式(Decorator)也叫包裝器模式(Wrapper)。GOF在《設計模式》一書中給出的定義為:動態地給一個物件新增一些額外的職責

裝飾器模式是指動態地給一個物件新增一些額外的職責,就增加功能來說裝飾模式比生成子類更為靈活。它通過建立一個包裝物件,也就是裝飾來包裹真實的物件。

這麼說可能有點抽象,我們直接上圖:

首先建立一個resolver介面,並設計一些具體的resolver實現類:

然後我們還需要一個resolver的包裝器,裡面包含了真正的resolver

當我們的gRPC需要呼叫ResolverNow方法的時候,他只需要呼叫resolverWrapper中的Resolve()方法,在這個方法中來呼叫真正的resoleNow()邏輯:

只要理解了這個設計模式,gRPC Client端建立連線的程式碼,你就能看懂一大半了。

至此,gRPC對連線的管理就結束了。

4 總結

最後我們再梳理一遍gRPC是如何管理連線的。

在第一次建立連線時,gRPC會呼叫服務端地址相對應的Resolver,來解析出所有能夠提供服務的服務端地址。隨後,經過指定的Balancer,選擇其中的一個地址,建立連線。

如果是已經建立過連線,在Resolver中存在一個協程,監聽了服務的狀態,當存在新上線或下線的服務,會重新進行地址解析,來獲取新的服務端地址集合,隨後通過Balancer來選擇一個地址,建立連線。

在這篇文章的鋪墊下,閱讀具體的實現程式碼可能就會比較容易了。對於這部分的程式碼,我們會在下一篇文章中進行分析。

而對於連線的建立,邏輯也比較複雜,我們在後面的文章中繼續分析。

寫在最後

首先,謝謝你能看到這裡!

這篇文章帶有了比較強的主觀判斷,因為作者才疏學淺,對於gRPC的設計思路可能存在了誤會。如果你覺得有哪裡是我說的不對的,還請不吝賜教,謝謝你!

在下一篇文章中,我也會盡可能的把程式碼也梳理的比較簡單一些,敬請期待。

最後,如果有任何問題,都可以留言或者在公眾號“紅雞菌”中找到我。

再次感謝你的閱讀:)

相關文章