5分鐘學(xué)會 gRPC

介紹
我猜測大部分長期使用 Java 的開發(fā)者應(yīng)該較少會接觸 gRPC,畢竟在 Java 圈子里大部分使用的還是 Dubbo/SpringClound 這兩類服務(wù)框架。
我也是近段時(shí)間有機(jī)會從零開始重構(gòu)業(yè)務(wù)才接觸到 gRPC 的,當(dāng)時(shí)選擇 gRPC 時(shí)也有幾個(gè)原因:

基于云原生的思路開發(fā)部署項(xiàng)目,而在云原生中 gRPC幾乎已經(jīng)是標(biāo)準(zhǔn)的通訊協(xié)議了。開發(fā)語言選擇了 Go,在 Go 圈子中 gRPC顯然是更好的選擇。公司內(nèi)部有部分業(yè)務(wù)使用的是 Python開發(fā),在多語言兼容性上gRPC支持的非常好。
經(jīng)過線上一年多的平穩(wěn)運(yùn)行,可以看出 gRPC 還是非常穩(wěn)定高效的;rpc 框架中最核心的幾個(gè)要點(diǎn):
序列化 通信協(xié)議 IDL(接口描述語言)
這些在 gRPC 中分別對應(yīng)的是:
基于 Protocol Buffer序列化協(xié)議,性能高效。基于 HTTP/2標(biāo)準(zhǔn)協(xié)議開發(fā),自帶stream、多路復(fù)用等特性;同時(shí)由于是標(biāo)準(zhǔn)協(xié)議,第三方工具的兼容性會更好(比如負(fù)載均衡、監(jiān)控等)編寫一份 .proto接口文件,便可生成常用語言代碼。
HTTP/2
學(xué)習(xí) gRPC 之前首先得知道它是通過什么協(xié)議通信的,我們?nèi)粘2还苁情_發(fā)還是應(yīng)用基本上接觸到最多的還是 HTTP/1.1 協(xié)議。

由于 HTTP/1.1 是一個(gè)文本協(xié)議,對人類非常友好,相反的對機(jī)器性能就比較低。
需要反復(fù)對文本進(jìn)行解析,效率自然就低了;要對機(jī)器更友好就得采用二進(jìn)制,HTTP/2 自然做到了。
除此之外還有其他優(yōu)點(diǎn):
多路復(fù)用:可以并行的收發(fā)消息,互不影響 HPACK節(jié)省header空間,避免HTTP1.1對相同的header反復(fù)發(fā)送。
Protocol
gRPC 采用的是 Protocol 序列化,發(fā)布時(shí)間比 gRPC 早一些,所以也不僅只用于 gRPC,任何需要序列化 IO 操作的場景都可以使用它。
它會更加的省空間、高性能;之前在開發(fā) https://github.com/crossoverJie/cim 時(shí)就使用它來做數(shù)據(jù)交互。
package order.v1;
service OrderService{
rpc Create(OrderApiCreate) returns (Order) {}
rpc Close(CloseApiCreate) returns (Order) {}
// 服務(wù)端推送
rpc ServerStream(OrderApiCreate) returns (stream Order) {}
// 客戶端推送
rpc ClientStream(stream OrderApiCreate) returns (Order) {}
// 雙向推送
rpc BdStream(stream OrderApiCreate) returns (stream Order) {}
}
message OrderApiCreate{
int64 order_id = 1;
repeated int64 user_id = 2;
string remark = 3;
repeated int32 reason_id = 4;
}
使用起來也是非常簡單的,只需要定義自己的 .proto 文件,便可用命令行工具生成對應(yīng)語言的 SDK。
具體可以參考官方文檔:https://grpc.io/docs/languages/go/generated-code/
調(diào)用
?protoc?--go_out=.?--go_opt=paths=source_relative?\
????--go-grpc_out=.?--go-grpc_opt=paths=source_relative?\
????test.proto
生成代碼之后編寫服務(wù)端就非常簡單了,只需要實(shí)現(xiàn)生成的接口即可。
func?(o?*Order)?Create(ctx?context.Context,?in?*v1.OrderApiCreate)?(*v1.Order,?error)?{
?//?獲取?metadata
?md,?ok?:=?metadata.FromIncomingContext(ctx)
?if?!ok?{
??return?nil,?status.Errorf(codes.DataLoss,?"failed?to?get?metadata")
?}
?fmt.Println(md)
?fmt.Println(in.OrderId)
?return?&v1.Order{
??OrderId:?in.OrderId,
??Reason:??nil,
?},?nil
}

