介紹
我猜測大部分長期使用 Java 的開發者應該較少會接觸 gRPC,畢竟在 Java 圈子里大部分使用的還是 Dubbo/SpringClound 這兩類服務框架。
我也是近段時間有機會從零開始重構業務才接觸到 gRPC 的,當時選擇gRPC 時也有幾個原因:
- 基于云原生的思路開發部署項目,而在云原生中 gRPC 幾乎已經是標準的通訊協議了。
- 開發語言選擇了 Go,在 Go 圈子中 gRPC 顯然是更好的選擇。
- 公司內部有部分業務使用的是 Python 開發,在多語言兼容性上 gRPC 支持的非常好。
經過線上一年多的平穩運行,可以看出 gRPC 還是非常穩定高效的;rpc 框架中最核心的幾個要點:
- 序列化
- 通信協議
- IDL(接口描述語言)
這些在 gRPC 中分別對應的是:
- 基于 Protocol Buffer 序列化協議,性能高效。
- 基于 HTTP/2 標準協議開發,自帶 stream、多路復用等特性;同時由于是標準協議,第三方工具的兼容性會更好(比如負載均衡、監控等)。
- 編寫一份 .proto 接口文件,便可生成常用語言代碼。
HTTP/2
學習 gRPC 之前首先得知道它是通過什么協議通信的,我們日常不管是開發還是應用基本上接觸到最多的還是 HTTP/1.1 協議。
由于 HTTP/1.1 是一個文本協議,對人類非常友好,相反的對機器性能就比較低。
需要反復對文本進行解析,效率自然就低了;要對機器更友好就得采用二進制,HTTP/2 自然做到了。
除此之外還有其他優點:
- 多路復用:可以并行的收發消息,互不影響。
- HPACK 節省 header 空間,避免 HTTP1.1 對相同的 header 反復發送。
Protocol
gRPC 采用的是 Protocol 序列化,發布時間比 gRPC 早一些,所以也不僅只用于 gRPC,任何需要序列化 IO 操作的場景都可以使用它。
它會更加的省空間、高性能;之前在開發 https://github.com/crossoverJie/cim 時就使用它來做數據交互。
package order.v1; service OrderService{ rpc Create(OrderApiCreate) returns (Order) {} rpc Close(CloseApiCreate) returns (Order) {} // 服務端推送 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 文件,便可用命令行工具生成對應語言的 SDK。
具體可以參考官方文檔:https://grpc.io/docs/languages/go/generated-code/
調用
protoc --go_out=. --go_opt=paths=source_relative \ --go-grpc_out=. --go-grpc_opt=paths=source_relative \ test.proto
生成代碼之后編寫服務端就非常簡單了,只需要實現生成的接口即可。
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 }
客戶端也非常簡單,只需要依賴服務端代碼,創建一個 connection 然后就和調用本地方法一樣了。
這是經典的 unary(一元)調用,類似于 http 的請求響應模式,一個請求對應一次響應。
Server stream
gRPC 除了常規的 unary 調用之外還支持服務端推送,在一些特定場景下還是很有用的。
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 }
服務端的推送如上所示,調用 Send 函數便可向客戶端推送。
for { msg, err := rpc.RecvMsg() if err == io.EOF { marshalIndent, _ := json.MarshalIndent(msgs, "", "\t") fmt.Println(msg) return } }
客戶端則通過一個循環判斷當前接收到的數據包是否已經截止來獲取服務端消息。
為了能更直觀的展示這個過程,優化了之前開發的一個 gRPC 客戶端,可以直觀的調試 stream 調用。
上圖便是一個服務端推送示例。
Client Stream
除了支持服務端推送之外,客戶端也支持。
客戶端在同一個連接中一直向服務端發送數據,服務端可以并行處理消息。
// 服務端代碼 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()
代碼與服務端推送類似,只是角色互換了。
Bidirectional Stream
同理,當客戶端、服務端同時都在發送消息也是支持的。
// 服務端 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) // 發送消息 rpc.SendMsg(messages[0]) // 接收消息 receive, _ := rpc.RecvMsg() marshalIndent, _ := json.MarshalIndent(receive, "", "\t") fmt.Println(string(marshalIndent)) } rpc.CloseSend()
其實就是將上訴兩則合二為一。
通過調用示例很容易理解。
元數據
gRPC 也支持元數據傳輸,類似于 HTTP 中的 header。
// 客戶端寫入 metaStr := `{"lang":"zh"}` var m map[string]string err := json.Unmarshal([]byte(metaStr), &m) md := metadata.New(m) // 調用時將 ctx 傳入即可 ctx := metadata.NewOutgoingContext(context.Background(), md) // 服務端接收 md, ok := metadata.FromIncomingContext(ctx) if !ok { return nil, status.Errorf(codes.DataLoss, "failed to get metadata") } fmt.Println(md)
gRPC gateway
gRPC 雖然功能強大使用也很簡單,但對于瀏覽器、APP的支持還是不如 REST 應用廣泛(瀏覽器也支持,但應用非常少)。
為此社區便創建了 https://github.com/grpc-ecosystem/grpc-gateway 項目,可以將 gRPC 服務暴露為 RESTFUL API。
為了讓測試可以習慣用 postman 進行接口測試,我們也將 gRPC 服務代理出去,更方便的進行測試。
反射調用
作為一個 rpc 框架,泛化調用也是必須支持的,可以方便開發配套工具;gRPC 是通過反射支持的,通過拿到服務名稱、pb 文件進行反射調用。
https://github.com/jhump/protoreflect 這個庫封裝了常見的反射操作。
上圖中看到的可視化 stream 調用也是通過這個庫實現的。
負載均衡
由于 gRPC 是基于 HTTP/2 實現的,客戶端和服務端會保持長連接;這時做負載均衡就不像是 HTTP 那樣簡單了。
而我們使用 gRPC 想達到效果和 HTTP 是一樣的,需要對請求進行負載均衡而不是連接。
通常有兩種做法:
- 客戶端負載均衡
- 服務端負載均衡
客戶端負載均衡在 rpc 調用中應用廣泛,比如 Dubbo 就是使用的客戶端負載均衡。
gRPC 中也提供有相關接口,具體可以參考官方demo。
https://github.com/grpc/grpc-go/blob/87eb5b7502/examples/features/load_balancing/README.md
客戶端負載均衡相對來說對開發者更靈活(可以自定義適合自己的策略),但相對的也需要自己維護這塊邏輯,如果有多種語言那就得維護多份。
所以在云原生這個大基調下,更推薦使用服務端負載均衡。
可選方案有:
- istio
- envoy
- apix
這塊我們也在研究,大概率會使用 envoy/istio。
總結
gRPC 內容還是非常多的,本文只是作為一份入門資料希望能讓不了解 gRPC 的能有一個基本認識;這在云原生時代確實是一門必備技能。
對文中的 gRPC 客戶端感興趣的朋友,可以參考這里的源碼:https://github.com/crossoverJie/ptg。