“Kubernetes 将延迟增加了 10 倍”:谁该为此负责?

笔记。 翻译。:这篇文章由欧洲公司 Adevinta 首席软件工程师 Galo Navarro 撰写,是基础设施运营领域的一项引人入胜且富有启发性的“调查”。 其原标题在翻译中略有扩展,原因作者在一开始就解释过。

“Kubernetes 将延迟增加了 10 倍”:谁该为此负责?

来自作者的注释: 好像是这个帖子 被吸引 比预期受到更多关注。 我仍然收到一些愤怒的评论,认为文章的标题具有误导性,一些读者感到难过。 我了解发生这种情况的原因,因此,尽管有毁掉整个阴谋的风险,我还是想立即告诉你这篇文章的内容。 当团队迁移到 Kubernetes 时,我看到的一件奇怪的事情是,每当出现问题(例如迁移后延迟增加)时,第一个受到指责的是 Kubernetes,但后来发现编排器并没有真正负责责备。 本文讲述了这样一个案例。 它的名字重复了我们一位开发人员的感叹(稍后你会发现 Kubernetes 与它无关)。 在这里你不会发现任何关于 Kubernetes 的令人惊讶的启示,但你可以期待一些关于复杂系统的好课程。

几周前,我的团队将单个微服务迁移到一个核心平台,其中包括 CI/CD、基于 Kubernetes 的运行时、指标和其他功能。 此次搬迁属于试点性质:我们计划以此为基础,在未来几个月内再转移约150个服务。 他们都负责西班牙一些最大的在线平台(Infojobs、Fotocasa 等)的运营。

在我们将应用程序部署到 Kubernetes 并将一些流量重定向到它之后,一个令人震惊的惊喜等待着我们。 延迟 (潜伏) Kubernetes 中的请求比 EC10 中的请求高 2 倍。 一般来说,要么找到这个问题的解决方案,要么放弃微服务(可能还有整个项目)的迁移。

为什么 Kubernetes 中的延迟比 EC2 中的延迟高得多?

为了找到瓶颈,我们收集了整个请求路径上的指标。 我们的架构很简单:API 网关 (Zuul) 将请求代理到 EC2 或 Kubernetes 中的微服务实例。 在 Kubernetes 中,我们使用 NGINX Ingress Controller,后端是普通对象,例如 部署 使用 Spring 平台上的 JVM 应用程序。

                                  EC2
                            +---------------+
                            |  +---------+  |
                            |  |         |  |
                       +-------> BACKEND |  |
                       |    |  |         |  |
                       |    |  +---------+  |                   
                       |    +---------------+
             +------+  |
Public       |      |  |
      -------> ZUUL +--+
traffic      |      |  |              Kubernetes
             +------+  |    +-----------------------------+
                       |    |  +-------+      +---------+ |
                       |    |  |       |  xx  |         | |
                       +-------> NGINX +------> BACKEND | |
                            |  |       |  xx  |         | |
                            |  +-------+      +---------+ |
                            +-----------------------------+

该问题似乎与后端的初始延迟有关(我在图表上将问题区域标记为“xx”)。 在 EC2 上,应用程序响应大约需要 20 毫秒。 在 Kubernetes 中,延迟增加到 100-200 毫秒。

我们很快就排除了与运行时更改相关的可能嫌疑。 JVM 版本保持不变。 容器化问题也与此无关:应用程序已经在 EC2 上的容器中成功运行。 加载中? 但我们观察到即使每秒 1 个请求,延迟也很高。 垃圾收集的暂停也可以忽略。

我们的一位 Kubernetes 管理员想知道应用程序是否具有外部依赖项,因为 DNS 查询过去曾引起过类似的问题。

假设 1:DNS 名称解析

对于每个请求,我们的应用程序都会在以下域中访问 AWS Elasticsearch 实例一到三次 elastic.spain.adevinta.com。 在我们的容器内 有一个壳,因此我们可以检查搜索域是否确实需要很长时间。

来自容器的 DNS 查询:

[root@be-851c76f696-alf8z /]# while true; do dig "elastic.spain.adevinta.com" | grep time; sleep 2; done
;; Query time: 22 msec
;; Query time: 22 msec
;; Query time: 29 msec
;; Query time: 21 msec
;; Query time: 28 msec
;; Query time: 43 msec
;; Query time: 39 msec

