关于linux内核模块:网易数帆内核团队memory-cgroup-泄漏问题的分析与解决

40次阅读

共计 4364 个字符,预计需要花费 11 分钟才能阅读完成。

memory cgroup 泄露是 K8s(Kubernetes) 集群中普遍存在的问题,轻则导致节点内存资源缓和,重则导致节点无响应只能重启服务器复原;大多数的开发人员会采纳定期 drop cache 或者敞开内核 kmem accounting 进行躲避。本文基于网易数帆内核团队的实际案例,对 memory cgroup 泄露问题的根因进行剖析,同时提供了一种内核层面修复该问题的计划。

背景
运维监控发现局部云主机计算节点,K8s(Kubernetes) 节点都有呈现负载异样冲高的问题,具体表现为零碎运行十分卡,load 继续在 40+,局部的 kworker 线程 cpu 使用率较高或者处于 D 状态,曾经对业务造成了影响,须要剖析具体的起因。

问题定位
景象剖析
对于 cpu 使用率异样的问题,perf 是必不可少的工具。通过 perf top 察看热点函数,发现内核函数 cache_reap 使用率会间歇性冲高。

打开对应的内核代码,cache_reap 函数实现如下:

不难看出,该函数次要是遍历一个全局的 slab_caches 链表,该链表记录的是零碎上所有的 slab 内存对象相干信息。

通过剖析 slab_caches 变量相干的代码流程发现,每一个 memory cgroup 都会对应一个 memory.kmem.slabinfo 文件。

该文件外面记录的是各个 memory cgroup 组过程申请的 slab 相干信息,同时该 memory cgroup 组的 slab 对象也会被对立增加到全局的 slab_caches 链表中,莫非是因为 slab_caches 链表数据太多,导致遍历工夫比拟长,进而导致 CPU 冲高?

slab_caches 链表数据太多,那么前提必定是 memory cgroup 数量要特地多,自然而然也就想到要去统计一下零碎上存在多少个 memory cgroup,但当咱们去统计 /sys/fs/cgroup/memory 目录下的 memory cgroup 组的数量时发现也就只有一百个不到的 memory cgroup,每个 memory cgroup 外面 memory.kmem.slabinfo 文件最多也就蕴含几十条记录,所以算起来 slab_caches 链表成员个数最多也不会超过一万个,所以怎么看也不会有问题。

最终还是从最原始的函数 cache_reap 动手,既然该函数会比拟耗费 CPU,那么间接通过跟踪该函数来剖析到底是代码外面什么中央执行工夫比拟长。

确认根因
通过一系列工具来跟踪 cache_reap 函数发现,slab_caches 链表成员个数达到了惊人的几百万个,该数量跟咱们理论计算出来的数量差别微小。

再通过 cat /proc/cgroup 查看零碎的以后 cgroup 信息,发现 memory cgroup 数量曾经累积到 20w+。在云主机计算节点上存在这么多的 cgroup,显著就不是失常的状况,即使是在 K8s(Kubernetes) 节点上,这个数量级的 cgroup 也不可能是容器业务能失常产生的。

那么为什么 /sys/fs/cgroup/memory 目录下统计到的 memory cgroup 数量和 /proc/cgroups 文件记录的数量会相差如此之大了?因为 memory cgroup 泄露导致!

具体解释参考如下:

零碎上的很多操作(如创立销毁容器 / 云主机、登录宿主机、cron 定时工作等)都会触发创立长期的 memory cgroup。这些 memory cgroup 组内的过程在运行过程中可能会产生 cache 内存(如拜访文件、创立新文件等),该 cache 内存会关联到该 memory cgroup。当 memory cgroup 组内过程退出后,该 cgroup 组在 /sys/fs/cgroup/memory 目录下对应的目录会被删除。但该 memory cgroup 组产生的 cache 内存并不会被被动回收,因为有 cache 内存还在援用该 memory cgroup 对象,所以也就不会删除该 memory cgroup 在内存中的对象。

在定位过程中,咱们发现每天的 memory cgroup 的累积数量还在迟缓增长,于是对节点的 memory cgroup 目录的创立、删除进行了跟踪,发现次要是如下两个触发源会导致 memory cgroup 泄露:

特定的 cron 定时工作执行

用户频繁登录和退出节点

这两个触发源导致 memory cgroup 透露的起因都是跟 systemd-logind 登录服务有关系,执行 cron 定时工作或者是登录宿主机时,systemd-logind 服务都会创立长期的 memory cgroup,待 cron 工作执行完或者是用户退出登录后,会删除长期的 memory cgroup,在有文件操作的场景下会导致 memory cgroup 透露。

复现办法
剖析分明了 memory cgroup 泄露的触发场景,那就复现问题就容易很多:

外围的复现逻辑就是创立长期 memory cgroup,并进行文件操作产生 cache 内存,而后删除 memory cgroup 长期目录,通过以上的形式,在测试环境可能很快的复现 40w memory cgroup 残留的现场。

解决方案
通过对 memory cgroup 透露的问题剖析,根本搞清楚了问题的根因与触发场景,那么如何解决泄露的问题呢?

计划一:drop cache
既然 cgroup 泄露是因为 cache 内存无奈回收引起的,那么最间接的计划就是通过“echo 3 > /proc/sys/vm/drop_caches”清理零碎缓存。

