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

前言

背景

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

微服務框架和RPC框架git

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

調研

在動手開始作以前,須要先了解學習一下其餘現有的產品,能夠從中學習一下優秀的經驗和方法,這裏列舉一下初步瞭解到的幾個框架:apache

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

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

Pluggable Interfacesbash

值得一提的是除了thrift,其餘三個稱得上微服務框架的產品,其特性都包含Pluggable Interfaces,也就是能夠經過插件替換部分功能。經過插件實現可替換的功能,實際上在一個微服務框架中基本是最低要求了,不然後續的功能擴展將會變得十分困難,相信我,這裏是飽含血淚的經驗之談。網絡

需求分析

在開始着手設計甚至是編寫代碼之前,咱們首先分析一下咱們的需求(來自學習軟件工程中的成果)。同時對於一部分可能不太熟悉RPC相關細節的同窗來講,對咱們後面要作的事情心中也可以有一個大體的概念。這裏就直接列舉幾個功能性需求:負載均衡

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

系統設計

分層

有了大體的需求,接下來就能夠開始着手設計了。首先咱們將框架劃分爲若干層,層與層之間約定經過接口交互。這裏就不要問爲何須要分層了,非要問就是經驗。分層做爲一種經典到不能在經典的設計模式,幾乎在軟件開發過程當中無處不在,在RPC框架當中也十分適用,下面畫出大體的層次圖:框架

  • service 是面向用戶的接口,好比客戶端和服務端實例的初始化和運行等等
  • client和server表示客戶端和服務端的實例,它們負責發出請求和返回響應
  • selector 表示負載均衡,或者叫作loadbanlancer,它負責決定具體要向哪一個server發出請求
  • registery 表示註冊中心,server在初始化完畢甚至是運行時都要向註冊中心註冊自身的相關信息,這樣client才能從註冊中心查找到須要的server
  • codec 表示編解碼,也就是將對象和二進制數據互相轉換
  • protocol 表示通訊協議,也就是二進制數據是如何組成的,RPC框架中不少功能都須要協議層的支持
  • transport 表示通信,它負責具體的網絡通信,將按照protocol組裝好的二進制數據經過網絡發送出去,並根據protocol指定的方式從網絡讀取數據

上面提到的各個層,除了service,實際上能夠提供多種實現,因此應該都以plugin的方式實現。異步

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

服務端的邏輯比較相似,這裏就不畫圖了。

過濾器鏈

經過上面的層次劃分能夠看到,一個請求或者響應實際上會依次穿過各個層而後經過網絡發送或者到達用戶邏輯,因此咱們採用相似過濾器鏈同樣的方式處理請求和響應,以此來達到對擴展開放,對修改關閉的效果。這樣一來對於一些附加功能好比熔斷降級和限流、身份認證等功能均可以在過濾器中實現。

消息協議

接下來設計具體的消息協議,所謂消息協議大概就是兩臺計算機爲了互相通訊而作的約定。舉個例子,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 //其餘元數據
}

複製代碼

結語

第一篇文章就到此爲止了,主要先作一下準備,整理一下思路,若是有不正確或者不合理的部分還請你們多多指教。

相關文章
相關標籤/搜索