我们实现了一个 DaemonSet,其唯一的工作就是解析 Istiod 服务的 FQDN。从其指标来看,我们观察到在新节点引导后 DNS 名称解析几分钟后会超时,推测 Envoy 遇到了相同的超时问题。
我们确定了导致该问题的根因是 dnsRefreshRate,其在 Istio 1.5.2 中的默认值为 5 分钟,并且与观察到的延迟大致匹配。由于在启动某些 Pod 之后新节点上的 DNS 客户端已准备就绪,因此较长的重试间隔导致 Envoy 无法及时检测到 DNS 客户端的就绪状态。通过强制 Envoy 进行更频繁的重试,将附加的 Pod 启动延迟从 360 秒减少到 60 秒。
请注意,在 Istio 1.6 中,默认的 dnsRefreshRate 已更改为 5 秒。
HHVM Pod 预热和级联伸缩
我们的 BE 服务内置于 HHVM 中,该服务在其代码缓存预热之前具有很高的 CPU 使用率和高延迟。预热阶段通常需要几分钟,因此在默认的 15 秒 HPA 同步周期或 HPA 评估 CPU 使用率指标并调整所需 Pod 数量的时间间隔内,它不能很好地工作。当由于负载增加而创建新的 Pod 时,HPA 从新 Pod 中检测到更高的 CPU 使用率,并扩展了更多 Pod。这种积极的反馈循环一直持续到新的 Pod 被完全预热或达到 Pod 的最大数量为止。新的 Pod 完全预热后,HPA 检测到 CPU 使用率显著下降,并缩减了大量 Pod。级联伸缩导致了不稳定和延迟峰值。
我们进行了两项更改以解决级联伸缩问题。我们根据官方建议改进了 HHVM 预热过程。预热期间的 CPU 使用率从正常使用的 11 倍减少到 1.5 倍。Pod 开始提供流量后的 CPU 使用率从正常使用的 4 倍降低到 1.5 倍。
负载不均衡是我们迁移到 Kubernetes 期间遇到的最值得注意的挑战,尽管它仅在最大的虚拟服务中发生。症状是某些 Pod 在重负载下无法通过就绪检查,然后更多的请求将路由到这些 Pod,从而导致 Pod 在就绪状态和未就绪状态之间摆动。在这种情况下添加更多的节点或 Pod 将导致更多的飘动 Pod。发生这种情况时,延迟和错误计数会大大增加。缓解此问题的唯一方法是强行缩小部署规模,以杀死不断波动的 Pod,而无需添加新 Pod。但是,这不是可持续的解决方案,因为更多的 Pod 很快就开始波动。由于此问题,我们多次回退了迁移。
我们观察到,每次发布后,Kubernetes 上的延迟都会随时间而增加,因此我们创建了一个仪表板来显示 Envoy 在 Ingress 网关,FE 应用程序和 BE 服务 Pod 中报告的入站 / 出站延迟。仪表盘表明,总体增加是由 Envoy 在 BE Pod 中报告的入站延迟的增加所驱动的,这包括服务延迟和 Envoy 本身的延迟。由于服务延迟没有显着增加,因此代理延迟被认为是延迟增加的驱动力。我们发现,每个版本发布后,Envoy 在 BE Pod 中的内存使用量也随着时间增加,这使我们怀疑延迟增加是由于 BE Pod 中的 Envoy 的内存泄漏引起的。我们 exec 到一个 BE Pod,并列出了 Envoy 和主容器中的连接,发现 Envoy 中有大约 2800 个连接,主容器中有 40 个连接。在 2800 个连接中,绝大多数是与 FE Pod(BE Pod 的客户)连接的。
为了解决 Envoy 内存泄漏问题,我们做了一些更改,包括:
• 将 FE Pod 到 BE Pod 之间的连接的 idleTimeout 从默认的 1 小时减少到 30 秒。此更改减少了错误数量并提高了请求成功率,但同时也增加了 FE 和 BE 容器之间每秒的连接请求数量。 • 将 Envoy 的并发或线程数从 FE Pod 中的 16 减少到 2。该更改取消了自第一次更改以来每秒大多数连接请求数量的增加。 • 在 BE Pod 中将 Envoy 内存限制设置为 300MB。观察到预期的行为,并且 Envoy 的内存使用量超出限制时重新启动。容器继续运行,但是内存使用率较低。重新启动 Envoy 时,有些 Pod 有短暂的准备就绪时间,这是对前两个更改的补充。虽然前两个更改减少了 Envoy 的内存使用量,但第三个更改将在 Envoy 的内存使用量超出限制时重新启动。与重新启动主容器相比,重新启动 Envoy 所导致的停机时间明显更少,因为后者会在 HHVM 中产生几分钟的预热时间。
随着我们将更多请求迁移到 Kubernetes 中最大的虚拟服务,我们遇到了跨集群范围的资源的问题,这些资源在虚拟服务之间共享,包括 APIServer,DNS 服务器和 Istio 控制平面。在事件期间,观察到持续了一两分钟所有虚拟服务的错误峰值,我们发现这是由于未能解析 FE Pod 中 BE 虚拟服务的 DNS 名称所致。错误峰值还与 DNS 解析错误和 DNS 请求下降有关。Ingress 服务调用不应依赖于 DNS。相反,应该将 FE Pod 中的 Envoy 定向为将出站 HTTP 请求定向到 BE 服务中的端点的 IP 地址。但是,我们发现 NodeJS Thrift 客户端库对无用的服务 IP 进行了 DNS 查找。为了消除 DNS 依赖性,我们部署了 Sidecar,将 Virtual Service 中 BE 服务的主机绑定到本地套接字地址。
尽管 Istio 从应用程序角度最大程度地提高了透明度,但除了在应用程序代码中用本地 IP 地址和端口号替换 DNS 名称之外,我们还必须显式添加 Host 标头。值得一提的是,sidecar 的一个附带好处是可以优化内存使用。默认情况下,无论是否需要,Istio 都会将跨 Kubernetes 集群的每个服务的上游集群添加到 Envoy。维护那些不必要的配置的一项重大成本是 Envoy 容器的内存使用。使用 sidecar 解决方案,我们将 DNS 服务器故障与关键路径中的服务调用隔离开来,将 DNS 服务器上的 QPS 从 30,000 减少到 6,000,并将 Envoy 的平均内存使用量从 100MB 减少到 70MB。
我们遇到的另一个错误高峰与不一致的集群成员身份(节点终止时出现这种情况)有关。尽管 Kubernetes 应该能够优雅地处理节点终止,但是节点终止时有一种特殊情况会导致错误尖峰:在终止的节点上运行 Istiod pod。节点终止后,一些 FE Pod 花费了大约 17 分钟的时间从新的 Istiod Pod 接收更新。在他们收到更新之前,他们对 BE 集群成员身份看法不一致。鉴于此,这些有问题的 FE Pod 中的集群成员很可能已过时,导致它们向终止或未就绪的 BE Pod 发送请求。