为了更好地管理不断增长的服务和流量,Houzz基础架构工程团队最近将Web Server服务从Amazon Elastic Compute Cloud(Amazon EC2)迁移到Kubernetes集群。这次迁移使资源减少了33%,首页延迟有了30%的改善。
新的Kubernetes集群的整体架构包含多个应用程序,包括用NodeJS编写的前端(FE)应用程序和用HHVM编写的后端(BE)服务。 FE应用程序通过HTTP上的Apache Thrift协议与BE服务进行通信。每个应用程序都启用了水平Pod自动缩放(HPA)。集群内以及与外部服务的通信由Istio管理,都会经过Envoy Sidecar。
迁移过程中有诸多挑战,本文主要是想和大家分享一下我们迁移过程中的最佳实践。
Pod 启动延迟
在开始Kubernetes迁移时,我们注意到Pod启动延迟有时会在新配置的节点上发生。 Envoy容器准备就绪大约花了六分钟,阻止了其他容器的启动。从Envoy日志中,我们观察到pilot-agent 不断报告Envoy尚未准备就绪,并提出了检查Istiod是否仍在运行的建议。
我们实现了一个守护程序集,其唯一的工作就是解析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倍。
此外,我们将HPA同步时间从15秒增加到10分钟。尽管HPA对负载增加的响应速度较慢,但它避免了级联扩展,因为大多数Pod可以在10分钟内完成预热并开始正常使用CPU。我们发现这是一个值得权衡的选择。
更改之前的HPA活动:
更改之后的活动:
负载均衡
负载不均衡是我们迁移到Kubernetes期间遇到的最值得注意的挑战,尽管它仅在最大的虚拟服务中发生。症状是某些Pod在重负载下无法通过就绪检查,然后更多的请求将路由到这些Pod,从而导致Pod在就绪状态和未就绪状态之间摆动。在这种情况下添加更多的节点或Pod将导致更多的飘动Pod。发生这种情况时,延迟和错误计数会大大增加。缓解此问题的唯一方法是强行缩小部署规模,以杀死不断波动的Pod,而无需添加新Pod。但是,这不是一个可持续的解决方案,因为更多的Pod很快就开始飘动。由于此问题,我们多次回退了迁移。
负载均衡:
为了排查方便,我们添加了额外的日志记录,发现负载不均衡时触发,一个可用区(AZ)的请求明显多于其他两个。我们怀疑这种不平衡是由于我们当时使用的最少请求负载均衡策略中的正反馈回路造成的。我们尝试了其他几种策略(Round Robin,Locality Aware和Random),都没有解决问题。 在排除了负载均衡策略之后,我们在其他两个方面寻找了积极的反馈循环:重试失败的请求和异常检测。尽管Istio的官方文档中指出默认情况下不会对失败的请求进行重试,但实际的默认重试次数设置为2。重试会导致级联失败,因为某些请求失败后会发送更多请求。此外,我们观察到异常检测中的某些行为(也称为被动健康检查)无法解释,因此我们决定禁用这两个功能。之后,不平衡问题消失了,我们能够将95%的请求迁移到Kubernetes。我们在旧平台上保留5%的资源用于性能比较和调整。 最初,我们不确定重试或异常检测这两个功能中的哪一个是造成负载不均衡的原因,尽管我们现在认为它与重试有关。在将Istio升级到1.6版,进行了一些性能改进并将100%的请求迁移到Kubernetes之后,我们尝试重新启用离群值检测-我们愿意承担这种风险,因为更改可以在几秒钟内恢复。在撰写本文时,我们还没有遇到负载不均衡的问题。就是说,我们用以下事实证明了我们的理论,即当前Istio版本的集群配置与发生不均衡时的配置不同。
发布后性能下降
我们观察到,每次发布后,Kubernetes上的延迟都会随时间增加,因此我们创建了一个仪表板来显示Envoy在Ingress网关,FE应用程序和BE服务Pod中报告的入站/出站延迟。仪表板表明,总体增加是由Envoy在BE Pod中报告的入站延迟的增加所驱动的,这包括服务延迟和Envoy本身的延迟。由于服务延迟没有显着增加,因此代理延迟被认为是延迟增加的驱动力。 我们发现,每个版本发布后,Envoy在BE Pod中的内存使用量也随着时间增加,这使我们怀疑延迟增加是由于BE Pod中的Envoy的内存泄漏引起的。我们exec到一个BE Pod,并列出了Envoy和主容器中的连接,发现Envoy中有大约2800个连接,主容器中有40个连接。在2,800个连接中,绝大多数是与FE Pod(BE Pod的客户)连接的。
为了解决Envoy内存泄漏问题,我们测试了一些更改,包括:
1.将FE Pod到BE Pod之间的连接的idleTimeout从默认的1小时减少到30秒。此更改减少了错误数量并提高了请求成功率,但同时也增加了FE和BE容器之间每秒的连接请求数量。 2.将Envoy的并发或线程数从FE Pod中的16减少到2。该更改取消了自第一次更改以来每秒大多数连接请求数量的增加。 3.在BE Pod中将Envoy内存限制设置为300MB。观察到预期的行为,并且Envoy的内存使用量超出限制时重新启动。容器继续运行,但是内存使用率较低。重新启动Envoy时,有些Pod有短暂的准备就绪时间,这是对前两个更改的补充。虽然前两个更改减少了Envoy的内存使用量,但第三个更改将在Envoy的内存使用量超出限制时重新启动。与重新启动主容器相比,重新启动Envoy所导致的停机时间明显更少,因为后者会在HHVM中产生几分钟的预热时间。
变更之前的延迟:
变更之前内存变化:
变更之后的延迟:
解决了发布后的性能下降问题之后,我们将100%的请求迁移到Kubernetes并关闭了旧主机。
集群范围内的瓶颈
随着我们将更多请求迁移到Kubernetes中最大的虚拟服务,我们遇到了跨集群范围的资源的问题,这些资源在虚拟服务之间共享,包括API服务器,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服务的主机绑定到本地套接字地址。
Sidecar清单示例:
尽管Istio从应用程序角度最大程度地提高了透明度,但除了在应用程序代码中用本地IP地址和端口号替换DNS名称之外,我们还必须显式添加Host标头。 值得一提的是,sidecar的一个附带好处是可以优化内存使用。默认情况下,无论是否需要,Istio都会将跨Kubernetes集群的每个服务的上游集群添加到Envoy。维护那些不必要的配置的一项重大成本是Envoy容器的内存使用。 使用sidecar解决方案,我们将DNS服务器故障与关键路径中的服务调用隔离开来,将DNS服务器上的QPS从30,000减少到6,000,并将Envoy的平均内存使用量从100MB减少到70MB。
coreDNS QPS的变化:
变更后内存使用情况:
我们遇到的另一个错误高峰与不一致的集群成员身份(节点终止时出现这种情况)有关。尽管Kubernetes应该能够优雅地处理节点终止,但是节点终止时有一种特殊情况会导致错误尖峰:在终止的节点上运行Istiod pod。节点终止后,一些FE Pod花费了大约17分钟的时间从新的Istiod Pod接收更新。在他们收到更新之前,他们对BE 集群成员身份看法不一致。鉴于此,这些有问题的FE Pod中的集群成员很可能已过时,导致它们向终止或未就绪的BE Pod发送请求。
集群成员身份不一致且是旧数据:
我们发现tcpKeepalive选项在检测终止的Istiod Pod中起作用。在我们的Istio设置中,将keepaliveTime,keepaliveProbes和keepaliveInterval分别设置为默认值300秒,9秒和75秒。从理论上讲,Envoy可能需要至少300秒加9,再乘以75秒(16.25分钟),才能检测到终止的Istiod Pod。我们通过将tcpKeepalive选项自定义为更低的值来解决了这个问题。
建立大规模的Kubernetes集群具有挑战性,并且对大家来说非常有意义。我们希望你从我们的经验中找到有用的信息。
PS: 本文属于翻译,原文