介紹
我猜測大部分長期使用 Java
的開發(fā)者應(yīng)該較少會接觸 gRPC
玖雁,畢竟在 Java
圈子里大部分使用的還是 Dubbo/SpringClound
這兩類服務(wù)框架费奸。
我也是近段時間有機(jī)會從零開始重構(gòu)業(yè)務(wù)才接觸到 gRPC
的裳瘪,當(dāng)時選擇 gRPC
時也有幾個原因:
- 基于云原生的思路開發(fā)部署項目贤徒,而在云原生中
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 框架中最核心的幾個要點(diǎn):
- 序列化
- 通信協(xié)議
- IDL(接口描述語言)
這些在 gRPC
中分別對應(yīng)的是:
- 基于
Protocol Buffer
序列化協(xié)議,性能高效量窘。 - 基于
HTTP/2
標(biāo)準(zhǔn)協(xié)議開發(fā)雇寇,自帶stream
、多路復(fù)用等特性蚌铜;同時由于是標(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
是一個文本協(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ā)布時間比 gRPC
早一些昆汹,所以也不僅只用于 gRPC
明刷,任何需要序列化 IO 操作的場景都可以使用它。
它會更加的省空間满粗、高性能辈末;之前在開發(fā) https://github.com/crossoverJie/cim 時就使用它來做數(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ù)端就非常簡單了捅彻,只需要實現(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)建一個 connection
然后就和調(diào)用本地方法一樣了从隆。
這是經(jīng)典的 unary
(一元)調(diào)用,類似于 http 的請求響應(yīng)模式缭裆,一個請求對應(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
}
}
客戶端則通過一個循環(huán)判斷當(dāng)前接收到的數(shù)據(jù)包是否已經(jīng)截止來獲取服務(wù)端消息缝其。
為了能更直觀的展示這個過程挎塌,優(yōu)化了之前開發(fā)的一個 gRPC
客戶端,可以直觀的調(diào)試 stream
調(diào)用氏淑。
上圖便是一個服務(wù)端推送示例勃蜘。
Client Stream
除了支持服務(wù)端推送之外,客戶端也支持假残。
客戶端在同一個連接中一直向服務(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ù)端同時都在發(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()
其實就是將上訴兩則合二為一快鱼。
通過調(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)用時將 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 項目喇闸,可以將 gRPC 服務(wù)暴露為 RESTFUL API袄琳。
為了讓測試可以習(xí)慣用 postman 進(jìn)行接口測試,我們也將 gRPC 服務(wù)代理出去燃乍,更方便的進(jìn)行測試唆樊。
反射調(diào)用
作為一個 rpc 框架,泛化調(diào)用也是必須支持的刻蟹,可以方便開發(fā)配套工具逗旁;gRPC 是通過反射支持的,通過拿到服務(wù)名稱座咆、pb 文件進(jìn)行反射調(diào)用痢艺。
https://github.com/jhump/protoreflect 這個庫封裝了常見的反射操作。
上圖中看到的可視化 stream
調(diào)用也是通過這個庫實現(xiàn)的介陶。
負(fù)載均衡
由于 gRPC
是基于 HTTP/2
實現(xiàn)的堤舒,客戶端和服務(wù)端會保持長連接;這時做負(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ù)多份澳厢。
所以在云原生這個大基調(diào)下环础,更推薦使用服務(wù)端負(fù)載均衡囚似。
可選方案有:
- istio
- envoy
- apix
這塊我們也在研究,大概率會使用 envoy/istio
线得。
總結(jié)
gRPC
內(nèi)容還是非常多的饶唤,本文只是作為一份入門資料希望能讓不了解 gRPC
的能有一個基本認(rèn)識;這在云原生時代確實是一門必備技能贯钩。
對文中的 gRPC 客戶端感興趣的朋友募狂,可以參考這里的源碼:
https://github.com/crossoverJie/ptg