解密 kubectl 性能瓶颈:获取资源缓慢问题的排查与优化

220 阅读11分钟

作者:bigdavidwong

首次编辑:2025-02-28

1. 背景

排查起始于一次某集群的资源巡查,我们发现在某个特定的master节点上执行kubectl时,第一次获得最终结果的速度非常慢(大概有半分钟左右,偶尔会到1分钟),但在同集群的其它master节点执行相同命令,基本都是立即返回。本着SRE对于海恩法则的坚信不疑,我们决定势必要查出其具体原因,以规避可能存在的风险。

海恩法则,是航空界关于飞行安全的法则。海恩法则指出: 每一起严重事故的背后,必然有29次轻微事故和300起未遂先兆以及1000起事故隐患。

2. 排查

一般来说,当这类成熟的工具使用中出现问题时,SRE优先怀疑的是自身环境问题而不是工具本身,仅从已有的现象来看,可以推出以下几个结论:

  1. apiserver本身没有问题(因为其它节点完全正常);
  2. kubectl​版本问题(因为所有节点的版本完全一致);
  3. 大概率是节点本身的问题;

所以,我们打算从一些基础的性能指标入手逐步排查;

异常节点为0号控制平面节点,所以我们后续用master0来代称它。

2.1 资源检查

2.1.1 CPU&内存

卡、慢,第一件事儿看下CPU、内存、负载:

  • CPU​ image.png
  • 内存 image.png
  • 负载 image.png umm...都不高,看来跟资源压力关系不大;

2.1.2 网络&磁盘

  • 网络 image.png image.png image.png

  • 网络没啥问题,再看下磁盘 image.png image.png

  • 嘶... IOPS​和BPS​倒还好,sdb的IO写延迟偏高了些,正常应该在几毫秒上下,不过问题应该不大?几十毫秒也不会导致几十秒的延迟吧,何况kubectl​应该不至于有非常高频的本地IO读写。 ‍

2.2 Debug日志

2.2.1 日志检查

基础指标都看完了,没看出啥问题,行吧,那就开下debug看下到底kubectl都在哪里慢了,执行命令

kubectl get nodes -v 9

检查日志,发现明显的异常点:

  • kubectl​在无缓存获取资源信息时,会先并发发送数十个http​请求,以获取已注册的所有资源信息,我们能看到,这些request都在0.1s内发送出去了,是预期内的: image.png

  • 但这些请求的响应,仅从日志时间点来看,速度极慢,最后一个甚至等待了接近10秒钟; image.png

好吧,看起来似乎跟网络还是有关系,咱们再回到网络侧入手;

