Sometimes people don't need advice, they just need someone to listen and care.
Toggle navigation
Home
Archives
Tags
About
调试 Kubernetes 占用 CPU 过高
2020-06-19 09:36:56
1051
0
0
william
在过去几年中,Kubernetes 已经成为事实的容器云标准。我们运行了大量容器在 Kubernetes 上。然而随着容器数量的增加,一些问题也逐渐暴露出来。 我们开始注意到 Kubelet 在特定场景下会占用 CPU 比较高,导致浪费额外的计算资源,甚至影响业务。 ## 运行环境 - 操作系统: Centos 7.6 - 内核:4.9.18 - Kubernetes 版本:1.15.2 ## 问题发现 排查机器问题的时候偶然发现 Kubelet 占用 CPU 飙高。有时候会占用到 1~2 个核心。  ## Debug ### 使用 go pprof 工具分析 kubelet. 早期的 Kubelet `debug/pprof` 接口暴露在 healthz 中,后来为了安全原因取消了。参考[CVE-2019-11248](https://github.com/kubernetes/kubernetes/issues/81023) 1.**启动 Kubectl proxy 代理 APiserver** ```bash kubectl proxy ``` 2.**使用 go pprof工具导出采集指标** ```bash go tool pprof -seconds=60 -raw -output=kubelet.pprof http://127.0.0.1:8001/api/v1/nodes/${NODENAME}/proxy/debug/pprof/profile ``` 3.**生成 svg 图** 这里推荐使用 [https://github.com/brendangregg/FlameGraph](https://github.com/brendangregg/FlameGraph) ```bash ./stackcollapse-go.pl kubelet.pprof > kubelet.out ./flamegraph.pl kubelet.out > kubelet.svg ```  通过分析上图发现 CPU 时间占用比较多的函数是 `k8s.io/kubernetes/vendor/github.com/opencontainers/runc/libcontainer/cgroups/fs.(*MemoryGroup).GetStats` 从 Kubernetes 代码找根因 ```go func (s *MemoryGroup) GetStats(path string, stats *cgroups.Stats) error { // Set stats from memory.stat. statsFile, err := os.Open(filepath.Join(path, "memory.stat")) if err != nil { if os.IsNotExist(err) { return nil } return err } defer statsFile.Close() sc := bufio.NewScanner(statsFile) for sc.Scan() { t, v, err := getCgroupParamKeyValue(sc.Text()) if err != nil { return fmt.Errorf("failed to parse memory.stat (%q) - %v", sc.Text(), err) } stats.MemoryStats.Stats[t] = v } stats.MemoryStats.Cache = stats.MemoryStats.Stats["cache"] memoryUsage, err := getMemoryData(path, "") if err != nil { return err } stats.MemoryStats.Usage = memoryUsage swapUsage, err := getMemoryData(path, "memsw") if err != nil { return err } stats.MemoryStats.SwapUsage = swapUsage kernelUsage, err := getMemoryData(path, "kmem") if err != nil { return err } stats.MemoryStats.KernelUsage = kernelUsage kernelTCPUsage, err := getMemoryData(path, "kmem.tcp") if err != nil { return err } stats.MemoryStats.KernelTCPUsage = kernelTCPUsage useHierarchy := strings.Join([]string{"memory", "use_hierarchy"}, ".") value, err := getCgroupParamUint(path, useHierarchy) if err != nil { return err } if value == 1 { stats.MemoryStats.UseHierarchy = true } return nil } ``` 发现这个函数就是读取文件。到这里好像一下子又陷入了迷茫  柳暗花明 继续分析火焰图发现读取文件这个函数占用了很多 CPU 时间,那究竟是读取什么文件会消耗这么多时间呢?  通过分析代码发现主要是在读取memory.stat,该文件显示cgroup的内存使用情况和限制。 那我们继续往回分析,发现是 Cadvisor 正在轮询此文件以获取容器的资源利用率详细信息。(Kubelet 默认内嵌了 Cadvisor)  好,到这里我们怀疑这个文件确实会读取慢,下面我们验证一下 ```bash [root@k8s]# time cat /sys/fs/cgroup/memory/memory.stat >/dev/null real 0m0.577s user 0m0.001s sys 0m0.573s ``` 我们发现读取这个文件竟然消耗了 577ms 因此我们怀疑可能是内核的 CGroup 出现了什么问题 ## 深入探究原因 上网搜索了一下 cadvisor 占用 CPU 高的问题,发先了如下issues,[严重的CPU使用问题](https://github.com/google/cadvisor/issues/1774)。 cadvisor消耗的CPU超出了预期,但似乎在我们服务器上没有引起太多问题,因为我们对 kubelet 进行了资源限制。 当该cgroup中的所有进程退出时,内存cgroup 由Docker释放。但是,“内存”不仅仅是进程内存,尽管进程内存使用本身已经释放,但事实证明,内核还向缓存cgroup分配了缓存内容,例如dentries和inode(目录和文件元数据)。 从那个 issues 中得知: “僵尸” cgroup 没有进程且已删除但仍然有指向它的引用。 内核不是选择在`cgroup`释放时对缓存中的每个页面进行遍历(这可能会很慢),而是选择等待这些页面被回收,然后在需要内存时懒惰地最终回收所有的cgroup,最后清理cgroup。 同时,在统计信息收集期间仍需要对cgroup进行计数。 从性能的角度来看,他们通过在回收每个页面上进行摊销来在缓慢的过程上进行时间折衷,并选择快速进行初始清理,以换取保留一些缓存的内存。 没关系,当内核回收缓存中的最后一个内存时,cgroup最终会被清理,因此这并不是一个“泄漏”。 不幸的是,memory.stat所执行的搜索,在我们在某些服务器上运行的内核版本(4.9)上内存很大,意味着最后一次可能要花费相当长的时间回收要缓存的数据,并清理僵尸cgroup。 解决该问题的方法是,立即释放系统范围内的dentries和inode缓存(重启机器...)。 查阅资料发现,较新的内核版本(4.19+)改进了memory.stat调用的性能,因此在移至该内核之后,这不再是问题。
Pre:
CentOS7 安装内存测试工具 memtester
Next:
kubernetes容器生成core文件
0
likes
1051
Weibo
Wechat
Tencent Weibo
QQ Zone
RenRen
Please enable JavaScript to view the
comments powered by Disqus.
comments powered by
Disqus
Table of content