微服務
1. 微服務是什麼
- 使用一套小服務來開發單個應用的方式,每個服務執行在獨立的程式裡,一般採用輕量級的通訊機制互聯,並且它們可以通過自動化的方式部署
微服務是設計思想,不是量的體現
- 專一的功能
- 程式碼量並不少
- 架構變複雜
2. 特點是啥
- 專一的職責,例如專注於許可權管理
- 輕量級的通訊,通訊與平臺和語言無關,例如http是輕量的
- 隔離性,資料隔離
- 有自己的資料
- 技術多樣
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的基本通訊如下:
RPC遠端過程呼叫,需要考慮的問題有如下四點:
- 引數傳遞
- 通訊協議機制
- 出錯處理
- 超時處理
2. 引數傳遞
- 值傳遞
一般預設是值傳遞,只需要將引數中的值複製到網路訊息中的資料中即可
- 引用傳遞
比較困難,單純傳遞引數的引用是完全沒有用意義的,因為引用的地址給到遠端的伺服器,伺服器上的該記憶體地址完全不是客戶端想要的資料,若非要這樣處理,客戶端還必須把資料的副本傳遞給到遠端伺服器,並將它們放到遠端伺服器記憶體中,伺服器複製引用的地址後,即可進行資料的讀取。
可是上述做法很麻煩,且很容易出錯,一般RPC不支援直接傳遞引用
- 資料格式統一問題
需要有一個標準來對所有資料型別進行編解碼 ,資料格式可以有隱式型別
和顯式型別
- 隱式型別
只傳遞值,不傳遞變數的名稱或 型別
- 顯式型別
傳遞欄位的型別和值
常見的傳輸資料格式有:
- ISO標準的ASN.1
- JSON
- PROTOBUF
- XML
3. 通訊協議機制
廣義上的協議棧分為共有協議
和私有協議
- 共有協議
例如HTTP,SMPP,WEBSERVICE都是共有協議,擁有通用型上,公網傳輸的能力上 有優勢
- 私有協議
內部約定而成的協議,弊端多,但是可以高度的定製化,提升效能,降低成本,提高靈活性和效率。企業內部往往採用私有協議開發
對於協議的制定需要考慮如下5個方面:
- 協議設計
需要考慮哪些問題
- 私有協議的編解碼
需要有業務針對性的編解碼方式方法,如下有案例
- 命令的定義和命令處理器的選擇
協議的過程一般會有2種
- 負載命令
傳輸業務具體的資料,如請求引數,響應結果的命令
- 控制命令
一般為功能管理命令,如心跳命令等
- 命令的協議
一般是使用序列化協議,不同的協議在編碼效率和傳輸效率上都不相同,如
- 通訊模式
- oneway – 不關心響應,請求執行緒不會被阻塞
- sync – 呼叫會被阻塞,知道返回結果為止
- future – 呼叫時不會阻塞縣執行緒,獲取結果的時候會阻塞執行緒
- callback – 非同步呼叫,不會阻塞執行緒
出錯處理和超時處理
遠端過程呼叫相對本地過程呼叫出錯的概率更大,因此需要考慮到呼叫失敗的各種場景:
- 服務端出錯,需要如何處理
- 客戶端請求服務時候出現錯誤或者超時,需要設定合適的重試機制
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使用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個位元組是資料頭,後面的得為真實的資料,如:
既然自定義了協議,那麼我們傳送資料和讀取資料的時候就需要遵守我們的協議規定,否則會出問題
那麼我們做資料傳輸的時候就會涉及到編碼和解碼,我們也需要自己封裝好編碼和解碼的函式
寫入資料和讀取資料的函式封裝
// 寫入資料
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 {}
}
結果:
以上均為學習所得,若有偏誤還請指正
技術是開放的,我們的心態也應如此。未來的道路上擁抱變化,勇敢前行。大家一起加油!
我是小魔童哪吒,歡迎吐槽,歡迎溝通
本作品採用《CC 協議》,轉載必須註明作者和本文連結