ARTICLE DETAIL

资讯详情

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

Linux内存诊断实战:从free到slab揪出隐形内存怪兽

Linux内存诊断实战:从free到slab揪出隐形内存怪兽 凌晨三点被内存告警吵醒这事儿干运维的应该都不陌生。我爬起来看了一眼监控面板内存使用率 95%剩余不到 2G。结果登录服务器跑free -h一看used 才占了一半大部分是 buff/cache再跑top看进程没有一个进程的 RSS 特别夸张。是不是很像见了鬼内存这种资源跟 CPU 不一样CPU 使用率飙高你一眼就能看到是哪个进程在作妖内存则是多本账叠在一起进程私有内存、共享内存、page cache、内核 slab、页表、tmpfs……单看任何一本账都找不到真凶。这是性能诊断系列的第二篇上一篇我们聊了 CPU 诊断这一篇就集中收拾内存问题。文章不会停留在“内存不够就加内存”这种层面而是带你从原理到命令、从现象到案例一步步把吃掉内存的那个“隐形怪兽”揪出来。不管是刚入门的新手还是已经在排查路上绕了好几圈的老手这篇都值得收藏慢慢看。1. 先搞清楚内存到底被谁吃掉了1.1 内存诊断的第一性原理要揪出内存问题先把内存分配这件事拆开。我们平时说的“内存使用率”往往是把物理内存当成一个大水池但池子里其实分了好几层用户态进程的匿名内存、内核态对象的 slab 缓存、文件系统用的 page cache、共享内存段、页表本身……这些每一块都可能吃内存而且它们的“可回收性”完全不同。从内核视角看物理内存页分为两类可回收页和不可回收页。page cache 属于可回收页系统内存紧张时会自动写回并释放而进程的匿名内存、内核 slab 中不可回收的部分属于不可回收页内存压力大时只能靠 swap 或者直接杀进程。理解了这一点你再看free命令的输出就不会一头雾水了。free命令展示的字段不同版本和不同系统上显示维度有差异但核心逻辑是一致的total物理内存总量used已被分配的内存含不可回收部分free完全空闲的物理内存sharedtmpfs 等共享内存占用量buff/cache文件缓存、块设备缓冲等可回收内存available在不触发 swap 的前提下还可以分配给进程的估算值很多人习惯盯着 used 和 free 看这是最大的误区。used 高不代表内存不够只要 available 还有余量系统就能正常服务。反过来free 接近 0 也不代表危险page cache 吃掉大量内存是正常现象Linux 的设计就是这样用尽量多的空闲内存来做缓存。1.2 常见的内存消耗类型我习惯把内存消耗分成以下几类每一类都对应不同的排查手段进程匿名内存anon进程堆、栈分配的内存不可回收属于真正“吃”内存的东西。文件缓存page cache buffer读文件、写文件时产生的缓存可回收内存不够时内核会自动清理。内核对象缓存slab内核为 dentry、inode、kmalloc 对象等维护的缓存部分可回收部分不可回收。共享内存tmpfs、SysV shm/dev/shm、ipcs -m创建的共享内存段本质占用物理内存且一般不自动释放。页表page table每个进程维护虚拟地址到物理地址映射的元数据进程数量多、虚拟内存大时页表也能吃掉不少内存。设备映射和显存映射GPU、DMA 等映射到物理内存的区域这类在服务器上常见于 GPU 计算场景。把内存分类这一步做好了后面所有的排查命令才有意义。比如你看到 page cache 占了 80G你完全可以淡定但如果你看到 slab 不可回收部分一直在涨那就要警惕内核模块或者文件系统层出的问题。我见过不少人一看到内存高就直接清 cache这在生产环境属于典型的治标不治本页缓存本来就是用来提升文件读写性能的你把它清了下次请求又要重新读盘业务反而变慢。2. 自上而下的快速定位法2.1 第一站free 与 /proc/meminfo接到内存告警第一件事不是直接 top而是先看free和/proc/meminfo把内存的“大账”拉出来。free -h我一般会关心 available 这一列而不是 used。available 是内核综合考虑 page cache 可回收性、slab 可回收性、内存水位等因素后估算出的“还能安全分配多少内存”的数值。如果 available 已经很低说明系统真的逼近内存极限了。接着看/proc/meminfo里的细节cat /proc/meminfo重点看这些字段MemTotal物理内存总量MemFree完全空闲的内存MemAvailable可用内存估算值Buffers块设备缓冲Cached文件缓存页SwapTotal / SwapFree交换空间总量和剩余Active / Inactive活跃和不活跃的匿名页与文件页Slab内核 slab 缓存总量SReclaimable 和 SUnreclaim 分开看Shmem共享内存tmpfs占用MemAvailable 这个值非常关键它不像 free 那样容易误判。举个例子如果 MemFree 只有 200M但 Cached 有 50GMemAvailable 可能是 40G说明系统虽然空闲内存少但可回收缓存充足短期内存压力不大反过来如果 MemAvailable 只有几百 M即使 MemFree 还有 2G也要小心了因为系统可能随时进入内存回收甚至 OOM 状态。2.2 top 与 htop 的组合拳确认整体内存水位之后下一步是看进程级别的内存占用。top是最常用的工具但很多人没抓住重点。top -o %MEM-o %MEM让 top 直接按内存占用降序排列省去交互按 M 的步骤。在 top 的进程列表里有几个关键字段VIRT进程虚拟内存总量包含共享库、映射文件、堆栈的虚拟地址空间不是真实物理内存占用看看就好。RES驻留内存即进程当前实际占用的物理内存包含匿名页和文件页。SHR共享内存的大小表示进程映射的共享内存和共享库的物理页。注意RES 是一个被高估的指标。两个进程如果同时映射了同一个 10G 的共享内存段ps和top里每个进程的 RES 都会显示 10G但物理内存实际只消耗 10G不是 20G。这就是共享内存导致的“重复计数”问题。如果你习惯用 htop操作更直观按 F6 选择 sort by再选 PERCENT_MEM 或 M_RESIDENT就能按内存排序。htop 还会用颜色把内存条分成 used、shared、buffers、cache 等一眼就能看出当前内存结构。实战中我遇到最多的情况是top 里所有进程的 RES 加起来不到 40Gfree显示 used 却有 70G那多出来的 30G 去哪了这个时候就要进入下一层去检查进程 RSS 之外的东西内核 slab、共享内存段、page cache。这也是我自己总结的一个排查原则先在“进程账本”里找答案找不到就立刻跳到“内核账本”和“文件系统账本”。3. 揪出隐形怪兽的三大手段3.1 用 ps 把每个进程的账本翻出来top 适合交互式观察但要快速导出数据、留作记录ps更顺手。ps aux --sort-%mem | head -20这个命令按内存占用从高到低列出前 20 个进程%MEM表示该进程 RSS 占物理内存的百分比。实际生产中我会多跑一条ps -eo pid,ppid,user,rss,vsz,args --sort-rss | head -30把 RSS 以 KB 为单位排出来进程参数也带上方便判断这个进程是做什么的。这里有一个细节rss是 KB不是字节在脚本里做阈值判断时要换算别踩坑。用 ps 排查时真正要警惕的不是那些 RES 高的进程而是那些 RES 不算高但虚拟内存 VIRT 高得离谱的进程。比如 Java 进程的 VIRT 轻松上几十 G这是 JVM 预留的堆外内存和直接内存不一定真的占用物理内存但如果是 C/C 程序 VIRT 持续增长就要考虑是否存在内存映射泄漏比如反复 mmap 没有 unmap。3.2 smem 与 RSS 的真相我在前面提过RSS 会把共享内存重复计算。要拿到更真实的内存占用推荐配合smem工具使用。smem -t -k -s uss | head -30smem 会输出三列关键数据USSUnique Set Size进程独占的物理内存、PSSProportional Set Size按比例分摊共享页后的进程内存、RSS。判断真实内存消耗优先看 USS 和 PSS。比如一个进程 RSS 显示 10G但 PSS 可能只有 2G说明绝大部分是共享映射不是你该盯的“怪兽”。典型的例子Java 的 ParallelGC、G1 都使用了大量共享内存映射使用 jemalloc 或 tcmalloc 的高并发服务也可能出现单个进程 RSS 虚高的情况。用smem -m还能查看按映射分类的内存占用比如哪些文件映射占了多少内存哪些匿名映射占了多少。如果没有条件安装 smem也可以用procfs的手工方式估算 PSS读取/proc/pid/smaps或/proc/pid/smaps_rollup把每个映射的 Private 和 Shared 按比例算出来。不过这个方式比较繁琐我还是推荐直接用 smem。3.3 OOM 记录与内核日志有时候你还没开始排查系统就已经帮你干掉了肇事进程——OOM Killer 出手了。OOM Killer 是内核在内存严重不足时根据评分选择杀掉一个或几个进程来释放内存的机制。很多运维第一次遇到服务突然消失都不知道是被内核杀的。查 OOM 记录的方法dmesg -T | grep -i -E oom|killed process | tail -50或者用 journalctljournalctl -k -b | grep -i -E oom|out of memory | tail -50输出里会看到类似这样的一行Out of memory: Killed process 12345 (java) total-vm:4065728kB, anon-rss:3104520kB, file-rss:132kB这几项信息很有用被杀的 PID 和进程名、虚拟内存、匿名 RSS、文件页 RSS。如果被杀的进程不是你预期中的内存大户而是某个不重要的小进程说明 OOM 评分机制认为它“该死”触发原因可能是它的 oom_score 太高也可能是内存 cgroup 限制导致。这里提醒一句千万别在生产环境直接echo f /proc/sysrq-trigger来模拟内存压力触发 OOM这种操作会让整个系统崩溃重启造成二次事故。应该用systemd的MemoryMax或者 cgroup v2 的memory.high在容器或测试机里复现。4. 内核态的内存开销不可忽视4.1 slab 与 kmem 的排查进程消耗是内存账本里最显眼的一页但很多“隐形怪兽”藏在内核态。最常见的就是 slab。slab 是内核为了复用对象而维护的缓存机制简单理解就是内核自己搞了个对象池避免频繁分配和释放内核数据结构。排查 slab 占用cat /proc/meminfo | grep -i slab重点关注两个值SReclaimable可回收的 slab比如 dentry cache、inode cache和 SUnreclaim不可回收的 slab。SUnreclaim 长期居高不下通常意味着内核模块或者驱动存在对象泄漏比如某些网卡驱动、存储驱动的 DMA 缓冲区未正确释放。再进一步看 slab 里到底谁是占内存的大头slabtop -s c-s c表示按 cache 大小排序。常见的占用大户是dentry和inode这两个是文件系统路径和文件元数据的缓存。如果 dentry 占用极高说明你的系统在反复访问大量小文件文件创建和删除频繁。这种情况下可以适当调低vfs_cache_pressure的默认值吗实际上如果 dentry/inode 占用太高调高vfs_cache_pressure比如从默认 100 调到 150会让内核更积极地回收这些缓存但副作用是文件系统路径解析的性能下降。我个人的建议是先找根因比如某个日志组件每次写入都创建临时文件这才是根本问题缓存的参数调整只能救急。4.2 页表与内存碎片页表是另一个容易被忽略的内存消费者。每个进程都有自己的页表用来维护虚拟地址到物理地址的映射。页表本身也要占内存规律是进程虚拟内存越大VMA 越多、页表项越多页表占用越高。查每个进程的页表大小grep VmPTE /proc/pid/status一个 PTE 通常是 8 字节映射一个 4KB 的物理页。如果进程的 RSS 是 40G那么页表大约需要40G / 4K * 8 80MB看着不大但如果服务器上跑了 100 个这样的进程页表就要吃掉 8G。这种场景在 Java 微服务的 K8s 节点上很常见——每个 Pod 里都是大堆 JVM页表总量很容易被忽视。内存碎片也要提一句。虽然现代内核的 buddy allocator 会努力做内存规整但在长时间运行、频繁分配和释放不同阶数内存页的服务器上/proc/buddyinfo里高阶连续内存块可能长期不足。这会导致即使系统还有大量空闲内存内核也无法分配多个连续的物理页进而触发内存回收甚至报错。你可以用以下命令观察cat /proc/buddyinfo如果Node 0的高阶块比如 order 10长期接近 0说明碎片化比较严重。不过这事一般不需要普通业务运维干预内核的 compact 机制会自动处理部分场景真要优化通常得靠调整 THP透明大页策略、避免内存碎片化太严重的分配模式。4.3 tmpfs 与共享内存tmpfs 是把内存当作文件系统来用的典型案例最典型的就是/dev/shm。默认情况下/dev/shm大小是物理内存的一半很多程序会自动往里面写临时文件比如某些数据库、消息队列、浏览器写多了物理内存就被吃了。查 tmpfs 占用非常简单df -h /dev/shm find /dev/shm -type f -exec ls -lh {} \; | sort -k5 -h | tail如果是 SysV 共享内存用ipcs -m查看ipcs -m看 nattach挂载进程数和 cpid创建进程 PID定位是哪个进程创建的共享内存段。共享内存的特点是不会自动释放即使创建它的进程退出了只要内核中的 shm 对象没被删除内存就继续被占着。我处理过一个真实案例某个消息队列组件的 worker 进程崩溃后残留的共享内存段没人清理free -h里 used 一直偏高进程列表却看不出异常。最后用ipcs -m找到残留段确认业务上没人再用之后用ipcrm -m shmid手动清理内存才降下来。5. 实战案例一次内存泄漏的完整追踪5.1 现象与初步判断光讲理论不过瘾我拿一个真实处理过的内存泄漏案例来完整走一遍排查流程。场景是一个 C 写的长连接网关部署在 8C16G 的机器上运行一周后内存使用率从 40% 慢慢爬到 95%重启进程后又会好两天反复循环。第一阶段的排查是这样做的先用free -h确认整体内存水位再用ps aux --sort-%mem | head看哪些进程内存高发现网关主进程 RSS 从占用 2G 涨到了 9G其他进程都正常然后用top -p pid持续观察心跳正常但 RSS 每分钟涨几十 MB。到这里基本可以断定是进程级内存泄漏不是系统级问题。接下来要回答的是这个进程里的内存到底漏在哪里。5.2 用 valgrind 和 jemalloc 的 heap profile 定位定位内存泄漏第一反应是 valgrind。但生产环境跑 valgrind 会让程序慢 20 到 50 倍根本没法直接上线上跑。所以我的做法是在测试环境用同样的流量模型复现再上 valgrind。valgrind --toolmemcheck --leak-checkfull --show-leak-kindsall --log-filevalgrind.log ./gateway跑上十几分钟让流量把代码路径全部触发一遍然后对输出里的definitely lost和indirectly lost做分析。这个方案适合程序能离线复现的情况。如果现场程序用的是 jemalloc还有更轻量的手段——直接用 jemalloc 自带的 heap profile 功能MALLOC_CONFprof:true,lg_prof_interval:30,prof_prefix:/tmp/jeprof ./gateway这样每 30 秒lg_prof_interval:30里的 30 是 2 的 30 次方字节也就是每 1GB 分配触发一次 dump不是秒注意别搞错或一定分配量后jeprof 会生成堆快照文件。再用脚本对比两次堆快照找出分配增长最多的调用栈。jeprof --show_bytes --base/tmp/jeprof.0001.heap /tmp/jeprof.0002.heapjeprof 的输出结果里哪个函数分配的内存增量最大基本就是泄漏点了。当时我们定位到是一个连接管理模块在建立每个长连接时都会 new 一块 8KB 的 buffer但连接正常关闭时这块 buffer 没有释放导致连接数越多RSS 涨得越快。修复方式就是补上释放逻辑或者改用智能指针。5.3 修复与验证修复代码后不能只是跑一下没问题就完事内存泄漏的验证要看趋势。我的验证方法是在灰度环境部署修复后的版本连续压测 4 小时用ps -o rss -p pid周期性采集 RSS把采集数据画成曲线观察是否趋于平稳对比修复前后的 RSS 增长斜率修复前每小时涨 200MB修复后 4 小时只波动了几十 MB。同时还要在监控平台上加上“进程 RSS 增长率”的指标。我习惯设置两级阈值比如 RSS 在 10 分钟内持续增长超过 200MB 就触发黄色告警30 分钟增长超过 1GB 就触发红色告警。很多内存泄漏不是一下炸掉系统的而是慢慢把内存池舔穿过程可能持续几天甚至几周没有趋势监控根本发现不了。6. 常见问题与排查技巧实录6.1 排查后内存高但进程列表看不到这是我最常被问的问题“free 显示 used 很高但 ps/top 里看不到哪个进程吃内存是不是中毒了”别急着下结论先按顺序排查看 SReclaimable 和 SUnreclaim 是否异常看 page cache 和 buffer 是否正常看 tmpfs 和 SysV shm 是否有残留如果上面都正常再看是不是跑在容器里宿主机的/sys/fs/cgroup/memory/下的 cgroup 统计是不是有不同口径。我的排查脚本如下grep -E ^(MemTotal|MemFree|MemAvailable|Cached|Buffers|Shmem|Slab|SReclaimable|SUnreclaim) /proc/meminfo ipcs -m df -h | grep -E tmpfs|shm绝大部分“进程列表看不到”的场景都是 tmpfs 或共享内存段在作祟。还有一个隐蔽的场景是内核模块或驱动通过kmalloc分配了不可回收内存在 slab 里看不出来但这属于少数需要结合dmesg日志和厂商驱动文档排查。6.2 内存告警但实际能正常服务告警平台显示内存使用率 98%但业务延迟很低、接口也正常到底要不要处理这种场景多半是监控指标的算法和内核 available 的口径不一致。很多监控平台用的是used / total其中 used total - free - buff/cache而 buff/cache 可能高达 70%导致 used 超过 90%看起来非常吓人。正确的做法是让监控平台改用 available 作为告警依据或者同时展示 available 和 used 两条曲线。在 Linux 上available 的计算逻辑大概是MemFree 可回收的 page cache SReclaimable 可回收的 swap cache再减去内存水位的保守值。所以MemAvailable才是“在不影响系统稳定的前提下还能给进程用多少”的参考值。如果 available 还有 30%但告警 says 98%那就是监控口径要改而不是系统真的要炸。6.3 定位内存问题的经验口诀这些年排查内存问题我总结了一套自己的执行顺序分享出来供参考先看 available不看 used再看进程 RSS但要注意共享内存重复计算然后查 slab 和共享内存段排除内核态和 tmpfs 的干扰最后才考虑用 valgrind 或 heap profile 深挖单个进程。另外还有几个经验值如果Cached占了总内存 70% 以上且 available 充足不用管如果SUnreclaim持续增长优先怀疑内核模块或驱动如果某个进程 RSS 和 PSS 差距过大优先排查共享内存映射如果容器经常 OOMKilled优先检查memory.limit是否设置得过小而不是一味加大宿主机内存。最后一个忠告不要在没搞清楚 root cause 的情况下直接kill -9。内存问题很多时候是业务代码的问题杀进程只能缓解症状过两天还会再来。你要做的是把那个进程的堆内存、文件缓存、线程栈、共享内存全部过一遍找到真正漏内存的那一行代码然后修复它。我自己从这些坑里爬出来的体会是内存诊断这件事七分靠对原理的理解三分靠命令熟练度。把/proc/meminfo里的每一列都搞明白把 RSS、PSS、USS 的区别吃透再配合几个实例走一遍以后再遇到内存“隐形怪兽”心里就有底了。下一次如果你也接到凌晨三点的内存告警不妨先泡杯茶按这篇的顺序走一遍多半能比折腾一晚上重启服务更快找到问题。
返回列表