gRPC - 概述

AI 摘要: gRPC是一种RPC协议,定义了服务的方法、参数和返回类型,通过Protocol Buffers序列化结构化数据。它采用基于IDL的方式描述服务接口和消息负载,并支持简单调用、流式RPC服务、流式RPC客户端和双向流式RPC服务等多种调用方式。gRPC的生命周期包括服务端和客户端的调用流程以及RPC的终止和取消。此外,gRPC还支持认证机制,可以与其他auth机制协同工作。

Protocol Buffers: Google数据交换格式

gRPC定义一个服务,指定可以被远程实现的方法,有参数和返回类型

  • 服务端实现了接口,运行gRPC服务处理客户端的调用
  • 客户端有一个stud(不同语言)提供了统一和服务端方一致的方法

1. gRPC背景

  1. 函数调用 => RPC
  2. gRPC定义一个服务,指定可以被远程实现的方法,有参数和返回类型;服务端实现了接口,运行gRPC服务处理客户端的调用;客户端有一个stud(不同语言)提供了统一和服务端方一致的方法;
  3. 图解gRPC: https://www.grpc.io/img/landing-2.svg
  4. gRPC支持的语言
  5. 使用Protocol Buffers序列化结构化数据(类似JSON的数据格式),.proto文件;pb是作为消息的结构,包含一系列kv项
  6. 利用protoc,pb的编译器,生成指定语言的数据访问类,提供类似name()set_name()访问器,方法被被序列化/解析为源字节
  7. gRPC使用protoc使用指定gRPC插件生成code:获取客户端和服务端代码,和标准的pb代码赋值、获取消息类型
  8. pb版本v3,支持新特性,golang/protobuf,推荐使用v3(默认)

1.1. RPC架构和生命周期

  1. gRPC关键概念,RPC架构和生命周期
  2. gRPC类似其他RPC,也是定义服务、指定可以被调用的方法,gRPC使用PB作为IDL(接口定义语言),描述服务接口和消息负载
  3. 定义四类服务方法:
  • 简单调用方式(请求服务,获取简单响应)
  • 流式RPC服务(请求服务,获取一个流)
  • 流式RPC客户端(请求流,服务端读完,应答简单响应)
  • 双向流式RPC服务(请求、服务响应都是流)
  1. 使用API,gRPC通过protoc编译机器编译.proto文件后,生成了客户端和服务端的代码。gRPC用户在客户端调用这些APIs,在服务端定义服务APIs
  2. 在服务端,服务实现了申明的服务,运行gRPC服务来处理调用,gRPC基础设施解析进入的请求,执行服务方法,编译服务的响应;
  3. 在客户端,客户端有一个叫做stub(桩子)作为客户端,实现了和服务端同样的方法;客户端只需要调用这些本地的对象方法,将调用的参数包装在PB消息类型,发送给服务端;
  4. 同步和异步:同步阻塞RPC调用直至服务器响应回来为止;网络本质是异步的,许多情况能够启动RPC而不阻塞当前线程
  5. 大多数语言都支持同步和异步两者方式

1.2. RPC生命周期

  1. 简单调用方式:客户端调用方法,通知服务端元数据;服务端可以立马响应元数据,或者等客户端的消息数据;服务端收到客户端请求消息后,处理完后,响应、状态信息,尾随的元数据一起返回;若状态OK客户端完成响应
  2. 服务端和客户端流式RPC与简单调用流程类似;
  3. 双向流式RPC,客户端调用后,服务端接收客户端的元数据、方法名、超时限制,服务端可以选择发回响应元数据或者等待客户端开始发送请求;接下来进行全双工的读写;
  4. gRPC允许客户端指定超时时间:DEADLINE_EXCEEDED,服务端可以查询特定的RPC是否超时,或者还剩下多少时间来完成RPC
  5. RPC终止,可能出现服务端响应完成,但客户端RPC超时的情况
  6. RPC取消,取消会立即终止RPC,无需进一步的处理,取消之前做的更改不会回滚
  7. 元数据,以键值对提供RPC的调用信息
  8. 通道,客户端能指定通道参数修改gRPC的默认行为,诸如关闭消息压缩,通道有连接和闲置状态