客戶端也非常簡單,只需要依賴服務(wù)端代碼,創(chuàng)建一個(gè) connection 然后就和調(diào)用本地方法一樣了。
這是經(jīng)典的 unary(一元)調(diào)用,類似于 http 的請求響應(yīng)模式,一個(gè)請求對應(yīng)一次響應(yīng)。

Server stream
gRPC 除了常規(guī)的 unary 調(diào)用之外還支持服務(wù)端推送,在一些特定場景下還是很有用的。

func?(o?*Order)?ServerStream(in?*v1.OrderApiCreate,?rs?v1.OrderService_ServerStreamServer)?error?{
?for?i?:=?0;?i?5;?i++?{
??rs.Send(&v1.Order{
???OrderId:?in.OrderId,
???Reason:??nil,
??})
?}
?return?nil
}
服務(wù)端的推送如上所示,調(diào)用 Send 函數(shù)便可向客戶端推送。
?for?{
??msg,?err?:=?rpc.RecvMsg()
??if?err?==?io.EOF?{
???marshalIndent,?_?:=?json.MarshalIndent(msgs,?"",?"\t")
???fmt.Println(msg)
???return
??}
?}
客戶端則通過一個(gè)循環(huán)判斷當(dāng)前接收到的數(shù)據(jù)包是否已經(jīng)截止來獲取服務(wù)端消息。
為了能更直觀的展示這個(gè)過程,優(yōu)化了之前開發(fā)的一個(gè) gRPC 客戶端,可以直觀的調(diào)試 stream 調(diào)用。

上圖便是一個(gè)服務(wù)端推送示例。
Client Stream

除了支持服務(wù)端推送之外,客戶端也支持。
客戶端在同一個(gè)連接中一直向服務(wù)端發(fā)送數(shù)據(jù),服務(wù)端可以并行處理消息。
//?服務(wù)端代碼
func?(o?*Order)?ClientStream(rs?v1.OrderService_ClientStreamServer)?error?{
?var?value?[]int64
?for?{
??recv,?err?:=?rs.Recv()
??if?err?==?io.EOF?{
???rs.SendAndClose(&v1.Order{
????OrderId:?100,
????Reason:??nil,
???})
???log.Println(value)
???return?nil
??}
??value?=?append(value,?recv.OrderId)
??log.Printf("ClientStream?receiv?msg?%v",?recv.OrderId)
?}
?log.Println("ClientStream?finish")
?return?nil
}
?//?客戶端代碼
?for?i?:=?0;?i?5;?i++?{
??messages,?_?:=?GetMsg(data)
??rpc.SendMsg(messages[0])
?}
?receive,?err?:=?rpc.CloseAndReceive()
代碼與服務(wù)端推送類似,只是角色互換了。

Bidirectional Stream

同理,當(dāng)客戶端、服務(wù)端同時(shí)都在發(fā)送消息也是支持的。
//?服務(wù)端
func?(o?*Order)?BdStream(rs?v1.OrderService_BdStreamServer)?error?{
?var?value?[]int64
?for?{
??recv,?err?:=?rs.Recv()
??if?err?==?io.EOF?{
???log.Println(value)
???return?nil
??}
??if?err?!=?nil?{
???panic(err)
??}
??value?=?append(value,?recv.OrderId)
??log.Printf("BdStream?receiv?msg?%v",?recv.OrderId)
??rs.SendMsg(&v1.Order{
???OrderId:?recv.OrderId,
???Reason:??nil,
??})
?}
?return?nil
}
//?客戶端
?for?i?:=?0;?i?5;?i++?{
??messages,?_?:=?GetMsg(data)
??//?發(fā)送消息
??rpc.SendMsg(messages[0])
??//?接收消息
??receive,?_?:=?rpc.RecvMsg()
??marshalIndent,?_?:=?json.MarshalIndent(receive,?"",?"\t")
??fmt.Println(string(marshalIndent))
?}
?rpc.CloseSend()
其實(shí)就是將上訴兩則合二為一。

