一切都始于我向我们的高级软件工程师提出的一个问题:“忘掉通信速度。你真的觉得在gRPC中开发通信比REST更好吗?”我不想听到的答案立刻就来了:“绝对是的。”
在我提出这个问题之前,我一直在监控我们的服务在滚动更新和扩展Pod时出现的奇怪行为。我们的大多数微服务以往都通过REST调用进行通信,没有任何问题。我们已经将一些这些集成迁移到了gRPC,主要是因为我们想摆脱REST的开销。最近,我们观察到了一些问题,都指向了同一个方向——我们的gRPC通信。当然,我们遵循了在Kubernetes中运行gRPC而不使用服务网格的建议实践,我们在服务器上使用了一个无头服务对象,并在gRPC中使用了客户端的“轮询”负载平衡与DNS发现等。
扩展Pod数量
Kubernetes内部负载均衡器不是用于负载均衡RPC,而是用于负载均衡TCP连接。第四层负载均衡器由于其简单性而很常见,因为它们与协议无关。但是,gRPC破坏了Kubernetes提供的连接级负载均衡。这是因为gRPC是基于HTTP/2构建的,而HTTP/2被设计为维护一个长期存在的TCP连接,该连接中的所有请求都可以在任何时间点同时处于活动状态。这减少了连接管理的开销。然而,在这种情况下,连接级别的负载平衡并不是非常有用,因为一旦建立了连接,就不再需要进行负载平衡。所有的请求都会固定到原始目标Pod,直到发生新的DNS发现(使用无头服务)。这不会发生,直到至少有一个现有连接断开。
问题示例:
- 2个客户端(A)调用2个服务器(B)。
- 自动缩放器介入并扩展了客户端。
- 服务器Pod负载过重,因此自动缩放器介入并增加了服务器Pod的数量,但没有进行负载平衡。甚至可以看到新Pod上没有传入的流量。
- 客户端被缩减。
- 客户端再次扩展,但负载仍然不平衡。
- 一个服务器Pod因过载而崩溃,发生了重新发现。
- 在图片中没有显示,但是当Pod恢复时,情况看起来与图3类似,即新Pod不会接收流量。
gRPC负载均衡的示例
两个配置解决这个问题,技术上说是一行
正如我之前提到的,我们使用“客户端负载均衡”,并使用无头服务对象进行DNS发现。其他选项可能包括使用代理负载均衡或实现另一种发现方法,该方法将询问Kubernetes API而不是DNS。
除此之外,gRPC文档提供了服务器端连接管理提案,我们也尝试过它。
以下是我为设置以下服务器参数提供的建议,以及gRPC初始化的Go代码片段示例:
- 将MAX_CONNECTION_AGE设置为30秒。这个时间段足够长,可以在没有昂贵且频繁的连接建立过程的情况下进行低延迟通信。此外,它允许服务相对快速地响应新Pod的存在,因此流量分布将保持平衡。
- 将MAX_CONNECTION_AGE_GRACE设置为10秒。定义了连接保持活动状态以完成未完成的RPC的最大时间。
grpc.KeepaliveParams(keepalive.ServerParameters{
MaxConnectionAge: time.Second * 30, // THIS one does the trick
MaxConnectionAgeGrace: time.Second * 10,
})