前提详情 在 3.x 的内核上,cgroup 的 kmem account 特性有内存泄露问题。kubelet 和 runc 都需要修复。
网上有言论说升级 Linux 内核至 kernel-3.10.0-1075.el7
及以上就可以修复这个问题,详细可见 slab leak causing a crash when using kmem control group 。但是我测试了下面的都不行:
CentOS7.4
CentOS7.6
CentOS7.7的 3.10.0-1062.el7.x86_64
CentOS Linux release 7.8.2003 (Core) - 3.10.0-1127.el7.x86_64
Linux其余发行版内核如果大于等于 4.4 应该没问题。 这里我们编译 kubelet 关闭 kmem。
准备条件 这里我们使用的编译参数会使用容器编译的,不需要宿主机上安装 golang,安装个 docker 就行了。
1c 4g
的机器,这里我是使用 CentOS 7.8.2003 (Core)
机器配置 2g 内存的时候编译提示 oom,升级到 4g 内存才编译成功的。
最好安装最新版本的docker
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 systemctl disable --now firewalld setenforce 0 sed -ri '/^[^#]*SELINUX=/s#=.+$#=disabled#' /etc/selinux/config cat>/etc/security/limits.d/custom.conf<<EOF * soft nproc 131072 * hard nproc 131072 * soft nofile 131072 * hard nofile 131072 root soft nproc 131072 root hard nproc 131072 root soft nofile 131072 root hard nofile 131072 EOF cat<<EOF > /etc/sysctl.d/docker.conf # 要求iptables对bridge的数据进行处理 net.bridge.bridge-nf-call-ip6tables = 1 net.bridge.bridge-nf-call-iptables = 1 net.bridge.bridge-nf-call-arptables = 1 # 开启转发 net.ipv4.ip_forward = 1 EOF sysctl --system curl -fsSL "https://get.docker.com/" | \ sed -r '/add-repo \$yum_repo/a sed -i "s#https://download.docker.com#http://mirrors.aliyun.com/docker-ce#" /etc/yum.repos.d/docker-*.repo ' | \ bash -s -- --mirror Aliyun mkdir -p /etc/docker/ cat>/etc/docker/daemon.json<<EOF { "bip": "172.17.0.1/16", "exec-opts": ["native.cgroupdriver=systemd"], "registry-mirrors": [ "https://fz5yth0r.mirror.aliyuncs.com", "https://dockerhub.mirrors.nwafu.edu.cn", "https://docker.mirrors.ustc.edu.cn", "https://reg-mirror.qiniu.com" ], "storage-driver": "overlay2", "storage-opts": [ "overlay2.override_kernel_check=true" ], "log-driver": "json-file", "log-opts": { "max-size": "100m", "max-file": "3" } } EOF mkdir -p /etc/systemd/system/docker.service.d/ cat>/etc/systemd/system/docker.service.d/10-docker.conf<<EOF [Service] ExecStartPost=/sbin/iptables --wait -I FORWARD -s 0.0.0.0/0 -j ACCEPT ExecStopPost=/bin/bash -c '/sbin/iptables --wait -D FORWARD -s 0.0.0.0/0 -j ACCEPT &> /dev/null || :' ExecStartPost=/sbin/iptables --wait -I INPUT -i cni0 -j ACCEPT ExecStopPost=/bin/bash -c '/sbin/iptables --wait -D INPUT -i cni0 -j ACCEPT &> /dev/null || :' EOF yum install -y epel-release bash-completion cp /usr/share/bash-completion/completions/docker /etc/bash_completion.d/ systemctl enable --now docker
编译 确定版本 查看下我们目前使用的 kubelet 版本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 kubectl version -o json { "clientVersion": { "major": "1", "minor": "15", "gitVersion": "v1.15.5", "gitCommit": "20c265fef0741dd71a66480e35bd69f18351daea", "gitTreeState": "clean", "buildDate": "2019-10-15T19:16:51Z", "goVersion": "go1.12.10", "compiler": "gc", "platform": "linux/amd64" }, "serverVersion": { "major": "1", "minor": "15", "gitVersion": "v1.15.5", "gitCommit": "20c265fef0741dd71a66480e35bd69f18351daea", "gitTreeState": "clean", "buildDate": "2019-10-15T19:07:57Z", "goVersion": "go1.12.10", "compiler": "gc", "platform": "linux/amd64" } }
安装编译的基础依赖
1 yum install -y rsync make
下载源码 这里我们使用容器编译,所以下载到啥地方都行,也不需要安装 go。
1 2 3 git clone https://github.com/kubernetes/kubernetes.git cd kubernetes git checkout v1.15.5
前提操作 查看 cross 镜像的版本号
1 2 $ cat build/build-image/cross/VERSION v1.12.10-1
拉国内的镜像,然后改名,高版本,好像 1.16 以上最终镜像名是 k8s.gcr.io/build-image/kube-cross
1 2 3 4 5 $ docker pull registry.aliyuncs.com/k8sxio/kube-cross:v1.12.10-1 $ docker tag registry.aliyuncs.com/k8sxio/kube-cross:v1.12.10-1 k8s.gcr.io/kube-cross:v1.12.10-1 # 高版本 docker tag registry.aliyuncs.com/k8sxio/kube-cross:xxx k8s.gcr.io/build-image/kube-cross:xxxx
编译,这个参数测试在 v1.15.5 里可用,网上的 make BUILDTAGS="nokmem" WHAT=cmd/kubelet GOFLAGS=-v GOGCFLAGS="-N -l"
会无法编译
1 2 3 4 # 在v1.15.5似乎无用 ./build/run.sh make kubelet KUBE_BUILD_PLATFORMS=linux/amd64 BUILDTAGS="nokmem" # 用下面的 ./build/run.sh make kubelet GOFLAGS="-v -tags=nokmem" KUBE_BUILD_PLATFORMS=linux/amd64
查看编译完成的
1 2 3 4 5 6 7 8 9 [root@centos7 kubernetes]# ls -l _output/dockerized/bin/linux/amd64/ total 202880 -rwxr-xr-x 1 root root 9203530 Apr 7 22:02 conversion-gen -rwxr-xr-x 1 root root 9207908 Apr 7 22:02 deepcopy-gen -rwxr-xr-x 1 root root 9156147 Apr 7 22:02 defaulter-gen -rwxr-xr-x 1 root root 4709220 Apr 7 22:02 go2make -rwxr-xr-x 1 root root 2894872 Apr 7 22:03 go-bindata -rwxr-xr-x 1 root root 157545104 Apr 7 22:13 kubelet -rwxr-xr-x 1 root root 15018430 Apr 7 22:03 openapi-gen
runc 关闭 kmem v1.0.0-rc94起 kmem 设置就被忽略了,意味着直接下载最新版本的 runc 就行了,不需要自己去编译 runc 了。
arm64
的 runc 可以直接下面这样编译,不过 arm64 目前国内的系统的内核都很高不存在这个问题,没必要编译 runc 。
1 2 make shell make localcross
1.0.0-rc10 版本 https://cloud.tencent.com/developer/article/1743789 这个文章里说了 runc 也需要关闭
如果下面命令能成功执行则说明 runc 没关闭 kmem ,当然此方法不是绝对的,新 runc 下命令执行没有问题的,要看文章最后面的 memory.kmem.slabinfo
1 docker run --rm --name test --kernel-memory 100M nginx:alpine
19.03.14
测试发现可以运行,说明没有关闭,查看它的 runc
版本
1 2 3 4 $ runc --version runc version 1.0.0-rc10 commit: dc9208a3303feef5b3839f4323d9beb36df0a9dd spec: 1.0.1-dev
根据 commit 跳转
1 https://github.com/opencontainers/runc/commit/dc9208a3303feef5b3839f4323d9beb36df0a9dd
根据这个 commit,找到了是 https://github.com/opencontainers/runc/tree/v1.0.0-rc10 这个 tag,
编译支持的 tag 见 https://github.com/opencontainers/runc/tree/v1.0.0-rc10#build-tags 编译参数从 release 脚本 找到是下面的参数
1 2 make BUILDTAGS="seccomp selinux apparmor" static # 后面的新版本貌似默认的 tags 是 seccomp 了
下载源码
1 2 3 git clone https://github.com/opencontainers/runc.git cd runc git checkout v1.0.0-rc10
直接上面的编译参数是无法编译成功的,因为很多依赖都是 ubuntu
下面的。看了下 Makefile
里面提供了一个起 ubuntu 的容器,我们可以进去编译。
直接 make shell
的话,它第一步是 make runcimage
会先构建镜像,然后用这个镜像起一个容器,构建的最后一步会失败,因为下面的 busybox
的 rootfs
下载地址变动了,我们得 hack
下。
1 2 3 4 5 6 7 8 9 10 get_busybox(){ case $(go env GOARCH) in arm64) echo 'https://github.com/docker-library/busybox/raw/dist-arm64v8/glibc/busybox.tar.xz' ;; *) echo 'https://github.com/docker-library/busybox/raw/dist-amd64/glibc/busybox.tar.xz' ;; esac }
hack 之前我们先测试下,获取下输出的镜像名是runc_dev:HEAD
1 2 3 $ make shell docker build -t runc_dev:HEAD . ^Cmake: *** [runcimage] Interruptaemon 557.1kB
因为过程会取 git 的一些信息,为了不影响,我们先拷贝文件 tests/integration/multi-arch.bash
和 Dockerfile
。我们先手动构建出镜像,再删除掉这俩文件保持 git status。
先修改 Dockerfile
让它使用 tests/integration/multi-arch.bash.new
1 2 3 4 5 6 cp Dockerfile Dockerfile.new vi Dockerfile.new tail -n2 Dockerfile.new RUN . tests/integration/multi-arch.bash.new \ && curl -o- -sSL `get_busybox` | tar xfJC - ${ROOTFS}
新文件下载地址可以在 master 分支最新的脚本里 找到
1 2 get "$BUSYBOX_IMAGE" \ "https://github.com/docker-library/busybox/raw/dist-${arch}/stable/glibc/busybox.tar.xz"
按照下面修改好
1 2 3 4 5 6 7 8 $ cp tests/integration/multi-arch.bash tests/integration/multi-arch.bash.new$ vi tests/integration/multi-arch.bash.new $ grep busybox tests/integration/multi-arch.bash.new get_busybox(){ echo 'https://github.com/docker-library/busybox/raw/dist-arm64v8/glibc/busybox.tar.xz' # echo 'https://github.com/docker-library/busybox/raw/dist-amd64/glibc/busybox.tar.xz' echo 'https://github.com/docker-library/busybox/raw/dist-amd64/stable/glibc/busybox.tar.xz'
手动编译镜像
1 docker build -t runc_dev:HEAD -f Dockerfile.new .
移走文件,保持 git status
1 mv Dockerfile.new tests/integration/multi-arch.bash.new /tmp
进入容器里
1 docker run -ti --privileged --rm -v $PWD:/go/src/github.com/opencontainers/runc runc_dev:HEAD bash
编译
1 2 3 $ make BUILDTAGS="seccomp selinux apparmor nokmem" static CGO_ENABLED=1 go build -tags "seccomp selinux apparmor nokmem netgo osusergo" -installsuffix netgo -ldflags "-w -extldflags -static -X main.gitCommit="dc9208a3303feef5b3839f4323d9beb36df0a9dd" -X main.version=1.0.0-rc10 " -o runc . CGO_ENABLED=1 go build -tags "seccomp selinux apparmor nokmem netgo osusergo" -installsuffix netgo -ldflags "-w -extldflags -static -X main.gitCommit="dc9208a3303feef5b3839f4323d9beb36df0a9dd" -X main.version=1.0.0-rc10 " -o contrib/cmd/recvtty/recvtty ./contrib/cmd/recvtty
查看信息
1 2 3 4 5 6 7 $ chmod u+x runc$ ldd runc not a dynamic executable $ ./runc --version runc version 1.0.0-rc10 commit: dc9208a3303feef5b3839f4323d9beb36df0a9dd spec: 1.0.1-dev
1.0.0-rc5 版本 修改 libcontainer/cgroups/fs/memory.go
里的 EnableKernelMemoryAccounting
、setKernelMemory
直接 return nil
后编译,比较麻烦:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 docker run --rm -ti --entrypoint bash -v $PWD://go/src/github.com/opencontainers/runc -w /go/src/github.com/opencontainers/runc golang:1.8 cat > /etc/apt/sources.list << EOF deb http://archive.debian.org/debian/ jessie-backports main deb-src http://archive.debian.org/debian/ jessie-backports main deb http://archive.debian.org/debian/ jessie main contrib non-free deb-src http://archive.debian.org/debian/ jessie main contrib non-free EOF apt-get update apt-get install -y --force-yes \ build-essential \ curl \ sudo \ gawk \ iptables \ jq \ pkg-config \ libaio-dev \ libcap-dev \ libprotobuf-dev \ libprotobuf-c0-dev \ libnl-3-dev \ libnet-dev \ libseccomp2/jessie-backports \ libseccomp-dev/jessie-backports \ protobuf-c-compiler \ protobuf-compiler \ python-minimal \ uidmap \ --no-install-recommends make static
查看 kmem 开启 环境信息:
1 2 3 4 $ uname -a Linux 82.174-zh 3.10.0-693.el7.x86_64 #1 SMP Tue Aug 22 21:09:27 UTC 2017 x86_64 x86_64 x86_64 GNU/Linux $ cat /etc/redhat-release CentOS Linux release 7.4.1708 (Core)
判断 cgroup kernel memory 是否激活的方式。查看对应 POD container 下的 memory.kmem.slabinfo
。
1 2 3 4 5 6 $ cd /sys/fs/cgroup/memory/kubepods# 有内容,说明kubelet开了 kmem $ cat memory.kmem.slabinfoslabinfo - version: 2.1 # name <active_objs> <num_objs> <objsize> <objperslab> <pagesperslab> : tunables <limit > <batchcount> <sharedfactor> : slabdata <active_slabs> <num_slabs> <sharedavail>
pod 目录下面的容器目录或者/sys/fs/cgroup/memory/docker/<uuid>
如果有 memory.kmem.slabinfo
则说明 runc
没关闭
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 $ ls -l | grep poddrwxr-xr-x 5 root root 0 4月 8 11:27 pod1f1bdb40-defe-44ad-9138-14f2dbcf3b28 drwxr-xr-x 4 root root 0 4月 8 11:26 pod64def35b-b44e-410d-9782-745bd47834ca $ ls -l pod1f1bdb40-defe-44ad-9138-14f2dbcf3b28/ | grep -E '^d' drwxr-xr-x 2 root root 0 4月 8 11:27 0c97468753e9933793457e90e9964e9ef6493daae048eb0841bae634e6d5d326 drwxr-xr-x 2 root root 0 4月 8 11:28 1b37f9f78f93546e3e4407f03aa84c92e95c99655467e62814ae17e0a0e68686 drwxr-xr-x 2 root root 0 4月 8 11:27 a578004f702d7b20d4b08d49c08cbb6c3ef2b3d08a62f087f5c7be0d022d9d9d $ cat pod1f1bdb40-defe-44ad-9138-14f2dbcf3b28/0c97468753e9933793457e90e9964e9ef6493daae048eb0841bae634e6d5d326/memory.kmem.slabinfo slabinfo - version: 2.1 # name <active_objs> <num_objs> <objsize> <objperslab> <pagesperslab> : tunables <limit > <batchcount> <sharedfactor> : slabdata <active_slabs> <num_slabs> <sharedavail> taskstats 0 0 328 24 2 : tunables 0 0 0 : slabdata 0 0 0 shmem_inode_cache 216 216 680 24 4 : tunables 0 0 0 : slabdata 9 9 0 inode_cache 162 162 592 27 4 : tunables 0 0 0 : slabdata 6 6 0 Acpi-ParseExt 112 112 72 56 1 : tunables 0 0 0 : slabdata 2 2 0 selinux_inode_security 0 0 40 102 1 : tunables 0 0 0 : slabdata 0 0 0 RAWv6 0 0 1216 26 8 : tunables 0 0 0 : slabdata 0 0 0 UDP 0 0 1088 30 8 : tunables 0 0 0 : slabdata 0 0 0 kmalloc-8192 0 0 8192 4 8 : tunables 0 0 0 : slabdata 0 0 0 net_namespace 0 0 5184 6 8 : tunables 0 0 0 : slabdata 0 0 0 pid_namespace 0 0 2200 14 8 : tunables 0 0 0 : slabdata 0 0 0 mqueue_inode_cache 0 0 896 36 8 : tunables 0 0 0 : slabdata 0 0 0 kmalloc-2048 112 112 2048 16 8 : tunables 0 0 0 : slabdata 7 7 0 kmalloc-32 768 768 32 128 1 : tunables 0 0 0 : slabdata 6 6 0 kmalloc-512 128 128 512 32 4 : tunables 0 0 0 : slabdata 4 4 0 kmalloc-128 256 256 128 32 1 : tunables 0 0 0 : slabdata 8 8 0 kmalloc-8 6144 6144 8 512 1 : tunables 0 0 0 : slabdata 12 12 0 anon_vma 306 306 80 51 1 : tunables 0 0 0 : slabdata 6 6 0 idr_layer_cache 165 165 2112 15 8 : tunables 0 0 0 : slabdata 11 11 0 vm_area_struct 259 259 216 37 2 : tunables 0 0 0 : slabdata 7 7 0 mnt_cache 231 231 384 21 2 : tunables 0 0 0 : slabdata 11 11 0 mm_struct 20 20 1600 20 8 : tunables 0 0 0 : slabdata 1 1 0 signal_cache 0 0 1152 28 8 : tunables 0 0 0 : slabdata 0 0 0 sighand_cache 0 0 2112 15 8 : tunables 0 0 0 : slabdata 0 0 0 files_cache 50 50 640 25 4 : tunables 0 0 0 : slabdata 2 2 0 kernfs_node_cache 108 108 112 36 1 : tunables 0 0 0 : slabdata 3 3 0 kmalloc-192 273 273 192 21 1 : tunables 0 0 0 : slabdata 13 13 0 task_xstate 156 156 832 39 8 : tunables 0 0 0 : slabdata 4 4 0 task_struct 24 24 4048 8 8 : tunables 0 0 0 : slabdata 3 3 0 kmalloc-1024 160 160 1024 32 8 : tunables 0 0 0 : slabdata 5 5 0 kmalloc-64 768 768 64 64 1 : tunables 0 0 0 : slabdata 12 12 0 sock_inode_cache 75 75 640 25 4 : tunables 0 0 0 : slabdata 3 3 0 proc_inode_cache 264 264 656 24 4 : tunables 0 0 0 : slabdata 11 11 0 dentry 273 273 192 21 1 : tunables 0 0 0 : slabdata 13 13 0 kmalloc-16 2816 2816 16 256 1 : tunables 0 0 0 : slabdata 11 11 0 kmalloc-96 294 294 96 42 1 : tunables 0 0 0 : slabdata 7 7 0 kmalloc-256 352 352 256 32 2 : tunables 0 0 0 : slabdata 11 11 0 shared_policy_node 85 85 48 85 1 : tunables 0 0 0 : slabdata 1 1 0 kmalloc-4096 104 104 4096 8 8 : tunables 0 0 0 : slabdata 13 13 0
memory.kmem.slabinfo
里有内容说明是开启的
复现 只有在 pod 配置了 memory limit 的时候才打开 memory accounting,即 kmem。我们下面利用 flannel pod测试下,先手动创建 cgroup
1 2 3 4 5 6 7 $ grep memory /proc/cgroups memory 8 87 1 $ mkdir /sys/fs/cgroup/memory/test $ for i in `seq 1 65535`;do mkdir /sys/fs/cgroup/memory/test/test-${i}; done $ grep memory /proc/cgroups memory 8 65513 1
释放出三个,删除当前节点的 flannel,可以创建出来,然后再删除新的,无法创建出来
1 2 3 4 $ rmdir /sys/fs/cgroup/memory/test/test-{1..3} $ kubectl -n kube-system delete pod kube-flannel-ds-z2cgq .... Warning FailedCreatePodContainer 2s (x4 over 35s) kubelet, 10.14.82.174 unable to ensure pod container exists: failed to create container for [kubepods burstable pod5a41f53f-5ce8-4123-8199-1a865219f297] : mkdir /sys/fs/cgroup/memory/kubepods/burstable/pod5a41f53f-5ce8-4123-8199-1a865219f297: no space left on device
替换编译好的后,先关闭 kubelet
和 docker
, 关闭自启动 systemctl disable docker kubelet
。reboot 后,查看目录 /sys/fs/cgroup/memory/
下的 kubepods
是不是不存在。然后启动 docker 和 kubelet。等带内存 limit 的 flannel 调度上来后下面命令查看。输出是 Input/output error
说明已经关闭了。下面 docker 目录不存在的话就 docker run 创建下 mem limit 的容器
1 2 find /sys/fs/cgroup/memory/docker/ -name memory.kmem.slabinfo -exec cat {} \; find /sys/fs/cgroup/memory/kubepods/ -name memory.kmem.slabinfo -exec cat {} \;
还有种方法是看 slab 的个数,删除 limit 的 pod后等重建看看数量增长否:
1 ls /sys/kernel/slab | wc -l
参考