zhangguanzhang's Blog

kubelet 和 runc 编译关闭 kmem

字数统计: 3.2k阅读时长: 17 min
2021/04/08 Share

前提详情

在 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
2
$ 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

编译,这个参数测试在 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

https://cloud.tencent.com/developer/article/1743789 这个文章里说了 runc 也需要关闭

如果下面命令能成功执行则说明 runc 没关闭 kmem

1
docker run --rm --name test --kernel-memory 100M nginx:1.14.2

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
make BUILDTAGS="seccomp selinux apparmor" static

下载源码

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

查看 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.13.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 说明已经关闭了。

1
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
  5. 5. 查看 kmem 开启
    1. 5.1. 复现
  6. 6. 参考