zhangguanzhang's Blog

kubelet 和 runc 编译关闭 kmem

字数统计: 3.4k阅读时长: 18 min
2021/04/08

前提详情

在 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 就行了。

  1. 1c 4g 的机器,这里我是使用 CentOS 7.8.2003 (Core)
    机器配置 2g 内存的时候编译提示 oom,升级到 4g 内存才编译成功的。

  2. 最好安装最新版本的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 的容器,我们可以进去编译。

1
make shell

直接 make shell 的话,它第一步是 make runcimage 会先构建镜像,然后用这个镜像起一个容器,构建的最后一步会失败,因为下面的 busyboxrootfs 下载地址变动了,我们得 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.bashDockerfile。我们先手动构建出镜像,再删除掉这俩文件保持 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 里的 EnableKernelMemoryAccountingsetKernelMemory 直接 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.slabinfo
slabinfo - 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 pod
drwxr-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

替换编译好的后,先关闭 kubeletdocker, 关闭自启动 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

参考

CATALOG
  1. 1. 前提详情
  2. 2. 准备条件
  3. 3. 编译
    1. 3.1. 确定版本
    2. 3.2. 下载源码
    3. 3.3. 前提操作
  4. 4. runc 关闭 kmem
    1. 4.1. 1.0.0-rc10 版本
    2. 4.2. 1.0.0-rc5 版本
  5. 5. 查看 kmem 开启
    1. 5.1. 复现
  6. 6. 参考