Linux服务器内存被Slab/dentry挤爆?实战排查与内核优化指南
在日常维护Linux服务器时,你可能会遇到一个诡异的现象:使用 free -m 查看,发现可用内存(available)所剩无几,但用 top 或 ps 把所有进程的 RES(常驻内存)加起来,却发现根本对不上账。
几十G的内存凭空消失了?
其实,这部分内存并没有消失,而是被内核的缓存占用了,其中最典型的元凶就是 Slab 缓存中的 dentry(目录项缓存)。本文将带你一步步定位这种因 dentry 堆积导致的“伪内存泄露”问题,并提供彻底解决的方案。
一、 什么是 dentry 缓存?为什么会“泄露”?
在 Linux 中,每次读取或操作一个文件,内核都需要通过目录项(dentry)来建立文件名与 inode 之间的映射关系。为了提高下一次访问的速度,内核会将这些 dentry 对象缓存在内存中。
- 正常情况下:当系统内存紧张时,内核的
kswapd守护进程会自动回收这部分缓存(它们属于SReclaimable可回收的 Slab 内存)。 - 异常情况下(即所谓的“泄露”):
- 高频创建/销毁临时文件:某些应用(如频繁生成临时文件的 PHP/Java 脚本,或配置不当的日志轮转)在短时间内操作了数千万个不同的文件路径。
- 容器 overlayfs 挂载未释放:Kubernetes 或 Docker 环境中,容器频繁启停,但由于某些引用未释放,导致对应的 overlayfs 目录项残留。
- 内核回收机制不积极:内核的回收压力参数配置过低,导致内核宁愿让系统去用 swap 甚至触发 OOM,也不愿意释放这些 dentry。
二、 第一步:确认是否存在 dentry 内存积压
首先,我们需要确认内存确实是被 Slab 中的 dentry 占用了。
1. 检查 /proc/meminfo
执行以下命令查看 Slab 的整体占用情况:
cat /proc/meminfo | grep -E 'Slab|SReclaim|SUnreclaim'
输出示例:
Slab: 32456128 kB
SReclaimable: 31124500 kB
SUnreclaim: 1331628 kB
- Slab:表示内核 Slab 分配器占用的总内存。
- SReclaimable:表示可回收的 Slab 内存。如果这个值非常大(比如达到了几十G),说明确实有大量的目录项或节点缓存处于非活跃状态,但尚未被回收。
2. 使用 slabtop 锁定元凶
运行 slabtop 工具,并按内存占用大小进行排序(按 c 键):
slabtop -o -s c | head -n 15
输出示例:
Active / Total Size (% used) : 31254.12M / 31890.45M (98.0%)
OBJS ACTIVE USE OBJ SIZE SLABS OBJ/SLAB CACHE SIZE NAME
120456120 119854120 99% 0.19K 5736005 21 22944024K dentry
8954120 8541200 95% 1.02K 288705 7 9238560K inode_cache
124508 110120 88% 0.20K 6225 20 24901K vm_area_struct
在上面的结果中,dentry 占用了接近 22G 的内存,inode_cache 占用了 9G。至此,基本可以判定:系统的物理内存被数以亿计的 dentry 缓存给吃光了。
三、 第二步:定位到底是哪个进程/目录在疯狂产生 dentry
dentry 缓存是全局的,内核不会直接告诉你这些 dentry 是由于哪个进程访问了哪些文件产生的。我们需要通过一些间接手段来排查。
1. 扫描文件系统,排查异常目录(常用方法)
dentry 暴涨通常是因为某个目录下存在海量的小文件,或者有程序在不断访问不存在的文件(也会产生 negative dentry 负目录项缓存)。
运行以下命令,找出系统中文件/目录数量异常庞大的路径(可能需要执行较长时间,建议在业务低峰期运行):
find / -xdev -printf '%h\n' | sort | uniq -c | sort -k 1 -n | tail -n 20
该命令会统计各目录下直接子项的数量,并输出数量最多的前 20 个目录。
典型排查案例:
- 发现
/tmp下有数百万个sess_xxx(PHP Session 垃圾未清理)。 - 发现
/var/spool/postfix/maildrop/下积压了数百万个系统邮件文件(通常是由于 crontab 任务输出未重定向导致的)。
2. 检查是否有大量死掉的挂载点 / 容器残留
如果你使用了 Docker 或 Kubernetes,容器频繁销毁后,部分 overlay 挂载点若没有被彻底卸载,其 dentry 会一直驻留在内核中。
检查当前的挂载情况:
cat /proc/mounts | wc -l
如果挂载点数量多达数千甚至上万,说明存在严重的容器/挂载残留,需要重启 runtime(如 docker/containerd)或强制卸载残留路径。
四、 第三步:紧急避险(安全释放内存)
如果生产环境内存已经告急,随时可能触发 OOM Killer 导致核心业务挂掉,可以先手动触发内核回收。
注意:在执行手动释放前,务必先执行 sync 命令,将脏数据刷入磁盘,避免数据丢失。
# 1. 刷盘
sync
# 2. 仅释放 slab 缓存(dentry 和 inode),而不释放 pagecache(页面缓存)
echo 2 > /proc/sys/vm/drop_caches
如果你希望连同 pagecache 一起释放,可以写入 3,但对于 dentry 泄露问题,写入 2 是最精准、最安全的。
执行后,再次运行 free -m 和 slabtop,你会发现可用内存瞬间恢复,dentry 的大小也回落到了正常水平。
五、 第四步:根治与内核参数优化
手动 drop_caches 只是治标不治本的方法。如果应用层代码无法立即修改,或者业务本身就必须处理海量文件,我们需要调整内核参数,让内核在后台更积极地回收 dentry。
1. 调整 vfs_cache_pressure
这个参数控制内核回收 VFS 缓存(dentry 和 inode)的倾向性。默认值是 100。
- 值 = 0:内核永远不会主动回收 dentry/inode,极易导致 OOM。
- 值 = 100:默认值,内核在回收 pagecache 和 dentry 时保持公平。
- 值 > 100:值越大,内核越倾向于优先回收 dentry 和 inode,而不是去回收 pagecache。
优化方案:
我们可以将该值临时调整为 1000 甚至 10000(根据业务压测决定):
# 临时生效
sysctl -w vm.vfs_cache_pressure=10000
要永久生效,请编辑 /etc/sysctl.conf 并添加以下行:
vm.vfs_cache_pressure = 10000
保存后执行 sysctl -p 即可。
2. 调整 min_free_kbytes
如果你的服务器物理内存非常大(例如 256G 以上),默认的内核后台回收水位可能会设得比较低,导致 kswapd 还没来得及启动,内存就被突发的 dentry 申请直接撑爆。
可以适当提高 vm.min_free_kbytes(比如设置为物理内存的 1% ~ 3%,但一般不建议超过 4G):
# 以 128G 内存服务器为例,设置保留 2G 内存用于紧急水位线回收
vm.min_free_kbytes = 2097152
六、 总结与排查闭环
遇到 Linux 内存耗尽却找不到进程责任人时,按照以下路径闭环排查:
[发现内存高]
└── free -m (检查 available 极低)
└── cat /proc/meminfo (发现 SReclaimable 异常庞大)
└── slabtop (确认 dentry 占用比例超高)
├── 临时避险: sync && echo 2 > /proc/sys/vm/drop_caches
├── 寻找病灶: 扫描文件系统大目录 / 排查残留挂载点
└── 长期防御: 调整 vm.vfs_cache_pressure = 10000
通过这套组合拳,不仅能快速化解线上内存告急的险情,还能通过内核调优让服务器在海量小文件场景下平稳运行,彻底告别“幻影”内存泄露。