作者介绍
谢泽钦,Rancher Labs技术支持工程师,负责Rancher中国客户的维护和相关技术支持。4年的云计算领域的经验,经历了OpenStack到Kubernetes的技术变革,对底层操作系统、KVM虚拟化和Docker容器等相关云技术拥有丰富的运维和实践经验。
本文是为Rancher客户进行问题排查的记录。
问题背景
我们发现客户的Kubernetes集群环境中所有的worker节点的Kubelet进程的CPU使用率长时间占用过高,通过pidstat可以看到CPU使用率高达100%。本文记录下了本次问题排查的过程。
集群环境
排查过程
使用strace工具对kubelet进程进行跟踪
1、由于Kubelet进程CPU使用率异常,可以使用strace工具对kubelet进程动态跟踪进程的调用情况,首先使用strace -cp
<section style="margin-left: 8px;margin-right: 8px;line-height: 2em;"><span class="code-snippet_outer"><span style="font-size: 15px;">futex(uint32_t *uaddr, int futex_op, uint32_t val,</span></span>
<span class="code-snippet_outer"><span style="font-size: 15px;">const struct timespec *timeout, /* or: uint32_t val2 */</span></span>
<span class="code-snippet_outer"><span style="font-size: 15px;">uint32_t *uaddr2, uint32_t val3);</span></span>
</section>
官方文档给出ETIMEDOUT的解释:
<section style="margin-left: 8px;margin-right: 8px;line-height: 2em;"><span class="code-snippet_outer"><span style="font-size: 15px;">ETIMEDOUT</span></span>
<span class="code-snippet_outer"><span style="font-size: 15px;"> The operation in futex_op employed the timeout specified in</span></span>
<span class="code-snippet_outer"><span style="font-size: 15px;"> timeout, and the timeout expired before the operation</span></span>
<span class="code-snippet_outer"><span style="font-size: 15px;"> completed.</span></span>
</section>
意思就是在指定的timeout时间中,未能完成相应的操作,其中futex_op对应上述输出结果的FUTEX_WAIT_PRIVATE和FUTEX_WAIT_PRIVATE,可以看到基本都是发生在FUTEX_WAIT_PRIVATE时发生的超时。
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函数调用进行分析
早期的Kubernetes版本,可以直接通过debug/pprof 接口获取debug数据,后面考虑到相关安全性的问题,取消了这个接口,具体信息可以参考CVE-2019-11248(https://github.com/kubernetes/kubernetes/issues/81023)。因此我们将通过kubectl开启proxy进行相关数据指标的获取:
<section style="margin-left: 8px;margin-right: 8px;line-height: 2em;"><span class="code-snippet_outer"><span style="font-size: 15px;">kubectl proxy --address='0.0.0.0' --accept-hosts='^*$'</span></span>
</section>
这里需要注意,如果使用的是Rancher UI上复制的kubeconfig文件,则需要使用指定了master IP的context,如果是RKE或者其他工具安装则可以忽略。
<section style="margin-left: 8px;margin-right: 8px;line-height: 2em;"><span class="code-snippet_outer"><span style="font-size: 15px;">docker run -itd --name golang-env --net host golang bash</span></span>
</section>
3、使用go pprof工具导出采集的指标,这里替换127.0.0.1为apiserver节点的IP,默认端口是8001,如果docker run的环境跑在apiserver所在的节点上,可以使用127.0.0.1。另外,还要替换NODENAME为对应的节点名称。
<section style="margin-left: 8px;margin-right: 8px;line-height: 2em;"><span class="code-snippet_outer"><span style="font-size: 15px;">docker exec -it golang-env bash</span></span>
<span class="code-snippet_outer"><span style="font-size: 15px;">go tool pprof -seconds=60 -raw -output=kubelet.pprof http://127.0.0.1:8001/api/v1/nodes/${NODENAME}/proxy/debug/pprof/profile</span></span>
</section>
4、输出好的pprof文件不方便查看,需要转换成火焰图,推荐使用FlameGraph工具生成svg图
<section style="margin-left: 8px;margin-right: 8px;line-height: 2em;"><span class="code-snippet_outer"><span style="font-size: 15px;">git clone https://github.com/brendangregg/FlameGraph.git</span></span>
<span class="code-snippet_outer"><span style="font-size: 15px;">cd FlameGraph/</span></span>
<span class="code-snippet_outer"><span style="font-size: 15px;">./stackcollapse-go.pl kubelet.pprof > kubelet.out</span></span>
<span class="code-snippet_outer"><span style="font-size: 15px;">./flamegraph.pl kubelet.out > kubelet.svg</span></span>
</section>
转换成火焰图后,就可以在浏览器直观地看到函数相关调用和具体调用时间比了。
<section style="margin-left: 8px;margin-right: 8px;line-height: 2em;"><span class="code-snippet_outer"><span style="font-size: 15px;">func (s *MemoryGroup) GetStats(path string, stats *cgroups.Stats) error {</span></span>
<span class="code-snippet_outer"><span style="font-size: 15px;">// Set stats from memory.stat.</span></span>
<span class="code-snippet_outer"><span style="font-size: 15px;"> statsFile, err := os.Open(filepath.Join(path, "memory.stat"))</span></span>
<span class="code-snippet_outer"><span style="font-size: 15px;">if err != nil {</span></span>
<span class="code-snippet_outer"><span style="font-size: 15px;">if os.IsNotExist(err) {</span></span>
<span class="code-snippet_outer"><span style="font-size: 15px;">return nil</span></span>
<span class="code-snippet_outer"><span style="font-size: 15px;"> }</span></span>
<span class="code-snippet_outer"><span style="font-size: 15px;">return err</span></span>
<span class="code-snippet_outer"><span style="font-size: 15px;"> }</span></span>
<span class="code-snippet_outer"><span style="font-size: 15px;">defer statsFile.Close()</span></span>
<span class="code-snippet_outer"><br></span>
<span class="code-snippet_outer"><span style="font-size: 15px;"> sc := bufio.NewScanner(statsFile)</span></span>
<span class="code-snippet_outer"><span style="font-size: 15px;">for sc.Scan() {</span></span>
<span class="code-snippet_outer"><span style="font-size: 15px;"> t, v, err := fscommon.GetCgroupParamKeyValue(sc.Text())</span></span>
<span class="code-snippet_outer"><span style="font-size: 15px;">if err != nil {</span></span>
<span class="code-snippet_outer"><span style="font-size: 15px;">return fmt.Errorf("failed to parse memory.stat (%q) - %v", sc.Text(), err)</span></span>
<span class="code-snippet_outer"><span style="font-size: 15px;"> }</span></span>
<span class="code-snippet_outer"><span style="font-size: 15px;"> stats.MemoryStats.Stats[t] = v</span></span>
<span class="code-snippet_outer"><span style="font-size: 15px;"> }</span></span>
<span class="code-snippet_outer"><span style="font-size: 15px;"> stats.MemoryStats.Cache = stats.MemoryStats.Stats["cache"]</span></span>
<span class="code-snippet_outer"><br></span>
<span class="code-snippet_outer"><span style="font-size: 15px;"> memoryUsage, err := getMemoryData(path, "")</span></span>
<span class="code-snippet_outer"><span style="font-size: 15px;">if err != nil {</span></span>
<span class="code-snippet_outer"><span style="font-size: 15px;">return err</span></span>
<span class="code-snippet_outer"><span style="font-size: 15px;"> }</span></span>
<span class="code-snippet_outer"><span style="font-size: 15px;"> stats.MemoryStats.Usage = memoryUsage</span></span>
<span class="code-snippet_outer"><span style="font-size: 15px;"> swapUsage, err := getMemoryData(path, "memsw")</span></span>
<span class="code-snippet_outer"><span style="font-size: 15px;">if err != nil {</span></span>
<span class="code-snippet_outer"><span style="font-size: 15px;">return err</span></span>
<span class="code-snippet_outer"><span style="font-size: 15px;"> }</span></span>
<span class="code-snippet_outer"><span style="font-size: 15px;"> stats.MemoryStats.SwapUsage = swapUsage</span></span>
<span class="code-snippet_outer"><span style="font-size: 15px;"> kernelUsage, err := getMemoryData(path, "kmem")</span></span>
<span class="code-snippet_outer"><span style="font-size: 15px;">if err != nil {</span></span>
<span class="code-snippet_outer"><span style="font-size: 15px;">return err</span></span>
<span class="code-snippet_outer"><span style="font-size: 15px;"> }</span></span>
<span class="code-snippet_outer"><span style="font-size: 15px;"> stats.MemoryStats.KernelUsage = kernelUsage</span></span>
<span class="code-snippet_outer"><span style="font-size: 15px;"> kernelTCPUsage, err := getMemoryData(path, "kmem.tcp")</span></span>
<span class="code-snippet_outer"><span style="font-size: 15px;">if err != nil {</span></span>
<span class="code-snippet_outer"><span style="font-size: 15px;">return err</span></span>
<span class="code-snippet_outer"><span style="font-size: 15px;"> }</span></span>
<span class="code-snippet_outer"><span style="font-size: 15px;"> stats.MemoryStats.KernelTCPUsage = kernelTCPUsage</span></span>
<span class="code-snippet_outer"><br></span>
<span class="code-snippet_outer"><span style="font-size: 15px;"> useHierarchy := strings.Join([]string{"memory", "use_hierarchy"}, ".")</span></span>
<span class="code-snippet_outer"><span style="font-size: 15px;"> value, err := fscommon.GetCgroupParamUint(path, useHierarchy)</span></span>
<span class="code-snippet_outer"><span style="font-size: 15px;">if err != nil {</span></span>
<span class="code-snippet_outer"><span style="font-size: 15px;">return err</span></span>
<span class="code-snippet_outer"><span style="font-size: 15px;"> }</span></span>
<span class="code-snippet_outer"><span style="font-size: 15px;">if value == 1 {</span></span>
<span class="code-snippet_outer"><span style="font-size: 15px;"> stats.MemoryStats.UseHierarchy = true</span></span>
<span class="code-snippet_outer"><span style="font-size: 15px;"> }</span></span>
<span class="code-snippet_outer"><br></span>
<span class="code-snippet_outer"><span style="font-size: 15px;"> pagesByNUMA, err := getPageUsageByNUMA(path)</span></span>
<span class="code-snippet_outer"><span style="font-size: 15px;">if err != nil {</span></span>
<span class="code-snippet_outer"><span style="font-size: 15px;">return err</span></span>
<span class="code-snippet_outer"><span style="font-size: 15px;"> }</span></span>
<span class="code-snippet_outer"><span style="font-size: 15px;"> stats.MemoryStats.PageUsageByNUMA = pagesByNUMA</span></span>
<span class="code-snippet_outer"><br></span>
<span class="code-snippet_outer"><span style="font-size: 15px;">return nil</span></span>
<span class="code-snippet_outer"><span style="font-size: 15px;">}</span></span>
</section>
从代码中可以看到,进程会去读取memory.stat这个文件,这个文件存放了cgroup内存使用情况。也就是说,在读取这个文件花费了大量的时间。这时候,如果我们手动去查看这个文件,会是什么效果?
<section style="margin-left: 8px;margin-right: 8px;line-height: 2em;"><span class="code-snippet_outer"><span style="font-size: 15px;"># time cat /sys/fs/cgroup/memory/memory.stat >/dev/null</span></span>
<span class="code-snippet_outer"><span style="font-size: 15px;">real 0m9.065s</span></span>
<span class="code-snippet_outer"><span style="font-size: 15px;">user 0m0.000s</span></span>
<span class="code-snippet_outer"><span style="font-size: 15px;">sys 0m9.064s</span></span>
</section>
从这里可以看出端倪了,读取这个文件花费了9s,显然是不正常的。
解决方法
<section style="margin-left: 8px;margin-right: 8px;line-height: 2em;"><span class="code-snippet_outer"><span style="font-size: 15px;">echo 2 > /proc/sys/vm/drop_caches</span></span>
</section>
2、升级内核版本
其实这个主要还是内核的问题,在GitHub上这个commit(https://github.com/torvalds/linux/commit/205b20cc5a99cdf197c32f4dbee2b09c699477f0)中有提到,在5.2+以上的内核版本中,优化了CGroup stats相关的查询性能,如果想要更好的解决该问题,建议可以参考自己操作系统和环境,合理的升级内核版本。 另外Redhat在kernel-4.18.0-176(https://bugzilla.redhat.com/show_bug.cgi?id=1795049)版本中也优化了相关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
文章来源:Rancher Labs / 原文链接
END