写点什么

Kubernetes 用了,延迟高了 10 倍,问题在哪?

  • 2019-11-20
  • 本文字数:4544 字

    阅读完需:约 15 分钟

Kubernetes用了,延迟高了10倍,问题在哪?

当我们团队将业务迁移至 Kubernetes 之后,一旦出现问题,总有人觉得“这是迁移之后的阵痛”,并把矛头指向 Kubernetes。我相信很多朋友肯定听人说过标题里这句话,但最终事实证明犯错的并不是 Kubernetes。虽然文章并不涉及关于 Kubernetes 的突破性启示,但我认为内容仍值得各位管理复杂系统的朋友借鉴。



近期,我所在的团队将一项微服务迁移到中央平台。这套中央平台捆绑有 CI/CD,基于 Kubernetes 的运行时以及其他功能。这项演习也将作为先头试点,用于指导未来几个月内另外 150 多项微服务的进一步迁移。而这一切,都是为了给西班牙的多个主要在线平台(包括 Infojobs、Fotocasa 等)提供支持。


在将应用程序部署到Kubernetes并路由一部分生产流量过去后,情况开始发生变化。Kubernetes 部署中的请求延迟要比 EC2 上的高出 10 倍。如果不找到解决办法,不光是后续微服务迁移无法正常进行,整个项目都有遭到废弃的风险。

为什么 Kubernetes 中的延迟要远高于 EC2?

为了查明瓶颈,我们收集了整个请求路径中的指标。这套架构非常简单,首先是一个 API 网关(Zuul),负责将请求代理至运行在 EC2 或者 Kubernetes 中的微服务实例。在 Kubernetes 中,我们仅代表和 NGINX Ingress 控制器,后端则为运行有基于 Spring 的 JVM 应用程序的 Deployment 对象。


                          EC2                          +---------------+                        |  +---------+  |                        |  |         |  |                     +-------> BACKEND |  |                    |    |  |         |  |                    |    |  +---------+  |                                      |    +---------------+         +------+  |Public       |      |  |      -------> ZUUL +--+traffic      |      |  |              Kubernetes             +------+  |    +-----------------------------+                       |    |  +-------+      +---------+ |                       |    |  |       |  xx  |         | |                       +-------> NGINX +------> BACKEND | |                            |  |       |  xx  |         | |                            |  +-------+      +---------+ |                            +-----------------------------+
复制代码


问题似乎来自后端的上游延迟(我在图中以「xx」进行标记)。将应用程序部署至 EC2 中之后,系统大约需要 20 毫秒就能做出响应。但在 Kubernetes 中,整个过程却需要 100 到 200 毫秒。


我们很快排除了随运行时间变化而可能出现的可疑对象。JVM 版本完全相同,而且由于应用程序已经运行在 EC2 容器当中,所以问题也不会源自容器化机制。另外,负载强度也是无辜的,因为即使每秒只发出 1 项请求,延迟同样居高不下。另外,GC 暂停时长几乎可以忽略不计。


我们的一位 Kubernetes 管理员询问这款应用程序是否具有外部依赖项,因为 DNS 解析之前就曾引起过类似的问题,这也是我们目前找到的可能性最高的假设。

可能原因

假设一:DNS 解析

在每一次请求时,我们的应用程序都像域中的某个 AWS ElasticSearch 实例(例如 elastic.spain.adevinta.com)发出 1 到 3 条查询。我们在容器中添加了一个 shell,用于验证该域名的 DNS 解析时间是否过长。


来自容器的 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 解析延迟。


但这种情况非常奇怪,原因有二:


  • Kubernetes 当中已经包含大量与 AWS 资源进行通信的应用程序,而且都没有出现延迟过高的情况。因此,我们必须弄清引发当前问题的具体原因。

  • 我们知道 JVM 采用了内存内的 DNS 缓存。从配置中可以看到,TTL 在 $JAVA_HOME/jre/lib/security/java.security 位置进行配置,并被设置为 networkaddress.cache.ttl = 10。JVM 应该能够以 10 秒为周期缓存所有 DNS 查询。


为了确认 DNS 假设,我们决定剥离 DNS 解析步骤,并查看问题是否可以消失。我们的第一项尝试是让应用程序直接与 ELasticSearch IP 通信,从而绕过域名机制。这需要变更代码并进行新的部署,即需要在/etc.hosts 中添加一行代码以将域名映射至其实际 IP:


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 查询部分一切正常(少部分值得讨论的细节,我将在后文提到)。但是,我们的服务处理各项请求的方式有些奇怪。以下是捕捉结果的截图,显示出在响应开始之前的请求接收情况。