1.3. gRPC认证

  1. gRPC被设计与其他各种auth机制协同工作:SSL/TLSToken
  2. 认证API:凭证类型:通道凭证,调用凭证
  3. Go不加密和加密

1.3.1. gRPC认证 - 不加密

1
2
3
4
5
6
7
8
9
// 客户端
conn, _ := grpc.Dial("localhost:50051", grpc.WithInsecure())
// error handling omitted
client := pb.NewGreeterClient(conn)
// 服务端
s := grpc.NewServer()
lis, _ := net.Listen("tcp", "localhost:50051")
// error handling omitted
s.Serve(lis)

1.3.2. gRPC认证 - 加密

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// 客户端
creds, _ := credentials.NewClientTLSFromFile(certFile, "")
conn, _ := grpc.Dial("localhost:50051", grpc.WithTransportCredentials(creds))
// error handling omitted
client := pb.NewGreeterClient(conn)

// 服务端
creds, _ := credentials.NewServerTLSFromFile(certFile, keyFile)
s := grpc.NewServer(grpc.Creds(creds))
lis, _ := net.Listen("tcp", "localhost:50051")
// error handling omitted
s.Serve(lis)

1.4. gRPC错误处理

  1. 标准错误模型:OK状态码,错误状态码
  2. 标准的错误类型:https://github.com/googleapis/googleapis/blob/master/google/rpc/error_details.proto (涵盖了最常见的错误,以额外错误信息在元数据中提供)
  3. grpc-web
  4. 问题:
  • 错误扩展在不同语言库中有差异
  • 错误无法用于监视
  • 可能干扰头阻塞,降低HTTP/2的压缩效率
  • 错误细节过大,可能导致协议限制
  1. 错误状态码: - https://www.grpc.io/docs/guides/error/
  • 常规错误
  • 网络失败
  • 协议错误
  1. 基准测试
  • Jenkins
  • netperf

2. PB文件编译

文档推荐:https://developers.google.com/protocol-buffers/

2.1. PB文件编译工具安装

  • https://github.com/protocolbuffers/protobuf
    • protoc安装,语言无关,平台无关的可扩展机制,用于序列化结构化数据
  • https://github.com/golang/protobuf
    • 需要为go编程语言安装协议编译器(用于编译.proto文件)和protobuf运行时:
      • 编译时刻:包含protocol compiler plugin - protoc-gen-go用于首次编译生成和管理protocol buffers
      • 运行时刻:包含library库proto "github.com/golang/protobuf/proto",实现运行时的编码和解码,以及pb的访问

2.1.1. protoc编译器安装,以及protoc-gen-go安装

  1. C++或Go编译器
  2. 安装protoc:编译器protocol compiler
  3. 安装Go编译器插件protoc-gen-go,用于编译.proto文件为.go文件:go get -u github.com/golang/protobuf/protoc-gen-go,其被安装到$GOPATH/bin
1
2
3
// Code generated by protoc-gen-go. DO NOT EDIT.
// source: user/addressbook.proto
...
  1. .proto文件和生成的.pb.go文件之间存在一对一的关系,但是.pb.go同一Go包中可以包含任意数量的文件。

2.1.2. protoc查看

  1. 编译输出 .pb.go后缀文件: protoc --go_out=. *.proto
  2. 在生成的Go代码中,每个源.proto文件都与一个Go包相关联。
  3. 编译过程中指定额外参数,见示例
  4. proto当前有proto3(推荐)和proto2两个版本
1
2
3
4
5
6
7
8
protoc [OPTION] PROTO_FILES

--version 版本信息

