在开发生产级 gRPC 应用程序时,通常需要确保该应用程序能够满足高可用性和高扩展性的需求。因此,在生产环境中,始终需要多个 gRPC服务器端。在这些服务之间分发 RPC 需要由某个实体来处理,这就需要使用负载均衡器了。gRPC 通常使用两种主要的负载均衡机制:负载均衡器代理和客户端负载均衡。这里先从负载均衡器代理开始讨论。
5.7.1 负载均衡器代理
如图 5-6 所示,在代理负载均衡场景中,客户端向负载均衡器代理发起RPC。随后,负载均衡器代理将 RPC 分发给一台可用的后端 gRPC 服务器,该后端 gRPC 服务器实现了满足服务调用的逻辑。负载均衡器代理会跟踪每台后端服务器的负载,并为后端服务分配负载提供不同的负载均衡算法。
后端服务的拓扑结构对 gRPC 客户端是不透明的,它们只知道负载均衡器的端点就可以了。因此,为了满足负载均衡的使用场景,除了使用负载均衡器作为 gRPC 连接的目的地外,在客户端无须任何变更。后端服务可以将负载情况报告给负载均衡器,这样它就能使用该信息确定负载均衡的逻辑。
在理论上,可以选择任意支持 HTTP/2 的负载均衡器作为 gRPC 应用程序的负载均衡器代理。但是,它必须完全支持 HTTP/2。因此,选择明确提供 gRPC 支持的负载均衡器是很明智的做法。例如,可以使用Nginx 代理、Envoy 代理等作为 gRPC 应用程序的负载均衡器代理。
如果不使用 gRPC 负载均衡器,那么可以在编写的客户端应用程序中实现负载均衡逻辑。下面来更详细地了解客户端负载均衡。
5.7.2 客户端负载均衡
这个方案不再借助负载均衡的中间代理层,而是在 gRPC 客户端层实现负载均衡的逻辑。在这种方法中,客户端要知道多台后端 gRPC 服务器,并为每个 RPC 选择一台后端 gRPC 服务器。如图 5-7 所示,负载均衡逻辑可以完全作为客户端应用程序(也被称为厚客户端)的一部分来进行开发,也可以实现为一个专用的服务器端,叫作后备负载均衡器。
客户端可以查询它,从而选择最优的 gRPC 服务器来进行连接。客户端直接连接到选定的 gRPC 服务器,其地址从后备负载均衡器获取。
为了理解客户端负载均衡的实现方式,这里看一个使用 Go 语言的厚客户端实现。在该使用场景中,假设有两个后端 gRPC 服务,它们分别在:50051
和 :50052
上运行 echo 服务器端。这些 gRPC 服务在 RPC 响应中会包含提供服务的服务器地址。因此,可以将这两个服务作为一个 echogRPC 服务集群的两个成员。现在,假设希望构建一个 gRPC 客户端应(round-robin algorithm,轮流执行每个端点),而另一个客户端始终选择第一个服务器端点。代码清单 5-16 展示了厚客户端负载均衡实现。
可以看到,客户端访问了 example:///lb.example.grpc.io
,这里使用example 模式名和 lb.example.grpc.io
作为服务器名称。基于该模式,它会查找命名解析器来发现后端服务地址的绝对值。根据命名解析器所返回的值列表,gRPC 针对这些服务器端运行不同的负载均衡算法。该行为是通过 grpc.WithBalancerName("round_robin")
配置的。
代码清单 5-16 使用厚客户端的客户端负载均衡
pickfirstConn, err := grpc.Dial(
fmt.Sprintf("%s:///%s",
// exampleScheme = "example"
// exampleServiceName = "lb.example.grpc.io"
exampleScheme, exampleServiceName), ➊
// pick_first是默认选项 ➋
grpc.WithBalancerName("pick_first"),
grpc.WithInsecure(),
)
if err != nil {
log.Fatalf("did not connect: %v", err)
}
defer pickfirstConn.Close()
log.Println("==== Calling helloworld.Greeter/SayHello with pick_first ====")
makeRPCs(pickfirstConn, 10)
// 使用round_robin策略生成另一个ClientConn
roundrobinConn, err := grpc.Dial(
fmt.Sprintf("%s:///%s", exampleScheme, exampleServiceName),
// "example:///lb.example.grpc.io"
grpc.WithBalancerName("round_robin"), ➌
grpc.WithInsecure(),
)
if err != nil {
log.Fatalf("did not connect: %v", err)
}
defer roundrobinConn.Close()
log.Println("==== Calling helloworld.Greeter/SayHello with round_robin ====")
makeRPCs(roundrobinConn, 10)
- ❶ 使用模式和服务名创建 gRPC 连接。模式是通过模式解析器解析的,它是客户端应用程序的一部分。
- ❷ 指定负载均衡算法,该算法会使用服务器端点列表中的第一个服务器端。
- ❸ 使用轮询调度算法。
gRPC 有两个默认支持的负载均衡算法:pick_first
和round_robin
。pick_first
会尝试连接第一个地址,如果能够连接成功,就会将该地址用于所有的 RPC;如果失败,则会尝试下一个地址。round_robin 会连接所有地址,并会按顺序每次向后端发送一个RPC。
在代码清单 5-16 所示的客户端负载均衡场景中,有一个解析 example模式的模式解析器,它包含发现端点 URL 实际值的逻辑。下面讨论关于压缩的话题。对于通过 RPC 发送大量数据的场景来说,这是 gRPC另一个常用的特性。
5.7.3 压缩
为了高效利用网络带宽,在执行客户端和服务之间的 RPC 时,可以使用压缩技术。如果要在客户端使用压缩技术,那么可以通过在发送 RPC时设置一个压缩器来实现。例如,在 Go 语言中,借助client.AddOrder(ctx, &order1,grpc.UseCompressor(gzip.Name))
便可以很容易地实现。可以在GoDoc 网站上搜索 encoding/gzip
来了解更多信息。
在服务器端,已注册的压缩器会自动解码请求消息,并编码响应消息。
在 Go 语言中,注册压缩器只需在 gRPC 服务器端应用程序中导入GoDoc 网站上的 gzip 包即可(获取方式同上)。服务器端始终会使用客户端所指定的压缩方法。如果对应的压缩器没有注册,则会向客户端返回一个 Unimplemented 状态。