golang如何使用原生RPC及微服務簡述

小魔童哪吒 發表於 2021-05-12
微服務 Go
[TOC]

微服務

1. 微服務是什麼

  • 使用一套小服務來開發單個應用的方式,每個服務執行在獨立的程式裡,一般採用輕量級的通訊機制互聯,並且它們可以通過自動化的方式部署

微服務是設計思想,不是量的體現

  • 專一的功能
  • 程式碼量並不少
  • 架構變複雜

2. 特點是啥

  • 專一的職責,例如專注於許可權管理
  • 輕量級的通訊,通訊與平臺和語言無關,例如http是輕量的
  • 隔離性,資料隔離
  • 有自己的資料
  • 技術多樣

golang如何使用原生RPC及微服務簡述

3. 微服務架構的優勢

  • 獨立性
  • 使用者容易理解
  • 技術棧靈活
  • 高效團隊

4. 微服務架構的不足

  • 額外的工作,服務的拆分
  • 保證資料一致性
  • 增加了溝通成本

微服務生態

1. 硬體層

  • docker+k8s去解決

2. 通訊層

  • 網路傳輸,用RPC(遠端過程呼叫)

    • HTTP傳輸,GET POST PUT DELETE
    • 基於TCP,更靠底層,RPC基於TCP,Dubbo(18年底改成支援各種語言),Grpc,Thrift
  • 需要知道呼叫誰,用服務註冊和發現

    • 需要分散式資料同步:etcd,consul,zk
  • 資料傳遞這裡面可能是各種語言,各種技術,各種傳遞

資料傳輸協議選型建議

1、對於公司間的系統呼叫,如果效能要求在100ms以上的服務,基於XML的SOAP協議是一個值得考慮的方案。

2、對於除錯環境比較惡劣的場景,採用JSON或XML能夠極大的提高除錯效率,降低系統開發成本。

3、當對效能和簡潔性有極高要求的場景,Protobuf,Thrift,Avro之間具有一定的競爭關係。

4、對於T級別的資料的持久化應用場景,Protobuf和Avro是首要選擇。如果持久化後的資料儲存在Hadoop子專案裡,Avro會是更好的選擇。

5、如果需要提供一個完整的RPC解決方案,Thrift是一個好的選擇

6、如果序列化之後需要支援不同的傳輸層協議,或者需要跨防火牆訪問的高效能場景,Protobuf可以優先考慮。

RPC 機制和實現過程

1. RPC機制

服務間通過輕量級的遠端過程呼叫,一般使用HTTP,RPC

  • HTTP呼叫應用層協議,結構相對固定
  • RPC的網路協議就相對靈活,並且可以定製

RPC遠端過程呼叫,一般採用C/S 模式,客戶端伺服器模式,客戶端程式,呼叫服務端程式的程式,服務端程式執行結果返回給客戶端,客戶端從阻塞狀態被喚醒,接收資料,提取資料。

上述過程中,客戶端呼叫伺服器的函式,來執行任務,它不知道操作是在本地作業系統進行,還是通過遠端過程呼叫進行的,全程無感

RPC的基本通訊如下:

golang如何使用原生RPC及微服務簡述

RPC遠端過程呼叫,需要考慮的問題有如下四點

  • 引數傳遞
  • 通訊協議機制
  • 出錯處理
  • 超時處理

2. 引數傳遞

  • 值傳遞

一般預設是值傳遞,只需要將引數中的值複製到網路訊息中的資料中即可

  • 引用傳遞

比較困難,單純傳遞引數的引用是完全沒有用意義的,因為引用的地址給到遠端的伺服器,伺服器上的該記憶體地址完全不是客戶端想要的資料,若非要這樣處理,客戶端還必須把資料的副本傳遞給到遠端伺服器,並將它們放到遠端伺服器記憶體中,伺服器複製引用的地址後,即可進行資料的讀取。

可是上述做法很麻煩,且很容易出錯,一般RPC不支援直接傳遞引用

  • 資料格式統一問題

需要有一個標準來對所有資料型別進行編解碼 ,資料格式可以有隱式型別顯式型別

  • 隱式型別

只傳遞值,不傳遞變數的名稱或 型別

  • 顯式型別

傳遞欄位的型別和值

常見的傳輸資料格式有:

  • ISO標準的ASN.1
  • JSON
  • PROTOBUF
  • XML