-I PATH, --proto_path=PATH: 指定proto源文件(支持多次指定、默认当前路径)
-o FILE: 编译好的二进制文件输出目标
--encode=MESSAGE_TYPE 基于文本消息,写成二进制消息(标准输入和输出)
--decode=MESSAGE_TYPE 基于二进制消息,解码成文本消息(标准输入和输出)

2.2. 使用protoc编译.protoc文件

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// 基于子目录生成
cd /data/github.com/tkstorm/go-gRPC/api/protobuf/idl/search
protoc --go_out=paths=source_relative:. ./serach_result.proto

// 基于上层目录,编译生成
cd ../search
protoc --go_out=paths=source_relative:. ./search/serach_result.proto

// 任意目录生成
cd data/github.com/tkstorm/go-gRPC
protoc -I=./api/protobuf/idl --gofast_out=./api/protobuf/generate ./api/protobuf/idl/search/serach_result.proto

3. gRPC使用

  1. gRPC使用PB即作为IDL,又作为底层消息交换格式
  2. QuickStart: https://www.grpc.io/docs/quickstart
  3. gRPC可以用于实现一个异构、分布式服务实现,支持跨平台和编程语言,gRPC客户端对服务端调用类似本地服务一样;
  4. gRPC默认使用PB用于数据序列结构化数据;利用.proto后缀PB文件定义数据结构,同时提供了类似set_name()name()类似功能
  5. gRPC虽然默认支持PB,但也可以换成其他数据格式如JSON

3.1. 使用PB定义IDL

1
2
3
4
5
message Person {
  string name = 1;
  int32 id = 2;
  bool has_ponycopter = 3;
}

用pb的编译器protoc生成指定语言数据访问类,提供简单的字段读和写(如set_name()name())

3.2. Greeter服务定义

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// The greeter service definition.
service Greeter {
  // Sends a greeting
  rpc SayHello (HelloRequest) returns (HelloReply) {}
}

// The request message containing the user's name.
message HelloRequest {
  string name = 1;
}

// The response message containing the greetings
message HelloReply {
  string message = 1;
}

gRPC使用protoc附带gRPC指定插件来生成代码

4. gRPC定义4类服务&生命周期

4.1. 普通一元调用

类似普通函数调用,客户端请求获取到相应

  1. 客户端调用客户端对象,会告知服务端使用该调用的元数据,指定服务名、服务数据、方法名称和超时时间
  2. 服务端首次会立即发送自己的初始化元数据或等待消息一并应答
  3. 服务端处理,响应成功或失败
1
2
rpc SayHello(HelloRequest) returns (HelloResponse){
}

4.2. 服务器流式RPC

客户端请求服务后,获得一个流用于读取服务数据,直至流关闭;

类似下载服务

1
2
rpc LotsOfReplies(HelloRequest) returns (stream HelloResponse){
}

4.3. 客户端流式RPC

客户端流式RPC,用于发送一系列消息给到服务端;一旦全部发送完毕,等待服务读取消息并处理返回,同时保证了消息的循序

类似HTTP1.1中的管道功能。

1
2
rpc LotsOfGreetings(stream HelloRequest) returns (HelloResponse) {
}

4.4. 双向流式RPC

支持客户端和服务端全双工的独立读写流,互不影响,每个流中的顺序是保障的!

1
2
rpc BidiHello(stream HelloRequest) returns (stream HelloResponse){
}

5. gRPC其他信息

5.1. 使用API的壳

  • 服务端申明方法,运行gRPC服务接收客户端调用
  • 客户端具有成为stub的本地对象(标准术语是client)实现了和服务端一样的服务,客户端仅需要调用本地对象,后续gRPC在将请求发送到服务器并返回PB消息

5.2. 同步与异步

  • 同步阻塞调用RPC服务是最常见的方式
  • 网络本质是异步的,在许多情况下启动RPC不阻止当前线程很有用

