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
執行命令后,顯示如下,則說明內存泄漏已修復。