来自运行应用程序的 EC2 实例之一的类似请求:

bash-4.4# while true; do dig "elastic.spain.adevinta.com" | grep time; sleep 2; done
;; Query time: 77 msec
;; Query time: 0 msec
;; Query time: 0 msec
;; Query time: 0 msec
;; Query time: 0 msec

考虑到查找花费了大约 30 毫秒,很明显访问 Elasticsearch 时的 DNS 解析确实导致了延迟的增加。

然而,这很奇怪,原因有二:

  1. 我们已经拥有大量 Kubernetes 应用程序,它们可以与 AWS 资源交互,而不会遭受高延迟的影响。 不管是什么原因,都与本案有关。
  2. 我们知道 JVM 会在内存中进行 DNS 缓存。 在我们的图像中,TTL 值写入 $JAVA_HOME/jre/lib/security/java.security 并设置为 10 秒: networkaddress.cache.ttl = 10。 换句话说,JVM 应该将所有 DNS 查询缓存 10 秒。

为了证实第一个假设,我们决定暂时停止调用 DNS,看看问题是否消失。 首先,我们决定重新配置应用程序,使其通过 IP 地址直接与 Elasticsearch 通信,而不是通过域名。 这需要更改代码和新的部署,因此我们只需将域映射到其 IP 地址 /etc/hosts:

34.55.5.111 elastic.spain.adevinta.com

现在容器几乎立即就收到了 IP。 这带来了一些改进,但我们仅稍微接近预期的延迟水平。 尽管DNS解析花了很长时间,但真正的原因我们仍然不明白。

通过网络诊断

我们决定使用以下方法分析来自容器的流量 tcpdump查看网络上到底发生了什么:

[root@be-851c76f696-alf8z /]# tcpdump -leni any -w capture.pcap

然后我们发送了几个请求并下载了它们的捕获(kubectl cp my-service:/capture.pcap capture.pcap)进一步分析 Wireshark的.

DNS 查询没有任何可疑之处(除了我稍后将讨论的一件小事)。 但我们的服务处理每个请求的方式存在某些奇怪之处。 下面是捕获的屏幕截图,显示在响应开始之前请求已被接受:

“Kubernetes 将延迟增加了 10 倍”:谁该为此负责?

包裹编号显示在第一列中。 为了清楚起见,我对不同的 TCP 流进行了颜色编码。

从数据包 328 开始的绿色流显示了客户端 (172.17.22.150) 如何建立到容器 (172.17.36.147) 的 TCP 连接。 初次握手(328-330)后,包 331 带来了 HTTP GET /v1/.. — 对我们服务的传入请求。 整个过程耗时1毫秒。

灰色流(来自数据包 339)显示我们的服务向 Elasticsearch 实例发送了一个 HTTP 请求(没有 TCP 握手,因为它正在使用现有连接)。 这花了 18 毫秒。

到目前为止,一切都很好,时间大致符合预期的延迟(从客户端测量时为 20-30 毫秒)。

然而,蓝色部分需要 86ms。 这里面到底发生了什么? 通过数据包 333,我们的服务向 /latest/meta-data/iam/security-credentials,紧接着,通过同一个 TCP 连接,另一个 GET 请求 /latest/meta-data/iam/security-credentials/arn:...

我们发现整个跟踪过程中的每个请求都会重复这种情况。 在我们的容器中,DNS 解析确实有点慢(对这种现象的解释非常有趣,但我将把它保存在单独的文章中)。 事实证明,长时间延迟的原因是每个请求都调用 AWS 实例元数据服务。

假设 2:不必要的 AWS 调用

两个端点都属于 AWS 实例元数据 API。 我们的微服务在运行 Elasticsearch 时使用此服务。 这两个调用都是基本授权过程的一部分。 第一个请求访问的终端节点颁发与实例关联的 IAM 角色。

/ # curl http://169.254.169.254/latest/meta-data/iam/security-credentials/
arn:aws:iam::<account_id>:role/some_role

第二个请求向第二个端点请求此实例的临时权限:

