WEBKT

Linux服务器内存被Slab/dentry挤爆?实战排查与内核优化指南

4 0 0 0

在日常维护Linux服务器时,你可能会遇到一个诡异的现象:使用 free -m 查看,发现可用内存(available)所剩无几,但用 topps 把所有进程的 RES(常驻内存)加起来,却发现根本对不上账。

几十G的内存凭空消失了?

其实,这部分内存并没有消失,而是被内核的缓存占用了,其中最典型的元凶就是 Slab 缓存中的 dentry(目录项缓存)。本文将带你一步步定位这种因 dentry 堆积导致的“伪内存泄露”问题,并提供彻底解决的方案。


一、 什么是 dentry 缓存?为什么会“泄露”?

在 Linux 中,每次读取或操作一个文件,内核都需要通过目录项(dentry)来建立文件名与 inode 之间的映射关系。为了提高下一次访问的速度,内核会将这些 dentry 对象缓存在内存中。

  • 正常情况下:当系统内存紧张时,内核的 kswapd 守护进程会自动回收这部分缓存(它们属于 SReclaimable 可回收的 Slab 内存)。
  • 异常情况下(即所谓的“泄露”)
    1. 高频创建/销毁临时文件:某些应用(如频繁生成临时文件的 PHP/Java 脚本,或配置不当的日志轮转)在短时间内操作了数千万个不同的文件路径。
    2. 容器 overlayfs 挂载未释放:Kubernetes 或 Docker 环境中,容器频繁启停,但由于某些引用未释放,导致对应的 overlayfs 目录项残留。
    3. 内核回收机制不积极:内核的回收压力参数配置过低,导致内核宁愿让系统去用 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 -mslabtop,你会发现可用内存瞬间恢复,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

通过这套组合拳,不仅能快速化解线上内存告急的险情,还能通过内核调优让服务器在海量小文件场景下平稳运行,彻底告别“幻影”内存泄露。

Linux运维实战 Linux内存泄露dentry

评论点评