如何调试Kubernetes集群中的网络延迟问题

如何调试Kubernetes集群中的网络延迟问题

作者:Theo Julienne 2022-03-07 10:41:09

云计算

运维 本文深入研究和解决了 Kubernetes 平台上的服务零星延迟问题。

巨鹿网站制作公司哪家好,找创新互联建站!从网页设计、网站建设、微信开发、APP开发、响应式网站等网站项目制作,到程序开发,运营维护。创新互联建站从2013年成立到现在10年的时间,我们拥有了丰富的建站经验和运维经验,来保证我们的工作的顺利进行。专注于网站建设就选创新互联建站

就在不久前我也遇到了类似的问题,看似是玄学事件,刚开始归结于网络链路抖动,一段时间后依然存在,虽然影响都是 P99.99 以后的数据,但是扰人心智,最后通过多方面定位,解决了该问题。最后发现跟业务、网络都没有什么关系,而是基础设施自身出了问题,如下文给了一个具体排查方案,并从一定程度上解释了容器、cgroup、CPU 会给网络延迟带来怎样的影响。

随着 Kubernetes 集群规模不断增长,我们对于服务延迟的要求越来越严苛。我们开始观察到一些运行在我们 Kubernetes 平台上的服务正在面临偶发的延迟问题,这些断断续续的问题并不是由于应用本身的性能问题导致的。

我们发现,Kubernetes 集群上的应用产生的延迟问题看上去似乎是随机的,对于某些网络连接的建立可能会超过 100ms,从而使得下游的服务产生超时或者重试。这些服务本身处理业务的响应时间都能够很好地保持在 100ms 以内,而建立连接就需要花费 100ms 以上对我们来说是不可忍受的。另外,我们也发现对于一些应该执行非常快的 SQL 查询(毫秒量级),从应用的角度看居然超过 100ms,但是在 MySQL 数据库的角度看又是完全正常的,并没有发现可能出现的慢查询问题。

通过排查,我们将问题缩小到与 Kubernetes 节点建立连接的这个环节,包括集群内部的请求或者是涉及到外部的资源和外部的访问者的请求。最简单的重现这个问题的方法是:在任意的内部节点使用 Vegeta 对一个以 NodePort 暴露的服务发起 HTTP 压测,我们就能观察到不时会产生一些高延迟请求。在这篇文章中,我们将聊一聊我们是如何追踪定位到这个问题的。

拨开迷雾找到问题的关键

我们想用一个简单的例子来复现问题,那么我们希望能够把问题的范围缩小,并移除不必要的复杂度。起初,数据在 Vegeta 和 Kubernetes Pods 之间的流转的过程中涉及了太多的组件,很难确定这是不是一个更深层次的网络问题,所以我们需要来做一个减法。

Vegeta 客户端会向集群中的某个 Kube 节点发起 TCP 请求。在我们的数据中心的 Kubernetes 集群使用 Overlay 网络(运行在我们已有的数据中心网络之上),会把 Overlay 网络的 IP 包封装在数据中心的 IP 包内。当请求抵达第一个 kube 节点,它会进行 NAT 转换,从而把 kube 节点的 IP 和端口转换成 Overlay 的网络地址,具体来说就是运行着应用的 Pod 的 IP 和端口。在请求响应的时候,则会发生相应的逆变换(SNAT/DNAT)。这是一个非常复杂的系统,其中维持着大量可变的状态,会随着服务的部署而不断更新。

在最开始利用 Vegeta 进行进行压测的时候,我们发现在 TCP 握手的阶段(SYN 和 SYN-ACK 之间)存在延迟。为了简化 HTTP 和 Vegeta 带来的复杂度,我们使用 hping3 来发送 SYN 包,并观测响应的包是否存在延迟的情况,然后把连接关闭。我们能够过滤出那些延迟超过 100ms 的包,来简单地重现 Vegeta 的 7 层压力测试或是模拟一个服务暴露在 SYN 攻击中。以下的一段日志显示的是以 10ms 间隔向 kube-node 的 30927 端口发送 TCP SYN/SYN-ACK 包并过滤出慢请求的结果,