3. 通訊協議機制

廣義上的協議棧分為共有協議私有協議

  • 共有協議

例如HTTP,SMPP,WEBSERVICE都是共有協議,擁有通用型上,公網傳輸的能力上 有優勢

  • 私有協議

內部約定而成的協議,弊端多,但是可以高度的定製化,提升效能,降低成本,提高靈活性和效率。企業內部往往採用私有協議開發

對於協議的制定需要考慮如下5個方面:

  • 協議設計

需要考慮哪些問題

golang如何使用原生RPC及微服務簡述

  • 私有協議的編解碼

需要有業務針對性的編解碼方式方法,如下有案例

  • 命令的定義和命令處理器的選擇

協議的過程一般會有2種

  1. 負載命令

傳輸業務具體的資料,如請求引數,響應結果的命令

  1. 控制命令

一般為功能管理命令,如心跳命令等

  • 命令的協議

一般是使用序列化協議,不同的協議在編碼效率和傳輸效率上都不相同,如

golang如何使用原生RPC及微服務簡述

  • 通訊模式
  1. oneway – 不關心響應,請求執行緒不會被阻塞
  2. sync – 呼叫會被阻塞,知道返回結果為止
  3. future – 呼叫時不會阻塞縣執行緒,獲取結果的時候會阻塞執行緒
  4. callback – 非同步呼叫,不會阻塞執行緒

出錯處理和超時處理

遠端過程呼叫相對本地過程呼叫出錯的概率更大,因此需要考慮到呼叫失敗的各種場景:

  • 服務端出錯,需要如何處理
  • 客戶端請求服務時候出現錯誤或者超時,需要設定合適的重試機制

golang如何使用原生RPC及微服務簡述

4. 簡易GO語言原生RPC

大概分為如下4個步驟:

  • 設計資料結構和方法

  • 實現方法

  • 註冊服務

  • 客戶端連線服務端,呼叫服務端的方法

    往下看有golang如何使用原生rpc的案例

rpc呼叫和服務監控

  • RPC相關內容
    • 資料傳輸:JSON Protobuf thrift
    • 負載:隨機演算法 輪詢 一致性hash 加權
    • 異常容錯:健康檢測 熔斷 限流
  • 服務監控
    • 日誌收集
    • 打點取樣

1. RPC簡介

  • 遠端過程呼叫(Remote Procedure Call,RPC)是一個計算機通訊協議
  • 該協議允許執行於一臺計算機的程式呼叫另一臺計算機的子程式,而程式設計師無需額外地為這個互動作用程式設計
  • 如果涉及的軟體採用物件導向程式設計,那麼遠端過程呼叫亦可稱作遠端呼叫或遠端方法呼叫

2. RPC呼叫流程

一般情況下,我們會將功能程式碼在本地直接呼叫,微服務架構下,我們需要將這個函式作為單獨的服務執行,客戶端通過網路呼叫

  • 微服務架構下資料互動一般是對內 RPC,對外 REST
  • 將業務按功能模組拆分到各個微服務,具有如下優點
    • 提高專案協作效率
    • 降低模組耦合度
    • 提高系統可用性
  • 有如下缺點:
    • 開發門檻比較高,比如 RPC 框架的使用、後期的服務監控等工作

3.rpc golang 原生處理方式

最簡單的golang原生rpc的使用

golang官方的net/rpc庫使用encoding/gob進行編解碼,支援tcp和http資料傳輸方式

server1.go

package main

import (
   "log"
   "net/http"
   "net/rpc"
)

type Happy struct{}

// 計算happy
func (r *Happy) CalHappy(num int, ret *int) error {
   *ret = num * 10
   return nil
}

// 主函式
func main() {
   // new一個服務
   ha := new(Happy)
   // 註冊一個Happy的服務
   rpc.Register(ha)
   // 服務處理繫結到http協議上
   rpc.HandleHTTP()
   // 監聽服務
   err := http.ListenAndServe(":9999", nil)
   if err != nil {
      log.Panicln(err)
   }
}

client1.go

package main

import (
   "fmt"
   "log"
   "net/rpc"
)

// 主函式
func main() {
   //連線遠端rpc服務
   conn, err := rpc.DialHTTP("tcp", ":9999")
   if err != nil {
      log.Fatal(err)
   }
   // 呼叫伺服器方法
   ret := 0
   err2 := conn.Call("Happy.CalHappy", 10, &ret)
   if err2 != nil {
      log.Fatal(err2)
   }
   fmt.Println("開心指數:", ret)
}