数据包编号显示在第一列当中。为了清楚起见,我对不同的 TCP 流填充了不同的颜色。


从数据包 328 开始的绿色部分显示,客户端(172.17.22.150)打开了容器(172.17.36.147)间的 TCP 连接。在最初的握手(328 至 330)之后,数据包 331 将 HTTP GET /v1/…(传入请求)引向我们的服务,整个过程耗时约 1 毫秒。


来自数据包 339 的灰色部分表明,我们的服务向 ElasticSearch 实例发送了 HTTP 请求(这里没有显示 TCP 握手,是因为其使用原有 TCP 连接),整个过程耗费了 18 毫秒。


到这里,一切看起来还算正常,而且时间也基本符合整体响应延迟预期(在客户端一侧测量为 20 到 30 毫秒)。


但在两次交换之间,蓝色部分占用了 86 毫秒。这到底是怎么回事?在数据包 333,我们的服务向/latest/meta-data/iam/security-credentials 发送了一项 HTTP GET 请求,而后在同一 TCP 连接上,又向/latest/meta-data/iam/security-credentials/arn:…发送了另一项 GET 请求。


我们进行了验证,发现整个流程中的每项单一请求都发生了这种情况。在容器内,DNS 解析确实有点慢(理由同样非常有趣,有机会的话我会另起一文详加讨论)。但是,导致高延迟的真正原因,在于针对每项单独请求的 AWS Instance Metadata Service 查询。

假设二:指向 AWS 的流氓调用

两个端点都是 AWS Instance Metadata 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 SDK 会在满足以下两项条件之一时刷新凭证:


  • Expiration 已经达到 EXPIRATION_THRESHOLD 之内,硬编码为 15 分钟。

  • 最后一次刷新凭证的尝试大于 REFRESH_THRESHOLD,硬编码为 60 分钟。


我们希望查看所获取凭证的实际到期时间,因此我们针对容器 API 运行了 cURL 命令——分别指向 EC2 实例与容器。但容器给出的响应要短得多:正好 15 分钟。现在的问题很明显了:我们的服务将为第一项请求获取临时凭证。由于有效时间仅为 15 分钟,因此在下一条请求中,AWS SDK 会首先进行凭证刷新,每一项请求中都会发生同样的情况。


为什么凭证的过期时间这么短?


AWS Instance Metadata Service 在设计上主要代 EC2 实例使用,而不太适合 Kubernetes。但是,其为应用程序保留相同接口的机制确实很方便,所以我们转而使用 KIAM,一款能够运行在各个 Kubernetes 节点上的工具,允许用户(即负责将应用程序部署至集群内的工程师)将 IAM 角色关联至 Pod 容器,或者说将后者视为 EC2 实例的同等对象。其工作原理是拦截指向 AWS Instance Metadata Service 的调用,并利用自己的缓存(预提取自 AWS)及时接上。从应用程序的角度来看,整个流程与 EC2 运行环境没有区别。


KIAM 恰好为 Pod 提供了周期更短的临时凭证,因此可以合理做出假设,Pod 的平均存在周期应该短于 EC2 实例——默认值为 15 分钟。如果将两种默认值放在一起,就会引发问题。提供给应用程序的每一份证书都会在 15 分钟之后到期,但 AWS Java SDK 会对一切剩余时间不足 15 分钟的凭证做出强制性刷新。


结果就是,每项请求都将被迫进行凭证刷新,这使每项请求的延迟提升。接下来,我们又在 AWS Java SDK 中发现了一项功能请求,其中也提到了相同的问题。


相比之下,修复工作非常简单。我们对 KIAM 重新配置以延长凭证的有效期。在应用了此项变更之后,我们就能够在不涉及 AWS Instance Metadata Service 的情况下开始处理请求,同时返回比 EC2 更低的延迟水平。

总结

根据我们的实际迁移经验,最常见的问题并非源自 Kubernetes 或者该平台其他组件,与我们正在迁移的微服务本身也基本无关。事实上,大多数问题源自我们急于把某些组件粗暴地整合在一起。


我们之前从来没有复杂系统的整合经验,所以这一次我们的处理方式比较粗糙,未能充分考虑到更多活动部件、更大的故障面以及更高熵值带来的实际影响。


