监控面板弹出「内存使用率 94%」的时候,第一反应往往是找哪个进程在漏内存。但在 Linux 上,这个数字本身经常是误报——它取决于监控 agent 用的是哪一列。这篇按我实际的排查顺序,把内存这件事讲清楚。
free 的每一列到底是什么
$ free -m
total used free shared buff/cache available
Mem: 7962 2140 312 188 5509 5301
Swap: 2047 104 1943
这台机器「只剩 312 MB 空闲」,但真正可用的是 available 列的 5301 MB。区别在于:
free:完全没被使用的物理页。这个值低是正常的,空闲内存对内核来说是浪费,它会尽量拿去做缓存。buff/cache:page cache(文件内容缓存)加上内核缓冲区。绝大部分在需要时可以立刻回收。available:内核估算的、在不触发 swap 的前提下可以给新进程使用的内存。做告警只应该看这一列。
如果监控用的是 (total - free) / total,那任何一台跑了一段时间的机器都会报警。正确的表达式是:
(MemTotal - MemAvailable) / MemTotal
对应到 /proc/meminfo 里的 MemTotal 和 MemAvailable 两行。
page cache 什么时候会成为问题
干净的 page cache 可以直接丢弃,回收成本几乎为零。有成本的是脏页——已经被修改、还没写回磁盘的部分:
$ grep -E 'Dirty|Writeback' /proc/meminfo
Dirty: 12844 kB
Writeback: 0 kB
脏页多、后端磁盘又慢的时候,进程申请内存会被卡在同步回写上,表现为「明明有内存,但系统很卡」。相关的两个内核参数:
# 脏页占可用内存的比例超过 10% 时,后台线程开始回写
vm.dirty_background_ratio = 10
# 超过 20% 时,写入的进程被强制同步刷盘
vm.dirty_ratio = 20
大内存机器上按比例算出来的绝对值可能有几个 GB,一次性刷盘会造成明显卡顿。这种情况改用字节版本更可控:
sysctl -w vm.dirty_background_bytes=268435456 # 256 MB
sysctl -w vm.dirty_bytes=1073741824 # 1 GB
注意 ratio 和 bytes 是互斥的,设置其中一个会把另一个清零。
被忽略的 slab
buff/cache 里还混着内核自己的对象缓存。当机器上有大量小文件操作,或者容器频繁创建销毁时,dentry 和 inode 缓存能涨到几个 GB:
$ grep -E 'Slab|SReclaimable|SUnreclaim' /proc/meminfo
Slab: 1893472 kB
SReclaimable: 1702208 kB
SUnreclaim: 191264 kB
$ slabtop -o -s c | head -8
SReclaimable 可回收,SUnreclaim 不可回收。后者持续增长而且不随负载下降,才是真正的内核内存泄漏信号,通常和某个内核模块或文件系统驱动有关。
网上流传的 echo 3 > /proc/sys/vm/drop_caches 不要在生产上用。它会丢掉全部 page cache,之后所有读请求都要重新走磁盘,I/O 会瞬间打满,「解决」了一个假问题却制造了一个真问题。它唯一的正当用途是做基准测试前清空缓存。
找到真正的内存占用者
按常驻内存排序是最快的第一眼:
ps -eo pid,comm,rss,vsz --sort=-rss | head -12
但 RSS 会把共享库重复计算,多个同族进程加起来往往超过物理内存总量。要看真实占用,用 PSS(按共享份额均摊):
# 单个进程的 PSS 汇总
grep '^Pss:' /proc/$PID/smaps_rollup
# 全局按命令排序(需要 root)
smem -k -s pss -r | head -12
如果某个进程 RSS 和 PSS 都不高,但系统内存就是紧张,检查一下有没有大量僵尸进程或者 tmpfs:
df -h -t tmpfs
findmnt -t tmpfs
/dev/shm 和 /run 写进去的数据是实打实占内存的,而且不会被自动回收。我见过一次「内存泄漏」,最后发现是应用把临时文件写到 /dev/shm 而没有删除。
OOM Killer 的判定逻辑
当内存真的不够、回收也来不及时,内核会挑一个进程杀掉。日志里能看到完整的现场:
$ dmesg -T | grep -iE 'killed process|out of memory'
[Tue Dec 3 04:12:57 2024] Out of memory: Killed process 21417 (java)
total-vm:6291456kB, anon-rss:3903120kB, file-rss:0kB, shmem-rss:0kB,
UID:1000 pgtables:8724kB oom_score_adj:0
选谁取决于 oom_score,它主要正比于进程占用的内存量。也就是说,被杀的通常是内存占用最大的进程,不一定是导致内存耗尽的那个。数据库、缓存这类「大而重要」的进程最容易背锅。
可以给关键进程降低被选中的概率(取值 -1000 到 1000,-1000 表示永不选中):
echo -500 > /proc/$(pidof postgres)/oom_score_adj
更规范的做法是写进 systemd unit,避免进程重启后失效:
[Service]
OOMScoreAdjust=-500
MemoryMax=4G
MemoryHigh=3G
容器里的内存和宿主机不是一回事
在容器里跑 free,看到的是宿主机的数字,这会误导所有基于 free 的判断。cgroup v2 下应该读这几个文件:
cat /sys/fs/cgroup/memory.current # 当前用量
cat /sys/fs/cgroup/memory.max # 硬上限,超过即触发 OOM
cat /sys/fs/cgroup/memory.high # 软上限,超过后被节流回收
cat /sys/fs/cgroup/memory.events # low/high/max/oom/oom_kill 计数
memory.events 里的 oom_kill 计数是判断「容器是不是被 OOM 过」的权威依据,比翻 dmesg 可靠得多。MemoryHigh 比 MemoryMax 更友好:它触发的是回收和节流而不是直接击杀,适合给不确定用量的服务留缓冲。
另外,JVM 从 10 开始默认开启 UseContainerSupport,会按 cgroup 限额而不是宿主机内存计算堆大小。老版本或者显式设了 -Xmx 又超过容器限额,结果就是容器被反复 OOM 重启。
swap 要不要开
常见的观点是「服务器一律关 swap」。我的做法是:留少量 swap,但把 swappiness 调低。
sysctl -w vm.swappiness=10
理由是完全没有 swap 时,内核在内存压力下唯一的选择就是杀进程;留一点 swap 至少能把长期不用的匿名页换出去,为回收争取时间,也让监控有机会先报警。当然,如果服务对延迟极度敏感(比如低延迟交易或者 Redis),那关掉 swap 是对的。
观察 swap 是否真的在被使用,看 si/so 两列而不是总量:
vmstat 1 5
总量不为零只说明历史上换出过,只有 si/so 持续非零才代表当前正在颠簸。
小结
- 告警指标改用
MemAvailable,能消掉八成的误报。 buff/cache高不是问题,脏页高才是;slab 里的SUnreclaim持续增长才值得深究。- 定位占用者用 PSS,别用 RSS 做加法。
- 容器场景一律看 cgroup 文件,尤其是
memory.events的oom_kill。 - 关键服务配
OOMScoreAdjust和MemoryHigh,别等 OOM Killer 替你做选择。