k8s最佳實踐:cgroup kmem的內存泄露問題

k8s最佳實踐:cgroup kmem的內存泄露問題

1.前言

這篇文章的全稱應該叫:[在某些內核版本上,cgroup 的 kmem account 特性有內存泄露問題],如果你遇到過 pod 的 "cannot allocated memory"報錯,node 內核日誌的“SLUB: Unable to allocate memory on node -1”報錯,那麼恭喜你中招了。

2.現象

我們的環境:

  • K8S 版本: 1.11、1.13、1.16
  • docker 版本:18.09
  • 機器操作系統:centos7、centos6
  • 機器內核版本:3.10

可能會出現以下幾種現象:

1.pod 狀態異常,describe pod 顯示原因爲: no allocated memory

2.節點上執行 dmesg 有日誌顯示:slub無法分配內存:SLUB: Unable to allocate memory on node -1

3.節點 oom開始按優先級殺進程,有可能會導致有些正常 pod 被殺掉

4.機器free 查看可用內存還有很多,卻無法分配,懷疑是內存泄露。

3.原因

一句話總結:

cgroup 的 kmem account 特性在 3.x 內核上有內存泄露問題,如果開啓了 kmem account 特性 會導致可分配內存越來越少,直到無法創建新 pod 或節點異常。

幾點解釋:

  1. kmem account 是cgroup 的一個擴展,全稱CONFIG_MEMCG_KMEM,屬於機器默認配置,本身沒啥問題,只是該特性在 3.10 的內核上存在漏洞有內存泄露問題,4.x的內核修復了這個問題。
  2. 因爲 kmem account 是 cgroup 的擴展能力,因此runc、docker、k8s 層面也進行了該功能的支持,即默認都打開了kmem 屬性
  3. 因爲3.10 的內核已經明確提示 kmem 是實驗性質,我們仍然使用該特性,所以這其實不算內核的問題,是 k8s 兼容問題。

4.解決方案

方案1:升級內核

既然是 3.x 的問題,直接升級內核到 4.x 及以上即可,內核問題解釋:

這種方式的缺點是:

  1. 需要升級所有節點,節點重啓的話已有 pod 肯定要漂移,如果節點規模很大,這個升級操作會很繁瑣,業務部門也會有意見,要事先溝通。
  2. 這個問題歸根結底是軟件兼容問題,3.x 自己都說了不成熟,不建議你使用該特性,k8s、docker卻 還要開啓這個屬性,那就不是內核的責任,因爲我們是雲上機器,想替換4.x 內核需要虛機團隊做足夠的測試和評審,因此這是個長期方案,不能立刻解決問題。
  3. 已有業務在 3.x 運行正常,不代表可以在 4.x 也運行正常,即全量升級內核之前需要做足夠的測試,尤其是有些業務需求對os做過定製。

因爲 2 和 3 的原因,我們沒有選擇升級內核,決定使用其他方案

方案2:修改機器啓動引導項

修改虛機啓動的引導項 grub 中的cgroup.memory=nokmem,讓機器啓動時直接禁用 cgroup的 kmem 屬性

修改/etc/default/grub 爲:
GRUB_CMDLINE_LINUX="crashkernel=auto net.ifnames=0 biosdevname=0 intel_pstate=disable cgroup.memory=nokmem"

生成配置:
/usr/sbin/grub2-mkconfig -o /boot/grub2/grub.cfg

重啓機器:
reboot 


