ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Linux内存排查实战:从free、vmstat到OOM现场定位

Linux内存排查实战:从free、vmstat到OOM现场定位 前段时间有台服务器被同事拉去排查现象是内存使用率一直卡在 92% 左右free 命令敲下去available 只剩不到 2G。当场大家都觉得是内存不够了准备加机器。但我在 /proc/meminfo 里翻了几个字段又看了几分钟的 vmstat发现真正的问题根本不是不够用而是某个服务的匿名页AnonPages一直在涨Cached 反而很健康。最后定位到一个循环里不断 append 的切片改了代码内存就稳了。这让我想到一个挺普遍的问题内存管理相关的命令行大家多少都会用但大多数时候只是会敲 free、top、ps遇到异常读数却解释不了。这篇文章我想从头捋一遍自己平时排查内存问题时真正会用的命令从底层数据源到日常观测从定位进程到追踪泄漏再到 OOM 现场的分析。内容适合刚接触系统运维的开发者也适合那些已经用过一段时间命令、但还停留在会敲不会读阶段的同行。读完你至少能回答一个问题当内存指标不对劲时下一步该看什么。1. 别急着敲命令先弄清内存指标从哪来1.1 /proc/meminfo 是所有内存命令的数据底座先说一个很多人忽略的事实free、top、ps 这些命令显示的物理内存数据几乎全部来自内核的 /proc/meminfo 和 /proc/ /statm、/proc/ /status。你敲 free -h 得到的结果本质上是内核把一组计数器的值格式化了一下。所以遇到内存指标对不上、命令之间互相矛盾的情况最可靠的做法是直接去看原始数据源。cat /proc/meminfo输出会有一长串字段排在前面的几个最关键MemTotal物理内存总量。MemFree完全没被使用的页帧数量。MemAvailable估算的还能分给新程序的实际可用内存这是比 MemFree 真实得多的指标。Buffers块设备、文件系统元数据占用的缓冲。Cached页面缓存也就是文件内容在内存里的副本。SwapTotal / SwapFree交换分区或交换文件的总量和剩余量。Dirty还没写回磁盘的脏页数量。Writeback正在写回磁盘的页。AnonPages匿名页常见于进程堆、栈以及私有映射。Mapped映射到进程地址空间的文件页。PageTables页表本身占用的内存进程越多、虚拟地址空间越大这个值越高。Slab内核对象缓存分 SReclaimable 和 SUnreclaim 两类。排查时我习惯先把 MemTotal、MemFree、MemAvailable、Buffers、Cached、Dirty、AnonPages 这七个字段抄下来再和 free 的结果对比。比如 free 第一行的 used 并不是物理内存真的被占满了而是 MemTotal 减去 MemFree 和 Buffers/Cached 后的中间值。真正的压力由 MemAvailable 体现它考虑了可回收缓存。1.2 用户态命令与内核态记账的偏差从哪来很多人在 top 里看到某个进程 RES 占用很大就断定是它泄漏了。其实 RES 是进程的页表中实际映射到物理内存的部分包含私有匿名页、文件映射的驻留部分、共享内存库等。多个进程共享同一份 so 库时这部分内存会计入每个进程的 RES但物理内存只存了一份。类似地ps 里的 %MEM 是基于 RES 和 MemTotal 计算的直观但也会被共享内存干扰。页表本身也在消耗内存。进程越多、虚拟地址空间越复杂PageTables 就越高。我在一台跑了三百多个容器的机器上见过 PageTables 超过 2G 的情况这不一定是泄漏但确实是实实在在的内存开销。理解记账方式比理解命令本身更重要free 看的是全局物理页帧ps 看的是每个进程的页表统计pmap 看的是进程地址空间布局。三者各管一段不能粗暴比较。1.3 操作系统课程里的内存管理与命令行怎么对应如果你是准备 408 或刚学完操作系统原理再看这些命令会发现课本里的知识点其实全都能映射到命令行上。伙伴系统维护物理页帧的连续块cat /proc/buddyinfo 能看到各阶空闲块的分布碎片严重时这个文件里低阶块很多、高阶块很少。slab 分配器缓存内核对象/proc/slabinfo 可以看到每个 slab 缓存的对象数量和内存占用。页面置换算法作用于物理页帧与 swap 之间vmstat 的 si/so 就是换入换出的实时速率。缺页异常尤其是主缺页major fault可以通过 ps -o majflt 看累计次数也可以用 perf 追踪。建议把命令当作验证原理的工具来学而不是背参数。理解了伙伴系统你就知道为什么 MemFree 还有几百兆但分配大块内存还是失败理解了 slab你就知道为什么 free 里可用内存不高时先去看 Slab 是否异常。2. free/vmstat/top 的日常读法什么场景看哪张表指标分别是什么意思2.1 free 的读法盯着 available而不是 freefree 是上手最快的命令但也是最容易被误读的。free -h free -m free -s 5-h 是人性化显示-m 固定以 MB 为单位方便脚本处理-s 5 表示每 5 秒刷新一次。典型输出是这样的total used free shared buff/cache available Mem: 31Gi 12Gi 4.5Gi 245Mi 15Gi 17Gi Swap: 31Gi 1.2Gi 30Gi很多人看到 used 12G、buff/cache 15G 会慌以为内存不够。其实 buff/cache 里绝大部分是页面缓存内核在物理内存不紧张时会把空闲内存拿去做文件缓存一旦有进程需要分配内存这部分可以快速回收。所以对可用内存的判断只看 MemAvailable。系统没有内存压力时buff/cache 偏大是健康状态。真正需要警惕的是两种现象一是 buff/cache 不大但 available 持续走低说明匿名页占用太高大概率有进程在频繁分配且不释放二是 Swap 里 used 持续占用且 si/so 频繁跳动说明物理内存已经吃紧内核在来回倒腾。顺便说一句buff 和 cache 是有区别的。buff 是块设备缓冲区等着写盘的数据会在那里排队cache 是文件内容缓存读过的文件页会留在那里加速下次访问。虽然现在内核里两者边界越来越模糊但排查写盘问题时Dirty 和 Writeback 字段更有参考价值。2.2 vmstat把内存问题变成时间序列free 适合拍快照但内存问题往往藏在变化趋势里。这时候用 vmstat 采样。vmstat 1 5输出字段里和内存强相关的是 swpd、free、buff、cache、si、so其次是 io 的 bi/bo 和 cpu 的 us/sy/wa。si 是从 swap 换入数据的速度so 是换出的速度。这两个值只在持续大于 0 时需要警惕。短时间出现几次 si/so 不一定是坏事可能是进程冷启动时发生过换入但如果持续几秒钟都在跳动说明物理内存长期不足系统在做无意义的交换。我一般这样用 vmstat在出现告警的窗口里连续执行 vmstat 1 30把输出重定向到文件然后观察 free 列的下降趋势、cache 列是否在被回收、si/so 是否出现周期性波动。这比单看一次 top 有价值得多。你甚至可以理解为free 是体检时的一张静态照片vmstat 是心电图的连续波形。照片能看出有没有问题波形能看出问题正在怎么发展。2.3 top/htop交互式进程内存观察top 是定位哪个进程在吃内存的入口。top -o %MEM这样启动会直接按内存占用排序。也可以用 htop交互体验更好F6 排序、F4 过滤都很顺手。top 里每个进程有几个内存列要分清VIRT进程虚拟地址空间大小。这个值大不代表占物理内存多可能是映射了很大的文件或者申请了很大的地址段但没实际访问。RES驻留内存就是真正占用物理内存的页。但注意它把共享库也计进来了。SHR共享内存大小包含共享库、共享内存段、tmpfs 文件等。%MEMRES 除以物理内存总量。线上排查时最误导人的就是 VIRT。见过有人拿着 VIRT 50G 的进程说它泄漏了结果 pmap 一看大部分是共享库映射RES 只有 400M。判断进程内存是否异常要以 RES 为准异常增长的参考依据是 RES 的走势而不是 VIRT。2.4 一个小实验自己验证 cache 是怎么涨上去又还回来的对 buff/cache 的理解光看文档容易飘建议实际做一次小实验。在一台测试机上先记录 free 的 cached 值然后用 dd 读一个 1G 的文件dd if/tmp/bigfile of/dev/null bs1M count1024 free -h读完后 cached 会明显上升因为文件内容被留在了页面缓存里。再执行一次同样的 dd第二次耗时通常会明显减少这就是缓存命中。然后观察如果此时启动一个大的 Java 程序内核会主动回收一部分页面缓存来满足匿名页需求cached 会下降。这个过程完美对应了操作系统课程里页缓存可回收的概念也让 free 里 buff/cache 大的疑问彻底打消。3. 定位谁吃掉了内存从 ps 到 pmap 再到 smem 的完整追查链路3.1 ps 排序先找出排名靠前的进程遇到内存告警时我不会盯着 free 一直看第一步永远是定位进程。ps aux --sort-%mem | head -20 ps -e -o pid,ppid,rss,vsz,comm --sort-rss | head -20第二条命令按 RSS 排序输出直接以 KB 为单位适合脚本做快照。要注意的是ps 显示的是一个瞬间值排查内存持续增长的问题时需要隔一段时间取两次样本。比如每 10 分钟执行一次上面的命令并把结果存到文件对比前后差异看哪个进程的 RSS 在稳定攀升。那种 15 秒涨 200M、半小时翻一倍的进程基本可以锁定为怀疑对象。还需要注意多进程架构的服务。Nginx、Gunicorn 这类会 fork 出很多 worker 的进程ps 结果会显示一长串同名进程RSS 加起来才是整体占用。但如果每个 worker 的 RSS 都很大往往不是 worker 的问题而是公共模块或公共连接池导致 COW 失效、每份都复制了一份。3.2 pmap看进程内部的内存地图找到嫌疑进程后下一步是看它内部的内存分布。pmap 是这里的主角。pmap -x pid输出会按地址列出进程的每一段映射并显示映射类型。重点关注三类映射heap 段进程运行时动态分配的 C 堆内存。如果 heap 段异常增长大概率是 malloc 之后没有 free或者 C 的 new/delete 不配对。大量匿名的私有映射某些运行时或中间件会通过 mmap 匿名映射申请大块内存这同样计入 RES。mapped file文件映射正常是只读的 so 库和资源文件。如果某些文件映射占几百 M 且持续增长可能是 mmap 了文件之后没有正确 munmap。pmap 的另一个作用是对比。同一个服务在不同时间点的 pmap 输出如果某个映射段的大小一直在涨追查范围就缩小到哪段内存了。我在定位一个 Go 服务的内存问题时就是通过两次 pmap -x 对比发现是 mmap 区段不断扩大最终定位到代码里频繁创建临时文件并建立映射的路径。3.3 smem把共享内存的分摊算清楚ps 和 top 把共享库计入每个进程的 RES这在分析总内存到底被谁占着时会产生误导。这时候需要 smem。smem -t -k -s rss smem -u apache smem -P ^nginx -t -ksmem 会计算三个指标RSS驻留内存包含共享部分。PSS按比例分摊后的物理内存把共享库按引用进程数均分。这个值更接近该进程对物理内存的真实消耗。USS唯一内存也就是进程独享的部分不包含任何共享页。实际排查中如果要找泄漏看 USS 更准。因为泄漏的内存通常是自己独占的匿名页不会出现在共享库里。如果要评估容量、计算还能不能加服务看 PSS把共享内存的分摊考虑进去才会算得准。有一次我看一个服务 RSS 显示 1.8G但 smem 里 PSS 才 1.2GUSS 才 900M原因就是它 load 了大量公共 so这类虚胖现象很常见。3.4 一次模拟追查从 top 到 ps 再到 pmap 的完整动作假设你收到告警free 显示 available 不断下跌。我的排查顺序是这样的先 top -o %MEM 拍快照找到一个 RSS 300M 且稳居第一的 Java 进程再用 ps -o rss,vsz,comm -p 确认它不是瞬时波动接着用 pmap -x 看映射段发现 anon 段占了 250M然后用 smem -p 看 PSS 与 USS确认其中的共享部分不多最后用 jstat 看堆内和堆外。这套组合下来问题窗口从整台机器缩小到单个进程再缩小到某类内存区域距离根因就不远了。很多人在第一步就停止排查直接 top 看到某个进程 RES 高就下结论。其实 RES 高只是表象你要证明的是它真的需要这么多内存还是只是在页表里看起来很大。pmap 和 smem 就是用来回答这两个问题的工具。4. 泄漏、越界与堆外分配valgrind、gdb、jstat 这类调测命令怎么配合用4.1 C/C 场景valgrind 是兜底命令如果你能复现问题且代码是 C/Cvalgrind 是排查内存泄漏最省事的工具。valgrind --leak-checkfull --show-leak-kindsall ./your_program输出里最需要关注的是 definitely lost 和 indirectly lost这两类基本可以确认是泄漏。still reachable 需要小心判断它表示内存块仍有指针指向、没有被释放但程序退出时依然存活。全局指针变量或常驻单例持有的一块内存会显示为 still reachable这不一定是 bug需要结合业务判断。possible lost 则可能是程序以非法方式操作了内存块让 valgrind 无法确定指针关系。valgrind 输出会给出泄漏内存的分配调用栈例如1234 40 bytes in 1 blocks are definitely lost in loss record 1 of 3 1234 at 0x483B7F3: malloc 1234 by 0x401156: main (test.c:6)顺着调用栈就能定位到具体源码位置。valgrind 的代价是程序运行速度会慢几十倍不适合直接压测适合在测试环境用固定用例复现。如果服务长期运行才泄漏不适合直接上 valgrind可以先用 gdb 附着到进程或者用 malloc 钩子做内存统计。4.2 不重启的现场排查/proc/ /status 与 gdb attach线上环境不能随便重启也不能轻易上 valgrind。这时候可以用 procfs 配合 gdb。cat /proc/pid/status重点关注 VmPeak、VmSize、VmRSS、VmData。VmPeak 是进程启动以来虚拟内存的峰值如果 VmPeak 远大于 VmSize说明之前申请过很大一段虚拟地址空间但已经释放了这本身不是泄漏如果 VmSize 不断增长且 VmRSS 同步增长才是真问题。gdb attach 适合看线程运行状态和堆内存的大致分布。gdb -p pid (gdb) thread apply all btthread apply all bt 会把所有线程的调用栈打出来能看是否卡在某个频繁分配内存的路径上。gdb 有交互式风险attach 会暂停进程一小会纯线上高并发环境需要谨慎最好在低峰期操作或者直接放弃改用更轻量的工具。我见过有人线上 gdb attach 后忘记 detach进程挂起几十分钟的教训很深。4.3 Java/JVM 场景jstat 与 jmap 看堆内外分配JVM 进程的内存问题经常被误判为Java 占太多。其实 Java 进程的常驻内存包含堆内、堆外、元空间、线程栈、JIT 编译产物、DirectByteBuffer 等。只看 top 的 RES 无法区分这些需要配合 JVM 自带命令。jstat -gcutil pid 1000 jmap -heap pidjstat -gcutil 每秒输出一次 GC 各区域的使用率能看到 Eden、Old、Metaspace 的使用情况和 GC 次数。如果 Old 使用率持续高位且 FGC 在增加说明堆内存压力大可能需要调 -Xmx 或查对象泄漏。jmap -heap 可以打印完整的堆配置和当前各代内存使用。另一个常见问题是 DirectByteBuffer 使用的堆外内存它在 NIO、Netty、gRPC 里很常见。这类内存不在 JVM 堆内jmap 看不到但会真实占用物理内存并计入 RES。排查时要看进程的 RES 与堆内存之和的差异差异过大优先怀疑堆外。4.4 语言层分配统计Julia 优化与 Python 的 tracemalloc热词里提到 Julia 性能优化与内存管理其实 Julia 的命令行里也有专门看内存分配的入口。在 Julia 里用 time 跑一段代码它会返回分配了多少内存用 allocated 可以精确拿到某段表达式的分配字节数加上 code_warntype 能看到是不是因为类型不稳定导致大量装箱分配。Julia 的性能优化很多时候不是算力问题而是大量小对象分配带来的 GC 压力time 的 allocation 字段就是首要排查对象。Python 的场景更容易直接用内置的 tracemalloc 就可以python -m tracemalloc --trace-start -o /tmp/tracemalloc.log your_script.pytracemalloc 能统计各调用点的内存分配量并排序适合定位脚本内存越跑越大的问题不用额外装任何库。无论是 Julia 还是 Python思路和前面是相通的先看总量涨不涨再看是哪个调用点分的最后改代码减少分配。4.5 strace 看 mmap 和 brk底层分配的系统调用视角如果到了这一步还没定位到可以看系统调用层。strace -f -e tracemmap,brk,munmap -p pidstrace 会打印进程每次调整堆、建立内存映射的系统调用。mmap 对应的往往是较大块的分配或文件映射brk 对应的是传统堆的伸展。如果看到某个线程反复 mmap 大量内存但很少 munmap那泄漏路径基本就锁定了。注意 strace 对性能影响极大生产环境只能短时间采样不能长时间开着。5. OOM 前后dmesg、PSI、oom_score 联合判读把内存事故变成可量化数据5.1 dmesg 里的 OOM 记录系统已经把谁杀了内存不足到一定程度内核的 OOM killer 会主动杀进程。这时候不要只盯 free应该第一时间看内核日志。dmesg -T | grep -i -E out of memory|killed process典型的信息像这样Out of memory: Killed process 1234 (java) total-vm:1234567kB, anon-rss:45678kB, file-rss:32kB, shmem-rss:0kB这个输出比 top 更可信因为它记录了进程被杀那一刻的准确内存状态。anon-rss 高说明匿名页吃掉大量物理内存file-rss 低说明页面缓存不是元凶。同一时刻往往伴随其他进程的日志可以结合时间点判断是哪个任务触发了内存申请高峰。还有一个重要习惯平时没事的时候记录一下 dmesg 的基线出问题时才有对比。很多服务器默认不持久化内核日志重启后 dmesg 就丢了。建议把 /var/log/kern.log 的持久化开起来否则出现 OOM 后一重启现场就没了。5.2 /proc/pressure/memory在 OOM 之前提前发现内存压力OOM 是结果压力是过程。Linux 4.20 以后的版本提供了 PSIPressure Stall Information可以提前感知系统是否处于内存饥饿状态。cat /proc/pressure/memory输出类似于some avg102.31 avg601.08 avg3000.72 total1832367 full avg100.15 avg600.08 avg3000.03 total48213some 表示至少有一个任务因内存而阻塞的时间比例full 表示所有任务都被阻塞的时间比例。avg10 是最近 10 秒的滑动平均值。如果 some avg10 持续大于 10说明系统经常有任务等内存需要关注如果 full 也开始有数值说明发生过全面停顿影响会很大。排查时我习惯把 PSI 的数值变化和 vmstat 的 si/so 结合看两者同时异常基本可以确认内存已经形成瓶颈。5.3 oom_score、oom_score_adj 与 cgroup 限制OOM killer 不是随机杀进程它会给每个进程打分分高的先杀。cat /proc/pid/oom_score echo -500 /proc/pid/oom_score_adjoom_score 的值受进程占用内存比例、运行时间、优先级等因素影响。占用内存越大的进程分数越高。如果想保护某个关键进程可以用 oom_score_adj 调低它的分数反过来也可以调高某些低价值进程的分数让内核优先杀它们。在容器环境中cgroup 对内存的限制更常见容器的 memory.max 如果设置过低会出现容器内进程被 OOM 杀掉但宿主机 dmesg 里没有明显记录的情况这时要去容器的 memory.events 里看cat /sys/fs/cgroup/memory.max cat /sys/fs/cgroup/memory.eventsmemory.events 里的 oom_kill 和 oom_group_kill 字段能看出容器内发生过多少次 OOM 击杀这是容器环境排查内存问题时最容易被忽略的一个命令。5.4 swap 与 swappiness内存回收方向上的最后一个旋钮最后提一下 vm.swappiness。sysctl vm.swappiness sysctl -w vm.swappiness10swappiness 控制内核在回收内存时对匿名页和文件页的偏好。值越大越倾向于把匿名页换出到 swap值越小越倾向于回收文件页缓存。很多生产服务器把 swappiness 调到 10 甚至 0目的是减少 swap 带来的延迟。但要注意不是设置了 swappiness 就完全不会用 swap。如果内存压力持续太久内核照样会写 swap。调整这个参数只是改变倾向不是禁用 swap。理解它能帮你回答一个经典问题明明物理内存还剩几个 G为什么 Swap 里已经有数据了因为内核根据 swappiness 提前把部分不太活跃的匿名页换出去了这可能是正常的。完整的 OOM 问题分析应该是这样的链路先确认是不是真的 OOM看 dmesg 和 cgroup 的 memory.events再看是哪个 cgroup、哪个进程被杀根据 oom_score 和它的内存占用确认合理性然后看 PSI 在 OOM 之前是否已经出现持续压力配合 vmstat 和 free 判断是整体不足还是某个进程失控最后落到代码层修复。这套流程跑完内存事故就不再是一句内存不够加机器的猜测而是有数据支撑的结论。我个人在实际排查中最大的体会是内存管理命令的难点从来不是参数而是指标之间的关系。每次拿到一组内存数据先问自己三个问题这个数字是从哪个文件读出来的它统计的是页表、物理页帧还是进程地址空间它和另一个指标之间是包含关系还是因果关系三个问题想清楚命令本身只是顺手的事。如果你想练手不用去找复杂的系统就拿一台负载普通的 Linux 机器把 /proc/meminfo、vmstat、ps、pmap、dmesg 这五个命令每天各打一次快照坚持一周再回头看这些数字它们之间的关联就会清晰得多。
返回列表