在这种情况下,导致延迟升高的并不是 Kubernetes、KIAM、AWS Java SDK 或者微服务层面的错误决策。相反,问题源自 KIAM 与 AWS Java SDK 当中两项看似完全正常的默认值。单独来看,这两个默认值都很合理:AWS Java SDK 希望更频繁地刷新凭证,而 KIAM 设定了较低的默认过期时间。但在二者结合之后,却产生了病态的结果。是的,各个组件能够正常独立运行,并不代表它们就能顺利协作并构成更庞大的系统。


原文链接:


https://srvaroa.github.io/kubernetes/migration/latency/dns/java/aws/microservices/2019/10/22/kubernetes-added-a-0-to-my-latency.html


2019-11-20 11:039648
用户头像
赵钰莹 极客邦科技 总编辑

发布了 891 篇内容, 共 659.1 次阅读, 收获喜欢 2684 次。

关注

评论 1 条评论

发布
用户头像
nice debug process:)
2019-11-27 23:42
回复
没有更多了
发现更多内容

RabbitMQ的高可用和高可靠

做梦都在改BUG

Java 高可用 RabbitMQ 消息中间件

实践篇(三):如何有效评审软件架构图?

京东科技开发者

架构 后端 软件架构 企业号 2 月 PK 榜 架构评审

预告|因“AI”而“深” 第四届OpenI/O 启智开发者大会高校开源专场25日开启!

OpenI启智社区

人工智能 开源 OpenI启智社区

Apache Flink 实时计算在美的多业务场景下的应用与实践

Apache Flink

大数据 flink 实时计算

混沌工程之 ChaosBlade 故障注入百宝箱

柠檬汁Code(binbin0325)

源码分析 混沌工程 故障注入 ChaosBlade Chaos

大咖说·阿里云云效|效能治标不治本的三个陷阱的解析

大咖说

通过 Istio、eBPF 和 RSocket Broker 深入探索服务网格

Kian.Lee

istio cncf ebpf sidecar-free rsocket

react源码中的协调与调度

flyzz177

React

Teradata 离场,企业数据分析平台如何应对变革?

Kyligence

数据分析 指标中台

流批一体架构在快手的实践和思考

Apache Flink

大数据 flink 实时计算

React源码分析7-state计算流程和优先级

goClient1992

React

预告|第四届OpenI/O启智开发者大会NLP大模型论坛强势来袭!

OpenI启智社区

人工智能 NLP 大模型 开发者大会 OpenI启智社区 ChatGPT

ChatGPT能做什么?(内附体验攻略)

FinFish

AI工程化 ChatGPT

优质的双机热备软件厂家是哪家?咨询电话多少?

行云管家

高可用 双机热备 双机热备软件

React源码分析5-commit

goClient1992

React

ThreadPoolExecutor源码细节探索

做梦都在改BUG

Java 多线程 线程池 ThreadPoolExecutor

react源码中的fiber架构

flyzz177

React

直播预告 | 嵌入式BI如何将数据分析真正融入业务流程

葡萄城技术团队

保持热爱,奔赴山海:Apache Calcite PMC 之路

字节跳动开源

flink 开源 技术 社区 Apache Calcite

大咖齐聚!OpenHarmony技术峰会豪华嘉宾阵容揭晓

OpenHarmony开发者

OpenHarmony

用javascript分类刷leetcode22.字典树(图文视频讲解)

js2030code

JavaScript LeetCode

镇江有具有资质的等保测评机构吗?在哪里?

行云管家

等保 等级保护 等保测评 镇江

实时数仓Hologres新一代弹性计算组实例技术揭秘

阿里云大数据AI技术

大数据 实时数仓 弹性计算 企业号 2 月 PK 榜

react源码中的hooks

flyzz177

React

有趣,在WSL2上运行VSCode

吴脑的键客

vscode WSL2

前端leetcde算法面试套路之双指针

js2030code

JavaScript LeetCode

前端leetcde算法面试套路之二叉树

js2030code

JavaScript LeetCode

时代背景下的 ChatGPT,到底能帮助开发者做什么呢?

泰罗凹凸曼

JavaScript ChatGPT

预告| 2月24日不见不散!开源治理专场约定你

OpenI启智社区

人工智能 开源社区 开发者大会 开源治理 OpenI启智社区

React源码分析6-hooks源码

goClient1992

React

ChatGPT背后:从0到1,OpenAI的创立之路

OneFlow

人工智能 深度学习 openai ChatGPT

Kubernetes用了,延迟高了10倍,问题在哪?_容器_Galo Navarro_InfoQ精选文章