kubelet CPU 使用率过高问题排查

问题背景

客户的k8s集群环境,发现所有的worker节点的kubelet进程的CPU使用率长时间占用过高,通过pidstat可以看到CPU使用率高达100%。针对此问题对kubelet进程的异常进行问题排查。

集群环境

软件 版本
kubernetes v1.18.8
docker 18.09.9
rancher v2.4.8-ent
CentOS 7.6
kernel 4.4.227-1.el7.elrepo.x86_64

排查过程

使用strace工具对kubelet进程进行跟踪

  1. 由于kubelet进程CPU使用率异常,可以使用strace工具对kubelet进程动态跟踪进程的调用情况,首先使用strace -cp <PID>命令统计kubelet进程在某段时间内的每个系统调用的时间、调用和错误情况.

从上图可以看到,执行系统调用过程中,futex抛出了五千多个errors,这肯定是不正常的,而且这个函数占用的时间也达到了99%,所以需要更深层次的查看kubelet进程相关的调用。

  1. 由于strace -cp命令只能查看进程的整体调用情况,所以我们可以通过strace -tt -p <PID>命令打印每个系统调用的时间戳,如下:

从strace输出的结果来看,在执行futex相关的系统调用时,有大量的Connect timed out,并返回了-1 ETIMEDOUT的error,所以才会在strace -cp中看到了那么多的error。

futex是一种用户态和内核态混合的同步机制,当futex变量告诉进程有竞争发生时,会执行系统调用去完成相应的处理,例如wait或者wake up,从官方的文档了解到,futex有这么几个参数:

  1. futex(uint32_t *uaddr, int futex_op, uint32_t val,
  2. const struct timespec *timeout, /* or: uint32_t val2 */
  3. uint32_t *uaddr2, uint32_t val3);

官方文档给出ETIMEDOUT的解释:

  1. ETIMEDOUT
  2. The operation in futex_op employed the timeout specified in
  3. timeout, and the timeout expired before the operation
  4. completed.

意思就是在指定的timeout时间中,未能完成相应的操作,其中futex_op对应上述输出结果的FUTEX_WAIT_PRIVATEFUTEX_WAIT_PRIVATE,可以看到基本都是发生在FUTEX_WAIT_PRIVATE时发生的超时。

从目前的系统调用层面可以判断,futex无法顺利进入睡眠状态,但是futex做了哪些操作还是不清楚,还无法判断kubeletCPU飙高的原因,所以我们需要进一步从kubelet的函数调用中去看到底是执行了卡在了哪个地方。

FUTEX_PRIVATE_FLAG:这个参数告诉内核futex是进程专用的,不与其他进程共享,这里的FUTEX_WAIT_PRIVATE和FUTEX_WAKE_PRIVATE就是其中的两种FLAG;

futex相关说明1:https://man7.org/linux/man-pages/man7/futex.7.html

fuex相关说明2: https://man7.org/linux/man-pages/man2/futex.2.html

使用go pprof工具对kubelet函数调用进行分析

早期的k8s版本,可以直接通过debug/pprof 接口获取debug数据,后面考虑到相关安全性的问题,取消了这个接口,参考CVE-2019-11248,我们可以通过kubectl开启proxy进行相关数据指标的获取

  1. 首先使用kubectl proxy命令启动API server代理
  1. kubectl proxy --address='0.0.0.0' --accept-hosts='^*$'

这里需要注意,如果使用的是Rancher UI上copy的kubeconfig文件,则需要使用指定了master IP的context,如果是RKE或者其他工具安装则可以忽略

  1. 构建golang环境。go pprof需要在golang环境下使用,本地如果没有安装golang,则可以通过docker快速构建golang环境
  1. docker run -itd --name golang-env --net host golang bash
  1. 使用go pprof工具导出采集的指标,这里替换127.0.0.1为apiserver节点的IP,默认端口是8001,如果docker run的环境跑在apiserver所在的节点上,可以使用127.0.0.1。另外,还要替换NODENAME为对应的节点名称。
  1. docker exec -it golang-env bash
  2. go tool pprof -seconds=60 -raw -output=kubelet.pprof http://127.0.0.1:8001/api/v1/nodes/${NODENAME}/proxy/debug/pprof/profile