theojulienne@shell ~ $ sudo hping3 172.16.47.27 -S -p 30927 -i u10000 | egrep --line-buffered 'rtt=[0-9]{3}\.'
len=46 ip=172.16.47.27 ttl=59 DF id=0 sport=30927 flags=SA seq=1485 win=29200 rtt=127.1 ms
len=46 ip=172.16.47.27 ttl=59 DF id=0 sport=30927 flags=SA seq=1486 win=29200 rtt=117.0 ms
len=46 ip=172.16.47.27 ttl=59 DF id=0 sport=30927 flags=SA seq=1487 win=29200 rtt=106.2 ms
len=46 ip=172.16.47.27 ttl=59 DF id=0 sport=30927 flags=SA seq=1488 win=29200 rtt=104.1 ms
len=46 ip=172.16.47.27 ttl=59 DF id=0 sport=30927 flags=SA seq=5024 win=29200 rtt=109.2 ms
len=46 ip=172.16.47.27 ttl=59 DF id=0 sport=30927 flags=SA seq=5231 win=29200 rtt=109.2 ms

根据日志中的序列号以及时间,我们首先观察到的是这种延迟并不是单次偶发的,而是经常聚集出现,就好像把积压的请求最后一次性处理完似的。

接着,我们想要具体定位到是哪个组件有可能发生了异常。是 kube-proxy 的 NAT 规则吗,毕竟它们有几百行之多?还是 IPIP 隧道或类似的网络组件的性能比较差?排查的一种方式是去测试系统中的每一个步骤。如果我们把 NAT 规则和防火墙逻辑删除,仅仅使用 IPIP 隧道会发生什么?

如果你同样也在一个 kube 节点上,那么 Linux 允许你直接和 Pod 进行通讯,非常简单:

theojulienne@kube-node-client ~ $ sudo hping3 10.125.20.64 -S -i u10000 | egrep --line-buffered 'rtt=[0-9]{3}\.'
len=40 ip=10.125.20.64 ttl=64 DF id=0 sport=0 flags=RA seq=7346 win=0 rtt=127.3 ms
len=40 ip=10.125.20.64 ttl=64 DF id=0 sport=0 flags=RA seq=7347 win=0 rtt=117.3 ms
len=40 ip=10.125.20.64 ttl=64 DF id=0 sport=0 flags=RA seq=7348 win=0 rtt=107.2 ms

从我们的结果看到,问题还是在那里!这排除了 iptables 以及 NAT 的问题。那是不是 TCP 出了问题?我们来看下如果我们用 ICMP 请求会发生什么。

theojulienne@kube-node-client ~ $ sudo hping3 10.125.20.64 --icmp -i u10000 | egrep --line-buffered 'rtt=[0-9]{3}\.'
len=28 ip=10.125.20.64 ttl=64 id=42594 icmp_seq=104 rtt=110.0 ms
len=28 ip=10.125.20.64 ttl=64 id=49448 icmp_seq=4022 rtt=141.3 ms
len=28 ip=10.125.20.64 ttl=64 id=49449 icmp_seq=4023 rtt=131.3 ms
len=28 ip=10.125.20.64 ttl=64 id=49450 icmp_seq=4024 rtt=121.2 ms
len=28 ip=10.125.20.64 ttl=64 id=49451 icmp_seq=4025 rtt=111.2 ms
len=28 ip=10.125.20.64 ttl=64 id=49452 icmp_seq=4026 rtt=101.1 ms
len=28 ip=10.125.20.64 ttl=64 id=50023 icmp_seq=4343 rtt=126.8 ms
len=28 ip=10.125.20.64 ttl=64 id=50024 icmp_seq=4344 rtt=116.8 ms
len=28 ip=10.125.20.64 ttl=64 id=50025 icmp_seq=4345 rtt=106.8 ms
len=28 ip=10.125.20.64 ttl=64 id=59727 icmp_seq=9836 rtt=106.1 ms

结果显示 ICMP 仍然能够复现问题。那是不是 IPIP 隧道导致了问题?让我们来进一步简化问题。

那么有没有可能这些节点之间任意的通讯都会带来这个问题?

