K8S内存泄漏问题处理
K8S内存泄漏问题处理
问题描述
我使用kubeadm 安装的K8S集群,随着pod增多,运行的时间久了,就会出现不能创建pod的情况。当kubectl describe pod,发现有 cannot allocate memory的错误信息。只有重启对应的服务器,才可以增加pod,异常提示才会消失。但继续随着时间的推移,pod的增多,该问题会继续出现。
问题分析
根据pod的异常信息,初步判断K8S可能造成了内存泄漏。
使用 cat /sys/fs/cgroup/memory/kubepods/memory.kmem.slabinfo 在出现问题的node查看时,如果显示如下图,则说明没有存在内存泄漏:
如果显示如下图,则说明存在内存泄漏:
具体原因
原因一句话:kmem导致内存泄露:
内核对于每个 cgroup 子系统的的条目数是有限制的,限制的大小定义在 kernel/cgroup.c #L139,当正常在 cgroup 创建一个 group 的目录时,条目数就加1。我们遇到的情况就是因为开启了 kmem accounting 功能,虽然 cgroup 的目录删除了,但是条目没有回收。这样后面就无法创建65535个 cgroup 了。也就是说,在当前内核版本下,开启了 kmem accounting 功能,会导致 memory cgroup 的条目泄漏无法回收。
Kmem在3.X内核的机器上存在内存泄漏
cgroup 的 kmem account 特性在 3.x 内核上有内存泄露问题,如果开启了 kmem account 特性 会导致可分配内存越来越少,直到无法创建新 pod 或节点异常。
几点解释:
- kmem account 是cgroup 的一个扩展,全称CONFIG_MEMCG_KMEM,属于机器默认配置,本身没啥问题,只是该特性在 3.10 的内核上存在漏洞有内存泄露问题,4.x的内核修复了这个问题。
- 因为 kmem account 是 cgroup 的扩展能力,因此runc、docker、k8s 层面也进行了该功能的支持,即默认都打开了kmem 属性
- 因为3.10 的内核已经明确提示 kmem 是实验性质,我们仍然使用该特性,所以这其实不算内核的问题,是 k8s 兼容问题。
k8s在 1.9版本开启了对 kmem 的支持,因此 1.9 以后的所有版本都有该问题,但必须搭配 3.x内核的机器才会出问题。一旦出现会导致新 pod 无法创建,已有 pod不受影响,但pod 漂移到有问题的节点就会失败,直接影响业务稳定性。因为是内存泄露,直接重启机器可以暂时解决,但还会再次出现
了解更多理论原因,可参考 https://blog.kelu.org/tech/2020/09/29/cgroup-kmem.html。
总而言之:K8S 1.9版本及以后的版本,在内核是3.X的服务器上,都会出现内存泄漏的问题。在4.X的内核上,则修复了这个问题
问题处理
处理这个问题,可以升级服务器的内核。不过推荐使用下载kubelet和runc的源码,编译、再替换原来的。
一、配置go语言环境
我的机器系统是centos7.6。配置我参考了 http://docs.studygolang.com/doc/install 、 https://www.cnblogs.com/biaopei/p/11883104.html 、https://studygolang.com/articles/7202。
1.下载go源码包,版本要>1.16
下载地址:https://golang.google.cn/dl/ 或者 https://studygolang.com/dl 官网 https://golang.org/dl/ 很可能访问通。我下载的是 go1.17.3.linux-amd64.tar.gz。
2.解压源码包
移除之前的源码包(如果有),并解压源码包到/usr/local:
rm -rf /usr/local/go && tar -C /usr/local -xzf go1.17.3.linux-amd64.tar.gz
3.配置golang的系统环境变量(选择一种配置方式即可)
临时配置:下面这个配置,是临时的,服务器重启后,要重新执行。
export PATH=$PATH:/usr/local/go/bin
永久配置方式一:
echo 'export PATH=$PATH:/usr/local/go/bin'>>/etc/profile #配置系统变量
source /etc/profile
永久配置方案二:
vi /etc/profile
在文件中,追加如下环境变量
export GOROOT=/usr/local/go #设置为go安装的路径
export GOPATH=$HOME/gocode #默认安装包的路径
export PATH=$PATH:$GOROOT/bin:$GOPATH/bin
追加后,再执行如下命令,才能让环境变量生效。
source /etc/profile
环境变量生效后,验证了。
go version
二、编译runc
参考 https://github.com/opencontainers/runc 和 https://www.cnblogs.com/zhangmingcheng/p/14309962.html
1.源码下载:
下载源码时,通过git下载,要有git工具,yum install git
安装。
mkdir -p /data/Documents/src/github.com/opencontainers/
cd /data/Documents/src/github.com/opencontainers/
git clone https://github.com/opencontainers/runc (或者 git clone git://github.com/opencontainers/runc)
也可以手动从 https://github.com/opencontainers/runc 下载,再放入/data/Documents/src/github.com/opencontainers/ 中 )
2.安装编译工具
安装编译runc的工具libseccomp。其中,centos安装 libseccomp-devel,ubuntu安装 libseccomp-dev。
yum install libseccomp-devel
安装 gcc编译器:编译runc,还需要gcc编译器。
yum -y install gcc gcc-c++ kernel-devel
3.执行编译
cd runc
make BUILDTAGS='seccomp nokmem'
执行make指令的时候,一定要加上 BUILDTAGS='seccomp nokmem'。这样,重新编译的runc才不会开启kmem属性,也就不会造成内存泄漏
编译完成之后会在当前目录下看到一个runc的可执行文件
三、编译kubelet
1.源码下载:
mkdir -p /root/k8s/
cd /root/k8s/
git clone https://github.com/kubernetes/kubernetes 或者 git clone git://github.com/kubernetes/kubernetes
(这一步,建议从国内的码云下载,github会非常慢: git clone https://gitee.com/mirrors/Kubernetes.git)
版本还原:根据自己安装的K8S版本,将源码还原到对应的版本:
cd Kubernetes/
git checkout v1.20.0
2.编译
GO111MODULE=on KUBE_GIT_TREE_STATE=clean KUBE_GIT_VERSION=v1.20.0 make kubelet GOFLAGS="-tags=nokmem"
make编译的时候,必须要加上参数 GOFLAGS="-tags=nokmem"。这样编译的kubelet才不会开启kmem属性,也就不会导致内存泄漏。
生成的kubelet二进制文件在生成的_output路径下的bin当中。
四、替换runc和kubelet
1.备份原有的kubelet和runc
cp /usr/bin/kubelet /home/kubelet
cp /usr/bin/runc /home/runc
2.停止kubelet和docker,然后替换runc和kubelet
systemctl stop docker
systemctl stop kubelet
cp kubelet /usr/bin/kubelet
cp kubelet /usr/local/bin/kubelet
cp runc /usr/bin/runc
3.重启服务器,检查内存泄漏
cat /sys/fs/cgroup/memory/kubepods/memory.kmem.slabinfo
执行命令后,显示如下,则说明内存泄漏已修复。