1. 内存都去哪了:一次典型的“幽灵占用”排查实录
线上跑着的一台业务机,监控突然告警说可用内存跌破阈值。我登上机器,习惯性地敲下free -h,结果确实不太好看:total 32G,used 29G,free 只剩几百兆,available 也就 1G 出头。按经验,这种量级的占用要么是某个 Java 服务堆开太大,要么是数据库缓存吃满了。于是我顺手ps aux --sort=-%mem | head,准备揪出那个“内存大户”。
结果出乎意料。排在前面的进程 RSS 加起来也就 6G 左右,剩下二十多个 G 像是凭空蒸发了。top里所有进程的 RES 列加一遍,也对不上账。这种“free 说没内存,ps 说没人用”的割裂感,是很多运维和开发都踩过的坑。它不是什么灵异事件,而是 Linux 内存管理机制在特定场景下给你的一个“障眼法”。
这篇内容就是把这个障眼法拆开讲清楚。我会从内存统计的底层逻辑说起,解释为什么会出现“可用物理内存很少但进程占用不大”的现象,然后给出几套可复现的排查命令和判断路径,最后把常见成因整理成速查表。适合有一定 Linux 基础、正在被内存问题困扰的开发和运维同学,小白也能跟着命令一步步看下去。
2. 先搞懂 free 到底在统计什么
2.1 free 的字段含义与常见误读
很多人看free只看 free 那一列,一看数字小就慌。其实free命令输出的每一列含义差别很大,误读往往就出在这里。先看一个典型输出:
total used free shared buff/cache available Mem: 31Gi 29Gi 512Mi 1.2Gi 1.5Gi 1.1Gi Swap: 8.0Gi 2.0Gi 6.0Gi这里有几个关键点。free列指的是完全未被使用的物理内存,这个数字小其实很正常,因为 Linux 的设计哲学就是“空闲内存是浪费”,它会主动把没用到的内存拿去做缓存。真正该关注的是available列,它表示在不触发 swap 的前提下,应用程序还能申请到多少内存,这个值才是判断内存是否真的紧张的指标。
buff/cache是块设备缓存和文件系统缓存的合计。这部分内存在应用需要时可以被立即回收,所以它不算真正的“被占用”。shared是 tmpfs 等共享内存的占用,这部分比较特殊,它既算在 used 里,又不会体现在普通进程的 RSS 中,是“幽灵占用”的头号嫌疑对象。
提示:判断内存是否真的吃紧,看 available,不要看 free。available 长期低于总内存的 10%,才需要认真排查。
2.2 used 的构成:为什么它和进程 RSS 对不上
used这个字段的计算方式是total - free - buff/cache。注意,它不等于所有进程 RSS 之和。这中间的差额来自好几个地方:内核自身的数据结构(slab)、页表、驱动占用的内存、tmpfs 共享内存、以及被进程申请但尚未真正写入物理页的匿名内存等。
换句话说,used是一个“系统级”的统计口径,而ps看到的 RSS 是“进程级”的口径,两者本来就不是一回事。当你发现 used 远大于进程 RSS 总和时,差额大概率藏在下面这几个地方:
- tmpfs / shm:挂在
/dev/shm、/run等位置的共享内存,占用算进 used,但不属于任何进程的 RSS。 - 内核 slab:内核对象缓存,比如 dentry、inode 缓存,用
slabtop能看到。 - 页表与内核栈:进程越多、内存越大,页表开销越大,这部分也不计入 RSS。
- 内存碎片与预留:某些驱动或大页配置会预留内存,free 看不到但实际不可用。
理解了这层,你就明白为什么“free 少但进程不大”是常态而非异常。接下来要做的,是把这个差额具体定位到某一类占用上。
3. 逐层排查:把消失的内存找出来
3.1 第一站:确认是不是缓存和共享内存
排查的第一步,永远是先排除“假警报”。执行free -h看 available,如果 available 还有好几个 G,那基本不用慌,是 buff/cache 在撑场面。如果 available 确实很低,再往下走。
接着看 shared 这一列。如果 shared 有几个 G,那基本可以锁定是 tmpfs 或共享内存的问题。用下面这条命令列出所有 tmpfs 挂载点及其占用:
df -h -t tmpfs常见的“重灾区”是/dev/shm。有些应用(比如某些数据库、消息队列、或者用了共享内存的中间件)会往这里写大量数据,进程退出后如果没清理干净,内存就一直挂着。我遇到过好几次,某个服务异常退出,/dev/shm里留了几个 G 的残留文件,free 一直上不来,重启服务也没用,最后手动清理才恢复。
ls -lh /dev/shm如果发现大文件且确认无进程使用,可以安全删除。但删之前一定要确认没有活着的进程还在引用它,否则可能引发数据损坏。
3.2 第二站:用 smem 看真实的进程内存分布
ps和top的 RSS 有个天然缺陷:它会把共享库的内存重复计算。比如十个进程都链接了同一个 50M 的库,每个进程的 RSS 都会算上这 50M,加起来就虚高了。反过来,有些内存又不计入任何进程,导致总和偏低。要看更真实的数据,推荐用smem:
smem -t -k -s rsssmem会给出 PSS(按比例分摊共享内存后的值)和 USS(进程独占内存),这两个指标比 RSS 靠谱得多。把所有进程的 PSS 加起来,再和 used 对比,差额就清晰多了。如果 PSS 总和依然远小于 used,那问题基本不在用户态进程,而在内核或共享内存。
3.3 第三站:内核 slab 与页表开销
内核态的内存占用,ps是完全看不到的。用slabtop可以查看内核对象缓存的分布:
slabtop -s c -o按缓存大小排序,重点看dentry、inode_cache、ext4_inode_cache这几项。如果它们加起来有好几个 G,说明文件系统元数据缓存偏大。这部分内存理论上可回收,但在内存压力大时回收不及时,就会表现为 available 偏低。
页表开销可以用pmap汇总,或者直接看/proc/meminfo里的PageTables字段:
grep -E "PageTables|Slab|Shmem|KernelStack" /proc/meminfo这几个字段是定位“幽灵内存”的黄金指标。Shmem对应共享内存,Slab对应内核缓存,PageTables对应页表,KernelStack对应内核栈。把它们加起来,往往就能解释掉大部分差额。
3.4 第四站:被忽略的匿名内存与内存泄漏
如果上面几项都正常,但 used 依然高企,那就要怀疑匿名内存了。匿名内存是进程通过 malloc 申请、但没有对应文件映射的内存。它会计入进程的 RSS,但如果进程申请后没实际写入,或者写入后被 swap 出去,统计上就会有偏差。
还有一种情况是内存泄漏。进程持续申请内存但不释放,RSS 会缓慢增长。这种问题用ps定时采样就能发现:
while true; do ps -o pid,rss,comm -p <PID>; sleep 60; done如果 RSS 只涨不跌,基本可以确认泄漏。这时候需要结合pmap看具体是哪段内存增长:
pmap -x <PID> | sort -k3 -n -r | head4. 实操复盘:一次 20G 内存消失的完整定位过程
4.1 现场信息采集
回到开头那台机器。我按下面的顺序采集了一轮信息,这套流程现在已经是我的标准动作:
free -h grep -E "MemTotal|MemFree|MemAvailable|Buffers|Cached|Shmem|Slab|PageTables|KernelStack" /proc/meminfo df -h -t tmpfs smem -t -k -s rss | tail -5 slabtop -s c -o | head -20采集完一对比,线索就出来了。/proc/meminfo里Shmem显示 18G,而df -h -t tmpfs显示/dev/shm占了 17G 多。进程 RSS 总和才 6G,slab 也就 1G 出头。答案基本锁定:/dev/shm里有大量残留数据。
4.2 定位到具体文件与进程
进/dev/shm一看,果然有几个大文件,命名像是某个中间件的临时数据。用lsof反查哪些进程还在引用:
lsof /dev/shm/* 2>/dev/null结果发现只有一个已经处于僵尸状态的进程还挂着文件描述符,实际业务进程早就重启过了。也就是说,这些共享内存段成了“孤儿”,既没有进程正常使用,也没被系统回收。
4.3 安全清理与验证
清理前我做了两件事:一是确认这些文件确实没有活跃进程在读写(通过lsof和fuser双重确认),二是备份了文件名和大小记录,以防后续需要追溯。然后执行删除:
fuser -m /dev/shm/<filename> rm -f /dev/shm/<filename>删完再free -h,available 立刻从 1G 回升到 19G。整个过程没有重启任何服务,业务无感知。
注意:清理
/dev/shm一定要谨慎。如果确实有活跃进程在用,删除文件会导致该进程后续读写失败甚至崩溃。稳妥做法是先停掉相关服务,再清理,最后重启服务。
4.4 根因分析与长效预防
事后复盘,根因是那个中间件在异常退出时没有正确释放共享内存,而它的启动脚本又没有做清理逻辑。我们做了两件事来预防:一是在服务启动脚本里加上清理旧共享内存段的逻辑,二是给/dev/shm单独加了监控告警,占用超过阈值就通知。
这里有个经验:/dev/shm默认大小通常是物理内存的一半,很多人不知道它可以调整。如果业务确实需要大共享内存,可以在/etc/fstab里显式配置大小,避免它无限制增长影响系统。
5. 常见成因速查与避坑指南
5.1 成因速查表
把这次排查和以往遇到的案例整理成一张表,方便对照:
| 现象 | 可能成因 | 定位命令 | 处理方式 |
|---|---|---|---|
| available 低,free 低,shared 高 | tmpfs/共享内存残留 | df -h -t tmpfs、ls /dev/shm | 确认无引用后清理 |
| used 高,进程 RSS 总和小 | 内核 slab 偏大 | slabtop -s c | 可手动回收或调参 |
| used 高,PageTables 大 | 进程数多或大内存进程 | grep PageTables /proc/meminfo | 减少进程数或优化 |
| 某进程 RSS 持续增长 | 内存泄漏 | 定时ps采样 | 修复代码或重启 |
| available 低但无明显占用 | 内存碎片/预留 | /proc/meminfo全字段 | 评估是否需重启 |
5.2 几个容易踩的坑
第一个坑是盲目重启服务。很多人一看内存高就重启业务,结果重启完内存没降,因为占用根本不在业务进程里。重启反而造成业务中断,得不偿失。正确做法是先定位再动手。
第二个坑是误删共享内存。/dev/shm里的文件看着像临时文件,但可能有活跃进程在用。删之前务必用lsof或fuser确认。我见过有人直接rm -rf /dev/shm/*,结果把正在运行的数据库搞崩了。
第三个坑是把 buff/cache 当成占用。buff/cache 是可回收的,看到它大不用慌。真正要盯的是 available。有些监控系统直接把 used 当告警指标,这是不准确的,容易误报。
第四个坑是忽略 cgroup 限制。在容器环境里,free看到的是宿主机内存,容器实际可用内存受 cgroup 限制。这时候要看/sys/fs/cgroup/memory/memory.limit_in_bytes和memory.usage_in_bytes,而不是宿主机视角的 free。
5.3 实操心得
分享几个我踩坑总结出来的小技巧。第一,排查内存问题前先sync一下,把脏页刷盘,这样 buff/cache 的统计会更准确。第二,/proc/meminfo里的MemAvailable是内核估算的,比 free 列靠谱,但它在某些老内核上可能不存在,需要留意内核版本。第三,如果怀疑是内核 slab 问题,可以尝试echo 2 > /proc/sys/vm/drop_caches手动回收,但这只是临时手段,治标不治本,而且生产环境慎用,可能引起 IO 抖动。
第四,对于长期运行的服务,建议定期用smem做一次内存快照,建立基线。这样一旦出现异常,有历史数据可以对比,定位会快很多。第五,容器环境下排查内存,一定要区分宿主机视角和容器视角,两者经常对不上,容易误导判断。
6. 从机制到工具:建立自己的内存排查方法论
6.1 分层排查的思路
内存排查最忌讳东一榔头西一棒子。我习惯按“系统层 → 内核层 → 进程层 → 应用层”的顺序逐层往下。系统层看 free 和 /proc/meminfo,内核层看 slab 和页表,进程层看 smem 和 pmap,应用层才去看具体代码和配置。这个顺序的好处是,每一层都能排除掉一批可能性,不会在无关方向上浪费时间。
具体来说,先看 available 判断是否真的紧张,再看 shared 和 Shmem 排除共享内存,接着看 Slab 和 PageTables 排除内核开销,最后才落到进程和应用上。大部分“幽灵占用”问题,在前三层就能定位到。
6.2 工具选型建议
工具不在多,在于用对场景。free和/proc/meminfo是基础,必看。smem比ps准确,建议装上。slabtop查内核缓存,pmap查进程内存映射,lsof和fuser查文件引用,这几个是排查共享内存和文件句柄的利器。
如果要做长期监控,建议把/proc/meminfo的关键字段采集起来,画成趋势图。单点数据看不出问题,趋势才能暴露泄漏和缓慢增长。我一般会重点监控MemAvailable、Shmem、Slab、PageTables这四个指标,它们基本覆盖了大部分异常场景。
6.3 预防胜于排查
与其等出问题再排查,不如提前做好预防。几个实用建议:给关键服务配置内存上限,避免单个服务吃光内存;给/dev/shm设置合理大小并加监控;定期巡检 slab 和共享内存占用;容器环境配置好 cgroup 限制和对应的监控。
还有一点很重要:建立内存使用的基线。正常运行时 available 大概多少、slab 大概多少、shmem 大概多少,心里要有数。有了基线,任何偏离都能第一时间发现,而不是等到告警了才手忙脚乱。
我个人在实际操作中的体会是,内存问题百分之八十的困惑都来自“统计口径不一致”。free 的口径、ps 的口径、cgroup 的口径各不相同,混着看必然对不上。把每个工具的口径搞清楚,再按分层思路去排查,那些看似消失的内存,其实都有迹可循。最后再分享一个小技巧:遇到实在定位不了的情况,cat /proc/meminfo从头到尾看一遍,往往能在某个不起眼的字段里找到答案。