Cgroup泄漏--潛藏在你的集群中


轉自: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 inseq 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系統調用

  1. centos 3.10.0-862.9.1.el7.x86_64及以下內核, 4G以上空閑內存,root權限。
  2. 把系統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。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM