当前位置: 首页 > 科技观察

使用了Kubernetes,延迟高了10倍,问题是什么?

时间:2023-03-14 01:16:37 科技观察

我们团队在业务迁移到Kubernetes的时候,一旦出现问题,总有人认为“这就是迁移后的痛苦”,把矛头指向Kubernetes,但最后发现不是Kubernetes那犯了错误。虽然文章没有涉及到关于Kubernetes的突破性爆料,但我觉得内容还是值得管理复杂系统的朋友学习的。最近,我的团队将微服务迁移到中央平台。这个中央平台捆绑了CI/CD、基于Kubernetes的运行时和其他功能。这项工作还将作为试点,指导未来几个月内进一步迁移150多个额外的微服务。所有这一切,都是为了支持西班牙的几个主要在线平台,包括Infojobs、Fotocasa等。将应用程序部署到Kubernetes并将部分生产流量路由到那里后,情况开始发生变化。Kubernetes部署中的请求延迟比EC2高10倍。如果找不到解决方案,不仅后续的微服务迁移会失败,整个项目都有被放弃的风险。为什么Kubernetes中的延迟比EC2高得多?为了查明瓶颈,我们收集了整个请求路径上的指标。该架构非常简单,从一个API网关(Zuul)开始,它负责将请求代理到运行在EC2或Kubernetes中的微服务实例。在Kubernetes中,我们只表示和NGINXIngresscontroller,后端是一个运行基于Spring的JVM应用程序的Deployment对象。EC2+----------------+|+--------+|||||+------>后端|||||||||+---------+||+-------------++-----+|公共|||------->ZUUL+--+raffic|||Kubernetes+------+|+-------------------------+||+------++--------+|||||xx|||+------>NGINX+------>后端|||||xx||||+--------++--------+|+-----------------------------+问题似乎来自后端的上游延迟(我在图中用“xx”标记)。将应用程序部署到EC2中后,系统需要大约20毫秒的时间进行响应。但在Kubernetes中,整个过程需要100到200毫秒。我们很快排除了可能在运行时发生变化的可疑对象。JVM版本完全一样,而且应用程序已经在EC2容器中运行,所以问题不可能出在容器化机制上。此外,负载强度是无害的,因为即使每秒只有1个请求,延迟仍然很高。此外,GC暂停几乎可以忽略不计。我们的一位Kubernetes管理员询问应用程序是否具有外部依赖性,因为DNS解析之前已经导致类似的问题,这是迄今为止我们能找到的最有可能的假设。假设1:DNS解析对于每个请求,我们的应用程序都会对域中的AWSElasticSearch实例(例如elastic.spain.adevinta.com)进行1到3次查询。我们在容器中添加了一个shell来验证域名的DNS解析时间不会太长。来自容器的DNS查询结果:[root@be-851c76f696-alf8z/]#whiletrue;dodig"elastic.spain.adevinta.com"|greptime;sleep2;done;;Querytime:22msec;;Querytime:22msec;;Querytime:29msec;;Querytime:21msec;;Querytime:28msec;;Querytime:43msec;;Querytime:39msec来自运行此应用程序的EC2实例的相同查询结果:bash-4.4#whiletrue;dodig"elastic.spain.adevinta.com"|greptime;sleep2;done;;Querytime:77msec;;Querytime:0msec;;Querytime:0msec;;Querytime:0msec;;Querytime:0msec前者的平均解析时间约为30毫秒,显然,我们的应用在它造成了额外的DNS解析ElasticSearch延迟。但这种情况非常奇怪,原因有二:Kubernetes已经包含了大量与AWS资源通信的应用程序,而且没有一个出现过高的延迟。所以我们必须弄清楚到底是什么导致了当前的问题。我们知道JVM使用内存中的DNS缓存。从配置中可以看出,TTL配置在$JAVA_HOME/jre/lib/security/java.security,设置为networkaddress.cache.ttl=10。JVM应该能够每10秒缓存一次所有DNS查询.为了证实DNS假设,我们决定去掉DNS解析步骤,看看问题是否会消失。我们的第一个尝试是让应用程序直接与ElasticSearchIP通信,从而绕过域名机制。这需要更改代码和新部署,这需要在/etc.hosts中添加一行以将域名映射到其实际IP:34.55.5.111elastic.spain.adevinta.com执行IP解析。我们发现延迟确实有所改善,但仍远未达到目标延迟。虽然DNS解析时间有问题,但一直没有找到真正的原因。网络管道我们决定在容器中执行tcpdump以准确了解网络的健康状况。[root@be-851c76f696-alf8z/]#tcpdump-leniany-wcapture.pcap然后我们发送了多个请求并下载了捕获结果(kubectlcpmy-service:/capture.pcapcapture.pcap),然后使用Wireshark进行检查.DNS查找部分一切正常(有一些细节值得讨论,我稍后会谈到)。但是,我们的服务处理单个请求的方式有些奇怪。下面是捕获结果的屏幕截图,显示了在响应开始之前收到的请求。数据包编号显示在第一列中。为了清楚起见,我为不同的TCP流填充了不同的颜色。以数据包328开头的绿色部分显示客户端(172.17.22.150)在容器(172.17.36.147)之间打开了TCP连接。在初始握手(328到330)之后,数据包331将HTTPGET/v1/..(传入请求)定向到我们的服务,这大约需要1毫秒。数据包339的灰色部分显示我们的服务向ElasticSearch实例发送了一个HTTP请求(这里没有显示TCP握手,因为它使用旧的TCP连接),整个过程耗时18毫秒。到目前为止,一切看起来都很正常,并且时间大致符合整体响应延迟预期(在客户端测量为20到30毫秒)。但在两次交换之间,蓝色部分需要86毫秒。究竟是怎么回事?在数据包333中,我们的服务向/latest/meta-data/iam/security-credentials发送一个HTTPGET请求,然后在同一TCP连接/arn:..上发送到/latest/meta-data/iam/security-credentials另一个GET请求。我们验证并发现整个流程中的每个请求都会发生这种情况。在容器中,DNS解析确实有点慢(原因也很有意思,有机会我会在另一篇文章中详细讨论)。但是,高延迟的真正原因是针对每个单独请求的AWS实例元数据服务查询。假设#2:对AWS的恶意调用的两个端点都是AWS实例元数据API的一部分。当从ElasticSearch读取信息时,我们的微服务将使用此服务。这两个调用是授权工作方式的基本流程,端点通过第一个请求生成与实例关联的IAM角色。/#curlhttp://169.254.169.254/latest/meta-data/iam/security-credentials/arn:aws:iam:::role/some_role第二个请求查询临时实例凭证的第二个端点:/#curlhttp://169.254.169.254/latest/meta-data/iam/security-credentials/arn:aws:iam:::role/some_role`{"Code":"Success","LastUpdated":"2012-04-26T16:39:16Z","Type":"AWS-HMAC","AccessKeyId":"ASIAIOSFODNN7EXAMPLE","SecretAccessKey":"wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY","令牌":"令牌","Expiration":"2017-05-17T15:09:54Z"}客户端可以在短时间内使用这些凭据,端点会定期(在Expiration到期之前)检索新的凭据。该模型很简单:出于安全原因,AWS会频繁轮换临时密钥,但客户端可以将密钥缓存几分钟,从而抵消检索新凭证的性能损失。从逻辑上讲,整个过程应该由AWSJavaSDK为我们处理。但出于某种原因,情况并非如此。通过搜索GitHub问题,我们在#1921中找到了所需的线程。当满足以下两个条件之一时,AWS开发工具包会刷新凭证:到期时间已达到EXPIRATION_THRESHOLD,硬编码为15分钟。最后一次刷新凭据的尝试大于REFRESH_THRESHOLD,硬编码为60分钟。我们想查看获得的凭据的实际过期时间,因此我们针对容器API运行了一个cURL命令——分别指向EC2实例和容器。但是容器的响应要短得多:恰好15分钟。现在问题很明显:我们的服务将获得第一个请求的临时凭证。由于有效时间只有15分钟,在下一次请求时,AWSSDK会先刷新凭证,每次请求都一样。为什么证书过期时间这么短?AWSInstanceMetadataService主要设计用于EC2实例,而不是Kubernetes。然而,它为应用程序保留相同接口的机制确实很方便,所以我们转向了KIAM,一个可以在每个Kubernetes节点上运行并允许用户(即负责将应用程序部署到集群的工程师)到IAM角色的工具附加到pod容器,或将它们视为等同于EC2实例。它的工作原理是拦截对AWS实例元数据服务的调用,并使用自己的缓存(从AWS预取)及时获取。从应用的角度来看,整个过程和EC2运行环境没有区别。KIAM恰好为Pod提供寿命较短的临时凭证,因此可以合理地假设Pod的平均寿命应该比EC2实例短——默认值为15分钟。如果将两个默认设置放在一起,就会出现问题。提供给应用程序的每个证书都会在15分钟后过期,但AWSJavaSDK将强制刷新剩余时间少于15分钟的任何证书。因此,每个请求都将被迫刷新凭据,这会增加每个请求的延迟。接下来,我们在AWSJavaSDK中发现了另一个功能请求,其中也提到了同样的问题。相比之下,修复工作就很简单了。我们重新配置了KIAM以延长凭证的有效期。应用此更改后,我们能够在不涉及AWS实例元数据服务的情况下开始处理请求,同时返回比EC2更低的延迟级别。总结根据我们真实的迁移经验,最常见的问题不是源于Kubernetes或平台的其他组件,与我们迁移的微服务本身关系不大。事实上,大多数问题源于我们急于将某些组件拼凑在一起。我们之前从未有过集成复杂系统的经验,所以这次我们以一种粗略的方式处理它,没有充分考虑到更多运动部件、更大的故障面和更高的熵的实际影响。在这种情况下,导致延迟增加的不是Kubernetes、KIAM、AWSJavaSDK或微服务级别的错误决策。相反,问题源于KIAM和AWSJavaSDK中两个看似完全正常的默认设置。单独来看,这两个默认值都是合理的:AWSJavaSDK期望更频繁地刷新凭证,而KIAM的默认到期时间很短。但当两者结合时,就会产生病态的结果。是的,仅仅因为每个组件能够正常独立运行,并不意味着它们能够顺利协作,形成一个更大的系统。