内存泄漏现象:

在Linux内核为3.x的机器上POD如果出现以下几种现象,很可能是因为kubernetes内存泄漏导致:

  1. pod 状态异常,describe pod 显示原因为: no allocated memory
  2. 节点上执行 dmesg 有日志显示:slub无法分配内存:SLUB: Unable to allocate memory on node -1
    node-memory-leak
  3. 节点 OOM 开始按优先级杀进程,有可能会导致有些正常 pod 被杀掉
  4. 机器free 查看可用内存还有很多,却无法分配,怀疑是内存泄露。

内存泄漏原因:

cgroup 的 kmem account 特性在 3.x 内核上有内存泄露问题,如果开启了 kmem account 特性 会导致可分配内存越来越少,直到无法创建新 pod 或节点异常。

在4.0以下版本的 Linux 内核对 kernel memory accounting 的支持并不完善,在3.x 的内核版本上,会出现 kernel memory 无法回收,bug 解释:
https://bugzilla.redhat.com/show_bug.cgi?id=1507149
https://github.com/kubernetes/kubernetes/issues/61937
https://support.d2iq.com/s/article/Critical-Issue-KMEM-MSPH-2018-0006

影响范围:

k8s在 1.9版本开启了对 kmem 的支持,因此 1.9 以后的所有版本都有该问题,但必须搭配 3.x内核的机器才会出问题。
一旦出现会导致新 pod 无法创建,已有 pod不受影响,但pod 漂移到有问题的节点就会失败,直接影响业务稳定性。因为是内存泄露,直接重启机器可以暂时解决,但还会再次出现

解决方案:

方案一:
直接升级内核到 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做过定制。

方案二:
修改虚机启动的引导项 grub 中的cgroup.memory=nokmem,让机器启动时直接禁用 cgroup的 kmem 属性

# /etc/default/grub
# 增加内核参数
GRUB_CMDLINE_LINUX="crashkernel=auto net.ifnames=0 biosdevname=0 intel_pstate=disable cgroup.memory=nokmem"

这个方式对一些机器生效,但有些机器替换后没生效,且这个操作也需要机器重启,暂时不采纳。

方案三:推荐
在 k8s 维度禁用该属性。issue 中一般建议修改 kubelet代码并重新编译。

  • 对于v1.13及其之前版本的kubelet,需要手动替换以下两个函数。
// vendor/github.com/opencontainers/runc/libcontainer/cgroups/fs/memory.go

func EnableKernelMemoryAccounting(path string) error {
    return nil
}

func setKernelMemory(path string, kernelMemoryLimit int64) error {
    return nil
}

重新编译并替换 kubelet:

make WHAT=cmd/kubelet GOFLAGS=-v GOGCFLAGS="-N -l"
  • 对于v1.14及其之后版本的kubelet 通过添加BUILDTAGS来禁止 kmem accounting.
make BUILDTAGS="nokmem" WHAT=cmd/kubelet GOFLAGS=-v GOGCFLAGS="-N -l"

关于 kmem account :

1、kmem account 是cgroup 的一个扩展,全称 CONFIG_MEMCG_KMEM,属于机器默认配置,该特性在 3.10 的内核上存在漏洞有内存泄露问题,4.x的内核修复了这个问题。
2、因为 kmem account 是 cgroup 的扩展能力,因此 runc、docker、k8s 层面也进行了该功能的支持,即默认都打开了kmem 属性。
3、因为3.10 的内核已经明确提示 kmem 是实验性质,我们仍然使用该特性,所以这其实不算内核的问题,是 k8s 兼容问题。

首先了解用户内存与内核内存:

  • 用户内存:用户内存是普通应用程序可访问的内存区域。
  • 内核内存:专用于Linux内核系统服务使用,是不可swap的,因而这部分内存非常宝贵的。但现实中存在很多针对内核内存资源的攻击,如不断地fork新进程从而耗尽系统资源,即所谓的“fork bomb”。

为了防止““fork bomb””这种攻击,社区中提议通过linux内核限制 cgroup中的 kmem 容量,从而限制恶意进程的行为,即kernel memory accounting 机制。使用如下命令查看 kmem 是否打开:

[aaron@centos7 root]$uname -r
3.10.0-1160.el7.x86_64
[aaron@centos7 root]$cat /boot/config-`uname -r`|grep CONFIG_MEMCG
CONFIG_MEMCG=y
CONFIG_MEMCG_SWAP=y
CONFIG_MEMCG_SWAP_ENABLED=y
CONFIG_MEMCG_KMEM=y
[aaron@centos7 root]$

cgroup 与 kmem 机制:

使用 cgroup 限制内存时,不但需要限制对用户内存的使用,也需要限制对内核内存的使用。kernel memory accounting 机制为 cgroup 的内存限制增加了 stack pages(例如新进程创建)、slab pages(SLAB/SLUB分配器使用的内存)、sockets memory pressure、tcp memory pressure等,以保证 kernel memory 不被滥用。

当开启了kmem 机制,具体体现在 memory.kmem.limit_in_bytes 这个文件上:

/sys/fs/cgroup/memory/kubepods/pod632f736f-5ef2-11ea-ad9e-fa163e35f5d4/memory.kmem.limit_in_bytes

实际使用中,一般将 memory.kmem.limit_in_bytes 设置成大于 memory.limit_in_bytes,从而只限制应用的总内存使用。

docker 与 k8s 使用 kmem:

对于k8s、docker 而言,kmem 属性属于正常迭代和优化,至于 3.x 的内核上存在 bug 不能兼容,不是k8s 关心的问题。但 issue 中不断有人反馈,因此在 k8s 1.14 版本的 kubelet 中,增加了一个编译选项 make BUILDTAGS=“nokmem”,就可以编译 kubelet 时就禁用 kmem,避免掉这个问题。而1.8 到1.14 中间的版本,只能选择更改 kubelet 的代码。

slub 分配机制:

因为节点 dmesg 的报错是:SLUB: Unable to allocate memory on node -1
cgroup 限制下,当用户空间使用 malloc 等系统调用申请内存时,内核会检查线性地址对应的物理地址,如果没有找到会触发一个缺页异常,进而调用 brk 或 do_map 申请物理内存(brk申请的内存通常小于128k)。而对于内核空间来说,它有2种申请内存的方式,slub和vmalloc:

  • slab用于管理内存块比较小的数据,可以在/proc/slabinfo下查看当前slab的使用情况,
  • vmalloc操作的内存空间为 VMALLOC_START~4GB,适用于申请内存比较大且效率要求不高的场景。可以在/proc/vmallocinfo中查看vmalloc的内存分布情况。
  • 可以在/proc/buddyinfo中查看当前空闲的内存分布情况。

如果 docker 创建容器时通过 cgroup memory 申请的 slab,但是docker关闭时,这些slab却没有释放。导致越积越多,将内存消耗殆尽。就会出现没有内存可以分配的情况!

其他的表现:

  • 除了最上面提到的无法分配内存问题,kmem 还会导致其他现象,如pod资源占用过高问题
  • 复现该问题还有一种方式,就是疯狂创建 cgroup 文件,直到 65535 耗尽,参考:https://github.com/kubernetes/kubernetes/issues/61937