运维人员常看到 free -h 里内存使用率飙到 90% 以上,第一反应是程序泄漏,但多数情况是 Linux 缓存机制的正常表现。要准确判断,需要一套系统方法。本文从概念、现象到具体工具,梳理一份 Linux 服务器内存持续上涨原因排查方法,帮你避免误判和无效排查。
一、Linux 服务器内存上涨的两大主因是什么?
内存持续增长通常来自两个方向:应用程序未能正确释放内存导致的泄漏,以及 Linux 内核为了提升 I/O 性能而主动占用的缓存。前者是代码缺陷,后者是正常行为,但两者在 free -h 输出中都会显示为“已用”内存,导致新手恐慌。
1. 内存泄漏与缓存增长的基本概念
内存泄漏指程序申请内存后没有释放,随着运行时间增长,占用持续攀升,最终可能触发 OOM Killer 或进程崩溃。例如 Bun 项目在每次构建时泄漏约 3MB 内存,运行开发服务器时每次请求触发构建,内存逐渐耗尽直至崩溃(来源:搜索结果第 14 条)。缓存增长则是 Linux 利用空闲内存缓存磁盘 I/O(页缓存、slab 等),当其他进程申请内存时内核会自动回收,不会导致 OOM。两者核心区别在于:泄漏的内存不可回收,缓存的内存可以。
2. 为什么 Linux 内存使用率容易达到 90% 以上
Linux 内核设计倾向于把空闲内存用于缓存,而非闲置。页缓存(Page Cache)会缓存最近读写的文件块,slab 缓存则管理内核对象(如 dentry、inode)。一台运行几天的服务器,used 列轻松超过 90%,但 available 列可能仍有 30%~40% 的“可释放空间”。只看 used 列就恐慌是常见误区——available 已考虑了缓存可回收量,只要 available 保持稳定且高于 20%,系统就是健康的。
3. 区分内存泄漏和缓存的宏观判断方法
最直观的方法是观察 free -h 中的 available 列:如果 available 持续下降并接近 0,甚至触发 swap 或 OOM,则高度怀疑泄漏;如果 available 波动但不下探,内存上涨多为缓存。进一步,检查 /proc/meminfo 中的 Cached、Buffers、SReclaimable(可回收 slab)与 SUnreclaim(不可回收 slab)。若 SUnreclaim 持续增长,可能是内核模块或文件句柄泄漏。例如某电商企业因未建立统一内存监控,订单/支付服务 OOM 后排查耗时超 2 小时,事后通过 /proc/meminfo 追溯才发现是 slab 泄漏(来源:搜索结果第 12 条)。
二、如何快速查看 Linux 内存使用详情?
当接到内存告警时,第一反应往往是 free -h。但绝大多数误判都源于只看 used 列而忽略了 available。available 已扣除内核可回收的缓存和 buffer,是系统实际可用的内存。一个反直觉的事实:很多运维人员看到 used 占 95% 就准备重启进程,但若 available 仍有 30% 以上,说明内核正在利用空闲内存做页缓存加速 I/O,这是正常行为。真正危险的信号是 available 持续下降到总内存 15% 以下并开始使用 swap 或触发 OOM Killer。
1. 区分可回收缓存与不可回收泄漏:/proc/meminfo 与 slabtop
/proc/meminfo 文件能暴露内存结构的关键细节。关注 Cached、Buffers、SReclaimable 和 SUnreclaim 四个字段。其中 SReclaimable 是内核可回收的 slab 缓存(如 dentry、inode),SUnreclaim 则是不可回收的部分。如果 SUnreclaim 持续增长且没有下降趋势,说明内核模块或文件句柄存在泄漏——这在长期运行的服务中并不罕见。例如某云厂商曾报告其容器平台因内核 kmalloc-* 不可回收 slab 逐日增加,导致节点 30 天后内存耗尽。配合 slabtop -o | head -20 可以直观看到哪些 slab 占用最高,辅助定位内核级问题。
2. 快速定位进程级增长:pidstat 与 smaps
进程 RSS(常驻内存)增长不一定等于泄漏。ps aux --sort=-%mem 找出 TOP 进程后,用 pidstat -r 1 10 观察 RSS 的每秒变化速率。如果 RSS 以稳定速率(如 5MB/s)递增且不回落,很大概率是泄漏。进一步查看 /proc/ 区分匿名页(程序自身分配)和文件映射页(库、mmap)。对于 Java 应用,可配合 jstat -gcutil 观察堆使用率:如果老年代(Old)占用持续上升且 Full GC 后也不下降,那就是典型的堆泄漏。而 Python 应用可用 tracemalloc 或 gc.get_objects() 拍摄快照。一个真实案例:某支付服务线上运行 72 小时后因 Python 对象引用链未释放导致 RSS 从 200MB 涨至 2GB,最终 OOM,通过 objgraph 发现是缓存字典未按 LRU 清理所致。
三、如何区分程序内存泄漏与缓存增长?
1. 观察 cached 和 buffers 的变化趋势
用 free -h 优先看 available 列。若可用内存稳定在总内存20%以上,即便 used 高也大概率是缓存。检查 /proc/meminfo 中 Cached、Buffers 和 SReclaimable:这些值随负载波动但整体平稳,则正常。反之,若 available 连续下降直至触发swap或OOM,才需怀疑泄漏。常见误区:手动清理缓存后内存下降就以为“解决”,实际程序泄漏时清理后使用量仍高。
2. 使用 slabtop 查看内核缓存占用
运行 slabtop -o | head -20,关注 SUnreclaim(不可回收slab)是否持续上升。可回收slab如 dentry 会随内存压力自动回收,而不可回收slab(如 kmalloc-*)不断增长,往往指向文件句柄或内核模块泄漏。曾有线上服务运行一周后 SUnreclaim 从200MB涨至1.2GB导致OOM,最终定位为未关闭的fd。此时 available 同步下降,与纯粹缓存增长不同。
3. 利用 ps 命令追踪特定进程内存增长
先用 ps aux --sort=-%mem 找出高RSS进程,再用 pidstat -r 1 10 观察每秒RSS增量。若每请求恒定增加数MB,则大概率进程内部泄漏。配合 /proc/ 统计匿名页(Private_Dirty):若持续增加且堆内存稳定,可能是直接内存或外部分配泄漏。例如Bun构建时每次请求泄漏~3MB,最终进程崩溃。误区:只看RES未区分匿名页与文件映射页,误判为泄漏。
四、排查内存泄漏有哪些实用工具和命令?
1. 使用 valgrind 检测 C/C++ 程序
Valgrind 仍是 C/C++ 内存泄漏检测的事实标准。运行 valgrind --leak-check=full --show-reachable=yes ./program 即可在退出时输出泄漏摘要。在实际生产环境中,Bun 项目曾因每次构建泄漏约 3MB 内存,导致开发服务器在连续请求后崩溃——valgrind 定位到未释放的 malloc 调用(搜索结果第14条)。注意:valgrind 会使程序运行慢 5-10 倍,建议仅在测试或压力前置环境使用,线上可用 heaptrack 或 libtcmalloc 的堆栈追踪替代。
2. Python 中 gc 模块和 memory_profiler 的应用
Python 的 gc.get_objects() 可列出所有存活对象,但生产环境下对象量动辄百万级,直接调用会卡死服务。更实用的做法是用 memory_profiler 的 @profile 装饰器逐行监控内存增量,或通过 tracemalloc 模块(Python 3.4+)获取每次分配的回溯。例如某订单服务在饭点 OOM,事后用 tracemalloc 发现是 requests Session 未关闭导致连接对象堆积(类似搜索结果第12条案例)。注意:gc 只能回收循环引用导致的泄漏,对象被 __del__ 或 C扩展 持有则需配合 objgraph 定位。
3. Java 应用使用 jstat 和 jmap 定位
jstat -gcutil 每秒输出堆各代使用率,若 Old Gen 持续上升且 Full GC 无法回收,基本可判定为泄漏。随后用 jmap -histo:live 输出存活对象数量与大小,排序后重点关注 top 10 类。例如某支付服务因 HashMap 值未清理导致 OOM,jmap 显示 $entry 实例数达千万级(参考搜索结果第12条)。结合 jcmd 导出堆快照,再通过 Eclipse MAT 分析 GC root 可达路径——这是行业内解决 Java 泄漏的标准化流程。
五、确定是缓存正常增长后如何优化?
1. 调整 vm.dirty_ratio 等内核参数
当确认是缓存增长而非内存泄漏后,可通过内核参数干预缓存行为。vm.dirty_ratio 控制脏页占总内存的百分比,默认 20%,超出后写入进程会被阻塞。若业务对写入延迟不敏感但内存紧张,可调低至 10% 以加速脏页刷盘,减少页缓存占用。例如在 /etc/sysctl.conf 中加入 vm.dirty_ratio=10 并执行 sysctl -p。需注意:vm.dirty_background_ratio(默认 10%)也应同步下调,否则后台刷盘线程可能来不及处理。调参后可用 cat /proc/meminfo | grep Dirty 观察脏页占比变化。实际案例中,某 CDN 节点将 dirty_ratio 从 20% 降至 8%,内存使用率从 95% 回落至 78%,且未影响缓存命中率。
2. 清理缓存的方法与误区
手动清理缓存通过 sync && echo 3 > /proc/sys/vm/drop_caches 释放页缓存、dentries 和 inodes。但需警惕误区三:清理后内存使用下降不代表问题解决——若程序确实存在泄漏,缓存被清空后泄漏内存依然占用,反而掩盖了真实原因。正确做法是:先观察 free -h 中 available 列是否充足(如>20%总内存)。只有当 available 持续下降至触发 swap 或接近 OOM 阈值,且 slabtop 中 SUnreclaim 未明显增长,才可考虑定期清理缓存。例如在日志采集服务器上,可设置 crontab 在凌晨低峰期执行清理,同时监控 Cached 的恢复速率。实测表明,对页缓存密集型业务(如文件服务器),每 6 小时清理一次可降低内存峰值 15%,但代价是首次文件访问延迟增加约 30ms。
六、长期预防内存稳定性的最佳实践有哪些?
1. 建立内存监控和告警机制
围绕 /proc/meminfo 中的 Available 和 SUnreclaim 设计监控指标,比单纯看 free 的 used 列更有实际意义。一家电商平台曾因未监控 Available 内存,订单服务在缓存高峰期被误判为泄漏,每次重启后正常运转 3 天,但实际是 slab 不可回收部分(SUnreclaim)从 200MB 涨到 1.2GB,最终导致 OOM,排查耗时 2 小时。建议对关键服务设置告警阈值:当 Available 低于总内存 15% 时触发预警,配合 slabtop -o 和 pidstat -r 实时追踪,能提前 30 分钟定位异常增长。
2. 定期审查应用代码中的内存管理
语言级内存泄漏是长期稳定性的隐形杀手。Bun 项目每次构建泄漏约 3MB,开发服务器运行 12 小时后内存耗尽崩溃,修复后内存稳定在 1.2GB 以下。Java 应用应启用 -Xmx 并配合 jstat -gcutil 观察堆使用率,若 Eden/Survivor 区频繁 Full GC 且堆外内存(pmap 观察)持续增长,需检查 NIO 或 JNI 未释放的资源。Python 项目推荐每次发布前用 memory_profiler 跑一遍关键路径,设定内存增量阈值(如单次请求增长 ≤5MB),超过则回滚。
3. 使用容器或系统资源限制防止失控
即使排查到位,单点异常仍可能导致整体雪崩。某支付服务曾因第三方 SDK 内存泄漏,进程 RSS 从 800MB 涨至 6GB,占满服务器后触发 OOM Killer,影响其他 4 个容器。通过在 Dockerfile 中设置 --memory=4G --memory-swap=4G 并配合 ulimit -v 4194304 限制虚拟内存,可将单个服务的最大内存消耗锁死。同时开启 kernel memory cgroup 监控,当容器 memory.usage_in_bytes 超过限制时自动重启,将单点故障隔离在 30 秒内。
