轉自:https://tencentcloudcontainerteam.github.io/2018/12/29/cgroup-leaking/
前言
絕大多數的kubernetes集群都有這個隱患。只不過一般情況下,泄漏得比較慢,還沒有表現出來而已。
一個pod可能泄漏兩個memory cgroup數量配額。即使pod百分之百發生泄漏, 那也需要一個節點銷毀過三萬多個pod之后,才會造成后續pod創建失敗。
一旦表現出來,這個節點就徹底不可用了,必須重啟才能恢復。
故障表現
騰訊雲SCF(Serverless Cloud Function)底層使用我們的TKE(Tencent Kubernetes Engine),並且會在節點上頻繁創建和消耗容器。
SCF發現很多節點會出現類似以下報錯,創建POD總是失敗:
Dec 24 11:54:31 VM_16_11_centos dockerd[11419]: time="2018-12-24T11:54:31.195900301+08:00" level=error msg="Handler for POST /v1.31/containers/b98d4aea818bf9d1d1aa84079e1688cd9b4218e008c58a8ef6d6c3c106403e7b/start returned error: OCI runtime create failed: container_linux.go:348: starting container process caused \"process_linux.go:279: applying cgroup configuration for process caused \\\"mkdir /sys/fs/cgroup/memory/kubepods/burstable/pod79fe803c-072f-11e9-90ca-525400090c71/b98d4aea818bf9d1d1aa84079e1688cd9b4218e008c58a8ef6d6c3c106403e7b: no space left on device\\\"\": unknown"
這個時候,到節點上嘗試創建幾十個memory cgroup (以root權限執行 for i in
seq 1 20;do mkdir /sys/fs/cgroup/memory/${i}; done
),就會碰到失敗:
mkdir: cannot create directory '/sys/fs/cgroup/memory/8': No space left on device
其實,dockerd出現以上報錯時, 手動創建一個memory cgroup都會失敗的。 不過有時候隨着一些POD的運行結束,可能會多出來一些“配額”,所以這里是嘗試創建20個memory cgroup。
出現這樣的故障以后,重啟docker,釋放內存等措施都沒有效果,只有重啟節點才能恢復。
復現條件
docker和kubernetes社區都有關於這個問題的issue:
網上有文章介紹了類似問題的分析和復現方法。如:
http://www.linuxfly.org/kubernetes-19-conflict-with-centos7/?from=groupmessage
不過按照文中的復現方法,我在3.10.0-862.9.1.el7.x86_64
版本內核上並沒有復現出來。
經過反復嘗試,總結出了必現的復現條件。 一句話感慨就是,把進程加入到一個開啟了kmem accounting的memory cgroup並且執行fork系統調用。
- centos 3.10.0-862.9.1.el7.x86_64及以下內核, 4G以上空閑內存,root權限。
- 把系統memory cgroup配額占滿
for i in `seq 1 65536`;do mkdir /sys/fs/cgroup/memory/${i}; done
會看到報錯:
mkdir: cannot create directory ‘/sys/fs/cgroup/memory/65530’: No space left on device
這是因為這個版本內核寫死了,最多只能有65535個memory cgroup共存。 systemd已經創建了一些,所以這里創建不到65535個就會遇到報錯。
確認刪掉一個memory cgroup, 就能騰出一個“配額”:
rmdir /sys/fs/cgroup/memory/1 mkdir /sys/fs/cgroup/memory/test
3. 給一個memory cgroup開啟kmem accounting
cd /sys/fs/cgroup/memory/test/ echo 1 > memory.kmem.limit_in_bytes echo -1 > memory.kmem.limit_in_bytes
4. 把一個進程加進某個memory cgroup, 並執行一次fork系統調用
最簡單的就是把當前shell進程加進去: echo $$ > /sys/fs/cgroup/memory/test/tasks sleep 100 & cat /sys/fs/cgroup/memory/test/tasks
5. 把該memory cgroup里面的進程都挪走
for p in `cat /sys/fs/cgroup/memory/test/tasks`;do echo ${p} > /sys/fs/cgroup/memory/tasks; done cat /sys/fs/cgroup/memory/test/tasks //這時候應該為空
6. 刪除這個memory cgroup
rmdir /sys/fs/cgroup/memory/test
7. 驗證剛才刪除一個memory cgroup, 所占的配額並沒有釋放
mkdir /sys/fs/cgroup/memory/xx
這時候會報錯:mkdir: cannot create directory ‘/sys/fs/cgroup/memory/xx’: No space left on devic
e
什么版本的內核有這個問題
搜索內核commit記錄,有一個commit應該是解決類似問題的:
4bdfc1c4a943: 2015-01-08 memcg: fix destination cgroup leak on task charges migration [Vladimir Davydov]
這個commit在3.19以及4.x版本的內核中都已經包含。 不過從docker和kubernetes相關issue里面的反饋來看,內核中應該還有其他cgroup泄漏的代碼路徑, 4.14版本內核都還有cgroup泄漏問題。
規避辦法
不開啟kmem accounting (以上復現步驟的第3步)的話,是不會發生cgroup泄漏的。
kubelet和runc都會給memory cgroup開啟kmem accounting。所以要規避這個問題,就要保證kubelet和runc,都別開啟kmem accounting。下面分別進行說明。
runc
查看代碼,發現在commit fe898e7 (2017-2-25, PR #1350)以后的runc版本中,都會默認開啟kmem accounting。代碼在libcontainer/cgroups/fs/kmem.go: (老一點的版本,代碼在libcontainer/cgroups/fs/memory.go)
const cgroupKernelMemoryLimit = "memory.kmem.limit_in_bytes" func EnableKernelMemoryAccounting(path string) error { // Ensure that kernel memory is available in this kernel build. If it // isn't, we just ignore it because EnableKernelMemoryAccounting is // automatically called for all memory limits. if !cgroups.PathExists(filepath.Join(path, cgroupKernelMemoryLimit)) { return nil } // We have to limit the kernel memory here as it won't be accounted at all // until a limit is set on the cgroup and limit cannot be set once the // cgroup has children, or if there are already tasks in the cgroup. for _, i := range []int64{1, -1} { if err := setKernelMemory(path, i); err != nil { return err } } return nil }
runc社區也注意到這個問題,並做了比較靈活的修復: https://github.com/opencontainers/runc/pull/1921
這個修復給runc增加了”nokmem”編譯選項。缺省的release版本沒有使用這個選項。 自己使用nokmem選項編譯runc的方法:
cd $GO_PATH/src/github.com/opencontainers/runc/ make BUILDTAGS="seccomp nokmem"
kubelet
kubelet在創建pod對應的cgroup目錄時,也會調用libcontianer中的代碼對cgroup做設置。在pkg/kubelet/cm/cgroup_manager_linux.go
的Create方法中,會調用Manager.Apply方法,最終調用vendor/github.com/opencontainers/runc/libcontainer/cgroups/fs/memory.go
中的MemoryGroup.Apply方法,開啟kmem accounting。
這里也需要進行處理,可以不開啟kmem accounting, 或者通過命令行參數來控制是否開啟。
kubernetes社區也有issue討論這個問題:https://github.com/kubernetes/kubernetes/issues/70324
但是目前還沒有結論。我們TKE先直接把這部分代碼注釋掉了,不開啟kmem accounting。