分析并解决遗留应用迁移至Kubernetes后延迟增加的问题

上周我们团队忙着将一个微服务迁移到中央平台上,包括CI/CD,Kubernetes运行时,metric和其他一些程序。这次实验是为了之后一个月里大概150个微服务的迁移作准备,所有这些服务支撑着西班牙在线市场的运营。

当我们将应用程序部署到Kubernetes上,并且将一些生产流量导入其中之后,事情开始有些不妙了。Kubernetes上的请求延迟比EC2上的高10倍左右。除非我们能找到解决方案,不然这会是微服务迁移的最大障碍,甚至可能彻底摧毁整个项目。

为什么Kubernetes上的延时比EC2高那么多?

为了找到系统瓶颈,我们收集了整个请求路径的metric。架构很简单,一个API网关(Zuul)将请求路由到EC2或者Kubernetes的微服务里。在Kubernetes上,我们使用NGINX Ingress控制器,后台是常规的Deployment运行一个基于Spring的JVM应用程序。

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

问题看上去是后台的上游延迟(上图用xx表示)。当应用程序部署在EC2上时,响应时间大概20ms。在Kubernetes上则需要100~200ms。

我们很快排除了运行时变更的影响。JVM版本是一致的。容器化的影响也被排除了,因为EC2上也是运行在容器里。也和压力无关,因为即使每秒只有1个请求仍然能看到很高的延时。也不是GC的影响。

一个Kubernetes管理员问应用程序是否有外部的依赖,比如以前DNS解析曾经导致过类似的问题,这是目前为止最可能的猜想。

猜想1: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

大概30ms的解析时间,似乎我们的应用程序在和ElasticSearch通信时增加了DNS解析的额外消耗。

但是这里有两点很奇怪:

  • 在Kubernetes里已经有很多应用程序和AWS资源通信,但是并没有这个问题。

  • 我们知道JVM实现了内存内的DNS缓存。查看这些镜像的配置,在 $JAVA_HOME/jre/lib/security/java.security里配置了TTL为 networkaddress.cache.ttl=10。JVM应该能够缓存10秒内的所有DNS查询。

为了确认是DNS的影响,我们决定避免DNS解析并且查看问题是否会消失。首先尝试让应用程序直接和Elasticsearch的IP通信,而不是域名。这要求代码变更并且重新部署,因此我们只是简单地在 /etc/hosts文件里添加了域名和IP的映射:

34.55.5.111 elastic.spain.adevinta.com

这样容器可以立刻解析IP。我们确实观察到了延时的改善,但是离我们的最终目标还是很远。即使DNS解析足够快,但是真实原因还是没有找到。

网络plumbing

我们决定在容器里执行 tcpdump,这样可以看到网络到底干了些什么。

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

然后发送了一些请求并且下载了capture文件( kubectl cpmy-service:/capture.pcap capture.pcap)在Wireshark[1]里查看。

DNS查询看上去很正常(除了一些细节,之后会提到)但是我们的服务处理请求的时候很奇怪。下图是capture的截图,显示一个请求从开始到响应的全过程。

第一列是packet序号。我用不同的颜色标示不同的TCP流。

绿色的流从packet 328开始,显示客户端(172.17.22.150)开启了容器(172.17.36.147)的TCP连接。最初的握手(328-330)之后,packet 331开始 HTTP GET/v1/..,这是对我们自己服务的入站请求。整个流程花了1ms。

灰色流从packet 339开始,展示了我们的服务发送一个HTTP请求给Elasticsearch实例(这里看不到TCP握手过程因为它使用了一个已有的TCP连接)。这里花了18ms。

至此都没有什么问题,所花的时间和预计差不多(~20-30ms)。

但是在这两次交互之间,蓝色部分花了86ms。这里发生了什么?在packet 333, 我们的服务发送了HTTP GET到 /latest/meta-data/iam/security-credentials,之后,在同一个TCP连接里,另一个GET发送到 /latest/meta-data/iam/security-credentials/arn:..。

我们发现每次请求里都会这样做。DNS解析在容器里确实有一点慢(解释很有意思,我会在另一篇文章里介绍)。但是高延迟的实际原因是每次请求里对AWS Instance Metadata的查询。

猜想2:AWS调用

