
有人在群里甩一张free -h的截图配一句话内存只剩 300M 了是不是要挂了这是我在运维群里见过最多的一类提问。真相往往是那台机器跑得好好的available还有好几个 G只是提问的人看错了列。查看 Linux 剩余内存这件事表面上是敲一条命令实际上是一整套关于MemFree、MemAvailable、buff/cache、cgroup 限额、RSS/PSS 的判读逻辑判错方向轻则白折腾一场重则把好好的 page cache 手动清掉让数据库的读性能直接掉一个档次。这篇就按我平时带人的思路把Linux 剩余内存怎么看从概念、命令、进程维度一路拆到容器和监控告警尽量让刚接触 Linux 的朋友看完能自己判断也让已经用了几年的人避开那几个经典陷阱。1. 先搞清楚 Linux 说的剩余内存是哪个数字1.1 free 输出里那七列分别是什么来路随便找台机器敲free -h你会看到类似这样的输出$ free -h total used free shared buff/cache available Mem: 31Gi 9.4Gi 1.2Gi 412Mi 21Gi 21Gi Swap: 2.0Gi 0.0Gi 2.0Gi很多人只盯free那一列看到 1.2Gi 就慌了。但内核自己给出的还能给新进程用多少其实是最后一列available这里是 21Gi。free低而available高恰恰说明系统状态非常健康内核把暂时用不到的内存拿去做磁盘缓存了需要的时候几毫秒就能交还。这七列的数据来源全部是/proc/meminfofree只是做了算术和单位换算。理解这一点很重要因为当free的输出让你困惑时去读/proc/meminfo的原始字段永远是最靠谱的验证手段free的列名在版本升级时改过一次而/proc/meminfo的字段名多年保持稳定。shared这一列也值得单独说一句。它统计的是Shmem也就是 tmpfs、/dev/shm、部分匿名共享内存占用的量。这块内存看起来像缓存但它不能像普通 page cache 那样被随意丢弃——tmpfs 里的数据如果被回收就等于丢文件。所以把/dev/shm挂载得很大、或者用 tmpfs 存了大量数据的机器available会比直觉上少这是正常现象。1.2 MemAvailable 和 MemFree 的区别决定了你会不会误判MemFree的定义非常字面完全空闲、没有分配给任何用途的物理页。这个数字在健康的 Linux 上本来就应该比较小因为空闲内存是一种浪费。MemAvailable是新一些的内核3.14 之后才提供的估算值它的思路是在不触发 swap 的前提下内核大概还能拿出多少内存给新应用。内核里的估算逻辑大致是这样MemAvailable ≈ MemFree - 低水位线(min watermark) 大部分可回收的 page cache 大部分可回收的 slab(SReclaimable) - 内核认为短期内不会被释放的部分注意这里说的是估算不是精确值。它基于经验系数对Active(file)这类被频繁访问的缓存会保守地只算一部分。所以你在MemAvailable上做监控阈值是合理的你在MemFree上做监控阈值就一定会天天误报。打个生活化的比方MemFree是你钱包里的现金MemAvailable是你现金加上随时能取出来的活期存款。只看现金你会觉得自己穷得吃不上饭实际上你随时能取钱。1.3 buff/cache 不是被吃掉的内存buff/cache大致等于Buffers Cached SReclaimable。Buffers是块设备层的小块元数据缓存通常很小Cached是文件内容的 page cache这是大头SReclaimable是可回收的 slab比如 dentry 和 inode 缓存。page cache 存在的唯一目的就是让后续读同一个文件变快。一个刚跑完大文件扫描的机器buff/cache涨到几十 G 是完全正常的而且这是好事。真正需要警惕的是下面这种组合现象说明是否需要处理free 低available 高正常的缓存利用不需要free 低available 也低swap 大量使用内存确实紧张需要排查buff/cache 高但 available 高缓存健康不需要available 高但应用报 OOM多半是 cgroup 限额或 JVM 堆外问题需要按第 4 章排查我在实际工作中定过一条规矩报告内存问题必须同时给 free、available、swap 三个数只给一个 free 的截图直接退回重查。这条规矩省掉了大量无意义的沟通。2. 三条命令看清实时内存以及它们互相打架的原因2.1 free 的参数组合比你想象的有用free的参数不多但组合起来能解决很多场景free -h # 人类可读单位日常首选 free -m # 以 MB 为单位脚本里更常见 free -w # 把 buff 和 cache 分开展示排查块设备缓存时有用 free -t # 加一行 Total把 Mem 和 Swap 汇总 free -s 2 -c 5 # 每 2 秒输出一次共输出 5 次 free -h -s 1 -c 10 # 盯着看内存变化趋势-s搭配-c是我排查内存缓慢上涨类问题的第一手段。单次快照看不出趋势连着看十几秒你立刻能分辨是稳态占用还是持续泄漏。如果available在十几次采样里持续单调下降且没有收敛迹象那就是真有问题了。另外提醒一个细节free的单位换算用的是 1024 进制free -h里的 G 是 GiB。如果你要跟云厂商账单或监控平台的数据对齐注意有些平台用的是 GB31GiB ≈ 33.3GB差出来的量在某些场景里不能忽略。2.2 /proc/meminfo 逐字段对照free看不明白的时候直接读原始文件cat /proc/meminfo字段很多但真正日常要看的就那么十来个字段含义判读要点MemTotal可用物理内存总量通常小于标称容量BIOS/内核保留了一部分MemFree完全空闲内存数值小是正常状态MemAvailable可交付给新应用的内存估算判断内存是否够用的首选指标Buffers块设备元数据缓存一般很小Cached文件 page cache大宗注意它包含 ShmemShmemtmpfs 等共享内存不可随意回收容易造成误判Active(file) / Inactive(file)文件缓存的热冷分区Inactive 部分更容易被回收AnonPages匿名页即应用堆栈等这部分只能靠 swap 换出Slab / SReclaimable / SUnreclaim内核对象缓存SUnreclaim 持续增长要警惕内核泄漏Dirty / Writeback待写回磁盘的数据突增说明 IO 压力大Committed_AS / CommitLimit已承诺/可承诺的虚拟内存超限时 malloc 会失败SwapTotal / SwapFree交换分区结合 si/so 一起看这里有个非常经典的坑tmpfs 占用的内存既算在Cached里又算在Shmem里。也就是说如果有人往/dev/shm写了 10G 文件Cached会涨 10G看起来像缓存但available会相应减少 10G因为它不可回收。很多内存莫名其妙被缓存吃掉了的案例最后追下去都是 tmpfs。顺手给一条快速定位 tmpfs 占用的命令df -h -t tmpfs输出里哪个挂载点容量大、使用率高就去那里找元凶。2.3 top 和 free 的 used 对不上是谁算错了top头部那行MiB Mem里也有 used 和 free经常跟free命令的数字差一点。原因有两个第一两者采样时刻不同。top是周期性刷新free是瞬时读取中间隔了几毫秒到几秒内存本来就在变。第二也是最容易被忽略的used的计算口径随版本变过。老版本free的used total - free - buff/cache这个算法把 tmpfs 当成缓存扣掉了于是used偏小新版本procps-ng 3.3.10 之后改成了used total - available之外的另一套算法把 Shmem 从 buff/cache 里拆出来算进 used于是used偏大。所以在不同发行版上跑freeused这一列本身就不完全可比。结论很直接做判断看available做报表看total和available不要拿used跨机器横向比较。我在写监控指标时从来只采集MemTotal、MemAvailable、SwapFree三个数够用且没歧义。top里的%MEM列也有类似问题它用的是进程 RSS 除以 MemTotal在容器里分母会算成宿主机的内存于是所有进程的%MEM都严重偏小。这个坑留到第 4 章细说。3. 从进程维度反推到底谁在吃内存3.1 RSS、VSZ、SHR 三个数字的真实含义ps aux输出里有两列内存相关VSZ虚拟内存集和RSS常驻内存集。VSZ是进程映射的虚拟地址空间总和包含大量根本没实际分配物理页的区域预留的堆、mmap 的文件、共享库的地址空间、以及 Java 那种一次性预留大块虚拟地址的运行时。看到某进程 VSZ 有 200G 完全不奇怪尤其 JVM 通常会把堆空间整块预留。VSZ 大不代表真占内存这条认知能帮你排除掉一半的假警报。RSS是真正驻留在物理内存里的页这个数字才有参考价值。但它也有两个缺陷一是包含共享内存比如多个进程映射同一个.so每个进程的 RSS 都会把这部分算进去二是排除了已经被 swap 换出的部分。所以把一台机器上所有进程的 RSS 加起来得到的数通常远大于实际使用的物理内存。SHR是 RSS 中可与其他进程共享的部分比如共享库和shmem。它只是告诉你这里面有多少是可共享的不代表这部分内存被重复占用。3.2 找出 TOP 消耗进程的正确命令日常排查我常用这几条# 按 RSS 倒序取前 15 个进程 ps -eo pid,ppid,user,rss,vsz,pmem,comm --sort-rss | head -n 16 # 把同一进程名的 RSS 汇总避免多 worker 拆散 ps -eo rss,comm --no-headers | awk {a[$2]$1} END {for(i in a) printf %8.1f MB %s\n, a[i]/1024, i} | sort -rn | head -n 15 # 直接看 /proc 里的精确值单位是页(通常 4KB) grep -E VmRSS|VmSwap|VmSize /proc/PID/status最后那条命令特别实用。当你想确认某个进程是不是真的在泄漏隔几分钟采几次VmRSS画个曲线就清楚了。VmSwap也值得看一眼一个进程VmRSS不大但VmSwap很大说明它曾经用得多现在被换出去了这种情况下进程一旦被访问就会产生大量 major fault表现出来的现象是偶发卡顿而不是内存不足。3.3 用 PSS 解决共享内存重复计数要精确知道这台机器上每个进程实际贡献了多少内存RSS是不够的需要看PSSProportional Set Size。PSS的做法是把共享的那部分按共享进程数平摊所以所有进程的 PSS 相加结果最接近系统真实占用。smem工具就是干这个的smem -tk # 系统总览最后一行是汇总 smem -tk -c pid user command pss rss uss swap smem -tk -P nginx # 只看 nginx 相关进程 smem -tk -u # 按用户汇总USS是进程独占的内存不含任何共享部分是这个进程结束能省下多少的最佳答案。不过smem需要额外安装有些精简镜像里没有而且要读/proc/*/smaps权限要求较高。如果装不了退而求其次的做法是用pmap -x PID看单个进程的内存映射明细再手工判断哪些是共享库。这条路麻烦但不需要装东西。这里有个我踩过的坑在一台跑着几十个 nginx worker 的机器上把每个 worker 的 RSS 相加得出nginx 占了 40G直接把人吓了一跳。实际用smem一看nginx 整体 PSS 只有几百 M因为所有 worker 共享同一份二进制和库文件。凡是多进程模型nginx、php-fpm、gunicorn、tomcat 多实例都可能出现这种虚高。4. 容器和 cgroup 环境下剩余内存彻底换了口径4.1 容器里 free 显示的是宿主机内存在没做特殊处理的容器里敲free -h你会看到宿主机的总内存而不是容器限额。比如容器限制了 512Mfree依然显示宿主机有 31Gavailable有 21G然后应用在用到 512M 的时候被 SIGKILL 杀掉日志里啥都没有。原因很直白free读的是/proc/meminfo而/proc默认是宿主机的视图。要修正这个行为需要在容器里挂载 lxcfs 这类专门做视图隔离的文件系统才能让/proc/meminfo返回 cgroup 限额。有这个机制的集群应该尽量开启否则开发同学在容器里排查内存问题基本等于盲人摸象。在没有 lxcfs 的情况下唯一可靠的判断方式是直接读 cgroup 的文件。这就是下一节的内容。4.2 cgroup v1 与 v2 的限额文件对照先确认是哪个版本stat -fc %T /sys/fs/cgroup # 输出 tmpfs 通常表示 v1输出 cgroup2fs 表示 v2两个版本的接口差别不小我把常用文件整理成一张表用途cgroup v1 路径cgroup v2 路径内存上限memory.limit_in_bytesmemory.max当前用量memory.usage_in_bytesmemory.current明细统计memory.statmemory.stat触发限额次数memory.failcntmemory.eventsmax 计数软限制memory.soft_limit_in_bytesmemory.low / memory.high换出用量memory.memsw.usage_in_bytesmemory.swap.current实用写法# v1 cat /sys/fs/cgroup/memory/memory.limit_in_bytes cat /sys/fs/cgroup/memory/memory.usage_in_bytes # v2 cat /sys/fs/cgroup/memory.max cat /sys/fs/cgroup/memory.current注意memory.max里可能是字符串max表示没设限额脚本里要单独处理这种情况否则awk比较会得到意外结果。memory.stat才是真正有信息量的地方重点看这几项anon匿名页应用堆栈这块基本不可回收filepage cache大部分可回收slab/kernel内核对象inactive_file冷缓存回收优先级最高pgmajfault主缺页次数配合换出看判断容器是否真的快撑不住我用的近似公式是容器可用预算 ≈ memory.max - anon - slab_unreclaimable也就是说别拿memory.current直接跟memory.max比因为memory.current里包含大量可回收的file缓存虚高。一台容器memory.current显示 480M / 512M 看起来快爆了实际anon只有 200M、file有 260M内核在压力上来时会先把 file 回收掉完全撑得住。4.3 Java 应用在容器里的内存账本最难算Java 应用的容器内存问题是个独立话题但既然聊剩余内存就绕不开。一个 JVM 进程的 RSS 大致等于下面几块加起来堆内存-Xmx上限但实际提交量随使用增长Metaspace类元数据-XX:MaxMetaspaceSize控制上限线程栈1M × 线程数几百线程就是几百 MJIT 编译产物 CodeCacheGC 结构本身G1 的卡表、标记位图通常占堆的百分之几到十几DirectByteBuffer / Netty 的堆外内存glibc 的 malloc arena每个线程 arena 可达 64M多线程程序容易堆积最后两项是最容易被忽略的。Netty 用了大量 DirectBuffer、或者程序里频繁用ByteBuffer.allocateDirect时堆内存看起来只用了一半进程 RSS 却一路涨到限额然后被 OOM 杀掉堆 dump 里什么都看不出来。这种问题的排查方式是看pmap -x里的匿名映射大块或者开-XX:NativeMemoryTrackingsummary再看jcmd PID VM.native_memory summary。glibc arena 的问题有个经典解法设置环境变量MALLOC_ARENA_MAX2或4。MALLOC_ARENA_MAX 不设时默认是 CPU 核数的 8 倍在一台 64 核的机器上开几十个 arena光内存碎片就能吃掉好几个 G。我在把容器内存限额设得比较紧的 Java 服务上基本都会加这个环境变量。顺带说一句JVM 从某个版本开始默认开启UseContainerSupport会自动按 cgroup 限额计算堆大小但这个自动值通常取限额的四分之一如果你没显式设-Xmx可能得到一个远小于预期的堆。显式设置-Xmx和-XX:MaxRAMPercentage永远比依赖默认值可靠。5. Swap、缓存回收与 OOM什么样的信号才算真的危险5.1 指标组合比单看一个数字可靠得多我判断一台机器内存是否危险看的是下面这组信号的组合信号命令危险表现available 持续下降free -h -s 5 -c 12单调下滑不收敛换入换出活跃vmstat 5si/so 长期非零主缺页频繁sar -B 5 或 /proc/vmstatpgmajfault 快速增长直接回收开销大/proc/vmstat 的 allocstall数值持续增长内存压力指数高cat /proc/pressure/memorysome/full avg10 明显大于 0内核不可回收缓存涨/proc/meminfo 的 SUnreclaim持续单边上涨OOM 记录dmesg -T | grep -i out of memory有记录就是硬信号PSI/proc/pressure/memory是这几年我越来越依赖的指标它的三个数字avg10/avg60/avg300表示过去这段时间里有任务因为内存不足而停顿的时间占比。输出大概长这样some avg100.00 avg600.00 avg3000.00 total0 full avg100.00 avg600.00 avg3000.00 total0some非零说明有任务在等内存full非零说明整个系统都被拖住了。这比看available高低灵敏得多因为available还有几个 G 的时候系统可能已经开始卡了比如内存严重碎片化或者回收路径开销过大。5.2 si/so 非零和 swap 使用量是两个不同的信号swap用掉了一些不代表有问题。Linux 有vm.swappiness这个参数默认 60在合适的场景下会把长时间不访问的匿名页换出去这是正常的内存调度。真正要看的是vmstat的 si/so 这两列它们代表每秒换入/换出多少 KB。缓存型的 swap 占用配 si/so 接近 0说明换出去的东西没人再访问了皆大欢喜。swap 占用不高但 si/so 持续几百上千说明系统在反复换入换出这时候每一次 IO 都会让应用延迟飙升是必须处理的状况。处理方向有三条按优先级排找出反复访问大内存的进程看是不是缓存策略有问题比如自己实现了一个无上限的本地缓存如果确认是内存需求真的超了扩容是唯一正解调 swap 只是拖时间在数据库、Redis 这类对延迟敏感的服务上通常建议vm.swappiness调低甚至设成1避免内核把它的页换出去5.3 手动回收缓存要谨慎也别迷信它drop_caches是很多人第一反应会用的命令sync # 先把脏页写回必须做 echo 1 /proc/sys/vm/drop_caches # 只清 page cache echo 2 /proc/sys/vm/drop_caches # 清 dentry 和 inode 缓存 echo 3 /proc/sys/vm/drop_caches # 两者都清它确实能让free的数字好看但从解决内存问题的角度看基本没什么用因为需要缓存的时候内核还会把缓存建回来只是白白损失了已有的缓存让随后一段时间的磁盘读全部变成真实 IO。在数据库机器上随手执行这条命令是我见过的最典型的自伤操作。它真正有用的场景是做内存基准测试前的环境清理或者需要精确对比两次测试结果时。日常排查请优先看available和 PSI。echo 0 /proc/sys/vm/drop_caches不需要执行写 1/2/3 是一次性动作不是开关。5.4 OOM 现场该怎么读真出事了第一时间做三件事dmesg -T | grep -i -E out of memory|oom-kill | tail -n 50 journalctl -k --since 1 hour ago | grep -i oom cat /proc/被杀进程PID/oom_score 2/dev/null内核 OOM 日志里有几个关键信息被选中杀掉的进程、它的total-vm和rss、以及当时的内存状态。重点看被杀的进程是不是你真正关心的那个。内核选谁杀是按oom_score来的大致和进程的 RSS 成正比还会受oom_score_adj影响。oom_score_adj范围是 -1000 到 1000设成 -1000 表示这个进程永不被杀。有些服务是被杀掉比拖垮整机更糟的类型这种可以适度调整oom_score_adj。但我不建议把关键服务的值设成 -1000那等于把风险转移给同机器上的其他进程尤其是系统进程被杀掉之后整机会不可用。更合理的做法是给关键服务留出足够的内存余量而不是靠优先级赌命。cgroup 环境下的 OOM 会好排查一些直接看对应的memory.eventsv2或memory.oom_controlv1能明确是哪个 cgroup 触发的限额。6. 把剩余内存变成可告警的数字6.1 用 awk 一次解析出需要的字段写脚本时我不建议用free的输出列名和格式随版本变过兼容性差。直接读/proc/meminfo更稳#!/bin/bash # mem_check.sh —— 输出可用内存百分比附人可读的数值 meminfo$(cat /proc/meminfo) total$(echo $meminfo | awk /^MemTotal:/ {print $2}) avail$(echo $meminfo | awk /^MemAvailable:/ {print $2}) swap_total$(echo $meminfo | awk /^SwapTotal:/ {print $2}) swap_free$(echo $meminfo | awk /^SwapFree:/ {print $2}) if [ -z $avail ]; then # 老内核没有 MemAvailable退化为 MemFree 可回收部分 avail$(echo $meminfo | awk /^MemFree:/ {f$2} /^Cached:/ {c$2} /^SReclaimable:/ {s$2} END {print f int((cs)*0.8)}) fi pct$(awk -v a$avail -v t$total BEGIN{ printf %.1f, a*100/t }) echo MemTotal: $((total/1024)) MB echo MemAvailable: $((avail/1024)) MB echo Available pct: ${pct}% if [ $swap_total -gt 0 ]; then echo SwapFree pct: $(awk -v f$swap_free -v t$swap_total BEGIN{printf %.1f, f*100/t})% fi awk -v p$pct BEGIN{ if (p0 15) exit 2; else if (p0 25) exit 1; else exit 0 }退出码 2 表示告警、1 表示预警、0 表示正常直接塞进监控系统就行。这套写法的好处是零依赖任何发行版、任何精简镜像都能跑。6.2 阈值到底定多少才合适这是个没有标准答案但有很多错误答案的问题。我的经验是分场景定场景建议阈值理由通用应用服务器available 15%留足突发余量数据库 / 缓存服务available 25%这类服务对内存回收敏感反应要早容器有限额anonslab_unreclaim 限额的 85%别用 memory.current 做阈值JVM 服务结合堆使用率和 RSS 双看堆满不一定是问题RSS 超才是再强调一次容器里绝对不要用宿主机的 available 做阈值。我见过监控系统在容器里采集MemAvailable一台 31G 的宿主机上所有 512M 的容器共享同一个内存指标结果要么全部不告警要么全部一起告警完全失去了监控意义。容器必须读 cgroup 文件。阈值定好之后还要加个持续时间条件。瞬时跌到 15% 以下但一秒后恢复的情况太常见了只有连续三到五个采样点都在阈值以下才值得发告警。这条规则能把无效告警砍掉七八成。6.3 采集长期趋势比盯实时数据更有价值实时数据解决现在有没有问题趋势数据解决下周会不会有问题。我一般让机器自己记# 每 5 分钟记一条保留最近 7 天 */5 * * * * /usr/bin/awk /^MemAvailable:/{print strftime(%F %T), $2/1024 MB} /proc/meminfo /var/log/mem_avail.log # 每天凌晨截断只留 7 天 0 0 * * * find /var/log/mem_avail.log -mtime 7 -delete用sar也行前提是装了sysstat并且启用了采集sar -r 1 5 # 实时看内存 sar -r -f /var/log/sa/sa15 # 看某天的历史有了趋势数据很多问题会变得非常直观。比如某服务每周重启一次、内存就在这一周里缓慢爬升你把七天曲线拉出来一眼就看到锯齿形状泄漏问题当场坐实。反过来如果曲线是一条平稳的线那这次内存告警就是误报可以放心地把阈值调一调。我自己的习惯是每月抽一次时间把所有生产机器的MemAvailable趋势图过一遍专门找那种看着平稳但整体缓慢下移的机器。这类机器不会告警能撑几个月甚至半年但它们最终一定会出问题而且往往是在你最不方便处理的时候。提前发现处理起来就是一次普通的变更窗口。最后分享一个判断小技巧当有人跟你说这台机器内存泄漏了你先让他跑一次free -h再跑一次free -w然后看SUnreclaim这一项。如果SUnreclaim在几个小时内持续单边上涨那才是真泄漏方向要往内核模块或者文件系统那边找如果SUnreclaim稳定、涨的只是Cached那通常不是泄漏只是某个定时任务在反复读文件缓存正常积累而已。这个区分能省掉大量走弯路的排查时间。