这里等待60s后,会将这60s内相关的函数调用输出到当前目录的kubelet.pprof文件中。

  1. 输出好的pprof文件不方便查看,需要转换成火焰图,推荐使用FlameGraph工具生成svg图
  1. git clone https://github.com/brendangregg/FlameGraph.git
  2. cd FlameGraph/
  3. ./stackcollapse-go.pl kubelet.pprof > kubelet.out
  4. ./flamegraph.pl kubelet.out > kubelet.svg

转换成火焰图后,就可以在浏览器很直观的看到函数相关调用和具体调用时间比了。

  1. 分析火焰图

从kubelet的火焰图可以看到,调用时间最长的函数是k8s.io/kubernetes/vendor/github.com/google/cadvisor/manager.(*containerData).housekeeping,其中cAdvisor是kubelet内置的指标采集工具,主要是负责对节点机器上的资源及容器进行实时监控和性能数据采集,包括CPU使用情况、内存使用情况、网络吞吐量及文件系统使用情况。

​ 深入函数调用可以发现k8s.io/kubernetes/vendor/github.com/opencontainers/runc/libcontainer/cgroups/fs.(*Manager).GetStats这个函数占用k8s.io/kubernetes/vendor/github.com/google/cadvisor/manager.(*containerData).housekeeping这个函数的时间是最长的,说明在获取容器CGroup相关状态时占用了较多的时间。

  1. 既然这个函数占用时间长,那么我们就分析一下这个函数具体干了什么事儿

查看源代码:https://github.com/kubernetes/kubernetes/blob/ded8a1e2853aef374fc93300fe1b225f38f19d9d/vendor/github.com/opencontainers/runc/libcontainer/cgroups/fs/memory.go#L162

  1. func (s *MemoryGroup) GetStats(path string, stats *cgroups.Stats) error {
  2. // Set stats from memory.stat.
  3. statsFile, err := os.Open(filepath.Join(path, "memory.stat"))
  4. if err != nil {
  5. if os.IsNotExist(err) {
  6. return nil
  7. }
  8. return err
  9. }
  10. defer statsFile.Close()
  11. sc := bufio.NewScanner(statsFile)
  12. for sc.Scan() {
  13. t, v, err := fscommon.GetCgroupParamKeyValue(sc.Text())
  14. if err != nil {
  15. return fmt.Errorf("failed to parse memory.stat (%q) - %v", sc.Text(), err)
  16. }
  17. stats.MemoryStats.Stats[t] = v
  18. }
  19. stats.MemoryStats.Cache = stats.MemoryStats.Stats["cache"]
  20. memoryUsage, err := getMemoryData(path, "")
  21. if err != nil {
  22. return err
  23. }
  24. stats.MemoryStats.Usage = memoryUsage
  25. swapUsage, err := getMemoryData(path, "memsw")
  26. if err != nil {
  27. return err
  28. }
  29. stats.MemoryStats.SwapUsage = swapUsage
  30. kernelUsage, err := getMemoryData(path, "kmem")
  31. if err != nil {
  32. return err
  33. }
  34. stats.MemoryStats.KernelUsage = kernelUsage
  35. kernelTCPUsage, err := getMemoryData(path, "kmem.tcp")
  36. if err != nil {
  37. return err
  38. }
  39. stats.MemoryStats.KernelTCPUsage = kernelTCPUsage
  40. useHierarchy := strings.Join([]string{"memory", "use_hierarchy"}, ".")
  41. value, err := fscommon.GetCgroupParamUint(path, useHierarchy)
  42. if err != nil {
  43. return err
  44. }
  45. if value == 1 {
  46. stats.MemoryStats.UseHierarchy = true
  47. }
  48. pagesByNUMA, err := getPageUsageByNUMA(path)
  49. if err != nil {
  50. return err
  51. }
  52. stats.MemoryStats.PageUsageByNUMA = pagesByNUMA
  53. return nil
  54. }

从代码中可以看到,进程会去读取memory.stat这个文件,这个文件存放了cgroup内存使用情况。也就是说,在读取这个文件花费了大量的时间。这时候,如果我们手动去查看这个文件,会是什么效果?

  1. # time cat /sys/fs/cgroup/memory/memory.stat >/dev/null
  2. real 0m9.065s
  3. user 0m0.000s
  4. sys 0m9.064s