結果

golang如何使用原生RPC及微服務簡述

golang使用jsonrpc

jsonrpc採用JSON進行資料編解碼,支援跨語言呼叫,jsonrpc庫是基於tcp協議實現的,暫不支援http傳輸方式

server2.go

package main

import (
   "fmt"
   "log"
   "net"
   "net/rpc"
   "net/rpc/jsonrpc"
)

type Happy struct{}

// 計算happy
func (r *Happy) CalHappy(num int, ret *int) error {
   *ret = num * 10
   return nil
}

// 主函式
func main() {
   // new一個服務
   ha := new(Happy)
   // 註冊一個Happy的服務
   rpc.Register(ha)
   // 監聽服務
   listen, err := net.Listen("tcp", ":9999")
   if err != nil {
      log.Panicln(err)
   }
   // 處理請求
   for {
      con, err := listen.Accept()
      if err != nil {
         continue
      }

      // 專門開一個協程處理相應請求
      go func(con net.Conn) {
         fmt.Println("process new client")
         jsonrpc.ServeConn(con)
      }(con)
   }
}

client2.go

package main

import (
   "fmt"
   "log"
   "net/rpc/jsonrpc"
)

// 主函式
func main() {
   //連線遠端rpc服務
   conn, err := jsonrpc.Dial("tcp", ":9999")
   if err != nil {
      log.Fatal(err)
   }
   // 呼叫伺服器方法
   ret := 0
   err2 := conn.Call("Happy.CalHappy", 10, &ret)
   if err2 != nil {
      log.Fatal(err2)
   }
   fmt.Println("開心指數:", ret)
}

golang原生rpc自定義協議

例如我們自定義協議,一段資料,前2個位元組是資料頭,後面的得為真實的資料,如:

golang如何使用原生RPC及微服務簡述

  • 既然自定義了協議,那麼我們傳送資料和讀取資料的時候就需要遵守我們的協議規定,否則會出問題

  • 那麼我們做資料傳輸的時候就會涉及到編碼和解碼,我們也需要自己封裝好編碼和解碼的函式

寫入資料和讀取資料的函式封裝
// 寫入資料
func MyWriteData(con net.Conn, data []byte) (int, error) {
    if con == nil {
        log.Fatal("con is nil")
    }

    buf := make([]byte, 2+len(data))
    // 先寫入頭部,把真實資料的長度寫到頭裡面
    binary.BigEndian.PutUint16(buf[:2], uint16(len(data)))
    // 再寫入資料
    copy(buf[2:], data)
    n, err := con.Write(buf)
    if err != nil {
        log.Fatal("Write error", err)
    }
    return n, nil
}

//讀取資料
func MyReadData(con net.Conn) ([]byte, error) {
    if con == nil {
        log.Fatal("con is nil")
    }
    // 協議頭2個位元組
    myheader := make([]byte, 2)
    // 讀取2個位元組的協議頭
    _, err := io.ReadFull(con, myheader)
    if err != nil {
        log.Fatal("ReadFull error", err)
    }
    //讀取真實資料
    // 從頭裡面讀取真實資料的長度
    len := binary.BigEndian.Uint16(myheader)
    data := make([]byte, len)
    _, err = io.ReadFull(con, data)
    if err != nil {
        log.Fatal("ReadFull error", err)
    }
    return data, nil
}
編寫編碼和解碼的函式封裝

我們設計成字串命令 與 具體呼叫的函式做繫結的方式,這樣為接下來的server3.go rpc的實現,打好基礎

// 具體的資料結構體
type MyData struct {
    Name   string
    MyArgs []interface{} // 引數列表
}

// 加密
func MyEncode(data *MyData) ([]byte, error) {
    if data == nil {
        log.Fatal("con is nil")
    }
    var bb bytes.Buffer
    buf := gob.NewEncoder(&bb)
    if err := buf.Encode(data); err != nil {
        log.Fatal("Encode error ", err)
    }
    return bb.Bytes(), nil
}

