從零開始實現一個RPC框架(零)

熊紀元發表於2019-03-03

前言

背景

最近決心開始學習go語言,但是苦於沒有實際的應用場景,學習始終停留在hello world層面,看過的教程和資料印象也不深刻。於是決定從go自帶的rpc實現開始切入,瞭解一下go語言在實際場景下是如何使用的,包括異常處理、代理和過濾、go routine的用法等等,同時也簡單瞭解了一下其他rpc的go語言實現,比如thrift和grpc等等。一陣走馬觀花,稍微加深了印象,也開始慢慢體會到go語言和java語言的種種差異和共性。接下來,為了進一步鞏固學習效果,也算是為了對自己目前為止的職業生涯做一次複習和彙報,決定使用go語言從零開始構建一個比較完整的RPC(或者說是微服務)框架。

微服務框架和RPC框架

本文中提到RPC框架,指的是提供基礎的RPC呼叫支援的框架;而本文中提到的微服務框架,指的是包含一些服務治理相關的功能(比如服務註冊發現、負載均衡、鏈路追蹤等)的RPC框架。

調研

在動手開始做之前,需要先了解學習一下其他現有的產品,可以從中學習一下優秀的經驗和方法,這裡列舉一下初步瞭解到的幾個框架:

  • grpc google推出的微服務框架,支援10種語言,支援基於http2的雙向的流式通訊
  • go-micro 一個開源的微服務框架,比較獨特的是支援Async Messaging,像是mq一樣的subpub功能
  • thrift-go thrift是facebook捐獻給apache的rpc框架(不包含服務治理相關的功能),根據官方文件,thrift支援20種語言的RPC呼叫
  • rpcx rpcx是一個國人開發並開源的微服務框架,宣傳的特性是“快、易用卻功能強大”,官網上的介紹提到效能是grpc的兩倍。這裡附上作者(應該是)的部落格

以上就是目前瞭解過的幾個已有的框架,比較慚愧的是瞭解得都不夠深入,後續還要持續學習。

Pluggable Interfaces

值得一提的是除了thrift,其他三個稱得上微服務框架的產品,其特性都包含Pluggable Interfaces,也就是可以通過外掛替換部分功能。通過外掛實現可替換的功能,實際上在一個微服務框架中基本是最低要求了,否則後續的功能擴充套件將會變得十分困難,相信我,這裡是飽含血淚的經驗之談。

需求分析

在開始著手設計甚至是編寫程式碼以前,我們首先分析一下我們的需求(來自學習軟體工程中的成果)。同時對於一部分可能不太熟悉RPC相關細節的同學來說,對我們後面要做的事情心中也能夠有一個大致的概念。這裡就直接列舉幾個功能性需求:

  • 支援RPC呼叫,包括同步呼叫和非同步呼叫
  • 支援服務治理的相關功能,包括:
    • 服務註冊與發現
    • 服務負載均衡
    • 限流和熔斷
    • 身份認證
    • 監控和鏈路追蹤
    • 健康檢查,包括端到端的心跳以及註冊中心對服務例項的檢查
  • 支援外掛,對於有多種實現的功能(比如負載均衡),需要以外掛的形式提供實現,同時需要支援自定義外掛 至於非功能性需求比如效能要好,要夠穩定這類的暫時不重點關注。

系統設計

分層

有了大致的需求,接下來就可以開始著手設計了。首先我們將框架劃分為若干層,層與層之間約定通過介面互動。這裡就不要問為什麼需要分層了,非要問就是經驗。分層作為一種經典到不能在經典的設計模式,幾乎在軟體開發過程中無處不在,在RPC框架當中也十分適用,下面畫出大致的層次圖:

