ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

psutil 系统监控实战:用 Python 实现 CPU、内存与进程管理

psutil 系统监控实战:用 Python 实现 CPU、内存与进程管理 psutil 大概是 Python 生态里最被低估的一个库很多人写脚本时会优先去翻ps、top、free这些命令或者用os模块拼拼凑凑结果要跨平台时一团乱要拿进程详细信息时更是无从下手。这个看着不起眼的库其实把系统层面的 CPU、内存、磁盘、网络、进程、传感器信息全部收拢到了同一套优雅的 API 里装完pip install psutil之后你就能用几行 Python 拿到本机全部关键状态。这些年我在 Linux 服务器上做资源监控、自动告警、进程异常排查几乎所有脚本的底层都是 psutil这篇文章就把我实际用下来的经验完整写出来从核心概念到 30 行代码的监控报警脚本一次性讲透。1. psutil 是什么它凭什么成为系统监控的事实标准1.1 一句话理解 psutil 的定位psutil 是 Python 的系统监控和进程管理库全称是 process and system utilities它做的事情说白了就是把操作系统的系统调用封装成 Python 对象。你在终端里敲top看到的 CPU 占用、free看到的内存情况、df -h看到的磁盘使用、ps aux看到的进程列表psutil 都能用函数直接拿出来而且是跨平台的。同一段代码在 Linux、Windows、macOS 上都能跑不需要针对不同系统写不同的解析逻辑这是它比直接调用系统命令强得多的根本原因。很多人会问直接用os.popen(ps aux)然后字符串解析不就行了吗确实能行但这种方案极其脆弱。系统命令的输出格式会因为发行版不同、语言环境不同、psutil 版本不同而变化而且每次调用都要启动一个新进程性能开销大。psutil 底层直接走的是 C 语言的系统 API比如在 Linux 上读取/proc文件系统效率高得多拿到的数据也是结构化对象直接访问属性就行不用对付字符串切分这种脏活。这个库从 2009 年一路维护到现在经历了 Python 2 到 Python 3 的完整迁移API 设计得很稳定生态里大量上层工具比如django-storages、celery、gunicorn、locust都在用它做系统资源统计。我自己用下来的体感是psutil 就是 Python 世界访问系统资源的标准入口它在绝大多数场景下都不该被绕过。1.2 主要能力地图CPU、内存、磁盘、网络、进程、传感器psutil 的能力可以分成六大块我列个清晰的对应关系出来能力维度核心函数你能拿到什么CPUcpu_times()、cpu_percent()、cpu_count()、cpu_freq()使用率、负载、核心数、主频、各核心独立统计内存virtual_memory()、swap_memory()总量、已用、可用、缓冲区、交换分区信息磁盘disk_partitions()、disk_usage()、disk_io_counters()分区列表、挂载点使用率、读写字节数、IO 次数网络net_io_counters()、net_connections()、net_if_addrs()网卡流量、连接状态、端口占用、网卡地址进程process_iter()、Process()进程 PID、父进程、命令行、内存占用、CPU 时间、打开的文件传感器sensors_temperatures()、sensors_fans()、sensors_battery()主板温度、风扇转速、笔记本电量有了这一套你能做的事就非常多了。最简单的就是我下面要讲的实时资源监控脚本复杂一点的可以做进程行为审计比如发现某进程压榨 CPU 超过阈值自动发告警、可以做服务健康检查端口挂了自动拉起进程、可以做负载生成配合 threads 和 Process 类手工控制进程数。这套能力和顶层工具链配合起来可以让一个没什么基础设施的小团队,也能用十来行代码打造出自己的监控体系。2. 上手准备安装、导入与基本用法里的细节2.1 安装方式和第一次调用安装没有任何特殊难度常规pip即可。如果你管理着多台机器建议用虚拟环境加requirements.txt固定版本避免上游 API 变动导致脚本失效。psutil 的版本迭代里偶尔会有接口调整比如某些函数返回结果增加了字段你的代码如果用了._fields之类的底层属性就可能被影响所以固定版本在监控服务场景里非常值得做。pip install psutil # 安装后确认版本 python -c import psutil; print(psutil.__version__)装好之后一个最简单的调用就能让你立刻感受到 psutil 的直观。比如取内存信息:import psutil mem psutil.virtual_memory() print(mem.total / (1024 ** 3)) # 总内存单位 GB print(mem.available / (1024 ** 3)) print(f内存使用率: {mem.percent}%)virtual_memory()返回的是一个svmem具名元组字段有total、available、percent、used、free、active、inactive、buffers、cached、shared、slab。不同操作系统返回的字段不完全一样比如 Windows 上就没有inactive所以跨平台脚本最好只依赖total、available、used、free、percent这种通用字段。2.2 必须搞懂的缓存机制与百分比计算逻辑第一次用cpu_percent()的人几乎都会踩到一个坑第一次调用返回的是 0.0。不是因为你的 CPU 真的空闲而是cpu_percent()默认计算的是“自上一次调用以来的使用率”第一次调用时没有基准所以返回 0.0。这背后的机制有点像汽车的平均油耗表刚通电显示的是上一次清零后的数据跑一段才有意义。解决办法很直接要么先调用一次做热身要么使用interval参数让它自己等一段时间再采集。比如psutil.cpu_percent(interval1)会阻塞一秒返回这一秒内的平均使用率。实时监控场景里我会用这种方式import time import psutil # 热身调用丢弃第一次的 0.0 _ psutil.cpu_percent() while True: cpu psutil.cpu_percent(interval1) print(fCPU 使用率: {cpu}%) time.sleep(0.5)注意我在interval1之后又sleep(0.5)这样实际的采集周期大约 1.5 秒既能保证数据平滑又不会让 CPU 监控本身就变成一个高负载进程。编程里有个经验法则监控工具的自我开销必须控制在 1% 以内psutil 的纯函数调用开销非常小但如果你在循环里频繁构造Process对象、遍历所有进程、还逐个统计命令行那么开销就会明显上升这点在进程管理部分会再展开。3. CPU 与内存监控最常用的两个维度深入拆解3.1 不只是百分比CPU 时间、按核统计与频率识别瓶颈cpu_percent()适合快速判断系统是否繁忙但要定位性能瓶颈光看一个百分比远远不够。我排查线上问题时有一套固定的观察路径先看整体cpu_percent然后立刻看cpu_times()里的user、system、iowait分布再看有没有进程在猛刷单核负荷。cpu_times()返回的是 CPU 累计时间单位秒在 Linux 上包含user、system、idle、iowait、irq、softirq、steal、guest等字段。用两次采样做差值就能算出某段时间内 CPU 花在用户态、内核态和等待 IO 上的比例。下面是我常用的一个慢速 IO 检测示例import psutil import time def cpu_io_wait_sample(duration2): t1 psutil.cpu_times() time.sleep(duration) t2 psutil.cpu_times() total sum(t2) - sum(t1) iowait (t2.iowait - t1.iowait) / total * 100 if total else 0 user (t2.user - t1.user) / total * 100 if total else 0 system (t2.system - t1.system) / total * 100 if total else 0 return {user: user, system: system, iowait: iowait} print(cpu_io_wait_sample())如果iowait长期超过 30%基本可以断定瓶颈在磁盘 IO 而不是 CPU 算力不够。这个时候去看psutil.disk_io_counters()的read_time和write_time变化就能进一步定位是不是某块盘在读写上卡住了。单核视角也有价值。cpu_percent(percpuTrue)返回每个物理核心的使用率列表如果某个核心接近 100% 而其他核心空闲这多半是 Python GIL 或者某个不并发线程导致的倾斜负载。这种“假多核瓶颈”在跑爬虫、跑数据分析脚本时特别常见看到这种分布我会直接去process_iter()里找出哪一个进程的cpu_num()异常再决定是否要改代码结构。3.2 内存里被忽略的那几个字段available 与 cached 的真实含义virtual_memory()里大家最熟悉的可能是percent但如果你只看percent判断服务器内存紧张大概率会误判。Linux 的内存缓存机制决定了空闲内存看起来很少文件缓存cached和内核缓冲区buffers占用了大量“看似已用”的内存系统实际上随时可以把这些缓存释放出来给新进程用。所以真正决定系统还能不能扛住新进程的是available这个字段的值约等于free buffers cached精确计算逻辑更复杂。psutil 的percent已经在较新版本里基于available做了修正比你自己用used / total算出来的更合理。used和available的差别你一定要理解清楚。很多新手写监控脚本用mem.used / mem.total算内存使用率在刚启动的大内存机器上会看到一个虚高的值因为 Linux 把磁盘文件预读到了缓存里。用 psutil 官方推荐的方式直接拿mem.percent是最省事的如果要自己算请记住基准是available而不是free。swap_memory()返回的sin和sout字段是很有用的它们分别表示从磁盘换入、换出的字节数。如果sout持续增长说明物理内存已经不够用系统正在把内存页疯狂写到交换分区这个时候机器会明显卡顿。线上排查我会同时看 swap 使用率和sout增量两个指标同时飙升基本可以直接定位为内存耗尽型问题。4. 磁盘与网络监控存储和流量的核心指标4.1 磁盘分区、使用率遍历和 IO 计数disk_usage(/)能直接拿到根分区的使用情况返回total、used、free、percent。要遍历所有挂载点用disk_partitions(allFalse)。这里的allFalse指的是只显示“真实物理磁盘分区”如果你用allTrue/proc、/sys、/dev这些虚拟文件系统也会被列出来它们大多占用内存或内核数据统计磁盘使用率没有意义只会干扰判断。想排除掉它们最简单的方式是过滤fstype比如只保留ext4、xfs、btrfs、ntfs、apfs等真实文件系统类型。一个写监控脚本时常遇到的细节是disk_usage()对 Windows 盘符路径和 Linux 挂载点要区分处理。跨平台覆盖时不要硬编码路径建议遍历psutil.disk_partitions()后对每个挂载点依次disk_usage(pt.mountpoint)。注意psutil.disk_usage()在访问一个已卸载或者权限受限的挂载点时会抛PermissionError或OSError遍历时要 try 包一层。import psutil # 对典型 Linux 服务器打印磁盘使用率 for part in psutil.disk_partitions(allFalse): try: usage psutil.disk_usage(part.mountpoint) print(f{part.device} mounted on {part.mountpoint}: f{usage.percent}% used, {usage.free / (1024**3):.1f} GiB free) except PermissionError: print(f跳过无权限挂载点: {part.mountpoint})磁盘 IO 层面的disk_io_counters()返回的信息里有几个关键字段read_bytes、write_bytes、read_time、write_time、read_count、write_count。单纯看字节数没有绝对意义要看增量。比如用两次采样算每秒读写速率这是最简单的 IO 吞吐量监控方式虽然不如 iostat 精确但做告警阈值已经足够。4.2 网络计数器与连接列表从网卡流量到端口占用排查网络监控最常用的是net_io_counters()它返回所有网卡累计收发的字节数和数据包数。你只需要在两次采样之间计算差值再除以时间间隔就能得到每秒的实时入站/出站速率。我把这个功能封装成一个get_net_speed()函数在定位“半夜带宽被打满”这种问题时帮了大忙。import psutil import time last psutil.net_io_counters(pernicTrue) def net_speed(interval1): global last time.sleep(interval) current psutil.net_io_counters(pernicTrue) speed {} for nic in current: if nic in last: sent (current[nic].bytes_sent - last[nic].bytes_sent) / interval recv (current[nic].bytes_recv - last[nic].bytes_recv) / interval speed[nic] {up: sent, down: recv} last current return speed print(net_speed(2))这个函数有个必须注意的地方pernicTrue时返回的是每个网卡的独立计数器如果你直接不传pernic得到的是全网卡汇总两台机器的流速混在一起可能掩盖某张内网卡跑满的问题。而bytes_sent和bytes_recv都是累计值不是瞬时值所以只拿一次是没有意义的必须做差值。net_connections()是一个容易踩坑的 API。它默认只返回当前用户能看到的连接想看所有进程的连接需要以 root 身份运行并把参数设成psutil.net_connections(kindall)。kind常见的可选值包括tcp、udp、unix最常用的是tcp。在 Linux 上遍历全部连接时开销不小几百上千个连接就有明显的耗时如果你的监控频率很高要控制它的调用次数。排除线上问题时我常用net_connections()按端口反查占用进程先筛选laddr.port 某个端口的连接提取pid再用psutil.Process(pid)拿到进程名和命令行。这比lsof -i快而且是纯 Python 的直接在监控脚本里就能接续处理。5. 进程管理psutil 真正拉开差距的功能5.1 进程快照的采集姿势process_iter 与 as_dict很多库只能看系统层面的量psutil 能深入到“每个进程”这一层这才是它真正的护城河。psutil.process_iter()是迭代所有进程的推荐入口它比在/proc下一个个os.listdir再组装os.kill的方式好太多。重点在于它能配合attrs参数只取你关心的字段让遍历开销可控import psutil for proc in psutil.process_iter([pid, name, cpu_percent, memory_percent]): try: print(proc.info) except psutil.NoSuchProcess: # 进程可能在遍历过程中已经退出 continue这里proc.info是一个字典包含你指定的字段。因为进程是动态的遍历过程中一个进程可能刚好退出或者变成了僵尸进程访问它的属性就会抛NoSuchProcess、AccessDenied。代码里必须 try 接住这些异常否则一个进程退出就会让整个监控脚本崩溃这是所有进程遍历脚本都要记得的底线。as_dict()则是把Process对象整体转成字典它会把几乎所有属性一次取出来。看起来方便但开销极大每次都要访问几十个系统接口在有很多进程的服务器上会非常慢。我现在的经验是明确要什么字段就用attrs精确取能用process_iter就不自己Process(pid)。as_dict()只在交互式排查单个进程时偶尔用用。5.2 僵尸进程识别、信号发送与进程拉起进程管理里有一个非常容易踩的现象僵尸进程。它的含义是进程已经退出但父进程没有调用wait()回收它的退出状态所以 PID 还留在进程表里。僵尸进程不占 CPU 和内存资源已释放但大量堆积会让process_iter()变慢也会让top显示异常。psutil 里识别僵尸进程非常方便import psutil for proc in psutil.process_iter([pid, name, status]): if proc.info[status] psutil.STATUS_ZOMBIE: print(f发现僵尸进程: {proc.info[pid]} {proc.info[name]})发现僵尸进程后普通kill信号是杀不掉的因为进程已经“死”过一次了。你需要做的是处理它的父进程——要么让父进程正常回收子进程要么重启父进程。这一条我要特别强调不然你对着僵尸 PID 发SIGKILL会发现毫无反应还以为是自己代码写错了。如果你想用 psutil 做服务守护proc.terminate()发送SIGTERMproc.kill()发送SIGKILLproc.is_running()判断存活。一个常见的启停模式是这样的import os import psutil import subprocess proc psutil.Process(12345) if proc.is_running() and proc.status() ! psutil.STATUS_ZOMBIE: proc.terminate() # 等待最多 5 秒超时再强杀 try: proc.wait(timeout5) except psutil.TimeoutExpired: proc.kill()这种优雅停机模式在服务管理脚本里非常实用先给进程一个善后机会SIGTERM处理不了再强杀SIGKILL。如果发现进程不在就直接用subprocess.Popen拉起来。把这一套放到定时任务里小型服务的高可用就基本成型了。6. 实战写一个 30 行系统监控报警脚本6.1 场景设定与阈值设计逻辑前面的知识点串起来就该做一个真正的项目了。我的需求很典型一台 Linux 服务器上没有部署重量级监控平台但我希望能在 CPU、内存、磁盘、网络、进程数五个维度出现异常时第一时间通过标准输出留痕方便后续接入告警系统比如把内容打到日志里再由外部采集。阈值设计有一些通用经验我给的这套参数比较适合普通的 4 核 8G 云主机指标告警阈值判断逻辑CPU 使用率 90%持续 3 个采样周期超过则触发内存使用率 85%检查virtual_memory().percent磁盘使用率 90%遍历根分区和/data等关键分区网络入站速率 200 MB/s按两次采样差值计算单位换算进程数 500用len(psutil.pids())获取多采样周期判定这个点是新手最容易忽略的。CPU 瞬时冲到 95% 可能在跑一次小任务不值得告警持续 3 个周期说明真的有问题告警才需要触发。这个逻辑能显著降低误报率。避免误报还有另一个做法第一个采样周期只做标记第二个周期才开始判定。6.2 脚本代码与运行效果脚本我控制在 30 行左右重点注释加在关键业务逻辑上import time import psutil def check_thresholds(): alerts [] # CPU采样 1 秒的平均使用率连续 3 次超过 90% 才告警 cpu_samples [psutil.cpu_percent(interval1) for _ in range(3)] if all(v 90 for v in cpu_samples): alerts.append(fCPU 持续高负载: {max(cpu_samples):.1f}%) # 内存直接使用官方 percent已基于 available 计算 mem psutil.virtual_memory() if mem.percent 85: alerts.append(f内存使用率过高: {mem.percent:.1f}%) # 磁盘只看根分区和常见数据分区 for part in psutil.disk_partitions(allFalse): if part.fstype in (ext4, xfs, btrfs): usage psutil.disk_usage(part.mountpoint) if usage.percent 90: alerts.append(f磁盘 {part.mountpoint} 使用率 {usage.percent:.1f}%) # 网络计算 2 秒内的入站平均速率 n1 psutil.net_io_counters() time.sleep(2) n2 psutil.net_io_counters() incoming_speed (n2.bytes_recv - n1.bytes_recv) / 2 / (1024 ** 2) if incoming_speed 200: alerts.append(f入站速率异常: {incoming_speed:.1f} MB/s) # 进程数超过 500 视为异常 pids_len len(psutil.pids()) if pids_len 500: alerts.append(f进程数过多: {pids_len}) return alerts if __name__ __main__: print(监控脚本启动每 60 秒检查一次) while True: alert_list check_thresholds() if alert_list: timestamp time.strftime(%Y-%m-%d %H:%M:%S) for alert in alert_list: print(f[{timestamp}] ALERT: {alert}) time.sleep(60)运行起来之后脚本每 60 秒做一次全面体检只在真正异常时才打印告警日志。把标准输出重定向到文件或者对接系统日志服务就能做到“异常可追踪”。我给好几台业务服务器装过类似的脚本日常负载基本是零因为psutil的调用开销都在毫秒级别。6.3 这个脚本的扩展方向和改进思路脚本虽然只有 30 行但它的扩展空间非常大。第一是告警渠道用smtplib发邮件或者把输出打到/var/log/host_monitor.log再交给 logstash 采集都很容易。第二是指标细化网络速率可以按网卡分别统计磁盘 IO 可以监控disk_io_counters()的延迟字段。第三是历史数据用sqlite3把每次巡检结果存下来就能画趋势图。更进一步的思路是让监控脚本“会动手”.比如进程数异常时主动打印最占资源的 Top 进程内存告警时自动抓取占用内存最多的前 5 个进程 PID 和命令行。我在生产环境里就是这么干的告警信息里直接带上现场快照省去登录服务器排查的时间。每一步改动都建立在 psutil 的同一套 API 上没有额外学习成本这是这个库最好的地方。7. 常见问题与踩坑实录速查表7.1 权限问题与僵尸进程这一节要专门讲一讲我踩过的坑。第一个是权限问题。psutil.Process(pid).name()这类调用如果目标进程属于别的用户而你又不是 root就会抛psutil.AccessDenied。这在普通用户跑监控脚本时非常常见。解决办法有两个要么用 root 权限跑脚本要么在遍历时捕获AccessDenied并跳过。后者更稳妥因为生产环境不总是愿意把监控脚本跑成 root。第二个坑是僵尸进程的cpu_percent()和memory_percent()会返回 0 或者直接抛异常因为这类进程的系统资源已经回收内核里只剩一个退出状态。你的遍历逻辑里如果不做状态判断僵尸进程泛滥时会看到一堆异常日志。前面已经给了通过proc.info[status]判断的写法在遍历大批进程时务必加上。Linux 上还有个隐蔽坑读取进程环境变量proc.environ()在权限不足时会抛AccessDenied。如果你只想拿命令行用cmdline()或者name()就够了不要画蛇添足取不必要的字段。字段越多遍历就越慢也越容易碰到权限异常。7.2 首次数值的特殊性、单位换算与跨平台差异cpu_percent()第一次调用返回 0.0 的问题前面提过了这里还有一个容易混淆的变体就是Process(pid).cpu_percent()也是同样的“自上次调用以来”语义。所以如果你想统计某个进程从启动到现在的平均 CPU 使用率正确写法是取两次采样加时间间隔而不是调一次就拿来用。如果不做热身第一次拿到的 0.0 可能会被当作业绩展示误导判断。单位换算是另一个高频出错点。psutil 里的内存、磁盘、网络数值默认单位都是字节bytes不是 KB 也不是 MB。很多人直接把mem.used输出发现一个十几位的数字然后一脸懵。换算经验是看仪表数据用 GB除以 1024 三次看网络实时速度用 MB/s除以 1024 两次再除以时间。千万不要混淆 KB 和 KiB 的差异psutil 底层用的是二进制单位如果你用 1000 做除法数据会有约 2% 的偏差长期统计趋势虽然能看但精细分析时会造成困扰。跨平台差异是最容易被本地开发骗到的点。我在 Windows 上开发Linux 上部署这段经历让我很早就意识到sensors_temperatures()在 Windows 上要么没有要么返回空字典disk_partitions(allTrue)在 Windows 上的挂载点是一个盘符在 Linux 上则要区分虚拟文件系统。写跨平台代码时所有平台相关 API 都要包一层条件判断。此外Windows 上某些net_connections(kindunix)是不支持的会直接抛NotImplementedError这些都在官方文档里有明确说明写代码前先看一眼对应平台的兼容表能少踩一半的坑。8. 把 psutil 用出真正的价值从工具到体系8.1 用进程树分析替代顶级命令排查如果你只把 psutil 当作top的替代品那它对你来说只是省了几行代码但当你开始用它组织“进程树”它就能帮你做更深刻的故障定位。psutil.process_iter()拿到所有进程后用ppid字段构造父子关系树再结合每个进程的 CPU 和内存占比可以快速找出“哪个服务的哪个子进程在拖垮整台机器”。“找出问题进程”是监控的核心诉求而不是仅仅展示数字。我实际处理过一个线上事故现象是机器负载高达 30但top按 CPU 排序时看起来没有特别突出的进程因为负载高不是 CPU 算力不够而是大量进程处于不可中断睡眠状态D状态全部在等磁盘 IO。此时用 psutil 遍历status就能把所有STATUS_DISK_SLEEP状态的进程筛出来再往它们各自的 IO 统计上看一眼立刻锁定是某块云盘性能骤降。这个排查思路用传统top很难一眼得出但 psutil 只需几十行代码就解决了。8.2 监控数据落地与日志体系和其他工具的配合psutil 拿到的监控数据如果不落地价值就少了一半。我通常的做法是把告警指标输出成 JSON 单行追加到日志文件里再交给 filebeat、prometheus 的 exporter 或者自研采集器去处理。这里有个关键最佳实践监控脚本本身必须轻量、稳定不要在主流程里做重数据处理。psutil 的调用耗时受环境因素影响不小遍历几百个进程时最差能达到秒级所以高频数据采集比如每秒一次只建议采集 CPU、内存这类轻量指标全量进程遍历可以放到低频率巡检任务里。顺手分享一个我在多台机器上部署的经验用cron或 systemd timer 运行监控脚本而不是用while True常驻。原因有两点一是常驻进程崩溃后很难自动恢复二是定时任务天然适合“每次巡检、输出结果、退出”的模式也方便单独测试某一次执行。只有需要持续平滑采集的场景才适合常驻进程加interval采样。实践里我会根据指标特性混搭慢指标用定时任务快指标用常驻进程再统一收集到同一个数据管道里。我的实操体会psutil 陪我排查过无数线上问题从最开始的 “用 Python 看服务器状态” 到后来的自动告警、进程守护、数据采集它的 API 始终没给我找过麻烦。如果你刚开始接触我建议不要贪多先把cpu_percent(interval1)、virtual_memory().percent、disk_usage(/)、net_io_counters()这四个函数吃透写一个每秒刷新的小面板你会立刻找到掌控一台机器的感觉。然后再去啃process_iter()和异常处理逐步构建自己的进程级监控能力。踩过权限、单位、首次数值这些坑之后这个库用起来会非常顺手也会成为你工具链里不可或缺的一环。
返回列表