theojulienne@kube-node-client ~ $ sudo hping3 172.16.47.27 --icmp -i u10000 | egrep --line-buffered 'rtt=[0-9]{3}\.'
len=46 ip=172.16.47.27 ttl=61 id=41127 icmp_seq=12564 rtt=140.9 ms
len=46 ip=172.16.47.27 ttl=61 id=41128 icmp_seq=12565 rtt=130.9 ms
len=46 ip=172.16.47.27 ttl=61 id=41129 icmp_seq=12566 rtt=120.8 ms
len=46 ip=172.16.47.27 ttl=61 id=41130 icmp_seq=12567 rtt=110.8 ms
len=46 ip=172.16.47.27 ttl=61 id=41131 icmp_seq=12568 rtt=100.7 ms
len=46 ip=172.16.47.27 ttl=61 id=9062 icmp_seq=31443 rtt=134.2 ms
len=46 ip=172.16.47.27 ttl=61 id=9063 icmp_seq=31444 rtt=124.2 ms
len=46 ip=172.16.47.27 ttl=61 id=9064 icmp_seq=31445 rtt=114.2 ms
len=46 ip=172.16.47.27 ttl=61 id=9065 icmp_seq=31446 rtt=104.2 ms

在这个复杂性的背后,简单来说其实就是两个 kube 节点之间的任何网络通讯,包括 ICMP。如果这个目标节点是“异常的”(某些节点会比另一些更糟糕,比如延迟更高,问题出现的频率更高),那么当问题发生时,我们仍然能看到类似的延迟。

那么现在的问题是,我们显然没有在所有的机器上发现这个问题,为什么这个问题只出现在那些 kube 节点的服务器上?是在 kube 节点作为请求发送方还是请求接收方时会出现呢?幸运的是,我们能够轻易地把问题的范围缩小:我们可以用一台集群外的机器作为发送方,而使用相同的“已知故障”的机器作为请求的目标。我们发现在这个方向上的请求仍然存在问题。

theojulienne@shell ~ $ sudo hping3 172.16.47.27 -p 9876 -S -i u10000 | egrep --line-buffered 'rtt=[0-9]{3}\.'
len=46 ip=172.16.47.27 ttl=61 DF id=0 sport=9876 flags=RA seq=312 win=0 rtt=108.5 ms
len=46 ip=172.16.47.27 ttl=61 DF id=0 sport=9876 flags=RA seq=5903 win=0 rtt=119.4 ms
len=46 ip=172.16.47.27 ttl=61 DF id=0 sport=9876 flags=RA seq=6227 win=0 rtt=139.9 ms
len=46 ip=172.16.47.27 ttl=61 DF id=0 sport=9876 flags=RA seq=7929 win=0 rtt=131.2 ms

然后重复以上操作,这次我们从 kube 节点发送请求到外部节点。

theojulienne@kube-node-client ~ $ sudo hping3 172.16.33.44 -p 9876 -S -i u10000 | egrep --line-buffered 'rtt=[0-9]{3}\.'
^C
--- 172.16.33.44 hping statistic ---
22352 packets transmitted, 22350 packets received, 1% packet loss
round-trip min/avg/max = 0.2/7.6/1010.6 ms

通过查看抓包中的延迟数据, 我们获得了更多的信息。具体来说,从发送端观察到了延迟(下图),然而接收端的服务器没有看到延迟(上图)——注意图中的 Delta 列(单位是秒):

另外,通过查看接收端的 TCP 以及 ICMP 网络包的顺序的区别(基于序列 ID), 我们发现 ICMP 包总是按照他们发送的顺序抵达接收端,但是送达时间不规律,而 TCP 包的序列 ID 有时会交错,其中的一部分会停顿。尤其是,如果你去数 SYN 包发送/接收的端口,这些端口在接收端并不是顺序的,而他们在发送端是有序的。

目前我们服务器所使用的网卡,比如我们在自己的数据中心里面使用的那些硬件,在处理 TCP 和 ICMP 网络报文时有一些微妙的区别。当一个数据报抵达的时候,网卡会对每个连接上传递的报文进行哈希,并且试图将不同的连接分配给不同的接收队列,并为每个队列(大概)分配一个 CPU 核心。对于 TCP 报文来说,这个哈希值同时包含了源 IP、端口和目标 IP、端口。换而言之,每个连接的哈希值都很有可能是不同的。对于 ICMP 包,哈希值仅包含源 IP 和目标 IP,因为没有端口之说。这也就解释了上面的那个发现。