// 解密
func MyDecode(data []byte) (MyData, error) {
    if data == nil {
        log.Fatal("con is nil")
    }
    buf := bytes.NewBuffer(data)
    myDe := gob.NewDecoder(buf)
    var res MyData
    if err := myDe.Decode(&res); err != nil {
        log.Fatal("Decode error ", err)
    }
    return res, nil
}
綜合上述功能server端的實現 my_server.go:
package main

import (
    "bytes"
    "encoding/binary"
    "encoding/gob"
    "fmt"
    "io"
    "log"
    "net"
    "reflect"
)

// 寫入資料
func MyWriteData(con net.Conn, data []byte) (int, error) {
    if con == nil {
        log.Fatal("con is nil")
    }

    buf := make([]byte, 2+len(data))
    // 先寫入頭部,把真實資料的長度寫到頭裡面
    binary.BigEndian.PutUint16(buf[:2], uint16(len(data)))
    // 再寫入資料
    copy(buf[2:], data)
    n, err := con.Write(buf)
    if err != nil {
        log.Fatal("Write error", err)
    }
    return n, nil
}

//讀取資料
func MyReadData(con net.Conn) ([]byte, error) {
    if con == nil {
        log.Fatal("con is nil")
    }
    // 協議頭2個位元組
    myheader := make([]byte, 2)
    // 讀取2個位元組的協議頭
    _, err := io.ReadFull(con, myheader)
    if err != nil {
        log.Fatal("ReadFull error", err)
    }
    //讀取真實資料
    // 從頭裡面讀取真實資料的長度
    len := binary.BigEndian.Uint16(myheader)
    data := make([]byte, len)
    _, err = io.ReadFull(con, data)
    if err != nil {
        log.Fatal("ReadFull error", err)
    }
    return data, nil
}

// 具體的資料結構體
type MyData struct {
    Name   string
    MyArgs []interface{} // 引數列表
}

// 加密
func MyEncode(data *MyData) ([]byte, error) {
    if data == nil {
        log.Fatal("con is nil")
    }
    var bb bytes.Buffer
    buf := gob.NewEncoder(&bb)
    if err := buf.Encode(data); err != nil {
        log.Fatal("Encode error ", err)
    }
    return bb.Bytes(), nil
}

// 解密
func MyDecode(data []byte) (MyData, error) {
    if data == nil {
        log.Fatal("con is nil")
    }
    buf := bytes.NewBuffer(data)
    myDe := gob.NewDecoder(buf)
    var res MyData
    if err := myDe.Decode(&res); err != nil {
        log.Fatal("Decode error ", err)
    }
    return res, nil
}

// 全域性的一個map, 命令與函式做對應關係
var myFun = make(map[string]reflect.Value)

// 註冊命令繫結函式
func MyRegister(name string, fn interface{}) {
    if _, ok := myFun[name]; ok { // 說明該命令已經繫結過函式
        return
    }
    myFun[name] = reflect.ValueOf(fn)
    log.Println("reflect.ValueOf(fn) == ", myFun[name])
}