通過調(diào)用示例很容易理解。
元數(shù)據(jù)
gRPC 也支持元數(shù)據(jù)傳輸,類似于 HTTP 中的 header。
?//?客戶端寫入
?metaStr?:=?`{"lang":"zh"}`
?var?m?map[string]string
?err?:=?json.Unmarshal([]byte(metaStr),?&m)
?md?:=?metadata.New(m)
?//?調(diào)用時(shí)將?ctx?傳入即可
?ctx?:=?metadata.NewOutgoingContext(context.Background(),?md)
?
?//?服務(wù)端接收
?md,?ok?:=?metadata.FromIncomingContext(ctx)
?if?!ok?{
??return?nil,?status.Errorf(codes.DataLoss,?"failed?to?get?metadata")
?}
?fmt.Println(md)?
gRPC gateway
gRPC 雖然功能強(qiáng)大使用也很簡單,但對于瀏覽器、APP的支持還是不如 REST 應(yīng)用廣泛(瀏覽器也支持,但應(yīng)用非常少)。
為此社區(qū)便創(chuàng)建了 https://github.com/grpc-ecosystem/grpc-gateway 項(xiàng)目,可以將 gRPC 服務(wù)暴露為 RESTFUL API。

為了讓測試可以習(xí)慣用 postman 進(jìn)行接口測試,我們也將 gRPC 服務(wù)代理出去,更方便的進(jìn)行測試。
反射調(diào)用
作為一個(gè) rpc 框架,泛化調(diào)用也是必須支持的,可以方便開發(fā)配套工具;gRPC 是通過反射支持的,通過拿到服務(wù)名稱、pb 文件進(jìn)行反射調(diào)用。
https://github.com/jhump/protoreflect 這個(gè)庫封裝了常見的反射操作。
上圖中看到的可視化 stream 調(diào)用也是通過這個(gè)庫實(shí)現(xiàn)的。
負(fù)載均衡
由于 gRPC 是基于 HTTP/2 實(shí)現(xiàn)的,客戶端和服務(wù)端會保持長連接;這時(shí)做負(fù)載均衡就不像是 HTTP 那樣簡單了。
而我們使用 gRPC 想達(dá)到效果和 HTTP 是一樣的,需要對請求進(jìn)行負(fù)載均衡而不是連接。
通常有兩種做法:
客戶端負(fù)載均衡 服務(wù)端負(fù)載均衡
客戶端負(fù)載均衡在 rpc 調(diào)用中應(yīng)用廣泛,比如 Dubbo 就是使用的客戶端負(fù)載均衡。
gRPC 中也提供有相關(guān)接口,具體可以參考官方demo。
https://github.com/grpc/grpc-go/blob/87eb5b7502/examples/features/load_balancing/README.md
客戶端負(fù)載均衡相對來說對開發(fā)者更靈活(可以自定義適合自己的策略),但相對的也需要自己維護(hù)這塊邏輯,如果有多種語言那就得維護(hù)多份。
所以在云原生這個(gè)大基調(diào)下,更推薦使用服務(wù)端負(fù)載均衡。
可選方案有:
istio envoy apix
這塊我們也在研究,大概率會使用 envoy/istio。
總結(jié)
gRPC 內(nèi)容還是非常多的,本文只是作為一份入門資料希望能讓不了解 gRPC 的能有一個(gè)基本認(rèn)識;這在云原生時(shí)代確實(shí)是一門必備技能。
對文中的 gRPC 客戶端感興趣的朋友,可以參考這里的源碼:https://github.com/crossoverJie/ptg