但清理缓存只能缓解,而且后续仍然会呈现 cgroup 泄露。一方面须要配置每天定时工作进行 drop cache,同时 drop cache 动作自身也会耗费大量 cpu 对业务造成影响,而对于曾经造成了大量 cgroup 透露的节点,drop cache 动作可能卡在清理缓存的流程中,造成新的问题。

计划二:nokmem
kernel 提供了 cgroup.memory = nokmem 参数,敞开 kmem accounting 性能,配置该参数后,memory cgroup 就不会有独自的 slabinfo 文件,这样即便呈现 memory cgroup 泄露,也不会导致 kworker 线程 CPU 冲高了。

不过该计划须要重启零碎能力失效,对业务会有肯定影响,且该计划并不能齐全解决 memory cgroup 泄露这个根本性的问题,只能在肯定水平缓解问题。

计划三:打消触发源
下面剖析发现的 2 种导致 cgroup 泄露的触发源,都能够想方法打消掉。

针对第 1 种状况,通过与相应的业务模块沟通,确认能够敞开该 cron 工作;

针对第 2 种状况,能够通过 loginctl enable-linger username 将对应用户设置成后盾常驻用户来解决。

设置成常驻用户后,用户登录时,systemd-logind 服务会为该用户创立一个永恒的 memory cgroup 组,用户每次登录时都能够复用这个 memory cgroup 组,用户退出后也不会删除,所以也就不会呈现透露。

到此时看起来,这次 memory cgroup 透露的问题曾经完满解决了,但实际上以上解决计划仅能笼罩目前已知的 2 个触发场景,并没有解决 cgroup 资源无奈被彻底清理回收的问题,后续可能还会呈现的新的 memory cgroup 泄露的触发场景。

内核里的解决方案
惯例计划
在问题定位过程中,通过 Google 就曾经发现了十分多的容器场景下 cgroup 透露导致的问题,在 centos7 系列,4.x 内核上都有报告的案例,次要是因为内核对 cgroup kernel memory accounting 个性反对的不欠缺,当 K8s(Kubernetes)/RunC 应用该个性时,就会存在 memory cgroup 泄露的的问题。

而次要的解决办法,不外乎以下的几种躲避计划:

定时执行 drop cache

内核配置 nokmem 禁用 kmem accounting 性能

K8s(Kubernetes) 禁用 KernelMemoryAccounting 性能

docker/runc 禁用 KernelMemoryAccounting 性能

咱们在思考有没有更好的计划,能在内核层面“彻底”解决 cgroup 泄露的问题?

内核回收线程
通过对 memoy cgroup 泄露问题的深入分析,咱们看到外围的问题是,systemd-logind 长期创立的 cgroup 目录尽管会被主动销毁,但因为文件读写产生的 cache 内存以及相干 slab 内存却没有被立即回收,因为这些内存页的存在,cgroup 治理构造体的援用计数就无奈清零,所以尽管 cgroup 挂载的目录被删除了,但相干的内核数据结构还保留在内核里。

依据对社区相干问题解决方案的跟踪剖析,以及阿里 cloud linux 提供的思路,咱们实现一个简略间接的计划:

在内核中运行一个内核线程,针对这些残留的 memory cgroup 独自做内存回收,将其持有的内存页开释到零碎中,从而让这些残留的 memory cgroup 可能被零碎失常回收。

这个内核线程具备以下个性:

只对残留的 memory cgroup 进行回收

此内核线程优先级设置为最低

每做一轮 memory cgroup 的回收就被动 cond_resched(),避免长时间占用 cpu

回收线程的外围流程如下:

性能验证
对合入内核回收线程的内核进行性能与性能测试,后果如下:

在测试环境开启回收线程,零碎残留的 memory cgroup 可能被及时的清理;

模仿清理 40w 个透露的 memory cgroup,回收线程 cpu 使用率最高不超过 5%,资源占用能够承受;

针对超大规格的残留 memory cgroup 进行测试,回收 1 个持有 20G 内存的 memory cgroup,外围回收函数的执行工夫散布,根本不超过 64us;不会对其余服务造成影响;

开启内核回收线程后,失常通过内核 LTP 稳定性测试,不会减少内核稳定性危险。

能够看到通过新增一个内核线程对残留的 memory cgroup 进行回收,以较小的资源使用率,可能无效解决 cgroup 泄露的问题,这个计划曾经在网易公有云大量上线应用,无效晋升了网易容器业务的稳定性。

总结
以上是咱们分享的 memory cgroup 泄露问题的剖析定位过程,给出了相干的解决方案,同时提供了内核层面解决该问题的一种思路。

在长期的业务实际中,粗浅的领会到 K8s(Kubernetes)/ 容器场景对 Linux kernel 的应用和需要是全方位的。一方面,整个容器技术次要是基于 kernel 提供的能力构建的,晋升 kernel 稳定性,针对相干模块的 bug 定位与修复能力必不可少;另一方面,kernel 针对容器场景的优化 / 新个性层出不穷。咱们也继续关注相干技术的倒退,比方应用 ebpf 优化容器网络,加强内核监控能力,应用 cgroup v2/PSI 晋升容器的资源隔离及监控能力,这些也都是将来次要推动在网易外部落地的方向。

作者介绍:张亚斌,网易数帆内核专家

正文完
 0