Cgroups泄漏:
Kubernetes中潜在的定时炸弹
我们是光大科技有限公司智能云计算部云计算团队容器云项目组,致力于容器技术在金融行业的落地与实践,以容器云助力金融行业科技转型。我们的团队拥有经验丰富的容器与云原生领域研发工程师和专家,将不定期分享容器和云原生行业的原创技术文章和实践经验,共同探索与见证云原生时代金融领域的前沿技术和发展趋势。
背景
在建设云昊容器云时发现有些Worker节点在使用过程中就会突然提示剩余空间不足的错误。并且在此之后任何新的Pod都无法成功的部署到该Worker节点中。而且越是配置大的节点越容易出现这一异常。该错误具体提示如下:
mkdir /sys/fs/cgroups/memory/docker/406cf…1eb13a: no space left on device
可实际上通过df命令查看硬盘使用情况的时候却发现这个目录所在的分区可能实际使用量很小。经过一段时间的排查后发现这其实是一个内核问题,而且是一个潜在的“定时炸弹”。
Technology,with Passion
●●●
Docker虚拟化原理
Docker是一个轻量级的虚拟化技术,与传统虚拟化计数不同,Docker并非是建立于Hypervisor之上的虚拟机应用。通过下图我们可以看到Docker和基于Hypervisor的虚拟机技术之间的区别:
在上图中我们可以看到,Docker容器在运行时并不会带入一个新的操作系统,换句话说Docker容器其实就是一个带有特殊视角的普通进程。但既然容器只是一个普通进程,那么为什么容器并不能直接访问到宿主机上的资源呢?而且通过设置,容器也不能无限制的使用宿主机的资源。为了实现容器计算资源和进程工作区的隔离,Docker使用了Namespace和Cgroups这两项技术。
Technology,with Passion
●●●
资源隔离与限额
Resource isolation and limits
Namespace和Cgroups都是Linux内核中的特性。
Namespace:通过Namespace,我们可以对进程提供内核资源上的隔离,即进程、网络、用户、UTS等多种资源的隔离。Docker使用了其中以下几种Namespace进行隔离:
-
pid:用以进程隔离
-
net:用以网络隔离
-
ipc:用以IPC(进程间通信)隔离
-
mnt:用以挂载点(文件)隔离
-
UTS:用以内核版本,主机名,域名隔离
简单来说,有了Namespace,我们就不需要担心容器会访问到不该访问的资源。
Cgroups:即Linux Control Group。这项技术主要用来限制物理资源的使用上限,如CPU、内存、磁盘和带宽等。Docker通常会通过Cgroups来实现CPU和内存的限制。
Minimalist technology,with Passion
●●●
故障成因
本次故障就是由于Cgroups的BUG所导致的。当我们为工作负载设置内存限制时,Docker_HOST会在/sys/fs/cgroups/memory/docker/目录下为容器创建对应的目录,并在其中设置内存的使用上限。
而Memcg是Linux中用来管理Cgroups内存的模块。在Memcg中默认会开启Kmem(Kernel memory accounting)功能,这个功能可以在设置Cgroups限制应用对用户内存的使用后再继续增加应用对内核内存的使用限制[1]。由于内核内存并不能使用swap,这意味着内核内存是一个非常有限的资源,所以开启Kmem后内核会限制memcg struct的数量。通常情况下,伴随着Pod的漂移或新建销毁过程中Cgroups的消亡,相应的memcg struct也应该随即释放,但是该对象只会在出现内存压力时被回收[2]。就这样memcg struct的数量不断上升,这个struct中的id值也不断的接近设定的临界值。
在内核内存中memcg struct的id到达65535临界值后,虽然可能实际上只有三五个Pod在运行,内核仍然会拒绝后续带有资源限制容器部署创建Cgroups的行为,最终导致前文中错误日志产生。在后续的内核更新(73f576c04)中memcg id和memcg struct得到了分离,其id能够在Cgroups销毁后被释放。[3]
当我们启用了弹性伸缩、CICD、Cronjob等自动化的Pod创建任务或者某些容器因为存在故障而不断crash backoff时,Pod的反复创建会加快id到达临界值的速度。即使没有上述情况,也有可能会在未来的某一个时间点中触发该故障。
如果在系统日志中发现类似前文中的日志产生,但是通过sudo cat /proc/cgroups |grep memory却发现num_groups(即输出内容的第三项)并没有达到临界值,就可以确认是本文中提到的问题了。
Technology,with Passion
●●●
处理方法
处理本类故障主要有三类方式,分别是安装更新、释放内存和禁用Kmem功能。在下文中会列出这几类处理方式的多种实践方式及其优劣,在实际遇到相关问题时可以依据实际情况结合一种或多种实践方式来执行。
1. 安装更新:通过以下其中一种方式安装更新可以一劳永逸的避免该类问题的复现。
-
升级内核版本到包含commit hash为73f576c04的4.6.7及更高版本
-
安装RHSA-2019:3055补丁
-
升级RHEL到7.8及更高版本
这种方式可以从源头上阻止问题的复现,但很多情况下我们并不能随意的去升级运行中的节点的内核、安装安全补丁之类的,因为这会导致机器重新启动,并带来未知的新问题。
2. 释放内存:通过以下其中一种做法,我们可以释放掉内存中已有的struct来临时恢复节点的可用性。
-
重启节点
-
设置一个定时任务6 */12 * * * root echo 3 > /proc/sys/vm/drop_caches,定期将释放计数。[4]
这两种做法都是仅适用于紧急情况的临时处理方法。用户需要慎重考虑这些操作会带来的副作用:前者可能会导致Pod漂移等意外结果,后者会影响性能并可能导致一些状态为dangling的容器Cgroups被释放。同时后者也无法应对发生由于容器运行错误导致的突发性溢出问题,无法作为正式的处理方案。优点就是见效快,能够在短期内处理问题。
3. 禁用Kmem功能:通过以下其中一种做法,我们可以暂停使用Kmem功能。禁用该功能后,将不再会有对内核内存的限制,自然也就没有了临界值的限制。
-
将内核参数设置为
cgroup.memory=nokmem[4]
重新编译Kubelet来禁止使用相关特性。以1.14.1版本为例,其编译命令如下:
$ git clone –branch v1.14.1 –single-branch –depth 1 [https://github.com/kubernetes/kubernetes](https://github.com/kubernetes/kubernetes)
$ cd kubernetes
$ KUBE_GIT_VERSION=v1.14.1 ./build/run.sh make kubelet
GOFLAGS=\\”-tags=nokmem\\”
编译完成后,更新将Kubernetes集群中的组件更新为新编译的版本。
在无法对系统进行更新的情况下可以采取这种方式。云昊容器云团队最后所采用的处理方法是通过重新编译Kubelet,这样我们就可以在不重启机器的情况下处理并避免该问题的再次发生。
Technology,with Passion
●●●
参考资料
[1] https://www.kernel.org/doc/Documentation/cgroup-v1/memory.txt
[2] https://bugzilla.kernel.org/show_bug.cgi?id=124641
[3] https://lore.kernel.org/patchwork/patch/690171/
[4] https://github.com/kubernetes/kubernetes/issues/61937
Technology,with Passion
●●●
–
原创文章,作者:EBCloud,如若转载,请注明出处:https://www.sudun.com/ask/32901.html