驗證:
cat /sys/fs/cgroup/memory/kubepods/burstable/pod*/*/memory.kmem.slabinfo 無輸出即可。

這個方式對一些機器生效,但有些機器替換後沒生效,且這個操作也需要機器重啓,暫時不採納

方案3:修改並重新編譯kubelet和runc

此方案屬於在 k8s 維度禁用該屬性(kmem account 特性)

對於v1.13及其之前版本的kubelet,需要手動替換以下兩個函數。

vendor/github.com/opencontainers/runc/libcontainer/cgroups/fs/memory.go

func EnableKernelMemoryAccounting(path string) error {
    return nil
}


func setKernelMemory(path string, kernelMemoryLimit int64) error {
    return nil
}

重新編譯並替換 kubelet

make WHAT=cmd/kubelet GOFLAGS=-v GOGCFLAGS="-N -l"

對於v1.14及其之後版本的kubelet 通過添加BUILDTAGS來禁止 kmem accounting.

make BUILDTAGS="nokmem" WHAT=cmd/kubelet GOFLAGS=-v GOGCFLAGS="-N -l"

我們遇到1.16 版本的BUILDTAGS=”nokmem“編譯出來的 let 還是有問題,還是通過修改代碼的方式使其生效:

修改文件:
vendor/github.com/opencontainers/runc/libcontainer/cgroups/fs/kmem.go

爲:

// +build linux,!nokmem

package fs

import (
    "errors"
)

func EnableKernelMemoryAccounting(path string) error {
    return nil
}

func setKernelMemory(path string, kernelMemoryLimit int64) error {
    return errors.New("kernel memory accounting disabled in this runc build")
}

Golang

Copy

編譯前,可以編輯下文件 hack/lib/version.sh,將 KUBE_GIT_TREE_STATE="dirty" 改爲 KUBE_GIT_TREE_STATE="clean",確保版本號乾淨。

這裏面提下兩篇文章:

都修改了 kubelet,pingcap 的文章有提到,docker18.09 默認關閉了 kmem,我們用的就是 18.09,但其實 docker 是打開了的,包括現在最新版的 docker-ce,直接 docker run 出來的容器也有 kmem

因此只修改 kubelet 在某些情況下是有問題的,判斷依據是:

  • /sys/fs/cgroup/memory/memory.kmem.slabinfo
  • /sys/fs/cgroup/memory/kubepods/memory.kmem.slabinfo
  • /sys/fs/cgroup/memory/kubepods/burstabel/pod123456/xxx/memory.kmem.slabinfo

上邊的三個文件,前兩個是由 let 生成,對應 pod 維度的,修復 kubelet 後cat 該文件發現沒有開啓 kmem符合預期,但第三個是開啓了的,猜測是 docker 層runc 生成容器時又打開了

因此,最簡單的方式是和騰訊一樣,直接修改下層的runc,在 runc層面將kmem直接寫死爲 nokmem

runc 文檔:https://github.com/opencontainers/runc/blob/a15d2c3ca006968d795f7c9636bdfab7a3ac7cbb/README.md

方式:用最新版的 runc, make BUILDTAGS="seccomp nokmem" 然後 替換 /usr/bin/runc

驗證:替換了 runc 後,不重啓 docker,直接 kubectl run 或者 docker run, 新容器都會禁用 kmem,當然如果 kill 老 pod,新產生的 pod也禁用了kmem,證明沒有問題

5.驗證

找到一個設置了 request、limit的 pod,然後獲取其 cgroup 中的 memory.kmem.slabinfo文件,如果報錯或爲 0,就證明沒開 kmem,就沒問題。

cat /sys/fs/cgroup/memory/kubepods/burstable/pod*/*/memory.kmem.slabinfo 

你也可以直接新建一個:

kubectl run nginx-1 --image=hub.baidubce.com/cce/nginx-alpine-go:latest --port=80 --restart=Never --requests='cpu=100m,memory=100Mi' --limits="cpu=200m,memory=200Mi"

然後 docker ps | grep nginx-1 得到容器 id

find /sys/fs/cgroup/memory -name "memory.kmem.slabinfo" | grep 容器 id,得到slabinfo的路徑,直接 cat看結果

這個驗證方式也是上邊的復現方式。

6.影響範圍

k8s在 1.9版本開啓了對 kmem 的支持,因此 1.9 以後的所有版本都有該問題,但必須搭配 3.x內核的機器纔會出問題。

一旦出現會導致新 pod 無法創建,已有 pod不受影響,但pod 漂移到有問題的節點就會失敗,直接影響業務穩定性。因爲是內存泄露,直接重啓機器可以暫時解決,但還會再次出現

7.原理解釋

1.kmem 是什麼?

kmem 是cgroup 的一個擴展,全稱CONFIG_MEMCG_KMEM,屬於機器默認配置。

內核內存與用戶內存:

