如何降低 gRPC 后端重复请求的问题

1,464 阅读6分钟

gRPC 是 Google 开源的非常优秀的 RPC 框架,但由于 gRPC 的链接是粘性的,会导致请求发送到同一个后端服务,从而导致流量长时间发送到同一个服务端。

FinClip 的工程师从他们的角度聊了聊如何降低后端重复请求的问题。

概述

随着微服务架构的日趋流行,传统的业务服务正在从大而全的单体结构解体转变成小而多的分散服务。在微服务架构中,每个后端服务的职责将被细分,整体架构由大量微小服务相互调用协作来运行。这样的架构可以带来很多好处,微服务的逻辑更加简单;不同的微服务方便选择适用的编程语言和服务框架;在云原生环境中更利于做服务的生命周期管理(部署、扩容/缩容、故障转移等)。

然而,微服务带来灵活性的同时,也带来了一些问题。典型的问题是:大量微服务相互调用所带来的性能成本。针对这样的问题,选择性能更高的服务调用协议是解决问题的最佳方法,在我们的业务系统中,我们选择了 gRPC

gRPC 是一个高性能的 RPC 框架。gRPC 支持很多特性,包括结构化数据作为通信媒介、开箱即用、支持多种语言等等。不仅如此,它还支持多路复用、双向数据流、头部压缩...它是基于 HTTP/2 的。

问题

由于 gRPC 的链接是粘性的,当客户端连接到服务端时,相同的连接将尽可能长时间地保留以重复利用(multiplexed)。这样的优点是可以尽量减少新连接建立消耗的时间和资源,但是会导致请求发送到同一个后端服务,从而导致流量长时间发送到同一个服务端。

一、基于 Kubernetes 的服务发现

通常,我们的微服务是通过容器化部署的,由 Kubernetes 进行编排管理,服务之间通过 Kubernetes 的 Service 控制器进行互相访问。Kubernetes Service是基于 DNS 的,它将访问地址映射到相应服务的所有容器实例上,来实现动态管理,如图:

但是,正如前文所述,当使用 gRPC 时,由于 gRPC 的链接复用,客户端的请求将持续发送给同一个服务端,导致负载不均衡。以下我们将在 Kubernetes 环境中进行实验测试:

  1. 客户端通过 Kubernetes 的 Headless Service 访问服务端。

  2. gRPC 获取了 Service 的多个地址后会与这些地址建立子通道。

  3. 默认情况下,之后的请求将发送给其中一个子通道。

  1. 当接收请求的子通道发生断开时,gRPC 会重新建立链接,然后又从新链接中选择一个子通道进行通信。

显然,这样的方式并不利于我们的负载平衡,因为 gRPC 默认选择了 pick_first算法进行负载均衡,也就是一个后端会被持续调用,gRPC 提供了另一种常见的负载均衡算法:round_robin,我们可以在创建 channel 的时候选择默认的负载算法为round_robin,例如:

ManagedChannel channel = ManagedChannelBuilder.forTarget(target)
    .usePlaintext()
    //使用轮训调度
    .defaultLoadBalancingPolicy("round_robin")
    .build();

那么,它的行为将变得更加合理一些:

  1. 客户端通过 Kubernetes 的 Headless Service 访问服务端。

  2. gRPC 获取了 Service 的多个地址后会与这些地址建立子通道。

3. 之后的请求将轮流发送给所有子通道, 看起来正常了些。

  1. 但是,由于 gRPC 是持续链接,当我们对服务进行扩容的时候,新的服务实例并不会加入其中,只有当我们有其中的链接断开时才能触发 gRPC 的重新链接。

由此可见,当我们的服务发生变化时,利用 Kubernetes 的 Service 并不能让 gRPC 实现完美的负载。以下是在 Kubernetes 中测试的过程:

二、客户端负载均衡

另外一种方法是在调用端自己实现调用逻辑,例如定时关闭链接以触发gRPC重连、自动刷新、心跳检测、负载均衡等等。

胖客户端的方式通常是不推荐的,因为这样会导致客户端变得复杂。尤其在跨团队协作中,客户端代码的统一维护会成为挑战。

三、外部协调

如果 gRPC 本身设计使得负载均衡变得棘手,那么我们是否可以借助外界工具来进行协调?答案当然是可以的!

gRPC 提供了相应的方法,以供用户对负载均衡和服务发现进行扩展,这种模式如下图所示:

用于服务协调的开源产品有很多,例如 ZooKeeper、Etcd、Consul、Eureka 等等。通常的模式是这样的:

  1. 服务实例在启动时将自己的地址注册到注册中心
  2. 注册中心维护所有服务地址增减、健康检测、元数据管理等等
  3. 客户端程序访问注册中心获得理想的调用地址

就目前来说,这种方式是相对比较理想的。一方面可以避免 gRPC 粘性链接导致的问题,另一方面又可以获得服务管理的灵活性,当然这种方式也有缺点,因为需要引入新的组件,由于协调中心维护所有服务的访问,所以也会引入风险点,同时其维护成本也会增加。

在后续迭代中,gRPC 已经不建议使用这种方式(grpclb),官方推荐改用 xDS。

四、 xDS

xDS API 实际上是一组名称为“x 发现服务”的 API,其中“x”有很多值(LDS、RDS、CDS、EDS...因此整个协议套件的名称为“xDS”)。该协议目前没有正式的规范。

xDS 主要由 Envoy 代理使用,可以进行多种类型的配置,负载平衡当然也不在话下,并且该 API 正在演变成一种标准。

不过,目前 xDS 在 gRPC 中的实现和应用都还不是非常成熟,我们将在后续持续跟进,在未来,服务网格与 gRPC 结合将是更加完美的模式。总之,优化不停,架构不止。

总结

gRPC 有很多优点,可以为微服务架构提供很好的解决方案,在实践的同时,我们需要填补一些坑,我们就在这些填坑的过程中成长,服务架构也在这个过程中被逐步完善,业务更加稳定。