手记

如何用三条配置行解决我们在Kubernetes中遇到的gRPC扩展问题

这一切都始于我向我们资深软件工程师提出的一个问题:
“嗯,不考虑通信的速度。你觉得用gRPC来开发通信真的比用REST更好吗?”
我没想到的答案立刻来了: “那还用说。”

在我提出这个问题之前,我在一个滚动更新期间监控我们服务的一种奇怪行为,尤其是在增加pod数量时。我们大多数微服务过去通过REST调用进行通信,一直都没有遇到问题。为了减少REST带来的开销,我们已经将一些集成从REST迁移到了gRPC。最近,我们注意到一些问题,这些问题都指向同一个方向:我们的gRPC通信。当然,我们遵循了在Kubernetes中运行gRPC而不需要服务网格的建议实践,比如在这篇博客文章中描述的方法,我们在服务器端使用了无头服务,并在gRPC客户端实现了基于DNS发现的“轮询”负载均衡等。

调整Pod副本数量

Kubernetes内部负载均衡器不均衡RPC请求,而是均衡TCP连接。你可以在我另一篇博客文章中了解更多关于Kubernetes如何平衡TCP连接的信息。
第4层负载均衡器很常见,因为它们简单且协议无关。然而,gRPC打破了Kubernetes提供的连接级负载均衡。这是因为gRPC是基于HTTP/2的,而HTTP/2设计为保持一个长时间的TCP连接,使得所有的请求可以在任何时间点都活跃在同一连接上。这减少了连接管理的开销。然而,在这种情况下,连接级均衡并不太有用,因为一旦连接建立,就不再需要进行均衡了。所有请求都会固定在原始目的Pod上,如下所示,直到新的DNS发现发生(带有无头服务)。这不会发生,除非现有的至少一个连接断开

问题示例:

  1. 2个客户端(A)与2个服务器(B)通信。
  2. 自动缩放器介入并增加客户端数量。
  3. 服务器Pod过载之后,自动缩放器介入并增加服务器Pod数量,但没有进行负载均衡。我们甚至可以看到新Pod没有收到任何流量。
  4. 客户端数量减少。
  5. 客户端数量再次增加,但负载仍然未均衡。
  6. 一个服务器Pod因过载而崩溃——重新发现开始。
  7. 虽然图片中没有显示,但当Pod恢复后,情况类似于图片3,即新Pod没有收到任何流量。

gRPC负载均衡 的例子

2 行配置就解决这个问题了。 不过其实是 1 行就够了

如我之前提到的,我们使用“客户端侧的负载均衡”,通过DNS发现使用无头服务。其他可能的选项包括使用代理负载均衡或实现另一种类似的方法,这种方法会通过Kubernetes API来查询,而不是通过DNS。

除此之外,gRPC 文档还提供了服务器端连接管理提案,我们也试了试。

以下是我对于设置服务器参数的建议,附有一个用Go代码初始化gRPC的示例:

  • MAX_CONNECTION_AGE 设置为 30 秒。这样足够长的时间可以实现低延迟通信,同时避免频繁且昂贵的连接建立过程。此外,它还允许服务能够相对快速地响应新 pod 的存在,使流量分配更为均衡。
  • MAX_CONNECTION_AGE_GRACE 设置为 10 秒。定义了连接在完成 RPC 请求前可保持活跃的最大时间。
      grpc.KeepaliveParams(keepalive.ServerParameters{  
          MaxConnectionAge:      time.Second * 30,  // 这一个参数奏效了  
          MaxConnectionAgeGrace: time.Second * 10,  // 最大连接年龄宽限期  
      })

它在现实生活中的表现:

应用 gRPC 配置变更前后的 pod 数量

在 gRPC 配置更改后,新 pod 中观察到的网络 I/O 活动情况

接下来是第三行

缩放问题已经搞定,但另一个问题变得更加明显。重点转向了客户端进行滚动更新(即逐个更新 Pod)时出现的 gRPC code=UNAVAILABLE 问题。有趣的是,这种情况只在滚动更新期间出现,而在单个 Pod 的缩放事件期间却没有发现。

滚动更新过程中出现的 gRPC 错误次数

滚动部署的过程很简单:创建一个新的replicaset,然后创建一个新的pod,当新pod准备好后,旧pod会被终止,依此类推。每次新pod启动之间的时间间隔为15秒。我们了解到,关于gRPC DNS重新发现,它只会在旧连接中断或收到_GOAWAY_信号时启动。因此,客户端每15秒启动一次新的重新发现,但得到的是过时的DNS记录。客户端会不断尝试重新发现,直到成功。

基本上都是 DNS 的问题……除非另有他因

DNS TTL缓存几乎在每个地方都可以找到。基础设施的DNS也有它自己的缓存。Java客户端受到默认30秒TTL缓存的影响比通常不实现DNS缓存的Go客户端要大得多。Go客户端报告的问题较少,而Java客户端报告了数百甚至数千次的问题。那么,在滚动更新期间仅影响gRPC时,为什么要做这样的改变呢?

幸运的是,有一个简单易行的解决办法:在新 pod 启动时设置 30 秒延迟。

最小就绪秒数设为30

Kubernetes部署规范允许我们设置一个新的Pod必须准备好之前的一个最短时间间隔,之后才会开始终止旧Pod。过了这段时间之后,连接会被终止,gRPC客户端会收到GOAWAY信号并开始重新寻找服务。TTL已经过期,因此客户端会获取新的、更新的记录。

结论部分

gRPC 在配置方面就像一把瑞士军刀,可能默认情况下并不适合你的基础设施或应用程序。多看看文档,微调一下,多试试,充分利用现有资源。我觉得可靠且有弹性的通信应该是你最想达到的目标。

我建议你也看看:

  • Keepalives。对于短寿命的内部集群连接来说,这个功能没有意义,但在其他情况下可能会很有用。
  • 重试。先尝试一些退避重试,而不是直接创建新连接,以避免过载基础设施。
  • 代码映射。将您的 gRPC 响应代码映射到众所周知的 HTTP 状态码,以更好地理解发生了什么。
  • 负载均衡。平衡至关重要。不要忘记设置退避并进行全面测试。
  • 服务器访问日志(gRPC code=OK)默认情况下设置为 info 级别,可能会过于详细。考虑将它们调整为 debug 级别并进行过滤。
0人推荐
随时随地看视频
慕课网APP