从这里可以看出端倪了,读取这个文件花费了9s,显然是不正常的,难怪kubeletCPU使用飙高,原来是堵在这里了。

基于上述结果,我们在cAdvisor的GitHub上查找到一个issue,从该issue中可以得知,该问题跟slab memory 缓存有一定的关系。从该issue中得知,受影响的机器的内存会逐渐被使用,通过/proc/meminfo看到使用的内存是slab memory,该内存是内核缓存的内存页,并且其中绝大部分都是dentry缓存。从这里我们可以判断出,当CGroup中的进程生命周期结束后,由于缓存的原因,还存留在slab memory中,导致其类似僵尸CGroup一样无法被释放。

也就是每当创建一个memory CGroup,在内核内存空间中,就会为其创建分配一份内存空间,该内存包含当前CGroup相关的cache(dentry、inode),也就是目录和文件索引的缓存,该缓存本质上是为了提高读取的效率。但是当CGroup中的所有进程都退出时,存在内核内存空间的缓存并没有清理掉。

内核通过伙伴算法进行内存分配,每当有进程申请内存空间时,会为其分配至少一个内存页面,也就是最少会分配4k内存,每次释放内存,也是按照最少一个页面来进行释放。当请求分配的内存大小为几十个字节或几百个字节时,4k对其来说是一个巨大的内存空间,在Linux中,为了解决这个问题,引入了slab内存分配管理机制,用来处理这种小量的内存请求,这就会导致,当CGroup中的所有进程都退出时,不会轻易回收这部分的内存,而这部分内存中的缓存数据,还会被读取到stats中,从而导致影响读取的性能。

解决方法

  1. 清理节点缓存,这是一个临时的解决方法,暂时清空节点内存缓存,能够缓解kubelet CPU使用率,但是后面缓存上来了,CPU使用率又会升上来。
  1. echo 2 > /proc/sys/vm/drop_caches
  1. 升级内核版本

    2.1. 其实这个主要还是内核的问题,在GitHub上这个commit中有提到,在5.2+以上的内核版本中,优化了CGroup stats相关的查询性能,如果想要更好的解决该问题,建议可以参考自己操作系统和环境,合理的升级内核版本。

    2.2. 另外Redhat在kernel-4.18.0-176版本中也优化了相关CGroup的性能问题,而CentOS 8/RHEL 8默认使用的内核版本就是4.18,如果目前您使用的操作系统是RHEL7/CentOS7,则可以尝试逐渐替换新的操作系统,使用这个4.18.0-176版本以上的内核,毕竟新版本内核总归是对容器相关的体验会好很多。

kernel相关commit:https://github.com/torvalds/linux/commit/205b20cc5a99cdf197c32f4dbee2b09c699477f0

redhat kernel bug fix:https://bugzilla.redhat.com/show_bug.cgi?id=1795049