5.3. 截止时间&超时限制

  • 支持客户端设定截止时间,超时错:DEADLINE_EXCEEDED,服务端支持RPC剩余时间查询
  • 基于语言不同,截止时间和超时时间都可能会在不同语言有用到
  • 即使服务端可能正常响应,但客户端可能由于超时中断了

5.4. 取消请求

  • 客户端或服务端都可以随时取消RPC

5.5. Metadata元数据

gRPC的元数据是以kv形式存在的,比如认证、版本等信息

5.6. Channel通道

通道提供客户端本地和远程服务端的连接,通道支持消息压缩打开和关闭,同时通道有连接(connected)和闲置(idle)的状态(一些语言支持通道状态的查询)

5.7. gRPC认证

  • SSL/TLS
  • Token-based authentication with Google

5.8. gRPC错误处理和Debug

// todo

5.9. gRPC压力测试

// todo

5.10. gRPC Over HTTP2

// todo

6. Go gRPC 试验

使用一个简单的路由映射应用,让客户端获取服务相关信息,高效、IDL约定

6.1. 基于.proto定义一个服务

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
syntax = "proto3";

package helloworld;

// The greeting service definition.(服务定义,指定rpc请求和响应类型,以下都是简单RPC请求和响应,还有流式等RPC模式支持)
service Greeter {
    // Sends a greeting
    rpc SayHello (HelloRequest) returns (HelloReply) {
    }

    // Sends another greeting
    rpc SayHelloAgain (HelloRequest) returns (HelloReply) {
    }
}

// The request message containing the user's name.
message HelloRequest {
    string name = 1;
}

// The response message containing the greetings
message HelloReply {
    string message = 1;
}

6.2. 编译gRPC服务

使用protocol buffer编译器 - proto生成服务和客户端的.pb.go包代码

1
$ protoc -I helloworld/ helloworld/helloworld.proto --go_out=plugins=grpc:helloworld

编译后的.pb.go文件实现了:

  • 用于填充,序列化和检索我们的请求和响应消息类型的所有协议缓冲区代码
  • 客户端使用服务中定义的方法调用的接口类型(或stub)
  • 服务端要实现的接口类型,以及服务定义的方法

6.3. 初始化客户端和服务端

6.3.1. 服务端

  • 实施根据我们的服务定义生成的服务接口:完成我们服务的实际“工作”。
  • 运行gRPC服务器以侦听来自客户端的请求,并将其分派到正确的服务实现。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
//go:generate protoc -I ../helloworld --go_out=plugins=grpc:../helloworld ../helloworld/helloworld.proto

// Package main implements a server for Greeter service.
package main

import (
    "context"
    "log"
    "net"

    pb "go-grpc/examples/helloworld/helloworld"
    "google.golang.org/grpc"
)

const (
    port = ":50051"
)

// 实现一个GreeterServer服务
type server struct{}

func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
    log.Printf("Received: %v", in.Name)
    return &pb.HelloReply{Message: "Hello " + in.Name}, nil
}

func (s *server) SayHelloAgain(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
    return &pb.HelloReply{Message: "Hello again " + in.GetName()}, nil
}

func main() {
    // TCP网络监听
    lis, err := net.Listen("tcp", port)
    if err != nil {
        log.Fatalf("failed to listen: %v", err)
    }
    // grpc服务创建
    s := grpc.NewServer()
    // Greeter服务注册
    pb.RegisterGreeterServer(s, &server{})
    // 服务绑定&提供服务,等待gRPC客户端的请求
    if err := s.Serve(lis); err != nil {
        log.Fatalf("failed to serve: %v", err)
    }
}

6.3.2. 客户端

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// Package main implements a client for Greeter service.
package main

import (
    "context"
    "log"
    "os"
    "time"

    "google.golang.org/grpc"
    pb "go-grpc/examples/helloworld/helloworld"
)

const (
    address     = "localhost:50051"
    defaultName = "world"
)

