淺析gRPC
RPC/">gRPC.io官網上的一篇 ofollow,noindex">Blog 大致講解了gRPC是如何使用HTTP/2的,覺得講的比較抽象,理解的不夠透徹,於是自己找了一些資料。大致總結一下,個人認為:gRPC之所以高效,除了在協議層使用Protobuffer之外,底層使用HTTP/2也是一個非常重要的原因。下面先上一張圖,再來看看HTTP/2的一些特徵。
HTTP/2
概念
訊息(Message) 流(Stream) 幀(Frame) 連線(Connection)
特徵
- 多路複用、亂序收發:可以亂序收發資料報文,不用使用單步:
發1->收1
或者流水線:發1->發2->收2->收1
的流程,提高效率; - Header壓縮:不用花大量篇幅重複傳送常用header,採用傳送增量的方法,由客戶端和伺服器端共同維護一個字典;
- stream優先順序:可以在一個連線上,為不同stream設定不同優先順序;
- 伺服器推送:提前傳送需要的資源;
gRPC
收發訊息流程
傳送流程
- 解析地址:client訊息傳送給gRpc,然後resolver解析域名,並獲取到目標伺服器地址列表;
- 負載均衡:客戶端基於負載均衡演算法,從連線伺服器列表中找出一個目標伺服器;
- 連線:如果到目標伺服器已有連線,則使用已有連線,訪問目標伺服器;如果沒有可用連線,則建立HTTP/2連線;
- 編碼:對請求訊息使用 Protobuf做序列化,通過 HTTP/2 Stream 傳送給 gRPC 服務端;
接收流程
- 編碼:接收到服務端響應之後,使用Protobuf 做反序列化;
- 回撥:回撥 GrpcFuture 的 set(Response) 方法,喚醒阻塞的客戶端呼叫執行緒,獲取 RPC 響應。
服務定義
service
在protobuf中定義的service,最終會被編譯為一個類;所有的RPC呼叫都是這個類的方法。
service RouteGuide { ... }
RPC型別
所有的RPC都是service的編譯成的類的一個方法,gRPC支援四種不同的方法。其中流式RPC中,使用 stream
關鍵字特殊定義了引數型別。
- 簡單RPC
rpc GetFeature(Point) returns (Feature) {}
- 伺服器端流式RPC
rpc ListFeatures(Rectangle) returns (stream Feature) {}
- 客戶端流式RPC
rpc RecordRoute(stream Point) returns (RouteSummary) {}
- 向流式RPC
rpc RouteChat(stream RouteNote) returns (stream RouteNote) {}
編譯結果
type routeGuideServer struct { ... } ... func (s *routeGuideServer) GetFeature(ctx context.Context, point *pb.Point) (*pb.Feature, error) { ... } ... func (s *routeGuideServer) ListFeatures(rect *pb.Rectangle, stream pb.RouteGuide_ListFeaturesServer) error { ... } ... func (s *routeGuideServer) RecordRoute(stream pb.RouteGuide_RecordRouteServer) error { ... } ... func (s *routeGuideServer) RouteChat(stream pb.RouteGuide_RouteChatServer) error { ... }
流式RPC
流式RPC是指,傳入或者返回的是一個stream(類似於一個套介面)。程式碼可以對該stream執行send或者recv操作來寫入或者讀出在RPC定義時指定的資料結構,可迴圈操作多次,直到讀完資料。
使用方法
// 建立gRPC連線 conn, err := grpc.Dial(*address, grpc.WithInsecure()) if err != nil { log.Fatalf("faild to connect: %v", err) } defer conn.Close() // 初始化客戶端程式碼 c := pb.NewGreeterClient(conn) // 呼叫伺服器端流式RPC,請求是一個HelloRequest結構體,返回是stream stream, err := c.SayHello1(context.Background(), &pb.HelloRequest{Name: *name}) if err != nil { log.Fatalf("could not greet: %v", err) } // 迴圈讀取在RPC中定義的資料結構,直到返回io.EOF for { reply, err := stream.Recv() if err == io.EOF { break } if err != nil { log.Printf("failed to recv: %v", err) } log.Printf("Greeting: %s", reply.Message) }
場景
當年不知道gRPC支援流式RPC,為了從日誌agent獲取日誌資訊,使用了簡單的RPC來逐條讀取訊息,效率極其低下。現在想來,其實如果使用gRPC的流式方法來獲取日誌應該也算是一個比較好的方案。比如,當用戶在頁面上要檢視日誌的時候,controller層就發起到日誌微服務的gGRPC流式請求,一個日誌請求傳送過去,從該RPC就可以流式的返回所需檢視的所有日誌;而不用傳送一條再請求第二條,再返回。