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 Cloud`、`Dubbo`等框架的服務註冊、服務發現的功能。**
我想`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`,選擇一個能夠解析`aaa`的`Resolver`,並進行解析,得到`ip:port`列表。
### 3.3 Balancer
**Balancer稱為負載均衡器,負責在Resolver解析出的一串地址中,選擇其中的一個建立連線。**
至於如何選擇,也是由使用者**自己編寫**LB的邏輯。
也就是說,`gRPC`實現了基礎的邏輯,但是也提供了很強大的外掛式程式設計的能力,將很多操作都留給開發人員自己去做選擇。
不過,很大的靈活度對應的是很複雜的程式碼結構,直接去看原始碼可能會讓人摸不著頭腦。所以在這一篇的文章中我們先來介紹整體的一個設計邏輯,在下一篇文章中我們再來聊具體的細節。
### 3.4 Wrapper
我們在上面聊到,`gRPC`的`Resolver`和`Balancer`都是支援自定義的。我們可以自己定義各種不同的`Resolver`和`Balancer`,來應對不同場景的需求。
這麼做雖然增加了程式碼複雜度,但是卻能夠讓gRPC變得更靈活,能夠對各種複雜情景提供支援。
**那麼,要怎麼才能夠實現外掛式的程式設計呢?**
答案是使用**裝飾器模式**。
裝飾模式(Decorator)也叫包裝器模式(Wrapper)。GOF在《設計模式》一書中給出的定義為:**動態地給一個物件新增一些額外的職責**。
裝飾器模式是指動態地給一個物件新增一些額外的職責,就增加功能來說裝飾模式比生成子類更為靈活。它通過建立一個包裝物件,也就是裝飾來包裹真實的物件。
這麼說可能有點抽象,我們直接上圖:
首先建立一個`resolver`介面,並設計一些具體的`resolver`實現類:
![](https://img2020.cnblogs.com/blog/1998080/202101/1998080-20210124165951391-1257785403.png)
然後我們還需要一個`resolver`的包裝器,裡面包含了真正的`resolver`。
當我們的gRPC需要呼叫ResolverNow方法的時候,他只需要呼叫`resolverWrapper`中的`Resolve()`方法,在這個方法中來呼叫真正的`resoleNow()`邏輯:
![](https://img2020.cnblogs.com/blog/1998080/202101/1998080-20210124170002834-1920577289.png)
只要理解了這個設計模式,`gRPC Client`端建立連線的程式碼,你就能看懂一大半了。
至此,`gRPC`對連線的管理就結束了。
## 4 總結
最後我們再梳理一遍`gRPC`是如何管理連線的。
在第一次建立連線時,`gRPC`會呼叫服務端地址相對應的`Resolver`,來解析出所有能夠提供服務的服務端地址。隨後,經過指定的Balancer,選擇其中的一個地址,建立連線。
如果是已經建立過連線,在`Resolver`中存在一個協程,監聽了服務的狀態,當存在新上線或下線的服務,會重新進行地址解析,來獲取新的服務端地址集合,隨後通過`Balancer`來選擇一個地址,建立連線。
在這篇文章的鋪墊下,閱讀具體的實現程式碼可能就會比較容易了。對於這部分的程式碼,我們會在下一篇文章中進行分析。
而對於連線的建立,邏輯也比較複雜,我們在後面的文章中繼續分析。
## 寫在最後
首先,謝謝你能看到這裡!
這篇文章帶有了比較強的主觀判斷,因為作者才疏學淺,對於`gRPC`的設計思路可能存在了誤會。如果你覺得有哪裡是我說的不對的,還請不吝賜教,謝謝你!
在下一篇文章中,我也會盡可能的把程式碼也梳理的比較簡單一些,敬請期待。
最後,如果有任何問題,都可以留言或者在公眾號“紅雞菌”中找到我。
再次感謝你的