func main() {
    // 初始化一个gRPC通路
    conn, err := grpc.Dial(address, grpc.WithInsecure())
    if err != nil {
        log.Fatalf("did not connect: %v", err)
    }
    defer conn.Close()
    // 初始化一个Greeter客户端
    c := pb.NewGreeterClient(conn)

    // 请求参数
    name := defaultName
    if len(os.Args) > 1 {
        name = os.Args[1]
    }
    // 设定gRPC服务请求的上下设定(超时设定)
    ctx, cancel := context.WithTimeout(context.Background(), time.Second)
    defer cancel()
    // Greeter客户端发起SayHello RPC调用,同步阻塞得到响应
    r, err := c.SayHello(ctx, &pb.HelloRequest{Name: name})
    if err != nil {
        log.Fatalf("could not greet: %v", err)
    }
    // 打印响应内容Message
    log.Printf("Greeting: %s", r.Message)
    // Greeter客户端调用另一个RPC请求调用,串行化的同步阻塞
    r, err = c.SayHelloAgain(ctx, &pb.HelloRequest{Name: name})
    if err != nil {
        log.Fatalf("could not greet: %v", err)
    }
    log.Printf("Greeting: %s", r.GetMessage())
}

7. Service定义 - 4类RPC模式

  • routeguide/route_guide.proto gRPC服务定义
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
// 版本
syntax = "proto3";

// 附加属性
option java_multiple_files = true;
option java_package = "io.grpc.examples.routeguide";
option java_outer_classname = "RouteGuideProto";

package routeguide;

// 路线导航服务接口(4类)
service RouteGuide {
  // 第一类: A simple RPC.
  //
  // 获取给定位置特征描述,如果指定位置没有特征描述,则返回空
  rpc GetFeature(Point) returns (Feature) {}

  // 第二类,服务端流下发:A server-to-client streaming RPC.
  // 
  // 在矩形区域内(可能包含大量特征),获取可用的特征,结果是流(stream关键字说明),而非一次返回
  rpc ListFeatures(Rectangle) returns (stream Feature) {}

  // 第三类,客户端流上报:A client-to-server streaming RPC.
  //
  // 服务接受一个位置点的流移动,当形成结束时候返回路径汇总
  rpc RecordRoute(stream Point) returns (RouteSummary) {}

  // 第四类,双向流:A Bidirectional streaming RPC.
  //
  // Accepts a stream of RouteNotes sent while a route is being traversed,
  // while receiving other RouteNotes (e.g. from other users).
  rpc RouteChat(stream RouteNote) returns (stream RouteNote) {}
}

// Points are represented as latitude-longitude pairs in the E7 representation
// (degrees multiplied by 10**7 and rounded to the nearest integer).
// Latitudes should be in the range +/- 90 degrees and longitude should be in
// the range +/- 180 degrees (inclusive).
message Point {
  int32 latitude = 1;
  int32 longitude = 2;
}

// A latitude-longitude rectangle, represented as two diagonally opposite
// points "lo" and "hi".
message Rectangle {
  // One corner of the rectangle.
  Point lo = 1;

  // The other corner of the rectangle.
  Point hi = 2;
}

// A feature names something at a given point.
//
// If a feature could not be named, the name is empty.
message Feature {
  // The name of the feature.
  string name = 1;

  // The point where the feature is detected.
  Point location = 2;
}

// A RouteNote is a message sent while at a given point.
message RouteNote {
  // The location from which the message is sent.
  Point location = 1;

  // The message to be sent.
  string message = 2;
}

// A RouteSummary is received in response to a RecordRoute rpc.
//
// It contains the number of individual points received, the number of
// detected features, and the total distance covered as the cumulative sum of
// the distance between each point.
message RouteSummary {
  // The number of points received.
  int32 point_count = 1;

  // The number of known features passed while traversing the route.
  int32 feature_count = 2;

  // The distance covered in metres.
  int32 distance = 3;

  // The duration of the traversal in seconds.
  int32 elapsed_time = 4;
}