排查内存泄漏时,很多人习惯只盯着进程的RSS看,但有一种泄漏发生在内核侧,进程内存看起来一切正常,机器的可用内存却持续下滑。这类问题大多出在slab缓存上。slab是Linux内核用于管理小对象内存分配的机制,文件系统的dentry缓存、inode缓存、网络协议栈的各种结构体,都由slab统一管理。一旦某个slab缓存的对象只分配不释放,就会表现为典型的内核内存泄漏,严重时触发OOM killer,把无辜进程杀掉。这篇文章从原理到实操,完整梳理一遍slab内存泄漏的排查思路。

先搞清楚slab是什么,泄漏发生在哪一层
Linux内核把物理内存按用途分成几个大块:用户态进程占用的匿名页、文件页缓存,以及内核自身使用的内存。内核内存中又有一大块交给slab分配器管理。slab分配器的思路很简单:内核里有大量固定大小的小对象需要频繁创建和销毁,比如每个打开的文件对应一个struct file,每个目录项对应一个dentry,如果每次都走伙伴系统按页分配,开销太大还容易产生碎片。于是内核预先按对象大小建好一批缓存(kmem_cache),每个缓存内部由若干slab页组成,分配时直接从空闲对象链表上摘一个,释放时挂回去,速度极快。
可以这么理解分层关系:伙伴系统管理4KB及以上的页,slab在页的基础上切分小对象,kmalloc系列函数则是对通用大小slab缓存的封装。所以slab泄漏的本质,是某个kmem_cache中active对象数量持续增长,说明对应的功能模块在不停创建对象却没有释放。
常见的泄漏大户有这几类:dentry缓存(文件路径频繁访问但不释放,比如某些程序会不停stat不存在的路径)、inode缓存(大量创建临时文件又异常退出没删干净)、kmalloc-XX通用缓存(驱动或内核模块自己kmalloc后忘了kfree)、以及网络相关的skbuff_head_cache(套接字缓冲区泄漏)。知道这些分类,后面看slabtop时心里就有谱了。
第一步:确认泄漏确实是slab引起的
排查从free命令开始。重点不是看free列,而是看buff/cache和available。如果available持续下降,而所有进程的RSS总和远小于已用内存,基本可以判断内存被内核吃掉了。再用下面这条命令确认:
cat /proc/meminfo | grep -i slab # 输出示例: # Slab: 12456984 kB # SReclaimable: 9800452 kB # 可回收部分,主要来自dentry/inode等 # SUnreclaim: 2656532 kB # 不可回收部分,泄漏常发生在这里
SReclaimable是可回收的slab,内存紧张时内核可以主动释放,一般不用担心。真正要警惕的是SUnreclaim持续增长且降不下来。判断方法很简单:观察SUnreclaim的数值变化趋势,可以写个循环定时记录:
while true; do echo "$(date +%T) SUnreclaim: $(grep SUnreclaim /proc/meminfo)" >> /var/log/slab_watch.log sleep 60 done
如果SUnreclaim每小时稳定上涨且从不回落,泄漏基本坐实。另一个佐证手段是对比运行中的内核模块:如果机器上有自研驱动或第三方内核模块,先把可疑模块卸载,观察SUnreclaim是否停止增长,能快速把范围缩小到具体模块。
第二步:用slabtop找出泄漏的缓存对象
确认是slab泄漏后,下一步是找出哪个kmem_cache在膨胀。slabtop是最直观的工具,它实时展示各缓存的内存占用排行:
slabtop -o -s c | head -20 # -o 不进入交互模式,-s c 按缓存大小排序 # 重点关注 OBJ ACTIVE列(活跃对象数)和 SIZE列
找到异常缓存后,还需要确认它对应的内核功能。kmalloc-8k、kmalloc-1k这类通用缓存没有符号信息,可以配合/proc/slabinfo观察对象数量增长节奏,再结合业务行为推断。例如每秒处理一个请求,kmalloc-512的对象数也每秒加一,那泄漏点大概率在请求处理路径上。
对于dentry和inode缓存,可以直接查看总量并尝试手动回收验证:
cat /proc/sys/fs/dentry-state # 输出: nr_dentry nr_unused ... # 第一个数字是dentry总数,泄漏时往往达到千万级 # 手动回收dentry和inode缓存(生产环境注意,会有短暂性能抖动) echo 2 > /proc/sys/vm/drop_caches
执行drop_caches后如果内存被大量释放,说明只是缓存堆积,还没到泄漏的程度,可以通过调低vm.vfs_cache_pressure来缓解;如果回收不了多少,说明这些dentry都被引用着,属于真泄漏,需要继续往深处查。一个很典型的案例:某个监控agent每隔几毫秒就去stat一批不存在的路径,每个失败路径的dentry都会缓存negative entry,日积月累就把Slab撑爆了,这种情况用strace跟踪一下系统调用就能发现。
第三步:用kmemleak或bpftrace定位到具体代码
缩小到某个缓存后,最后一公里是找到是谁在分配不释放。如果是自己能改内核配置的环境,kmemleak是最强的武器。它类似内核版的垃圾回收器,开启后内核会记录所有kmalloc的分配栈,定期扫描内存,找不到引用指针的分配就视为疑似泄漏:
# 内核需开启 CONFIG_DEBUG_KMEMLEAK # 启动扫描 echo scan > /sys/kernel/debug/kmemleak # 查看泄漏报告,包含完整内核调用栈 cat /sys/kernel/debug/kmemleak
报告中的调用栈会直接告诉你泄漏发生在哪个函数,比如看到ext4_xattr_block_cache相关的栈,就知道是扩展属性处理路径的问题。kmemleak的代价是约10%的性能损耗,且需要重新编译内核或发行版预置支持,适合在测试环境复现问题时使用。
生产环境不方便开kmemleak时,可以用bpftrace做低开销追踪。它能把探针挂在kmem_cache_alloc上,按调用栈聚合统计,几行脚本就能找出分配大户:
#!/usr/bin/env bpftrace
# 统计kmalloc的调用栈,按栈聚合出现次数
kprobe:kmem_cache_alloc
{
@stack[kstack] = count();
}
interval:s:30 { print(@stack); clear(@stack); exit(); }跑三十秒,输出中出现次数异常多的调用栈就是重点怀疑对象。再对比kmem_cache_free的统计,某个栈只分配不释放,泄漏点就锁定了。拿到栈信息后,回到对应的内核子系统或驱动代码,检查错误路径上是否漏掉了释放逻辑——绝大多数内核内存泄漏都藏在异常分支里,正常路径往往是对的。
排查思路小结
整个过程可以归纳成一条递进的线索:先用free和/proc/meminfo确认是内核内存问题,区分SReclaimable和SUnreclaim;再用slabtop和/proc/slabinfo定位膨胀的缓存对象,结合drop_caches验证是真泄漏还是缓存堆积;最后用kmemleak或bpftrace拿到分配调用栈,回到源码修复。平时建议对线上机器配置SUnreclaim的监控告警,超过经验阈值就提前介入,比等到OOM再排查从容得多。另外养成一个习惯:排查slab泄漏时同步记录当时的业务流量和内核模块版本,很多泄漏只在特定内核版本或特定负载形态下触发,这些信息对后续定位至关重要。