这两个endpoint都是AWS Instance Metadata API[2]的一部分。我们的微服务从Elasticsearch里读取时会用到这个服务。这两个调用都是基础的授权工作流。第一个请求里查询的endpoint得到和该实例相关联的IAM角色。

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

第二个请求查询第二个endpoint得到该实例的临时credential。

/ # 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之前)去获取新的credencial。模型很简单:AWS为了安全考虑经常轮询临时密钥,但是客户端可以将密钥缓存几分钟来弥补获得新credencial所带来的性能影响。

AWS Java SDK应该处理这些,但是,因为某种原因,它没有这么做。

在GitHub issue里搜索后找到了#1921[3],里面有我们需要的线索。

AWS SDK在下面两种情况的某一种满足时就会刷新credential:

  • Expiration在 EXPIRATION_THRESHOLD内,硬编码为15分钟。

  • 前一次刷新credential的尝试所花时间大于 REFRESH_THRESHOLD,硬编码为60分钟。

我们需要查看得到的证书里的实际过期时间,因此运行了两个 cURL命令调用AWS API,一次从容器里,一次从EC2实例里。从容器里获得的证书过期时间短得多,是15分钟。

问题变得清晰了:我们的服务在第一个请求里会获取临时credential。因为它有15分钟的过期时间,在下一次请求里,AWS SDK会重新刷新credential。每次请求都会这样。

为什么credential过期时间变短了?

AWS Intance Metadata Service设计上是在EC2实例里使用,而不是Kubernetes上。我们希望应用程序保留相同的接口。因此使用了KIAM[4],在每个Kubernetes节点上运行一个agent,允许用户(部署应用程序到集群里的工程师)将IAM角色关联到Pod容器上,就像它是个EC2实例一样。它会截获发送到AWS Instance Metadata服务的调用,并且使用agent提前从AWS获取并放在缓存里的内容响应。从应用程序的角度来看,和运行在EC2上没什么区别。

KIAM给Pod提供的正是短期的credencial,这有道理,因为它假定Pod的平均生命周期比EC2实例要短。默认值就是15分钟。

但是如果两处都使用默认值就有问题了。提供给应用程序的证书过期时间为15分钟。AWS Java SDK会强制刷新任何过期时间少于15分钟的证书。

结果就是每个请求都会强制刷新临时证书,这需要两次调用AWS API,给每次请求都带来了巨大的延迟。之后我们找到了AWS Java SDK的一个功能请求[5],里面提到了同样的问题。

解决办法很简单,我们重新配置了KIAM,请求更长过期时间的credencial。当这一变更生效后,请求就不用每次都调用AWS Metadata服务了,而且延迟比EC2还要小。

收获

从我们迁移的经验里,最经常遇到的问题不是Kubernetes的bug或者平台的问题。也不是微服务本身的问题。问题通常只是因为集成。我们将以前从来没有一起集成过的复杂系统混合在一起,并且期望它们组成单个的大系统。可移动组件越多,可能发生问题的地方就越多。

在这个问题里,高延迟并不是因为bug或者Kubernetes、KIAM、AWS Java SDK或我们自己微服务本身有什么问题。它是KIAM和AWS Java SDK里两个独立的默认值组合在一起导致的问题。独立来看,两个默认值都没什么问题:AWS Java SDK强制credential刷新策略和KIAM比较低的默认过期时间。但是组合起来就导致了问题。两个单独看都是正确的决定合在一起并不一定是正确的。

相关链接:

  1. https://wiki.wireshark.org/FrontPage

  2. https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html#instance-metadata-security-credentials

  3. https://github.com/aws/aws-sdk-java/issues/1921

  4. https://github.com/uswitch/kiam

  5. https://github.com/aws/aws-sdk-java/issues/1893

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

Kubernetes入门与实战培训

Kubernetes入门与实战培训将于2020年2月28日在北京开课,3天时间带你系统掌握Kubernetes,学习效果不好可以继续学习。本次培训包括:Docker基础、容器技术、Docker镜像、数据共享与持久化、Docker实践、Kubernetes基础、Pod基础与进阶、常用对象操作、服务发现、Helm、Kubernetes核心组件原理分析、Kubernetes服务质量保证、调度详解与应用场景、网络、基于Kubernetes的CI/CD、基于Kubernetes的配置管理等等,点击下方图片或者阅读原文链接查看详情。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值