// 服務端執行的方法
func MyRun(addr string) {
    listen, err := net.Listen("tcp", addr)
    if err != nil {
        log.Fatal("Listen is nil")
    }
    log.Println("啟動服務端....")
    // 開始阻塞等待客戶端的連線
    for {
        con, err := listen.Accept()
        if err != nil {
            log.Println("Accept is nil")
            return
        }
        // 讀取資料
        b, err := MyReadData(con)
        if err != nil {
            log.Println("MyReadData error ", err)
            return
        }
        log.Println("MyReadData =============== ")
        // 解析資料
        my, err := MyDecode(b)
        if err != nil {
            log.Println("MyDecode =============== ")
            log.Println("MyDecode error ", err)
            return
        }
        f, ok := myFun[my.Name]
        if !ok {
            fmt.Printf("命令 %s 沒有繫結函式\n", my.Name)
            return
        }
        // 獲取引數
        args := make([]reflect.Value, 0, len(my.MyArgs))
        for _, arg := range my.MyArgs {
            args = append(args, reflect.ValueOf(arg))
            log.Println("reflect.ValueOf(arg) - ", reflect.ValueOf(arg))
        }

        //反射
        res := f.Call(args)
        log.Println("f.Call(args) == ", res)
        // 包裝結果資料給到客戶端
        out := make([]interface{}, 0, len(res))
        for _, arg := range res {
            log.Println("arg  == ", arg)
            out = append(out, arg.Interface())
        }
        log.Println("out  == ", out)
        // 編碼資料
        bb, err := MyEncode(&MyData{Name: my.Name, MyArgs: out})
        if err != nil {
            log.Println("MyEncode error ", err)
            return
        }
        // 將資料寫給客戶端
        _, err = MyWriteData(con, bb)
        if err != nil {
            log.Println("MyWriteData ======== ")
            log.Println("MyWriteData error ", err)
            return
        }
    }
}
// 客戶端通過命令呼叫函式
func CallRPCFun(con net.Conn, rpcName string, args interface{}) {
    // 通過反射,獲取args未初始化的函式原型
    fn := reflect.ValueOf(args).Elem()
    log.Println("fn == ", fn)
    // 需要另一個函式,作用是對第一個函式引數操作
    f := func(args []reflect.Value) []reflect.Value {
        // 處理引數
        inArgs := make([]interface{}, 0, len(args))
        for _, arg := range args {
            inArgs = append(inArgs, arg.Interface())
        }
        // 連線
        // 編碼資料
        reqRPC := &MyData{Name: rpcName, MyArgs: inArgs}
        b, err := MyEncode(reqRPC)
        if err != nil {
            log.Println("MyEncode =============== ")
            log.Println("MyEncode error ", err)
        }
        // 寫資料
        _, err = MyWriteData(con, b)
        if err != nil {
            log.Println("MyWriteData =============== ")
            log.Fatal("MyWriteData error ", err)
        }
        // 服務端發過來返回值,此時應該讀取和解析
        respBytes, err := MyReadData(con)
        if err != nil {
            log.Fatal("MyReadData error ", err)
        }
        // 解碼
        res, err := MyDecode(respBytes)
        if err != nil {
            log.Println("MyDecode =============== ")
            log.Fatal("MyDecode error ", err)
        }
        // 處理服務端返回的資料
        outArgs := make([]reflect.Value, 0, len(res.MyArgs))
        for i, arg := range res.MyArgs {
            // 必須進行nil轉換
            if arg == nil {
                // reflect.Zero()會返回型別的零值的value
                // .out()會返回函式輸出的引數型別
                outArgs = append(outArgs, reflect.Zero(fn.Type().Out(i)))
                continue
            }
            outArgs = append(outArgs, reflect.ValueOf(arg))
        }
        return outArgs
    }

    v := reflect.MakeFunc(fn.Type(), f)

    // 為函式f賦值
    fn.Set(v)
}

// 定義使用者物件
type Data struct {
    CmdName string
    Param   string
}

// 用於測試使用者查詢的方法
func GetData(id int) (Data, error) {
    data := make(map[int]Data)
    // 假資料
    data[0] = Data{"PullInfo", "xiaoxiong"}
    data[1] = Data{"PutInfo", "daxiong"}

    // 查詢
    if u, ok := data[id]; ok {
        return u, nil
    }
    return Data{}, fmt.Errorf("%d err", id)
}

// 主函式
func main() {
    // 簡單設定log引數
    log.SetFlags(log.Lshortfile | log.LstdFlags)

    // rpc 服務端
    // 編碼中有一個欄位是interface{}時,進行註冊
    gob.Register(Data{})
    addr := "127.0.0.1:9999"
    // 建立服務端
    // 將服務端方法,註冊一下
    MyRegister("GetData", GetData)

    // 服務端等待呼叫
    go MyRun(addr)

    //-------------我是分割線-----------

    // rpc客戶端獲取連線
    conn, err := net.Dial("tcp", addr)
    if err != nil {
        fmt.Println("Dial err")
        return
    }
    log.Println("客戶端撥號成功了,開始呼叫函式了...")
    // 建立客戶端物件
    // 需要宣告函式原型
    var getdata func(int) (Data, error)

    CallRPCFun(conn, "GetData", &getdata)
    // 得到查詢結果
    u, err := getdata(1)
    if err != nil {
        fmt.Println("getdata err")
        return
    }
    log.Println(u)
    select {}
}

結果:

golang如何使用原生RPC及微服務簡述

以上均為學習所得,若有偏誤還請指正

技術是開放的,我們的心態也應如此。未來的道路上擁抱變化,勇敢前行。大家一起加油!

我是小魔童哪吒,歡迎吐槽,歡迎溝通

本作品採用《CC 協議》,轉載必須註明作者和本文連結