Linux服务器内存泄漏初判:RSS上涨是缓存还是泄漏

    发布时间:2026-09-26 04:31 更新时间:2026-09-26 04:31 阅读量:0

    运维夜里收到告警,某个 PHP-FPM 或 Java 进程的 RSS 一路向上爬,重启后回落,过几天又涨回去——这到底是正常的内存缓存,还是真的泄漏?直接下结论容易误判:RSS 里既有进程自己 malloc 出来的堆,也有加载的动态库、mmap 的文件页,还有和其他进程共享的内存。本文带你把 RSS 拆开看,用 /proc/PID/smaps 和 pmap 判断增长来自哪一段,再用 mtrace 做一块堆内存的初判。

    先弄清 RSS 里都装了什么

    Linux 上 ps 和 top 显示的 RSS(Resident Set Size)是进程当前驻留在物理内存中的页数,它不等于进程独占的内存。同一份 libc 被 50 个进程加载,物理上只存一份,但每个进程的 RSS 都会算上一份,所以 RSS 之和大于机器物理内存是常见现象。RSS 的主要构成有三块:匿名页(堆、栈、malloc 出来的内存),文件映射页(可执行文件、.so、mmap 的数据文件),以及共享内存段(shm、大页等)。

    判断的关键不在总量,而在增长的是哪一段。堆增长且不回落,多半是真泄漏;文件映射增长通常是读缓存,属于正常;共享内存增长要看是不是别的进程在写。所以要拿到按段分类的数据,而不是盯着一个 RSS 数字焦虑。

    先记录基线和时间点,隔一段时间再对比,比单次采样有意义得多:

    # 找到目标进程 PID
    pgrep -f php-fpm | head
    
    

    记录当前 RSS 与虚拟内存

    ps -o pid,rss,vsz,etime,cmd -p 12345

    把 smaps 按映射名汇总,看每段占多少

    sudo awk '/^[0-9a-f]/{name=$6} /^Rss:/{sum[name]+=$2} END{for(n in sum) print sum[n], n}' \ /proc/12345/smaps | sort -nr | head -20

    这条 awk 会把 smaps 里每个映射的 Rss 字段按映射文件名累加,输出占用靠前的段。过一段时间再跑一次,比较哪些段的数字在涨,方向就清楚了。注意 smaps 读取需要权限,普通用户读别的用户的进程会失败,以实际环境为准。

    用 pmap 和 smaps 区分增长来源

    pmap 是查看进程内存映射更直观的工具,很多发行版由 procps 或 psmisc 提供,没有的话装对应包即可。

    # -x 显示详细,-p 显示完整路径
    pmap -x 12345 | sort -k3 -nr | head -20
    
    

    只看匿名映射(堆、栈等无文件来源的段)

    pmap -x 12345 | awk '$NF=="[ anon ]" || $NF=="[anon]"'

    pmap 输出里,最后一列是映射来源:[ anon ] 是匿名映射,通常对应堆、线程栈、malloc 大块;有具体路径的是文件映射,比如 libc.so、业务代码文件;[ heap ] 是主堆,[ stack ] 是主线程栈。如果增长集中在 anon 和 heap,说明进程自己在申请内存,这是最需要警惕的情况。

    要更细地定位堆增长,可以看 smaps 里的几个字段:Rss 是实际驻留,Pss 是按共享比例分摊后的独占值,Private_Dirty 是进程私有的脏页。判断泄漏时,Pss 和 Private_Dirty 比 Rss 更能说明“这块内存是不是我独占的”:文件映射的缓存页 Pss 通常很小,而泄漏出来的堆 Pss 会跟着涨。

    # 对比两次采样中 heap 段的 Pss 变化
    sudo grep -A12 '\[heap\]' /proc/12345/smaps | grep -E 'Rss|Pss|Private_Dirty'
    
    

    或用 smaps_rollup 看进程整体汇总(内核 4.14+ 通常支持)

    sudo cat /proc/12345/smaps_rollup

    smaps_rollup 是内核提供的汇总文件,字段含义与 smaps 一致但只给总数,读取开销小,适合放进监控脚本定期采集。字段可用性以实际内核版本和发行版为准。

    用 mtrace 对堆分配做初判

    如果确认增长来自堆,可以借助 glibc 自带的 mtrace 做初判。它通过拦截 malloc/free 记录分配点,适合 C/C++ 程序,需要在代码里或启动前设置环境变量。前提是程序用 glibc 且没有被自定义内存池完全接管,否则记录可能不准。

    # 让 glibc 把分配记录写到指定文件
    MALLOC_TRACE=/tmp/mtrace.log ./your_program
    
    

    也可以用 mtrace 命令分析日志,输出未释放的分配点

    mtrace ./your_program /tmp/mtrace.log

    mtrace 的输出会列出“分配了但没释放”的调用位置,属于线索而非定论:全局缓存、连接池、故意常驻的对象也会表现为未释放。它的价值在于把“怀疑泄漏”缩小到具体代码行,而不是让你直接下结论。对于 PHP、Java 这类运行时,语言层有自己的内存管理,应优先用各自的工具(如 PHP 的 memory_get_usage、JVM 的 jmap/jstat),mtrace 不适用。

    还有几个常见坑要避开:只看 RSS 不看 Pss 会把文件缓存误判为泄漏;容器里 cgroup 的内存统计和进程 RSS 口径不同,要分开看;glibc 的 arena 碎片会让堆在高水位后不归还操作系统,看着像涨其实可复用,判断时要结合业务负载和 free 情况。拿不准时,用“重启后是否稳定回落、增长是否与请求量线性相关”这两个现象交叉验证。

    小结与下一步

    面对 RSS 上涨,先别急着改代码:用 smaps 按映射汇总、用 pmap 看来源类型,确认增长落在匿名页和堆,再上 mtrace 或语言层工具定位分配点。文件映射增长基本可以放心,共享内存增长要结合其他进程判断。确认是泄漏后,建议在测试环境复现并配合 valgrind 等工具深入,生产环境优先做内存水位监控和优雅重启兜底。具体命令参数与内核字段支持情况,请以官方文档和实际环境为准。

    继续阅读

    📑 📅
    MySQL死锁日志定位实战:读懂SHOW ENGINE INNODB STATUS 2026-09-26
    Nginx 上游节点健康检查实战:max_fails、fail_timeout 与 backup 配置 2026-09-26
    MySQL主从复制中断后如何安全恢复 2026-09-25
    宿主机与容器时间不一致导致接口签名失败排查 2026-09-25
    Nginx与上游服务时间不同步导致签名校验失败排查 2026-09-25
    Docker 挂载 NFS 卷卡死排查:stat、df 与 nfsstat 定位失联 2026-09-26
    PostgreSQL WAL 堆积与复制槽不释放排查实战 2026-09-26
    Nginx proxy_pass 带 URI 与不带 URI:路径拼接错乱排查 2026-09-27
    Docker 容器 PID 1 与信号处理:docker stop 超时强制 kill 的成因与写法 2026-09-27
    PostgreSQL连接池选型与pgbouncer事务池踩坑 2026-09-27