從零開始實現一個RPC框架(零)

  • service 是面向使用者的介面,比如客戶端和服務端例項的初始化和執行等等
  • client和server表示客戶端和服務端的例項,它們負責發出請求和返回響應
  • selector 表示負載均衡,或者叫做loadbanlancer,它負責決定具體要向哪個server發出請求
  • registery 表示註冊中心,server在初始化完畢甚至是執行時都要向註冊中心註冊自身的相關資訊,這樣client才能從註冊中心查詢到需要的server
  • codec 表示編解碼,也就是將物件和二進位制資料互相轉換
  • protocol 表示通訊協議,也就是二進位制資料是如何組成的,RPC框架中很多功能都需要協議層的支援
  • transport 表示通訊,它負責具體的網路通訊,將按照protocol組裝好的二進位制資料通過網路傳送出去,並根據protocol指定的方式從網路讀取資料

上面提到的各個層,除了service,實際上可以提供多種實現,所以應該都以plugin的方式實現。

這樣一來按照我們劃分的層次,一個客戶端從發出請求到收到響應的流程大概就是這樣:

從零開始實現一個RPC框架(零)
服務端的邏輯比較類似,這裡就不畫圖了。

過濾器鏈

通過上面的層次劃分可以看到,一個請求或者響應實際上會依次穿過各個層然後通過網路傳送或者到達使用者邏輯,所以我們採用類似過濾器鏈一樣的方式處理請求和響應,以此來達到對擴充套件開放,對修改關閉的效果。這樣一來對於一些附加功能比如熔斷降級和限流、身份認證等功能都可以在過濾器中實現。

訊息協議

接下來設計具體的訊息協議,所謂訊息協議大概就是兩臺計算機為了互相通訊而做的約定。舉個例子,TCP協議約定了一個TCP資料包的具體格式,比如前2個byte表示源埠,第3和第4個byte表示目標埠,接下來是序號和確認序號等等。而在我們的RPC框架中,也需要定義自己的協議。一般來說,網路協議都分為head和body部分,head是一些後設資料,是協議自身需要的資料,body則是上一層傳遞來的資料,只需要原封不動的接著傳遞下去就是了。

接下來我們就試著定義自己的協議:

-------------------------------------------------------------------------------------------------
|2byte|1byte  |4byte       |4byte        | header length |(total length - header length - 4byte)|
-------------------------------------------------------------------------------------------------
|magic|version|total length|header length|     header    |                    body              |
-------------------------------------------------------------------------------------------------
複製程式碼

根據上面的協議,一個訊息體由以下幾個部分嚴格按照順序組成:

  • 兩個byte的magic number開頭,這樣一來我們就可以快速的識別出非法的請求
  • 一個byte表示協議的版本,目前可以一律設定為0
  • 4個byte表示訊息體剩餘部分的總長度(total length)
  • 4個byte表示訊息頭的長度(header length)
  • 訊息頭(header),其長度根據前面解析出的長度(header length)決定
  • 訊息體(body),其長度為前面解析出的總長度減去訊息頭所佔的長度(total length - 4 - header length)

協議中訊息頭的資料主要是RPC呼叫過程中的後設資料,後設資料跟方法引數和響應無關,主要記錄額外的資訊以及實現附屬功能比如鏈路追蹤、身份認證等等;訊息體的資料則是由實際的請求引數或者響應編碼而來。 在實際的處理中,訊息頭在傳送端通常是一個結構體,在傳送時會被編碼成二進位制新增在訊息頭的前面,在接收端接收時又解碼成一個結構體,交給程式進行處理。這裡試著列舉訊息頭包含的各個資訊:

type Header struct {
        Seq uint64 //序號, 用來唯一標識請求或響應
        MessageType byte //訊息型別,用來標識一個訊息是請求還是響應
        CompressType byte //壓縮型別,用來標識一個訊息的壓縮方式
        SerializeType byte //序列化型別,用來標識訊息體採用的編碼方式
        StatusCode byte //狀態型別,用來標識一個請求是正常還是異常
        ServiceName string //服務名
        MethodName string  //方法名
        Error string //方法呼叫發生的異常
        MetaData map[string]string //其他後設資料
}

複製程式碼

結語

第一篇文章就到此為止了,主要先做一下準備,整理一下思路,如果有不正確或者不合理的部分還請大家多多指教。

相關文章