为什么要升级内核
本文中所使用到的OS为Ubuntu 16.04,用户均为root用户。升级内核为必须条件。
在低版本的内核中会出现一下很让人恼火的Bug,时不时来一下,发作时候会导致整个OS Hang住无法执行任何命令。
现象如下:
kernel:unregister_netdevice: waiting for lo to become free. Usage count = 1
关于这个Bug,你可以从以下地方追踪到:
还有老哥放出了重现这个Bug的代码:https://github.com/fho/docker-samba-loop。
而根据我实际的实验(采坑)下来,这个问题我花费了差不多1个多月的时间先后尝试了内核版本3.10、4.4、4.9、4.12、4.14、4.15版本,均会不同程度的复现上述Bug,而一旦触发并无他法,只能重启(然后祈祷不要再次触发)。
实在是让人寝食难安,睡不踏实,直到我遇到了内核4.17,升级完毕之后,从6月到现在。重来没有复现过。似乎可以认为该Bug已经修复了。故而强烈建议升级内核到4.17+。
前言
这篇文章的全称应该叫:[在某些内核版本上,cgroup 的 kmem account 特性有内存泄露问题],如果你遇到过 pod 的 "cannot allocated memory"报错,node 内核日志的“SLUB: Unable to allocate memory on node -1”报错,那么恭喜你中招了。
这个问题在 pingcap 的文章和腾讯云的官方修复都发过,原因也讲的很清楚,不过因为版本差异,文章里的方法有所变动,这里做下总结
3.10的kernel 问题太多了。也是k8s 支持的最低版本。
为什么要升级内核?
由于Docker 在CentOS系统中需要安装在 CentOS 7 64 位的平台,并且内核版本不低于 3.10;CentOS 7.× 满足要求的最低内核版本要求,但由于 CentOS 7默认内核版本比较低,部分功能(如 overlay2 存储层驱动)无法使用,并且部分功能可能不太稳定。所以建议大家升级到最新的稳定内核版本。
今天k8s集群服务器突然出现这个警告错误,网上查了一下,建议升级内核版本
kernel:unregister_netdevice: waiting for lo to become free. Usage count = 1
原因
一句话总结:
cgroup 的 kmem account 特性在 3.x 内核上有内存泄露问题,如果开启了 kmem account 特性会导致可分配内存越来越少,直到无法创建新 pod 或节点异常。
几点解释:
- kmem account 是cgroup 的一个扩展,全称CONFIG_MEMCG_KMEM,属于机器默认配置,本身没啥问题,只是该特性在 3.10 的内核上存在漏洞有内存泄露问题,4.x的内核修复了这个问题。
- 因为 kmem account 是 cgroup 的扩展能力,因此runc、docker、k8s 层面也进行了该功能的支持,即默认都打开了kmem 属性
- 因为3.10 的内核已经明确提示 kmem 是实验性质,我们仍然使用该特性,所以这其实不算内核的问题,是 k8s 兼容问题。
其他细节原因下面会解释
解决方案
既然是 3.x 的问题,直接升级内核到 4.x 及以上即可,内核问题解释:
- https://github.com/torvalds/linux/commit/d6e0b7fa11862433773d986b5f995ffdf47ce672
- https://support.mesosphere.com/s/article/Critical-Issue-KMEM-MSPH-2018-0006
这种方式的缺点是:
- 需要升级所有节点,节点重启的话已有 pod 肯定要漂移,如果节点规模很大,这个升级操作会很繁琐,业务部门也会有意见,要事先沟通。
- 这个问题归根结底是软件兼容问题,3.x 自己都说了不成熟,不建议你使用该特性,k8s、docker却 还要开启这个属性,那就不是内核的责任,因为我们是云上机器,想替换4.x 内核需要虚机团队做足够的测试和评审,因此这是个长期方案,不能立刻解决问题。
- 已有业务在 3.x 运行正常,不代表可以在 4.x 也运行正常,即全量升级内核之前需要做足够的测试,尤其是有些业务需求对os做过定制。
因为 2 和 3 的原因,我们没有选择升级内核,决定使用其他方案
[root@jenkins-master ~]# cat /etc/redhat-release
CentOS Linux release 7.4.1708 (Core)
[root@jenkins-master ~]# uname -r
3.10.0-693.el7.x86_64
7.4版本操作系统升级 rpms.tar
# 升级Master节点和Node节点内核至 4.19.20-1.el7.x86_64 版本,步骤:
# 上传rpm安装包,执行rpm安装
yum localinstall -y kernel-ml-* --skip-broken
# 修改内核启动参数
sed -i 's/saved/0/g' /etc/default/grub
# 重建grub
grub2-mkconfig -o /boot/grub2/grub.cfg
# 检查grub启动参数
grep "^menuentry" /boot/grub2/grub.cfg
menuentry 'CentOS Linux (4.19.20-1.el7.x86_64) 7 (Core)' --class centos --class gnu-linux --class gnu --class os --unrestricted $menuentry_id_option 'gnulinux-4.19.20-1.el7.x86_64-advanced-918bc4e0-3f40-4ee7-89f3-0acbd5e60266'
# awk -F\' '$1=="menuentry " {print i++ " : " $2}' /etc/grub2.cfg
0 : CentOS Linux (4.19.20-1.el7.x86_64) 7 (Core)
1 : CentOS Linux (3.10.0-1127.el7.x86_64) 7 (Core)
2 : CentOS Linux (0-rescue-ce96d80aad324672909914d327a2d91c) 7 (Core)
# 重启服务器
# sudo reboot
Why Kernel 4?
Kernel requirement for Containerd
https://github.com/containerd/containerd/blob/master/README.md
Kernel requirement for Kubernetes
Kernel 3.10 is just the minimum requirement, you can see 4+ or even 5+ is also supported by Kubernetes. The latest CentOS7.6 releases on kernel 3.10. No any plan for kernel 4. While some of the kernel feature like ‘kernel memory accounting (kmem)’ that Kubernetes enables by default is only experimental in kernel 3, but stable on kernel 4. Docker provides the kernel version check and disables kmem if kernel is 3.x. But I can’t find any flags to disable kmem from Kubernetes. One possible way to fix it is to rebuild Kubernetes from source code with build flag to disable the kmem accounting from runc. https://github.com/kubernetes/kubernetes/blob/master/cmd/kubeadm/app/util/system/types_unix.go
Kernel 3.10只是最低要求,你可以看到Kubernetes也支持4+甚至5+。内核 3.10 上最新的 CentOS7.6 版本。没有针对内核 4 的任何计划。虽然 Kubernetes 默认启用的一些内核功能,例如“内核内存记帐 (kmem)”,仅在内核 3 中处于试验阶段,但在内核 4 上是稳定的。
Docker 提供内核版本检查并在以下情况下禁用 kmem内核是 3.x。但是我找不到从 Kubernetes 禁用 kmem 的任何标志。修复它的一种可能方法是使用构建标志从源代码重建 Kubernetes,以禁用来自 runc 的 kmem 记帐。
Other situations
- The kernel memory leak bug in 3.x that has been happening for quite a few weeks for us: SLUB: Unable to allocate memory on node -1, which crashes the Docker node. The kernel memory accounting is only experimental on kernel 3, but stable on kernel 4.No fix for CentOS now. See issue, https://github.com/kubernetes/kubernetes/issues/61937.
- The kernel socket leak in 3.x I have met: unregister_netdevice: waiting for eth0 to become free. Usage count = 1. It is fixed in Ubuntu kernel 4.4.114. net: tcp: close sock if net namespace is exiting. See changlog. You can also find the fix by linus torvalds in net: tcp: close sock if net namespace is exiting. But no CentOS fix was ever found so far. I have rebuilt an enhanced CentOS on kernel 3.10.0.957.10.1.
- Kernel 3.10 is only tech preview on overlayfs. kernel: [ 24.062493] TECH PREVIEW: Overlay filesystem may not be fully supported.
- As you can see from the above requirement examples, seems the container ecosystem mainly develops on kernel 4.x now. It should be so in the future as well. Vendors will not prefer to provide fixes for kernel 3, take the kernel socket leak bug for example, it has been existing for several years on CentOS.
- Even if we fix those 2 kernel bugs, how can we be sure that the future unknown potential bugs can be officially fixed in time? But the kernel 4.x can be timely patched once such critical bugs are found, as the whole ecosystem mainly plays on it.
- Docker has the fixes for kernel memory leak, while k8s doesn’t have, seems it mainly considers & develops on kernel 4.x. Also Kubernetes is going to work without Docker, it has its own crictl tools which can replace docker client. Moreover it includes the containerd/runc as its vendor code directly. It is now possible to totally discard Docker when setting up a Kubernetes cluster, see Kubernetes: Using containerd 1.1 without Docker - aboutsimon.com
- Kubernetes only patches the last 3 minor versions. And Minor releases occur approximately every 3 months. So we are moving to the next minor version in 9 months, as Kubernetes will depend more and more heavily on kernel 4.x features. We will be in high risk on kernel 3.x when k8s version goes up. If we won’t upgrade following the pace, we can’t get the latest security patches from Kubernetes.
- Docker now only patches 17.06+. The latest version is 1809.3. Redhat still uses Docker 1.13.1, because their kernel is too old to run newer Docker. The 1.13.1 is even older than 17.03, which is no longer in maintenance scope.
- CoreOS adopted kernel 4 very early. In 2015
- RHEL 8 defaults on kernel 4.x, but it is beta. No timeline for GA yet. Red Hat Enterprise Linux Overview | Red Hat Developer
- CentOS officially doesn’t support kernel 4.x by for now. https://en.wikipedia.org/wiki/CentOS.
其他情况
- 3.x 中的内核内存泄漏错误已经为我们发生了好几个星期:SLUB:无法在节点 -1 上分配内存,导致 Docker 节点崩溃。内核内存记帐仅在内核 3 上是实验性的,但在内核 4 上稳定。现在 CentOS 没有修复。请参阅问题,https://github.com/kubernetes/kubernetes/issues/61937。
- 我遇到过的 3.x 内核 socket 泄漏:unregister_netdevice: waiting for eth0 to become free。使用次数 = 1。在 Ubuntu 内核 4.4.114 中已修复。net: tcp: close sock if net namespace is exiting。见changlog。您还可以在net: tcp: close sock if net namespace is exiting 中找到 linus torvalds 的修复。但是到目前为止还没有找到 CentOS 修复程序。我在内核 3.10.0.957.10.1 上重建了一个增强的 CentOS。
- Kernel 3.10 只是overlayfs 上的技术预览。内核:[24.062493] 技术预览:可能不完全支持覆盖文件系统。
- 从上面的需求例子可以看出,现在看来容器生态主要是在内核4.x上发展的。未来也应该如此。供应商不会更愿意为内核 3 提供修复,以内核套接字泄漏 bug 为例,它在 CentOS 上已经存在好几年了。
- 即使我们修复了那 2 个内核错误,我们如何确保未来未知的潜在错误能够及时得到官方修复?但是一旦发现这样的严重错误,内核 4.x 可以及时修补,因为整个生态系统主要在它上面玩。
- Docker 有内核内存泄漏的修复,而 k8s 没有,似乎它主要考虑和开发内核 4.x。Kubernetes 也可以在没有 Docker 的情况下工作,它有自己的 crictl 工具,可以替代 docker 客户端。此外,它还直接包含 containerd/runc 作为其供应商代码。现在可以在设置 Kubernetes 集群时完全丢弃 Docker,参见Kubernetes: Using containerd 1.1 without Docker - aboutsimon.com
- Kubernetes 只修补最后 3 个次要版本。次要版本大约每 3 个月发生一次。因此,我们将在 9 个月后转向下一个次要版本,因为 Kubernetes 将越来越依赖内核 4.x 功能。当 k8s 版本上升时,我们将在内核 3.x 上处于高风险中。如果我们不跟上进度,我们就无法从 Kubernetes 获得最新的安全补丁。
- Docker 现在只修补 17.06+。最新版本是 1809.3。Redhat 仍然使用 Docker 1.13.1,因为他们的内核太旧而无法运行更新的 Docker。1.13.1 比 17.03 更旧,不再处于维护范围。
- CoreOS 很早就采用了内核 4。2015年
- RHEL 8 默认为内核 4.x,但它是测试版。GA 还没有时间表。Red Hat Enterprise Linux Overview | Red Hat Developer
- CentOS 目前正式不支持内核 4.x。https://en.wikipedia.org/wiki/CentOS。
The Analysis of Kernel & OS for running Kubernetes - Gary's Understandings
问题一:修复 K8S 内存泄露问题
问题描述
-
当 k8s 集群运行日久以后,有的 node 无法再新建 pod,并且出现如下错误,当重启服务器之后,才可以恢复正常使用。查看 pod 状态的时候会出现以下报错。
applying cgroup … caused: mkdir …no space left on device
或者在 describe pod 的时候出现 cannot allocate memory。
这时候你的 k8s 集群可能就存在内存泄露的问题了,当创建的 pod 越多的时候内存会泄露的越多,越快。
-
具体查看是否存在内存泄露
$ cat /sys/fs/cgroup/memory/kubepods/memory.kmem.slabinfo
当出现 cat: /sys/fs/cgroup/memory/kubepods/memory.kmem.slabinfo: Input/output error 则说明不存在内存泄露的情况 如果存在内存泄露会出现
slabinfo - version: 2.1
# name <active_objs> <num_objs> <objsize> <objperslab> <pagesperslab> : tunables <limit> <batchcount> <sharedfactor> : slabdata <active_slabs> <num_slabs> <sharedavail>
解决方案
-
解决方法思路:关闭 runc 和 kubelet 的 kmem,因为升级内核的方案改动较大,此处不采用。
-
kmem 导致内存泄露的原因:
内核对于每个 cgroup 子系统的的条目数是有限制的,限制的大小定义在 kernel/cgroup.c #L139,当正常在 cgroup 创建一个 group 的目录时,条目数就加 1。我们遇到的情况就是因为开启了 kmem accounting 功能,虽然 cgroup 的目录删除了,但是条目没有回收。这样后面就无法创建 65535 个 cgroup 了。也就是说,在当前内核版本下,开启了 kmem accounting 功能,会导致 memory cgroup 的条目泄漏无法回收。
2.1 编译 runc
-
配置 go 语言环境
$ wget https://dl.google.com/go/go1.12.9.linux-amd64.tar.gz
$ tar xf go1.12.9.linux-amd64.tar.gz -C /usr/local/
# 写入bashrc
$ vim ~/.bashrc
$ export GOPATH="/data/Documents"
$ export GOROOT="/usr/local/go"
$ export PATH="$GOROOT/bin:$GOPATH/bin:$PATH"
$ export GO111MODULE=off
# 验证
$ source ~/.bashrc
$ go env
-
下载 runc 源码
$ mkdir -p /data/Documents/src/github.com/opencontainers/
$ cd /data/Documents/src/github.com/opencontainers/
$ git clone https://github.com/opencontainers/runc
$ cd runc/
$ git checkout v1.0.0-rc9 # 切到v1.0.0-rc9 tag
-
编译
# 安装编译组件
$ sudo yum install libseccomp-devel
$ make BUILDTAGS='seccomp nokmem'
# 编译完成之后会在当前目录下看到一个runc的可执行文件,等kubelet编译完成之后会将其替换
2.2 编译 kubelet
-
下载 kubernetes 源码
$ mkdir -p /root/k8s/
$ cd /root/k8s/
$ git clone https://github.com/kubernetes/kubernetes
$ cd kubernetes/
$ git checkout v1.15.3
-
制作编译环境的镜像(Dockerfile 如下)
FROM centos:centos7.3.1611
ENV GOROOT /usr/local/go
ENV GOPATH /usr/local/gopath
ENV PATH /usr/local/go/bin:$PATH
RUN yum install rpm-build which where rsync gcc gcc-c++ automake autoconf libtool make -y \
&& curl -L https://studygolang.com/dl/golang/go1.12.9.linux-amd64.tar.gz | tar zxvf - -C /usr/local
-
在制作好的 go 环境镜像中来进行编译 kubelet
$ docker run -it --rm -v /root/k8s/kubernetes:/usr/local/gopath/src/k8s.io/kubernetes build-k8s:centos-7.3-go-1.12.9-k8s-1.15.3 bash
$ cd /usr/local/gopath/src/k8s.io/kubernetes
#编译
$ GO111MODULE=off KUBE_GIT_TREE_STATE=clean KUBE_GIT_VERSION=v1.15.3 make kubelet GOFLAGS="-tags=nokmem"
-
替换原有的 runc 和 kubelet
-
将原有 runc 和 kubelet 备份
$ mv /usr/bin/kubelet /home/kubelet
$ mv /usr/bin/docker-runc /home/docker-runc
-
停止 docker 和 kubelet
$ systemctl stop docker
$ systemctl stop kubelet
-
将编译好的 runc 和 kubelet 进行替换
$ cp kubelet /usr/bin/kubelet
$ cp kubelet /usr/local/bin/kubelet
$ cp runc /usr/bin/docker-runc
-
检查 kmem 是否关闭前需要将此节点的 pod 杀掉重启或者重启服务器,当结果为 0 时成功
$ cat /sys/fs/cgroup/memory/kubepods/burstable/memory.kmem.usage_in_bytes
-
检查是否还存在内存泄露的情况
$ cat /sys/fs/cgroup/memory/kubepods/memory.kmem.slabinfo