/ # curl http://169.254.169.254/latest/meta-data/iam/security-credentials/arn:aws:iam::<account_id>:role/some_role`
{
    "Code" : "Success",
    "LastUpdated" : "2012-04-26T16:39:16Z",
    "Type" : "AWS-HMAC",
    "AccessKeyId" : "ASIAIOSFODNN7EXAMPLE",
    "SecretAccessKey" : "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
    "Token" : "token",
    "Expiration" : "2017-05-17T15:09:54Z"
}

客户端可以在短时间内使用它们,并且必须定期获取新证书(在它们被使用之前) Expiration)。 该模型很简单:出于安全原因,AWS 会频繁轮换临时密钥,但客户端可以将它们缓存几分钟,以补偿与获取新证书相关的性能损失。

AWS Java SDK 应该接管组织此过程的责任,但由于某种原因,这种情况没有发生。

在GitHub上搜索问题后,我们遇到了一个问题 #1921。 她帮助我们确定了进一步“挖掘”的方向。

当发生以下情况之一时,AWS 开发工具包会更新证书:

  • 截止日期 (Expiration) 掉进 EXPIRATION_THRESHOLD,硬编码为 15 分钟。
  • 自上次尝试更新证书以来已过去的时间超过 REFRESH_THRESHOLD,硬编码 60 分钟。

为了查看我们收到的证书的实际到期日期,我们从容器和 EC2 实例运行上述 cURL 命令。 从集装箱收到的证书的有效期要短得多:正好 15 分钟。

现在一切都变得清楚了:对于第一个请求,我们的服务收到了临时证书。 由于它们的有效期超过 15 分钟,AWS 开发工具包将决定根据后续请求更新它们。 每次请求都会发生这种情况。

为什么证书的有效期变短了?

AWS 实例元数据旨在与 EC2 实例配合使用,而不是与 Kubernetes 配合使用。 另一方面,我们不想改变应用程序界面。 为此我们使用了 凯姆 - 该工具使用每个 Kubernetes 节点上的代理,允许用户(将应用程序部署到集群的工程师)将 IAM 角色分配给 Pod 中的容器,就像它们是 EC2 实例一样。 KIAM 拦截对 AWS 实例元数据服务的调用,并从其缓存中处理这些调用(之前已从 AWS 接收到这些调用)。 从应用程序的角度来看,没有任何变化。

KIAM 向 Pod 提供短期证书。 考虑到 pod 的平均寿命比 EC2 实例的平均寿命短,这是有道理的。 证书默认有效期 等于同样的15分钟.

因此,如果将两个默认值叠加在一起,就会出现问题。 提供给应用程序的每个证书都会在 15 分钟后过期。 但是,AWS Java SDK 会强制续订距到期日期不足 15 分钟的任何证书。

因此,每次请求都必须更新临时证书,这需要多次调用 AWS API,并导致延迟显着增加。 在AWS Java SDK中我们发现 功能要求,其中提到了类似的问题。

事实证明,解决方案很简单。 我们只是重新配置 KIAM 以请求具有更长有效期的证书。 一旦发生这种情况,请求就开始在没有 AWS 元数据服务参与的情况下流动,并且延迟下降到比 EC2 更低的水平。

发现

根据我们的迁移经验,最常见的问题来源之一不是 Kubernetes 或平台其他元素中的错误。 它也没有解决我们正在移植的微服务中的任何根本缺陷。 问题常常仅仅因为我们将不同的元素放在一起而出现。

我们将以前从未相互作用过的复杂系统混合在一起,期望它们一起形成一个更大的系统。 唉,元素越多,出错的空间就越大,熵就越高。

在我们的案例中,高延迟并不是 Kubernetes、KIAM、AWS Java SDK 或我们的微服务中的错误或错误决策造成的。 这是两个独立的默认设置组合的结果:一个在 KIAM 中,另一个在 AWS Java SDK 中。 单独来看,这两个参数都是有意义的:AWS Java SDK 中的主动证书续订策略和 KAIM 中的证书有效期较短。 但当你把它们放在一起时,结果就变得不可预测。 两个独立且合乎逻辑的解决方案组合起来不一定有意义。

译者PS

您可以了解有关用于将 AWS IAM 与 Kubernetes 集成的 KIAM 实用程序架构的更多信息: 本文 来自它的创造者。

另请阅读我们的博客:

来源: habr.com

添加评论