內核內存:專用於Linux內核系統服務使用,是不可swap的,因而這部分內存非常寶貴的。但現實中存在很多針對內核內存資源的攻擊,如不斷地fork新進程從而耗盡系統資源,即所謂的“fork bomb”。

爲了防止這種攻擊,社區中提議通過linux內核限制 cgroup中的kmem 容量,從而限制惡意進程的行爲,即kernel memory accounting機制。

使用如下命令查看KMEM是否打開:

# cat /boot/config-`uname -r`|grep CONFIG_MEMCG
CONFIG_MEMCG=y
CONFIG_MEMCG_SWAP=y
CONFIG_MEMCG_SWAP_ENABLED=y
CONFIG_MEMCG_KMEM=y

2.cgroup 與 kmem機制

使用 cgroup 限制內存時,我們不但需要限制對用戶內存的使用,也需要限制對內核內存的使用。

kernel memory accounting 機制爲 cgroup 的內存限制增加了 stack pages(例如新進程創建)、slab pages(SLAB/SLUB分配器使用的內存)、sockets memory pressure、tcp memory pressure等,以保證 kernel memory 不被濫用。

當你開啓了kmem 機制,具體體現在 memory.kmem.limit_in_bytes 這個文件上:

/sys/fs/cgroup/memory/kubepods/pod632f736f-5ef2-11ea-ad9e-fa163e35f5d4/memory.kmem.limit_in_bytes

實際使用中,我們一般將 memory.kmem.limit_in_bytes 設置成大於 memory.limit_in_bytes,從而只限制應用的總內存使用。

kmem 的 limit 與普通 mem 的搭配,參考這篇文章:https://lwn.net/Articles/516529/

cgroup 文檔: https://www.kernel.org/doc/Documentation/cgroup-v1/memory.txt

3.kmem屬性的漏洞

在4.0以下版本的 Linux 內核對 kernel memory accounting 的支持並不完善,在3.x 的內核版本上,會出現 kernel memory 無法回收,bug 解釋:

4.docker 與 k8s 使用 kmem

以上描述都是cgroup層面即機器層面,但是 runc 和 docker 發現有這個屬性之後,在後來的版本中也支持了 kmem ,k8s 發現 docker支持,也在 1.9 版本開始支持。

1.9版本及之後,kubelet 纔開啓 kmem 屬性

kubelet 的這部分代碼位於:

https://github.com/kubernetes/kubernetes/blob/release-1.12/vendor/github.com/opencontainers/runc/libcontainer/cgroups/fs/memory.go#L70-L106

對於k8s、docker 而言,kmem 屬性屬於正常迭代和優化,至於 3.x 的內核上存在 bug 不能兼容,不是k8s 關心的問題

但 issue 中不斷有人反饋,因此在 k8s 1.14 版本的 kubelet 中,增加了一個編譯選項 make BUILDTAGS="nokmem",就可以編譯 kubelet 時就禁用 kmem,避免掉這個問題。而1.8 到1.14 中間的版本,只能選擇更改 kubelet 的代碼。

5.slub 分配機制

因爲節點 dmesg 的報錯是:SLUB: Unable to allocate memory on node -1

cgroup 限制下,當用戶空間使用 malloc 等系統調用申請內存時,內核會檢查線性地址對應的物理地址,如果沒有找到會觸發一個缺頁異常,進而調用 brk 或 do_map 申請物理內存(brk申請的內存通常小於128k)。而對於內核空間來說,它有2種申請內存的方式,slub和vmalloc:

  • slab用於管理內存塊比較小的數據,可以在/proc/slabinfo下查看當前slab的使用情況,
  • vmalloc操作的內存空間爲 VMALLOC_START~4GB,適用於申請內存比較大且效率要求不高的場景。可以在/proc/vmallocinfo中查看vmalloc的內存分佈情況。
  • 可以在/proc/buddyinfo中查看當前空閒的內存分佈情況,

6.其他表現

  • 除了最上面提到的無法分配內存問題,kmem 還會導致其他現象,如pod資源佔用過高問題
  • 復現該問題還有一種方式,就是瘋狂創建 cgroup 文件,直到 65535 耗盡,參考:https://github.com/kubernetes/kubernetes/issues/61937
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章