kubelet CPU 使用率过高问题排查的更多相关文章

  1. java应用cpu使用率过高问题排查

    ---------------------------------------linux下如何定位代码问题------------------------------- 1.先通过top命令找到消耗c ...

  2. 服务器CPU使用率过高排查与解决思路

    发现服务器的cpu使用率特别高 排查思路: -使用top或者mpstat查看cpu的使用情况# mpstat -P ALL 2 1Linux 2.6.32-358.el6.x86_64 (linux— ...

  3. 排查tomcat服务器CPU使用率过高

    tomcat要运行依赖于JDK,tomcat服务器的CPU使用率过高,大多都是因为部署的web程序的问题. 一.现象描述 在一次线上环境,前台访问页面的速度越来越慢,从浏览器F12中看到发出的请求都是 ...

  4. 空循环导致CPU使用率很高

    业务背景 业务背景就是需要将多张业务表中的数据增量同步到一张大宽表中,后台系统基于这张大宽表开展业务,所以就开发了一个数据同步工具,由中间件采集binlog消息到kafka里,然后我去消费,实现增量同 ...

  5. 线上cpu使用率过高解决方案

    一个应用占用CPU很高,除了确实是计算密集型应用之外,通常原因都是出现了死循环. 下面我们将一步步定位问题,详尽的介绍每一步骤的相关知识. 一.通过top命令定位占用cpu高的进程 执行top命令得到 ...

  6. 06 案例篇:系统的 CPU 使用率很高,但为啥却找不到高 CPU 的应用?

    上一节我讲了 CPU 使用率是什么,并通过一个案例教你使用 top.vmstat.pidstat 等工具,排查高 CPU 使用率的进程,然后再使用 perf top 工具,定位应用内部函数的问题.不过 ...

  7. 06讲案例篇:系统的CPU使用率很高,但为啥却找不到高CPU的应用

    小结 碰到常规问题无法解释的 CPU 使用率情况时,首先要想到有可能是短时应用导致的问题,比如有可能是下面这两种情况. 第一,应用里直接调用了其他二进制程序,这些程序通常运行时间比较短,通过 top ...

  8. 性能分析(3)- 短时进程导致用户 CPU 使用率过高案例

    性能分析小案例系列,可以通过下面链接查看哦 https://www.cnblogs.com/poloyy/category/1814570.html 系统架构背景 VM1:用作 Web 服务器,来模拟 ...

  9. 4 系统的 CPU 使用率很高,但为啥却找不到高 CPU的应用?

    上一节讲了 CPU 使用率是什么,并通过一个案例教你使用 top.vmstat.pidstat 等工具,排查高 CPU 使用率的进程,然后再使用 perf top 工具,定位应用内部函数的问题.不过就 ...

随机推荐

  1. 活动可视化搭建系统——你的KPI被我承包了

    前言 对于C端业务偏多的公司来说,在增长.运营等各方同学的摧残下永远绕不过去的一个坑就是大量的H5页面开发,它可能是一个下载.需求告知.产品介绍.营销活动等页面.此类需求都有几个明显的缺点: •开发性 ...

  2. CodeForces 1093F Vasya and Array

    题意 给一个长度为 \(n\) 的整数序列 \(a\),其中 \(a_i\) 要么为 \(-1\),要么为 \(1\sim k\) 中的整数. 求出将所有 \(-1\) 替换为 \(1\sim k\) ...

  3. 01 . Go之Gin+Vue开发一个线上外卖应用

    项目介绍 我们将开始使用Gin框架开发一个api项目,我们起名为:云餐厅.如同饿了么,美团外卖等生活服务类应用一样,云餐厅是一个线上的外卖应用,应用的用户可以在线浏览商家,商品并下单. 该项目分为客户 ...

  4. python使用redis缓存数据库

    Redis 关注公众号"轻松学编程"了解更多. Windows下直接解压可用,链接:https://pan.baidu.com/s/1rD4ujoN7h96TtHSu3sN_hA ...

  5. Docker(9)- docker pull 命令详解

    如果你还想从头学起 Docker,可以看看这个系列的文章哦! https://www.cnblogs.com/poloyy/category/1870863.html 作用 从镜像仓库中拉取或更新镜像 ...

  6. 关于java中的类加载器

    什么是类加载器? 类加载器是专门负责加载类的命令或者说工具 ClassLoader java中的3个类加载器 JDK中自带了3个类加载器 启动类加载器 扩展类加载器 应用类加载器 假设有这样一段代码 ...

  7. Typora + picgo + sm.ms 图床设置笔记

    Typora + picgo + sm.ms 图床设置笔记 编辑于2020-03-26 本文部分内容在作者教程的基础上进行了二次编辑,如有重复,纯属必然 在此感谢大佬们的无私付出与分享 之前 用了 g ...

  8. python的数据处理一

    def load_data(filename): features = [] labels = [] f = open(filename, encoding='utf-8') medical = js ...

  9. CentOS6.x 安装 nginx-1.19.4

    1.下载nginx http://nginx.org/en/download.html wget  http://nginx.org/download/nginx-1.19.4.tar.gz 2.解压 ...

  10. JavaScript变量污染

    定义过多的全局变量,有可能造成全局变量冲突,这种现象称为变量污染. 全局变量在全局作用域内外都是可见的.若是已经声明了一个全局变量,再以相同的关键字和标识符重新声明全局变量,后者的赋值会替代前者的赋值 ...