另一个新的发现是一段时间内两台主机之间的 ICMP 包都发现了停顿,然而在同一段时间内 TCP 包却没有问题。这似乎在告诉我们,是接收的网卡队列的哈希在“开玩笑”,我们几乎确定停顿是发生在接收端处理 RX 包的过程中,而不是发送端的问题。

这排除了 kube 节点之间的传输问题,所以我们现在知道了这是在处理包的阶段发生了停顿,并且是一些作为接收端的 kube 节点。

深入挖掘 Linux 内核的网络包处理过程

为了理解为什么问题会出现在 kube 节点服务的接收端,我们来看下 Linux 是如何处理网络包的。

在最简单原始的实现中,网卡接收到一个网络包以后会向 Linux 内核发送一个中断,告知有一个网络包需要被处理。内核会停下它当前正在进行的其他工作,将上下文切换到中断处理器,处理网络报文然后再切换回到之前的工作任务。

上下文切换会非常慢,对于上世纪 90 年代 10Mbit 的网卡可能这个方式没什么问题,但现在许多服务器都是万兆网卡,最大的包处理速度可能能够达到 1500 万包每秒:在一个小型的 8 核心服务器上这意味着每秒会产生数以百万计的中断。

许多年前,Linux 新增了一个 NAPI,Networking API 用于代替过去的传统方式,现代的网卡驱动使用这个新的 API 可以显著提升高速率下包处理的性能。在低速率下,内核仍然按照如前所述的方式从网卡接受中断。一旦有超过阈值的包抵达,内核便会禁用中断,然后开始轮询网卡,通过批处理的方式来抓取网络包。这个过程是在“softirq”中完成的,或者也可以称为软件中断上下文(software interrupt context)。这发生在系统调用的最后阶段,此时程序运行已经进入到内核空间,而不是在用户空间。

这种方式比传统的方式快得多,但也会带来另一个问题。如果包的数量特别大,以至于我们将所有的 CPU 时间花费在处理从网卡中收到的包,但这样我们就无法让用户态的程序去实际处理这些处于队列中的网络请求(比如从 TCP 连接中获取数据等)。最终,队列会堆满,我们会开始丢弃包。为了权衡用户态和内核态运行的时间,内核会限制给定软件中断上下文处理包的数量,安排一个“预算”。一旦超过这个"预算"值,它会唤醒另一个线程,称为“ksoftiqrd”(或者你会在 ps 命令中看到过这个线程),它会在正常的系统调用路径之外继续处理这些软件中断上下文。这个线程会使用标准的进程调度器,从而能够实现公平的调度。

通过整理 Linux 内核处理网络包的路径,我们发现这个处理过程确实有可能发生停顿。如果 softirq 处理调用之间的间隔变长,那么网络包就有可能处于网卡的 RX 队列中一段时间。这有可能是由于 CPU 核心死锁或是有一些处理较慢的任务阻塞了内核去处理 softirqs。

将问题缩小到某个核心或者方法

到目前为止,我们相信这个延迟确实是有可能发生的,并且我们也知道我们似乎观察到一些非常类似的迹象。下一步是需要确认这个理论,并尝试去理解是什么原因导致的问题。

让我们再来看一下发生问题的网络请求:

len=46 ip=172.16.53.32 ttl=61 id=29573 icmp_seq=1953 rtt=99.3 ms
len=46 ip=172.16.53.32 ttl=61 id=29574 icmp_seq=1954 rtt=89.3 ms
len=46 ip=172.16.53.32 ttl=61 id=29575 icmp_seq=1955 rtt=79.2 ms
len=46 ip=172.16.53.32 ttl=61 id=29576 icmp_seq=1956 rtt=69.1 ms
len=46 ip=172.16.53.32 ttl=61 id=29577 icmp_seq=1957 rtt=59.1 ms
网站名称:如何调试Kubernetes集群中的网络延迟问题
本文URL:http://www.shufengxianlan.com/qtweb/news19/267069.html

网站建设、网络推广公司-创新互联,是专注品牌与效果的网站制作,网络营销seo公司;服务项目有等

广告

声明:本网站发布的内容(图片、视频和文字)以用户投稿、用户转载内容为主,如果涉及侵权请尽快告知,我们将会在第一时间删除。文章观点不代表本网站立场,如需处理请联系客服。电话:028-86922220;邮箱:631063699@qq.com。内容未经允许不得转载,或转载时需注明来源: 创新互联