2.2.2 回查网络

  1. 首先,因为所有的请求在发出时基本立即返回,ping也是微秒级,所以基本排除3、4层故障;

  2. 同时应该有同学注意到了,我们的apiserver​端口是8443,这是因为用到了经典的KeepAlived​+HAProxy​架构,所以这里先贴一张架构简图: image.png

  3. 所有的APIServer​请求,都是先经过VIP​落到活跃节点,然后通过节点的HAProxy​再轮询到后面的APIServer​集群,因此为了减少影响因子,我们直接修改master0master2kubeconfig​文件跳过KeepAlived​和HAProxy​,将所有访问指向指定的另一台APIServer​后,再次执行命令; image.png

  4. 再次请求,速度仍然很慢(测速对比,异常节点master0|正常节点master2image.pngimage.png

    • 可以看到,在首次获取的时候,master0仍然需要20秒,而正常节点只需要1秒,同时我们观察到异常节点的Debug日志行为与之前基本一致(返回的响应很慢); ‍

2.2.3 抓包 初现端倪

既然直接访问也很慢,那就排除了所有中间件的问题,异常点一定在两端之间的某个行为上,那就不得不上抓包大法了,我们直接看一下大致的结果图;

  • TCP重传率,0% image.png

  • 正常请求交互请求,初始的TCP握手OK,TLS协商正常; image.png

  • 我们往下翻,当找到响应体返回阶段的数据包日志时,发现了端倪:在整个交互过程中,虽然数据包传输正常,但经常出现 master0 收到或发送数据包后,隔了2-3秒,才会发送下一个数据包: image.png

同时,我们对照了对端的抓包数据,发现基本所有数据包的Seq和Ack时间戳基本吻合,并没有出现延迟的情况;

那,大概率就是kubelet​本地的逻辑出现了什么问题... ‍

2.2.4 意外发现

讲真,到这种程度了,基本就要查源码了,但笔者实在不想干这事儿,毕竟这世界上最痛苦的事情就是看别人的代码~

image.png

所以我们决定先尝试下重装或者更新下kubectl​的版本看下,毕竟重装能解决99%的问题!执行命令:

sudo apt-get update
sudo apt install kubectl

PS: kubectl只是一个命令行工具,所以临时升个版本没啥,切勿用相同方式升级kubelet等核心组件;

此时发现,问题居然消失了! image.png

难道是版本问题?还是说之前的kubectl​有什么损坏?为了验证,笔者又将kubectl​装回之前的版本,结果发现问题再次出现: image.png

好吧,看来确实和版本有关系,重装了一次也异常的话,那就属于kubectl​自身的问题,那接下来,就要查kubectl了。

2.3 问题解决

2.3.1 发现issue

既然是kubectl本身的问题,那么肯定不会是只有我们遇见,因此我们尝试去社区内查了查关键字,果然找到了一篇相关的issue:

github.com/kubernetes/…

  • issue中提到,之所以会非常慢,是因为kubectl每次获取缓存后,都会将其写入到磁盘并fsync,如果将其缓存目录link到/dev/shm下,就能解决该问题 image.png
  • 我们将信将疑,也测试了下,果然快多了! image.png

这时我们正好想起来,之前检查基础指标时,确实发现磁盘的io延迟较高(接近100ms),只不过当时认为其不会造成数十秒的延迟;

所以我们再次去看下,如果当速度非常慢的时候,节点的IO表现是怎样的: image.png

增量的io粗略估计300,按照每次60ms来算,一共大概18000ms=18s,基本吻合了20s的延迟;

而之所以link​到/dev/shm​就正常了,原因是/dev/shm​是linux下基于内存的一个临时目录,所以基本也就不存在fsync​导致的io延迟困扰;

2.3.2 什么是fsync

问题根因算是找到了,那么在上面issue中提到的fsync​到底是什么?为什么它在高IO延迟下表现如此之差?

fsync​ 是一种系统调用,用于将数据从内存缓冲区(比如操作系统的文件系统缓存)写入磁盘。这个操作确保了在调用 fsync​ 后,文件的内容已经持久化到硬盘中,不会因为系统崩溃或电力中断而丢失。它和常规的write​有以下主要区别:

  • 写入操作的目的不同

    • write​:它将数据写入操作系统的缓存中,而不一定会立刻写入磁盘。换句话说,数据可能会留在内存中,直到操作系统决定将其刷新到磁盘。这种方式提高了性能,但也意味着数据可能会在突然断电时丢失。
    • fsync​:它强制操作系统将文件的内容以及相关的元数据(如文件大小、修改时间等)同步写入磁盘。无论操作系统如何安排缓存数据的刷新,fsync​ 都会确保数据持久化到物理存储中。
  • 持久化保障的层次不同

    • write​:数据只保证在操作系统的缓存中存在,可能还未写入磁盘。
    • fsync​:通过强制将数据写入磁盘,fsync​ 保证数据被持久化,这对于需要数据一致性和持久性的场景(比如数据库)至关重要。
  • 性能差异

    • write​:由于数据不立即写入磁盘,它通常比 fsync​ 快。
    • fsync​:fsync​ 会阻塞当前进程,直到所有的数据被确认写入磁盘,因此性能较差,尤其是在大文件或频繁同步的情况下。

IO缓冲流程(图来自网络) image.png

假设你正在开发一个数据库应用,你执行了一个数据写入操作。这时,数据可能会先写入操作系统的缓存而未立即持久化。如果系统突然崩溃,数据可能会丢失。为了确保数据可靠保存,你可以在每次写入后调用 fsync​,强制操作系统将数据写入磁盘,避免崩溃导致数据丢失,这也是我们通常所说的刷盘。

由此可知,kubectl​在缓存k8s对象http结果时,高频的io和fsync​会放大io延迟的影响,而切换为内存目录会直接去除磁盘本身的io瓶颈,从而带来提升。

2.4 深入源码

2.4.1 新的疑问

问题本身是解决了,但笔者心里始终有一个疑问,为什么升级kubectl​到最新的版本就没有问题了?难道是禁用了缓存文件吗?所以我们在删除本地缓存后,用新版本kubectl​测试了下:

image.png 发现缓存文件依然存在,并没有禁用缓存;

同时我们更换了多个版本,发现在1.24.00​的时候问题存在,但是1.25.00​就解决了,且表象均一致;

‍ 好吧,看来为了搞明白这个问题,还是只能去扒源码了,逃不掉呀~

2.4.2 旧版本源码走读

  1. kubectl的入口还算还找,可以明确看到它使用了经典的pflag库作为命令行管理,这也是kubectl的入口​ image.png

  2. kubectl主要使用client-go​下的rest​包来进行APIServer​资源获取和处理的,这一点从上面的Debug日志内也能知晓,图中展示了它创建DiscoverClient​的位置,这也是kubectl​向APIServer​请求发现资源的核心模块;

    image.png

  3. ToDiscoverClient​是一个接口方法,我们找到其具体传入的实现,就发现了缓存目录的源头; image.png

  4. 继续追踪,在NewCachedDiscoveryClientForConfig​方法中,会根据http缓存路径是否存在,来启用http缓存逻辑: image.png

  5. newCacheRoundTripper​使用httpcache​包的NewTransport​方法来初始化缓存后端介质,以此来实现缓存的写入和读取:​ image.png

  6. NewTransport​方法的入参是一个Cache​接口,而图中使用了diskcache​的NewWithDiskv​方法来创建一个内置的符合条件的Cache对象​ image.pngimage.png

    • diskcache​内置的Cache​接口实现​ image.png
  7. 如果深入到Set​方法内,就会发现,默认情况下,每一次缓存写入,都会执行*os.File.Sync​方法(类似于fsync​系统调用) image.png

看到这,基本就能确定旧版本的基本缓存逻辑了,同时结合之前我们在源码内找到,API资源的信息是维护在一个map内,所以每次的资源读写会加互斥锁避免竞争,这也是前面我们发现有不少请求是都会等待2-3秒才会发出的原因(因为上一个请求会将结果fsync​后才会释放锁,而并发的请求只能等待,基本失去了并发的意义)。

2.4.3 新版本源码改进

1.25.00​版本的代码中,我们主要去观察了httpcache​缓存相关的代码,发现它的基本逻辑并没有修改,但在创建缓存介质时,并没有再使用diskcache​内置的缓存对象:

image.png image.png

而在独立实现的新的缓存对象中,它的缓存写入通过调用*diskv.Diskv​的Write​方法来实现: image.png

  • 而这里的Write​方法虽然一样调用了WriteStream​,但是传入的sync​参数为fasle​,代表不执行fsync​同步;

    image.png 同时,你也能看到在它的缓存实现中,加入了 sha256​ 校验和的写入,便能解释为什么新版本能做到保留了缓存文件的逻辑,去除了fsync​的可靠性依赖,还能快速而准确地实现数据读取和写入。

3. 结语

在本次排查中,我们找到了导致kubectl​获取缓慢的根本原因,同时顺便了解了从执行命令到获得结果中大致发生了一些什么事情。对于我们的问题,最终的解决方案有两种:

  • 一是可以将新缓存的逻辑加入到1.21版本后重新编译kubectl​;
  • 二是使用issue中提到的方案将cache​目录link​到内存存储/dev/shm​内。

我们最终选择的是后者,因为缓存时间只有10min,总体文件并不会很大。同时,虽然从结果上来看,这次的根因并没有太大的安全隐患,但从SRE的视角出发, 我们更了解了当下系统瓶颈的一些可能的影响因子,并且解决了kubectl​操作的长延迟问题,对于日常工作也算提效,这些看似微小的改善正是稳定性建设道路上稳定的基石。