Contents [hide]
前言
這篇文章的全稱應該叫:[在某些內核版本上,cgroup 的 kmem account 特性有內存泄露問題],如果你遇到過 pod 的 "cannot allocated memory"
報錯,node 內核日志的“SLUB: Unable to allocate memory on node -1”
報錯,那么恭喜你中招了。
這個問題在 pingcap 的文章和騰訊雲的官方修復都發過,原因也講的很清楚,不過因為版本差異,文章里的方法有所變動,這里做下總結
現象
我們的環境:
- 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 查看可用內存還有很多,卻無法分配,懷疑是內存泄露。
原因
一句話總結:
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 兼容問題。
其他細節原因下面會解釋
解決方案
推薦方案三
方案一
既然是 3.x 的問題,直接升級內核到 4.x 及以上即可,內核問題解釋:
- https://github.com/torvalds/linux/commit/d6e0b7fa11862433773d986b5f995ffdf47ce672
- https://support.mesosphere.com/s/article/Critical-Issue-KMEM-MSPH-2018-0006
這種方式的缺點是:
- 需要升級所有節點,節點重啟的話已有 pod 肯定要漂移,如果節點規模很大,這個升級操作會很繁瑣,業務部門也會有意見,要事先溝通。
- 這個問題歸根結底是軟件兼容問題,3.x 自己都說了不成熟,不建議你使用該特性,k8s、docker卻 還要開啟這個屬性,那就不是內核的責任,因為我們是雲上機器,想替換4.x 內核需要虛機團隊做足夠的測試和評審,因此這是個長期方案,不能立刻解決問題。
- 已有業務在 3.x 運行正常,不代表可以在 4.x 也運行正常,即全量升級內核之前需要做足夠的測試,尤其是有些業務需求對os做過定制。
因為 2 和 3 的原因,我們沒有選擇升級內核,決定使用其他方案
方案二
修改虛機啟動的引導項 grub 中的cgroup.memory=nokmem
,讓機器啟動時直接禁用 cgroup的 kmem 屬性
這個方式對一些機器生效,但有些機器替換后沒生效,且這個操作也需要機器重啟,暫時不采納
方案三
在 k8s 維度禁用該屬性。issue 中一般建議修改 kubelet代碼並重新編譯
對於v1.13及其之前版本的kubelet,需要手動替換以下兩個函數。
重新編譯並替換 kubelet
對於v1.14及其之后版本的kubelet 通過添加BUILDTAGS來禁止 kmem accounting.
我們遇到1.16 版本的BUILDTAGS=”nokmem“編譯出來的 let 還是有問題,還是通過修改代碼的方式使其生效:
編譯前,可以編輯下文件 hack/lib/version.sh
,將 KUBE_GIT_TREE_STATE="dirty" 改為 KUBE_GIT_TREE_STATE="clean"
,確保版本號干凈。
這里面提下兩篇文章:
- pingcap:https://pingcap.com/blog/try-to-fix-two-linux-kernel-bugs-while-testing-tidb-operator-in-k8s/
- 騰訊雲:https://tencentcloudcontainerteam.github.io/2018/12/29/cgroup-leaking/
都修改了 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,證明沒有問題
驗證方式
找到一個設置了 request、limit的 pod
,然后獲取其 cgroup 中的 memory.kmem.slabinfo
文件,如果報錯或為 0,就證明沒開 kmem,就沒問題。
你也可以直接新建一個:
然后 docker ps | grep nginx-1
得到容器 id
這個驗證方式也是上邊的復現方式。
影響范圍
k8s在 1.9版本開啟了對 kmem 的支持,因此 1.9 以后的所有版本都有該問題,但必須搭配 3.x內核的機器才會出問題。
一旦出現會導致新 pod 無法創建,已有 pod不受影響,但pod 漂移到有問題的節點就會失敗,直接影響業務穩定性。因為是內存泄露,直接重啟機器可以暫時解決,但還會再次出現
原理解釋
kmem 是什么
kmem 是cgroup 的一個擴展,全稱CONFIG_MEMCG_KMEM,屬於機器默認配置。
內核內存與用戶內存:
內核內存:專用於Linux內核系統服務使用,是不可swap的,因而這部分內存非常寶貴的。但現實中存在很多針對內核內存資源的攻擊,如不斷地fork新進程從而耗盡系統資源,即所謂的“fork bomb”。
為了防止這種攻擊,社區中提議通過linux內核限制 cgroup中的kmem 容量,從而限制惡意進程的行為,即kernel memory accounting機制。
使用如下命令查看KMEM是否打開:
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 這個文件上:
實際使用中,我們一般將 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
kmem屬性的漏洞
在4.0以下版本的 Linux 內核對 kernel memory accounting 的支持並不完善,在3.x 的內核版本上,會出現 kernel memory 無法回收,bug 解釋:
- https://bugzilla.redhat.com/show_bug.cgi?id=1507149
- https://github.com/kubernetes/kubernetes/issues/61937
- https://support.d2iq.com/s/article/Critical-Issue-KMEM-MSPH-2018-0006
docker 與 k8s 使用 kmem
以上描述都是cgroup層面即機器層面,但是 runc 和 docker 發現有這個屬性之后,在后來的版本中也支持了 kmem ,k8s 發現 docker支持,也在 1.9 版本開始支持。
1.9版本及之后,kubelet 才開啟 kmem 屬性
kubelet 的這部分代碼位於:
對於k8s、docker 而言,kmem 屬性屬於正常迭代和優化,至於 3.x 的內核上存在 bug 不能兼容,不是k8s 關心的問題
但 issue 中不斷有人反饋,因此在 k8s 1.14 版本的 kubelet 中,增加了一個編譯選項 make BUILDTAGS="nokmem",
就可以編譯 kubelet 時就禁用 kmem,避免掉這個問題。而1.8 到1.14 中間的版本,只能選擇更改 kubelet 的代碼。
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
中查看當前空閑的內存分布情況,
其他的表現
- 除了最上面提到的無法分配內存問題,kmem 還會導致其他現象,如pod資源占用過高問題
- 復現該問題還有一種方式,就是瘋狂創建 cgroup 文件,直到 65535 耗盡,參考:
https://github.com/kubernetes/kubernetes/issues/61937