
在我维护的这批代号“沧桑”的旧电脑系统上内存监控一直是个高频需求。机器规格不高内存只有 4GB 到 8GB业务进程一旦出现异常增长经常直接触发 OOM。真正需要的不是打开任务管理器看一眼而是能脚本化、能对接日志、能定时运行的“进程内存监控”工具。接下来从指标含义、Linux 底层数据来源、Python 实现、运行验证和常见坑五个层面把一个可提交的进程内存统计工具讲透。1. 先搞清楚进程内存统计要看的指标否则工具做出来也不可用1.1 从任务管理器到脚本监控为什么需要专用工具在很多场景里人工查看进程内存的方式仍然是“打开任务管理器按内存排序挑出占用最高的进程”。这种方式在小机器上够用但遇到下面的需求就撑不住了能不能每 5 分钟生成一份内存占用 Top 列表能不能在进程超过阈值时自动上报能不能在 OOM 之前保留现场这些需求要求监控逻辑能进入脚本能调用系统接口能按固定格式输出。而做这些事情的第一步是准确理解进程内存统计里的各项指标。如果把内存统计工具设计成只输出“程序占了多少内存”十有八九会踩到共享内存、虚拟内存、页缓存的坑。例如两个进程共享同一个动态库RSS 会把这段共享内存分别计入两个进程而不是只算一次。于是统计结果看起来每个进程都不高总和却超过了物理内存。要解释这种现象就要分清 VSZ、RSS、PSS、USS 这些指标。1.2 四个核心指标VSZ、RSS、PSS、USS在 Linux 系统中进程内存统计可以从/proc/PID/status和/proc/PID/smaps读取。psutil 等工具读取的也是这些文件。下面这张表把常见指标的含义和使用场景整理出来指标中文含义简单理解常见使用场景VSZ虚拟内存大小进程申请的地址空间总量包含尚未真正占用的部分关注早期设计缺陷或调试 malloc 申请异常RSS常驻内存集进程当前驻留在物理内存中的页面总数包含共享库日常查看进程占用top 的 RES 列就是它PSS比例内存集RSS 中共享内存按进程数均摊后的结果评估系统整体内存分配比 RSS 更合理USS唯一内存集进程独占且不与其他进程共享的内存定位私有内存泄漏看单进程业务占用最合适RSS 是最常用的日常指标但它有一个明显问题共享库、共享内存都被重复计入。PSS 引入了按比例分摊机制更适合做全系统分布分析。USS 只统计独占内存尤其适合判断某个业务模块自身是不是一直在积累内存。1.3 长期监控时该选哪个指标如果做的是单机、短时间、快速定位任务直接用 RSS 排序就能发现问题。如果做的是长期内存趋势统计推荐同时记录 RSS 和 USS。RSS 反映实际物理压力USS 帮助判断是不是进程自身的私有内存涨了。中间再配合 PSS 看系统级分摊。默认情况下工具可以先按 RSS 排序同时输出 PSS 和 USS这样在权限允许时能直接看到多维数据。实际项目中容易犯的错是拿 VSZ 判断“内存泄漏”。很多进程启动时申请了很大的虚拟地址空间但实际没写入页面物理内存占用并不高。如果监控告警只看 VSZ会出现大量误报。所以统计工具默认排序应该用 RSS调试私有时再看 USS。2. 环境准备先确认 Python 环境和 /proc 文件系统可用2.1 系统要求和版本确认下面的实现基于 Python 3 和 psutil。psutil 是一个跨平台的进程和系统监控库在 Linux 上读取/proc在 Windows 上读取性能计数器。为了后面代码一致建议先确认环境项目建议值说明操作系统Linux 2.6 及以上重点讲 Linux /proc其他平台实测后调整Python3.6 及以上示例代码兼容3.6 足够psutil5.9 及以上旧版本缺少部分内存字段建议安装新版本root 权限可选想读到 PSS/USS 时最好有 root 权限版本检查命令python3 --version如果系统里没有 Python 3在 Debian/Ubuntu 上可以用 apt 安装sudo apt update sudo apt install -y python3 python3-pip2.2 安装 psutilpsutil 是第三方库安装方式通常是pip3 install psutil如果当前环境用的是系统 Python可能出现权限限制。推荐创建虚拟环境避免污染系统环境python3 -m venv venv source venv/bin/activate pip install psutil在 CentOS/RHEL 系列系统上可能还需要先安装python3-devel等编译依赖。如果安装报错优先先升级 pippip install --upgrade pip setuptools2.3 最小可运行验证用一行 Python 看当前进程内存安装完成后不要急着写完整工具先跑一个最小验证import psutil, os p psutil.Process(os.getpid()) print(p.memory_info())正常输出类似pmem(rss4505600, vms12328960, pfaults..., pageins...)这里rss和vms分别对应/proc/PID/status中的 VmRSS 和 VmSize。能看到这一行说明 psutil 可以正常读取内存信息。2.4 学习环境与生产环境差异本地测试时随便一个普通用户就能读到自己进程的大部分内存信息。但生产环境有很多服务进程属于 root 或系统用户普通用户读取它们时可能遇到 Permission denied。因此学习环境普通用户跑脚本能看自己的进程和部分系统进程。生产环境建议用独立监控账号或直接使用 root 权限并严格管控脚本落盘位置和上报接口地址。容器环境要注意当前进程在哪个 PID namespace 中下面第 5 节会专门讲。3. 实现进程内存统计工具从读 /proc 到可提交结果3.1 工具设计目标这个工具要解决三个问题第一快速列出当前机器上内存占用最高的 N 个进程第二支持按进程名过滤按 RSS、PSS、USS 等字段排序第三结果能以文本或 JSON 输出并能提交到指定 HTTP 接口用于后续对接日志系统或监控平台。目录结构不需要复杂一个脚本文件即可proc_mem_stat.py脚本依赖 psutil。为了不引入额外框架命令行解析使用标准库argparseHTTP 提交使用标准库urllib。3.2 Linux 底层数据来源/proc/PID/status 里有什么在 Linux 系统中每个进程都有一个/proc/PID/status文件。可以使用下面的命令查看当前 shell 进程的内存字段cat /proc/$$/status | grep -E VmPeak|VmSize|VmRSS|VmSwap输出类似VmPeak: 123456 kB VmSize: 45678 kB VmRSS: 8901 kB VmSwap: 0 kB字段说明VmPeak进程启动以来虚拟内存历史峰值。VmSize当前虚拟内存总大小。VmRSS当前常驻物理内存总大小。VmSwap进程使用 swap 空间的大小。如果只想精确算某个进程的 RSS不依赖 psutil 也可以直接解析这个文件def read_rss(pid): try: with open(/proc/{}/status.format(pid), r) as f: for line in f: if line.startswith(VmRSS:): return int(line.split()[1]) * 1024 except (FileNotFoundError, PermissionError, ValueError): return 0 return 0这里把单位从 kB 转成了字节方便和 psutil 返回的字节数统一。不过/proc/PID/status只能提供 RSS 和 VSZ提供不了 PSS、USS。要读 PSS/USS需要看/proc/PID/smaps或/proc/PID/smaps_rollup这些文件内容更细但普通用户不一定有权限。psutil 的memory_full_info()在 Linux 上正是读取这些文件所以直接用 psutil 更省事。3.3 用 psutil 封装采集逻辑psutil 提供process_iter()一次性遍历进程并且遍历时传入需要的字段避免每次循环都多次发起系统调用。核心采集函数可以这样写def collect(keywordNone, sort_byrss, top20): rows [] for proc in psutil.process_iter([pid, name, username, cmdline]): try: info proc.info mem proc.memory_info() full None try: full proc.memory_full_info() except (psutil.AccessDenied, AttributeError): pass row { pid: info[pid], name: info[name] or , username: info[username] or , cmdline: .join(info[cmdline] or [])[:200], rss: mem.rss if mem else 0, vms: mem.vms if mem else 0, pss: getattr(full, pss, 0) if full else 0, uss: getattr(full, uss, 0) if full else 0, swap: getattr(full, swap, 0) if full else 0, } if keyword: text {} {}.format(row[name], row[cmdline]).lower() if keyword.lower() not in text: continue rows.append(row) except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess): continue rows.sort(keylambda r: r.get(sort_by, 0), reverseTrue) return rows[:top]这段代码有两个设计点。第一任何可能因为进程退出而抛出的异常都在循环内捕获避免一个进程退出导致整个统计中断。第二memory_full_info()不是在所有平台上都可用所以用getattr和默认值兜底。3.4 完整脚本进程内存统计与提交工具把上面的采集逻辑、格式化输出和 HTTP 提交组合起来得到一个可以直接运行的完整脚本。下面代码用于说明整体思路实际项目请根据自己的路径、Python 环境和服务地址调整。#!/usr/bin/env python3 # -*- coding: utf-8 -*- # proc_mem_stat.py # 进程内存统计工具列出进程内存占用支持 JSON 输出和 HTTP 提交。 import argparse import json import sys from urllib import request, error try: import psutil except ImportError: sys.stderr.write(缺少 psutil请先执行pip install psutil\n) sys.exit(1) def format_bytes(size): if size is None or size 0: return - units [B, KB, MB, GB, TB] value float(size) index 0 while value 1024 and index len(units) - 1: value / 1024.0 index 1 return {:.1f} {}.format(value, units[index]) def collect(keywordNone, sort_byrss, top20): rows [] for proc in psutil.process_iter([pid, name, username, cmdline]): try: info proc.info mem proc.memory_info() full None try: full proc.memory_full_info() except (psutil.AccessDenied, AttributeError): pass row { pid: info[pid], name: info[name] or , username: info[username] or , cmdline: .join(info[cmdline] or [])[:200], rss: mem.rss if mem else 0, vms: mem.vms if mem else 0, pss: getattr(full, pss, 0) if full else 0, uss: getattr(full, uss, 0) if full else 0, swap: getattr(full, swap, 0) if full else 0, } if keyword: text {} {}.format(row[name], row[cmdline]).lower() if keyword.lower() not in text: continue rows.append(row) except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess): continue rows.sort(keylambda r: r.get(sort_by, 0), reverseTrue) return rows[:top] def print_text(rows): header {:8} {:18} {:12} {:10} {:10} {:10} {:10} {:40}.format( PID, Name, User, RSS, PSS, USS, Swap, Cmdline ) print(header) print(- * len(header)) for row in rows: print({:8} {:18} {:12} {:10} {:10} {:10} {:10} {:40}.format( row[pid], row[name][:16], row[username][:10], format_bytes(row[rss]), format_bytes(row[pss]), format_bytes(row[uss]), format_bytes(row[swap]), row[cmdline][:38], )) def submit(data, endpoint, timeout5): payload json.dumps(data, ensure_asciiFalse).encode(utf-8) req request.Request( endpoint, datapayload, headers{Content-Type: application/json; charsetutf-8}, methodPOST, ) try: with request.urlopen(req, timeouttimeout) as resp: return resp.status, resp.read(1024) except error.URLError as exc: return -1, str(exc).encode(utf-8) def main(): parser argparse.ArgumentParser(description进程内存统计工具) parser.add_argument(--top, typeint, default20, help只显示内存占用最高的 N 个进程默认 20) parser.add_argument(--sort, choices[rss, pss, uss, vms, swap], defaultrss, help排序字段默认 rss) parser.add_argument(--keyword, default, help按进程名或命令行过滤关键字不区分大小写) parser.add_argument(--json, actionstore_true, help以 JSON 格式输出) parser.add_argument(--endpoint, default, help提交到指定 HTTP 接口POST JSON 数组) parser.add_argument(--timeout, typefloat, default5.0, helpHTTP 提交超时默认 5 秒) args parser.parse_args() rows collect(keywordargs.keyword, sort_byargs.sort, topargs.top) if args.endpoint: status, body submit(rows, args.endpoint, args.timeout) if status -1: sys.stderr.write(提交失败{}请检查接口地址和网络。\n.format( body.decode(utf-8, errorsreplace))) sys.exit(1) print(已提交 {} 条进程记录到 {}HTTP 状态码{}.format( len(rows), args.endpoint, status)) elif args.json: print(json.dumps(rows, ensure_asciiFalse, indent2)) else: print_text(rows) if __name__ __main__: main()脚本里有两个地方需要留意。上面的提交逻辑没有重试机制生产环境可以在脚本外面包一层重试逻辑。下面的验证示例中由本地测试服务接收数据正式接口必须设置鉴权、超时和长度限制。3.5 关键参数说明参数作用默认值注意事项--top输出条目数20不要设成很大进程多时 JSON 会很大--sort排序字段rss如果 PSS/USS 读不到会变成 0排序会失真--keyword按名称或命令行过滤空不区分大小写适合按业务关键字抓进程--jsonJSON 输出关闭适合对接脚本和上报--endpoint提交接口地址空指定后只输出提交结果不打印进程表--timeoutHTTP 超时5 秒内网接口可以按实际延迟调整4. 运行验证模拟内存占用并核对统计结果4.1 模拟一个持续占用内存的测试进程为了验证工具能统计到真实增长先启动一个模拟进程让它每 100 毫秒申请 1MB 内存并保留引用import time data [] while True: data.append(bx * 1024 * 1024) time.sleep(0.1)在另一个终端运行这个脚本。等十几秒后这个 Python 进程的 RSS 应该会稳定增加最终可能因为系统内存不足被 OOM killer 杀掉或者被失败的内存分配中断。验证阶段只跑一小段时间即可。4.2 运行统计工具查看输出回到工具目录执行python3 proc_mem_stat.py --top 5输出可能类似下面的示例具体数字取决于机器上运行的进程PID Name User RSS PSS USS Swap Cmdline 12345 python3 root 256.0 MB 248.0 MB 246.0 MB 0.0 MB /usr/bin/python3 mem_sim.py ...这里测试进程的 RSS 明显排在前面说明采集逻辑生效。如果工具输出的进程列表为空先检查是否有权限再检查是不是所有进程都已经退出。4.3 与 top 和 free 交叉验证可以用系统命令交叉验证结果top -b -n 1 | head -30ps -eo pid,rss,comm --sort-rss | head对比时注意单位top默认显示 KiBps的 RSS 也是 KiB而工具脚本输出的是可读字符串但内部数据源相同都是/proc/PID/status的 VmRSS。因此同一进程的三个数值换算成相同单位后应当一致。如果出现明显差异优先怀疑取到了不同 PID 或换算单位弄混。同时可以看系统级内存free -h如果工具里所有进程 RSS 相加后远超物理内存而free显示 available 还有很多通常是因为共享内存被重复计算。这是 RSS 指标的天然限制不是工具 bug。4.4 验证提交到 HTTP 接口写一个极简接收端验证--endpoint提交逻辑from http.server import HTTPServer, BaseHTTPRequestHandler class Handler(BaseHTTPRequestHandler): def do_POST(self): length int(self.headers.get(Content-Length, 0)) data self.rfile.read(length) print(收到数据长度{}.format(length)) print(data.decode(utf-8, errorsreplace)[:500]) self.send_response(200) self.end_headers() self.wfile.write(bok) HTTPServer((0.0.0.0, 8000), Handler).serve_forever()新开终端启动接收端然后提交python3 proc_mem_stat.py --top 3 --endpoint http://127.0.0.1:8000/stats接收端打印出 JSON 数据长度和内容片段工具本身输出“已提交 3 条进程记录”说明提交链路正常。注意本地验证接口没有鉴权和长度限制只适合测试。生产环境必须把上报接口放在内网或加 Token 校验避免任意伪造数据。5. 常见坑与排查链路统计值不稳定、权限不足、容器干扰5.1 PID 复用导致统计串线现象同一时刻脚本里出现两个进程使用同一个 PID或者历史数据里 PID 对应的进程名变了。原因Linux 的 PID 会循环复用进程退出后新进程可能继承旧 PID。如果上报数据只带pid后续分析时无法区分是同进程还是新进程。检查方式采集时记录进程启动时间关键字段加create_time。处理建议在脚本的row里增加create_time或pid_start_time上报到服务端时用(pid, create_time)作为唯一标识。5.2 PSS/USS 读不到或全部为 0现象--sort pss排序时很多进程的 PSS 为 0排序结果离谱。原因读取/proc/PID/smaps和/proc/PID/smaps_rollup通常需要 root 权限或 CAP_SYS_PTRACE 能力。普通用户读取系统进程时会被拒绝。检查方式sudo cat /proc/1/smaps_rollup | head如果普通用户无法读取输出为 Permission denied。处理建议生产环境用 root 用户运行采集脚本并且在脚本里对psutil.AccessDenied做好兜底避免一个进程的权限问题中断整个采集。5.3 RSS 总和超过物理内存现象把工具输出的所有 RSS 相加发现比物理内存还大而系统实际没有崩。原因多个进程共享动态库或共享内存RSS 没有去重。PSS 会均摊共享部分因此总和更接近物理内存的真实占用。检查方式比较同一次采集中 RSS 总和与 PSS 总和。处理建议系统级分析优先看free的 available 列和 PSS 总和定位进程私有泄漏时看 USS。5.4 容器里看到大量宿主机进程现象在容器内运行工具发现列出的是宿主机所有进程不是容器内进程统计数据没有隔离。原因如果容器创建时没有使用隔离的 PID namespace进程看到的就是宿主机的/proc自然能枚举宿主机进程。检查方式在容器内执行ps -ef | wc -l对比容器内已知进程数。处理建议容器场景不要直接统计全部进程优先使用 cgroup 维度指标例如cat /sys/fs/cgroup/memory.current或使用容器运行时提供的docker stats一类命令。实现上也应该在脚本里增加参数指定只统计某个 cgroup 或 PID namespace 内的进程。5.5 排查总表问题现象可能原因检查方式处理建议脚本输出为空运行权限不足或进程瞬时退出用 root 运行一次对比输出提升权限捕获 AccessDeniedPSS/USS 都是 0root 或 CAP_SYS_PTRACE 缺失sudo cat /proc/1/smaps_rollup用 root 账号运行采集服务内存总和超物理内存RSS 重复计算共享页对比 PSS 总和与 free系统总量看 PSS私有泄漏看 USS提交接口失败网络不通、接口无鉴权、超时太短curl 手动 POST 测试增加超时、重试和日志采集结果忽大忽小采样时间点不固定或进程刚启动连续采样多次对比固定采样周期多次采样取中位数6. 生产化建议从脚本到可持续运行的监控方案6.1 用 systemd timer 定时执行如果要每 5 分钟采集一次并提交不建议直接写死循环脚本而是交给 systemd timer。先创建服务单元[Unit] DescriptionCollect process memory stats [Service] Typeoneshot ExecStart/usr/bin/python3 /opt/scripts/proc_mem_stat.py --top 30 --json --endpoint http://127.0.0.1:8000/stats Userroot Grouproot再创建定时器单元[Unit] DescriptionRun proc mem stat every 5 minutes [Timer] OnCalendar*:0/5 Persistenttrue [Install] WantedBytimers.target启动并查看执行结果sudo systemctl daemon-reload sudo systemctl enable --now proc-mem-stat.timer journalctl -u proc-mem-stat.service -f如果脚本采集耗时较长需要保证两次执行不重叠可以在[Service]中使用ExecStartPre/usr/bin/flock -n /tmp/proc-mem-stat.lock或用 systemd 的TimeoutStartSec控制执行上限。6.2 上报失败的处理本地缓存和重试当前脚本提交失败后会直接退出到主程序 exit code 1长期运行场景下会丢数据。最简单的改进是在脚本外层做重试例如把submit中的一次提交改成最多重试 3 次每次间隔 1 秒。更稳妥的做法是当接口不可用时把 JSON 数据写到本地/var/spool/proc-mem-stat/目录等接口恢复后再补传。补传时务必带上timestamp字段避免覆盖时序语义。如果要新增字段需要同时修改collect函数中的row字典。上报服务端最好能按timestamp去重防止网络重试导致重复数据。6.3 设置内存告警阈值内存告警不能只看单次采样。短时申请内存并不等于泄漏比如 Web 服务收到大请求时 RSS 会瞬时升高。推荐连续 3 次采样都超过阈值后再告警否则会产生大量噪音。阈值设计建议单进程 RSS 超过物理内存的 20%触发提醒。单进程 USS 连续增长且无回落触发泄漏检查。系统可用内存低于 10% 且持续 1 分钟触发 OOM 风险告警。命令层面可以使用cron或 systemd timer 完成告警方式可以是写日志、调用内部 Webhook或直接写入 Prometheus Alertmanager。具体协议要根据内部平台确定。6.4 自研工具和成熟监控方案怎么选自研脚本不是万能方案。单机、几台旧机器、只需定时抓一次进程内存列表时脚本加 systemd 已经足够。但如果是几十台服务器组成的集群需要看历史趋势、跨机对比、长时间告警策略还是建议用成熟方案。常见替代方案方案优势适合场景自研 Python 脚本代码可控无额外依赖旧系统、内网限制严格、只需要 Top N 列表Prometheus node_exporter指标开放、生态成熟、支持告警集群环境、需要趋势图和长期存储process_exporter直接监控进程名称和数量需要按进程维度设置告警云厂商云监控 Agent免运维、告警通道完善已经使用同一云厂商体系自研工具的维护成本往往被低估。PSS 读取权限、PID 复用、时区、JSON 字段兼容、接口鉴权都需要持续维护。所以建议先跑通脚本再评估是否需要切换成熟方案。6.5 落地检查清单生产环境上线前可以按下面清单核对[ ] Python 版本和 psutil 依赖已确认脚本在本机可执行。[ ] 使用 root 或带 CAP_SYS_PTRACE 权限的账号运行PSS/USS 能读到。[ ] 进程记录带有 pid、create_time、hostname、timestamp 等标识字段。[ ] 上报接口有鉴权、超时、失败日志脚本支持重试。[ ] systemd timer 或 cron 配置已完成执行日志能看到结果。[ ] 告警阈值设置为连续 N 次超阈值避免瞬时误报。[ ] 容器环境使用 cgroup 指标或限制采集范围避免统计宿主机进程。[ ] 数据保留策略明确避免 JSON 日志无限占满磁盘。进程内存监控的难点不是执行一条 top 命令而是理解指标、选对工具、处理好权限和上下文。自研脚本的价值在于轻量可控适合旧系统快速接入一旦规模上来还是应该把长期趋势交给专业监控平台。建议先把上面的脚本跑通再根据实际机器把上报、告警和日志补齐最后再评估是否迁移到成熟方案。