Python进阶探索24_应用案例_linux系统的监控

文章目录

一、痛点场景:凌晨三点的服务器惊魂夜

你有没有经历过这样的夜晚?凌晨三点,手机突然响起钉钉告警,你睡眼惺忪地爬起来,发现线上一台核心API服务器已经处于半瘫痪状态。CPU使用率飙到百分之百,内存被吃干耗尽触发了OOM Killer,根分区磁盘也被日志写满,连ls命令都开始报错。业务接口大面积超时,用户投诉像雪片一样飞来,你一边救火一边暗骂:为什么没有一个靠谱的监控系统能提前告诉我这些?

这不是个例。在企业运维场景中,以下几个痛点几乎是每个团队的噩梦:

痛点一:发现即晚期。 传统的人工巡检往往是每天早上看一眼服务器状态,等发现问题时业务已经受损。就像古代守城的士兵,等敌人已经爬上城墙才敲警钟,为时已晚。

痛点二:命令碎片化。 查CPU用top,查内存用free,查磁盘用df,查网络用netstat,查进程用ps。十几个命令来回切换,输出格式各不相同,想把数据汇总到一个报表里简直是一场灾难。

痛点三:无法自动化。 Shell脚本虽然能做一些简单的采集,但解析命令输出、处理异常、跨平台兼容,每一项都是坑。想写一个自动告警的脚本,往往比写业务代码还费劲。

痛点四:历史数据丢失。 top和htop只能看实时状态,关掉终端数据就没了。想回溯昨天凌晨两点CPU为什么飙高,只能靠猜。

正如《黄帝内经》所言:"上工治未病,不治已病。" 监控的最高境界,是在故障发生之前就发现端倪,而不是等服务挂了再去救火。那么,有没有一把瑞士军刀,能把这些痛点一次性解决?答案就是------psutil。

二、解决方案:psutil登场

psutil(Process and System Utilities)是一个Python第三方库,它的核心价值可以用一句话概括:用统一的Python接口,获取系统运行的一切关键指标。

它就像给服务器装了一个全面的"体检仪",CPU、内存、磁盘、网络、进程,所有数据都能通过几行Python代码拿到,而且返回的是结构化的数据对象,不是需要正则解析的文本输出。

专业解释

psutil是一个跨平台的Python库,用于检索运行中进程的信息和系统利用率(CPU、内存、磁盘、网络、传感器)。它主要用于系统监控、性能分析、限制进程资源和管理运行中的进程。它实现了经典UNIX命令行工具(如ps、top、iotop、lsof、netstat、ifconfig、free等)提供的许多功能,目前支持Linux、Windows、macOS、FreeBSD、Solaris等主流平台。

大白话

你就把psutil想象成一个"全能翻译官"。以前你要跟Linux操作系统打听服务器状态,得学会十几种方言(top、free、df、netstat......),每种方言的语法还不一样。现在有了psutil这个翻译官,你只用说Python这一种语言,它就能帮你把所有信息都问回来,而且整理得清清楚楚,直接给你结构化的数据,不用你自己再去抠文本。

生活案例

就像你去医院做体检。以前的方式是:测血压去一个科室,抽血去另一个科室,做B超再换一个楼,每个科室给你一张不同格式的报告单,你得自己拼起来看。而psutil就像一个高端体检中心,抽一次血、做一次全身扫描,所有指标(CPU、内存、磁盘、网络)都在一张报告里,数据格式统一,还能自动跟正常范围对比,异常项直接标红。

古代典故

三国时期,诸葛亮北伐中原,最头疼的就是粮草运输和军情传递。蜀道难行,各路斥候回报的消息格式不一、时间不一,常常导致决策延误。如果当时有psutil这样的"统一情报系统",所有前线的粮草消耗、兵力部署、道路状况都用统一格式实时汇总,诸葛亮的北伐或许会是另一番局面。这就是统一接口的力量。

三、psutil是什么

3.1 官方定义

根据psutil官方文档的定义,psutil是一个跨平台库,能够轻松实现获取系统运行的进程和系统利用率信息。它的名字来源于"process and system utilities"的缩写,直译就是"进程与系统工具集"。

3.2 核心能力矩阵

psutil的能力可以分为五大维度:

维度 核心API 对应命令行工具 能获取什么
CPU cpu_percent(), cpu_times(), cpu_count() top, mpstat 使用率、核心数、负载、频率
内存 virtual_memory(), swap_memory() free, vmstat 总量、已用、可用、交换分区
磁盘 disk_usage(), disk_io_counters(), disk_partitions() df, iostat 分区使用、IO读写、挂载信息
网络 net_io_counters(), net_connections(), net_if_addrs() netstat, ifconfig, ss 流量统计、连接状态、网卡地址
进程 process_iter(), Process() ps, kill, lsof PID、名称、资源占用、终止进程

3.3 一个最简单的例子

先来看一段代码,感受一下psutil的简洁:

python 复制代码
import psutil

# 获取CPU使用率
cpu_percent = psutil.cpu_percent(interval=1)
print(f"CPU使用率: {cpu_percent}%")

# 获取内存信息
mem = psutil.virtual_memory()
print(f"内存总量: {mem.total / 1024**3:.2f} GB")
print(f"内存已用: {mem.used / 1024**3:.2f} GB")
print(f"内存使用率: {mem.percent}%")

# 获取磁盘使用情况
disk = psutil.disk_usage("/")
print(f"磁盘使用率: {disk.percent}%")

运行输出:

text 复制代码
CPU使用率: 12.5%
内存总量: 16.00 GB
内存已用: 8.32 GB
内存使用率: 52.0%
磁盘使用率: 67.3%

看到了吗?三行代码拿到三个核心指标,返回的都是可以直接使用的数值,不需要任何文本解析。这就是psutil的魅力。

四、为什么选择psutil

4.1 跨平台一致性

psutil最大的优势之一是跨平台。同样一段代码,在Linux、Windows、macOS上都能运行,返回的数据结构完全一致。对于需要管理混合环境的企业来说,这意味着一套监控代码可以覆盖所有服务器,不用为每个操作系统写不同的逻辑。

python 复制代码
import psutil
import platform

print(f"当前系统: {platform.system()}")
print(f"CPU核心数: {psutil.cpu_count()}")
print(f"内存总量: {psutil.virtual_memory().total / 1024**3:.2f} GB")

这段代码在任何支持的平台上都能正常工作,这是Shell脚本永远做不到的。

4.2 结构化数据,告别正则

用Shell脚本做监控,最痛苦的部分就是解析命令输出。比如解析free命令的输出:

bash 复制代码
# Shell方式:需要awk来提取字段
free -m | awk 'NR==2{print "内存使用率: "$3/$2*100"%"}'

而psutil直接返回对象属性:

python 复制代码
# Python方式:直接访问属性
mem = psutil.virtual_memory()
print(f"内存使用率: {mem.percent}%")

没有正则,没有awk,没有字段偏移,代码可读性和可维护性提升了一个量级。

4.3 可编程性强,可嵌入业务系统

psutil是一个库,不是一个独立工具。这意味着你可以把它直接嵌入到你的Python应用中,在业务代码里实时获取系统状态。比如在一个Web服务中,你可以在每次请求处理时检查当前服务器负载,如果负载过高就自动降级或限流。

python 复制代码
from flask import Flask, jsonify
import psutil

app = Flask(__name__)

@app.route("/api/health")
def health_check():
    return jsonify({
        "cpu_percent": psutil.cpu_percent(interval=0.5),
        "memory_percent": psutil.virtual_memory().percent,
        "disk_percent": psutil.disk_usage("/").percent,
        "status": "healthy" if psutil.cpu_percent() < 80 else "warning"
    })

4.4 轻量高效,资源消耗极低

psutil本身是用Python加C扩展实现的,采集数据时的资源开销非常小。相比Glances这类完整的监控工具,psutil只做数据采集这一件事,把展示和告警的灵活性完全交给开发者。

五、psutil的演进历程

了解一个工具的历史,能帮助我们更好地理解它的设计哲学。psutil的发展可以分为几个关键阶段:

5.1 诞生期(2009年)

psutil由意大利开发者Giampaolo Rodola于2009年创建。最初的动机很简单:作者需要一个能在Python中获取进程和系统信息的库,但当时没有一个令人满意的解决方案。早期版本(0.1.0到0.4.x)主要聚焦Linux平台,提供了基本的进程信息获取功能。

5.2 跨平台期(2010-2013年)

从0.5版本开始,psutil逐步加入了Windows和macOS的支持,开始向真正的跨平台库演进。这个阶段的核心工作是为不同操作系统实现统一的API接口,把底层的系统调用差异封装起来。到2012年的0.5.1版本,psutil已经支持Linux、Windows、OS X和FreeBSD四大平台。

5.3 API重构期(2014年)

2014年发布的psutil 2.0是一个里程碑版本。作者对API进行了重大重构,目标是一致性和易用性。很多旧的方法名被重命名,比如get_cpu_percent()改为cpu_percent(),get_memory_info()改为memory_info()。这次重构虽然造成了不兼容,但奠定了后续版本的API基础,让psutil的接口变得更加统一和直觉化。

5.4 生态成熟期(2015-2020年)

从3.0到5.x版本,psutil进入了功能丰富和生态成熟阶段。新增了传感器支持(温度、电池)、进程网络连接监控、磁盘IO统计、Windows服务管理等功能。5.0版本之后,psutil成为Python生态中系统监控领域的事实标准,被Glances、Ansible、SaltStack等众多知名项目使用。

值得一提的是,psutil的一些设计甚至反哺了Python标准库。2011年,psutil的disk_usage()功能启发了Python 3.3中shutil.disk_usage()的加入;同样在Python 3.3中,os.getpriority()和os.setpriority()的加入也受到了psutil的影响。

5.5 现代化期(2021年至今)

2025年,psutil 7.0版本正式放弃了对Python 2.7的支持,全面转向Python 3。最新的6.x/7.x版本在性能上做了大量优化,比如Windows平台上某些API调用速度提升了百分之五十到百分之百。同时继续完善对新操作系统版本的适配。

从2009年到今天,psutil已经走过了十七年的历程,GitHub星标超过一万,是Python生态中最活跃、最可靠的系统工具库之一。

六、psutil怎么用

6.1 安装

psutil的安装非常简单,一条命令搞定:

bash 复制代码
pip install psutil

如果是在Linux系统上,也可以用系统包管理器安装:

bash 复制代码
# Ubuntu/Debian
sudo apt-get install python3-psutil

# CentOS/RHEL
sudo yum install python3-psutil

验证安装:

python 复制代码
import psutil
print(psutil.__version__)

6.2 获取主机信息

这是原教程中的核心案例,我们来详细讲解。获取当前主机信息,包含操作系统名、主机名、内核版本、硬件架构等:

python 复制代码
import os
import psutil
from datetime import datetime

print('主机信息'.center(50, '*'))
info = os.uname()
print("""
    操作系统:%s
    主机名称:%s
    内核版本:%s
    硬件架构:%s
""" % (info.sysname, info.nodename, info.release, info.machine))

运行输出:

text 复制代码
********************主机信息********************

    操作系统:Linux
    主机名称:web-server-01
    内核版本:5.15.0-91-generic
    硬件架构:x86_64

代码解析:

  • os.uname()是Python标准库提供的函数,返回一个包含系统信息的元组对象
  • info.sysname:操作系统名称,如Linux
  • info.nodename:主机名
  • info.release:内核版本号
  • info.machine:硬件架构,如x86_64、aarch64

注意:os.uname()在Windows系统上不可用,如果需要跨平台获取主机信息,可以使用platform模块:

python 复制代码
import platform

print(f"操作系统: {platform.system()}")
print(f"主机名称: {platform.node()}")
print(f"内核版本: {platform.release()}")
print(f"硬件架构: {platform.machine()}")

6.3 获取开机时间和开机时长

python 复制代码
print('开机信息'.center(50, '*'))
# boot_time返回的是一个时间戳
boot_time = psutil.boot_time()
# 将时间戳转化为datetime类型的时间
boot_time_obj = datetime.fromtimestamp(boot_time)
now_time = datetime.now()
delta_time = now_time - boot_time_obj

print('开机时间:', boot_time_obj)
# str是为了将时间对象转换为字符串,实现分离
print('当前时间:', str(now_time).split('.')[0])
print('开机时长:', str(delta_time).split('.')[0])

运行输出:

text 复制代码
********************开机信息********************
开机时间: 2026-09-28 08:15:32
当前时间: 2026-10-02 14:30:45
开机时长: 4 days, 6:15:13

代码解析:

  • psutil.boot_time()返回系统开机时间的时间戳(Unix时间戳,单位秒)
  • datetime.fromtimestamp()将时间戳转换为人类可读的datetime对象
  • now_time - boot_time_obj得到一个timedelta对象,表示已经运行了多长时间
  • str(delta_time).split('.')[0]是为了去掉微秒部分,让输出更整洁

6.4 获取当前登录用户

python 复制代码
print('当前登陆用户'.center(50, '*'))
login_user = psutil.users()
print(login_user)
info = psutil.users()[0]
print(info.name)

运行输出:

text 复制代码
******************当前登陆用户******************
[suser(name='root', terminal='pts/0', host='192.168.1.100', started=1727834567.0, pid=12345)]
root

代码解析:

  • psutil.users()返回当前登录用户的列表,每个元素是一个suser命名元组
  • name:用户名
  • terminal:登录终端,如pts/0表示远程终端,tty1表示本地终端
  • host:远程登录的来源IP
  • started:登录时间的时间戳
  • pid:对应登录进程的PID

6.5 CPU监控详解

CPU监控是psutil最常用的功能之一,涉及多个API:

python 复制代码
import psutil

# 1. CPU使用率(interval=1表示统计1秒内的平均值)
cpu_percent = psutil.cpu_percent(interval=1)
print(f"CPU总使用率: {cpu_percent}%")

# 2. 每个核心的使用率
cpu_percent_per_core = psutil.cpu_percent(interval=1, percpu=True)
print(f"各核心使用率: {cpu_percent_per_core}")

# 3. CPU核心数
print(f"逻辑核心数: {psutil.cpu_count()}")
print(f"物理核心数: {psutil.cpu_count(logical=False)}")

# 4. CPU时间统计
cpu_times = psutil.cpu_times()
print(f"用户态时间: {cpu_times.user:.1f}s")
print(f"系统态时间: {cpu_times.system:.1f}s")
print(f"空闲时间: {cpu_times.idle:.1f}s")
print(f"IO等待时间: {cpu_times.iowait:.1f}s")

# 5. 系统负载(1/5/15分钟)
load_avg = psutil.getloadavg()
print(f"1分钟负载: {load_avg[0]:.2f}")
print(f"5分钟负载: {load_avg[1]:.2f}")
print(f"15分钟负载: {load_avg[2]:.2f}")

# 6. CPU频率
cpu_freq = psutil.cpu_freq()
print(f"当前频率: {cpu_freq.current:.0f} MHz")
print(f"最小频率: {cpu_freq.min:.0f} MHz")
print(f"最大频率: {cpu_freq.max:.0f} MHz")

关键知识点:interval参数的原理

cpu_percent(interval=1)中的interval参数非常重要。当interval > 0时,psutil会先记录当前CPU时间,然后阻塞等待interval秒,再记录一次CPU时间,通过两次时间差计算出这段时间内的平均使用率。当interval = 0或None时,它会比较自上次调用以来的CPU时间,第一次调用会返回0.0。

这就是为什么在循环中调用时,第一次总是0.0的原因:

python 复制代码
import psutil
import time

# 错误用法:第一次总是0.0
for i in range(3):
    print(psutil.cpu_percent())  # 第一次是0.0
    time.sleep(1)

# 正确用法:使用interval参数
for i in range(3):
    print(psutil.cpu_percent(interval=1))  # 每次都是准确的

6.6 内存监控详解

python 复制代码
import psutil

# 物理内存
mem = psutil.virtual_memory()
print(f"内存总量: {mem.total / 1024**3:.2f} GB")
print(f"已用内存: {mem.used / 1024**3:.2f} GB")
print(f"可用内存: {mem.available / 1024**3:.2f} GB")
print(f"空闲内存: {mem.free / 1024**3:.2f} GB")
print(f"内存使用率: {mem.percent}%")
print(f"缓冲/缓存: {mem.buffers + mem.cached / 1024**3:.2f} GB")

# 交换分区
swap = psutil.swap_memory()
print(f"交换分区总量: {swap.total / 1024**3:.2f} GB")
print(f"交换分区已用: {swap.used / 1024**3:.2f} GB")
print(f"交换分区使用率: {swap.percent}%")

关键知识点:available和free的区别

这是面试高频考点。free是真正完全空闲的内存,而available是应用程序实际可用的内存,它包含了可以被回收的缓存和缓冲区内存。Linux系统会尽量利用空闲内存做文件缓存,所以free通常很小,但available可能很大。判断内存是否紧张应该看available,而不是free。

6.7 磁盘监控详解

python 复制代码
import psutil

# 磁盘分区信息
partitions = psutil.disk_partitions()
for p in partitions:
    print(f"设备: {p.device}, 挂载点: {p.mountpoint}, 文件系统: {p.fstype}")

# 指定分区的使用情况
disk = psutil.disk_usage("/")
print(f"根分区总量: {disk.total / 1024**3:.2f} GB")
print(f"根分区已用: {disk.used / 1024**3:.2f} GB")
print(f"根分区剩余: {disk.free / 1024**3:.2f} GB")
print(f"根分区使用率: {disk.percent}%")

# 磁盘IO统计
disk_io = psutil.disk_io_counters()
print(f"累计读字节: {disk_io.read_bytes / 1024**3:.2f} GB")
print(f"累计写字节: {disk_io.write_bytes / 1024**3:.2f} GB")
print(f"累计读次数: {disk_io.read_count}")
print(f"累计写次数: {disk_io.write_count}")

企业级注意事项:Inode监控

很多新手只监控磁盘空间,忽略了Inode使用率。在大量小文件的场景下(比如邮件服务器、缓存目录),磁盘空间还没满,但Inode已经耗尽,同样会导致无法写入文件。psutil没有直接提供Inode统计的API,需要用os.statvfs()补充:

python 复制代码
import os

def get_inode_usage(path):
    stat = os.statvfs(path)
    total = stat.f_files
    free = stat.f_ffree
    used_percent = (total - free) / total * 100
    return total, free, used_percent

total, free, percent = get_inode_usage("/")
print(f"Inode总量: {total}")
print(f"Inode可用: {free}")
print(f"Inode使用率: {percent:.1f}%")

6.8 网络监控详解

python 复制代码
import psutil

# 网络流量统计
net_io = psutil.net_io_counters()
print(f"累计发送字节: {net_io.bytes_sent / 1024**3:.2f} GB")
print(f"累计接收字节: {net_io.bytes_recv / 1024**3:.2f} GB")
print(f"累计发送包数: {net_io.packets_sent}")
print(f"累计接收包数: {net_io.packets_recv}")
print(f"收包错误数: {net_io.errin}")
print(f"丢包数: {net_io.dropin}")

# 网卡信息
net_if = psutil.net_if_addrs()
for name, addrs in net_if.items():
    for addr in addrs:
        if addr.family.name == 'AF_INET':
            print(f"网卡: {name}, IP: {addr.address}")

# 网络连接(需要root权限)
connections = psutil.net_connections(kind='tcp')
print(f"当前TCP连接数: {len(connections)}")
for conn in connections[:5]:  # 只看前5个
    print(f"  本地地址: {conn.laddr}, 远程地址: {conn.raddr}, 状态: {conn.status}")

6.9 进程管理

python 复制代码
import psutil

# 遍历所有进程
for proc in psutil.process_iter(['pid', 'name', 'cpu_percent', 'memory_percent']):
    try:
        info = proc.info
        print(f"PID: {info['pid']}, 名称: {info['name']}, "
              f"CPU: {info['cpu_percent']}%, 内存: {info['memory_percent']:.1f}%")
    except (psutil.NoSuchProcess, psutil.AccessDenied):
        pass

# 获取Top 5 CPU占用进程
processes = []
for proc in psutil.process_iter(['pid', 'name', 'cpu_percent']):
    try:
        processes.append(proc.info)
    except (psutil.NoSuchProcess, psutil.AccessDenied):
        pass

top5 = sorted(processes, key=lambda x: x['cpu_percent'], reverse=True)[:5]
for p in top5:
    print(f"PID:{p['pid']} {p['name']} CPU:{p['cpu_percent']}%")

# 操作指定进程
pid = 12345
proc = psutil.Process(pid)
print(f"进程名: {proc.name()}")
print(f"进程状态: {proc.status()}")
print(f"进程创建时间: {proc.create_time()}")
# proc.terminate()  # 优雅终止
# proc.kill()       # 强制杀死

七、企业项目实战:自动巡检告警系统

理论知识讲完了,现在来看一个真正能在企业中落地的项目。我们将从零构建一个工业级的Linux服务器自动巡检系统,具备以下功能:

  • 定时采集CPU、内存、磁盘、Inode、网络、进程等指标
  • 阈值判断,异常自动告警
  • 钉钉机器人通知
  • 生成HTML巡检报表
  • crontab定时调度

7.1 项目结构

text 复制代码
server_monitor/
├── config.yaml          # 配置文件
├── monitor.py           # 主程序
├── collector.py         # 数据采集模块
├── notifier.py          # 告警通知模块
├── report.py            # 报表生成模块
├── requirements.txt     # 依赖
└── reports/             # 报表输出目录

7.2 配置文件

yaml 复制代码
# config.yaml
thresholds:
  cpu_percent: 85          # CPU使用率告警阈值
  memory_percent: 90       # 内存使用率告警阈值
  disk_percent: 85         # 磁盘使用率告警阈值
  inode_percent: 80        # Inode使用率告警阈值
  load_avg: 10             # 1分钟负载阈值

monitor:
  interval: 60             # 采集间隔(秒)
  check_processes:         # 需要监控的关键进程
    - nginx
    - mysql
    - redis
  check_ports:             # 需要监控的关键端口
    - 80
    - 443
    - 3306

alert:
  enabled: true
  dingtalk_webhook: "https://oapi.dingtalk.com/robot/send?access_token=你的token"
  dingtalk_secret: "你的加签密钥"

report:
  output_dir: "./reports"
  retain_days: 30

7.3 数据采集模块

python 复制代码
# collector.py
import psutil
import os
import socket
import platform
from datetime import datetime


class SystemCollector:
    """系统指标采集器"""

    def collect_all(self):
        """采集所有指标"""
        return {
            "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
            "hostname": socket.gethostname(),
            "os": platform.system(),
            "kernel": platform.release(),
            "cpu": self._collect_cpu(),
            "memory": self._collect_memory(),
            "disk": self._collect_disk(),
            "network": self._collect_network(),
            "process": self._collect_process(),
            "boot_time": self._collect_boot_time(),
        }

    def _collect_cpu(self):
        return {
            "percent": psutil.cpu_percent(interval=1),
            "percpu": psutil.cpu_percent(interval=0, percpu=True),
            "count_logical": psutil.cpu_count(),
            "count_physical": psutil.cpu_count(logical=False),
            "load_avg": psutil.getloadavg(),
        }

    def _collect_memory(self):
        mem = psutil.virtual_memory()
        swap = psutil.swap_memory()
        return {
            "total_gb": round(mem.total / 1024**3, 2),
            "used_gb": round(mem.used / 1024**3, 2),
            "available_gb": round(mem.available / 1024**3, 2),
            "percent": mem.percent,
            "swap_total_gb": round(swap.total / 1024**3, 2),
            "swap_used_gb": round(swap.used / 1024**3, 2),
            "swap_percent": swap.percent,
        }

    def _collect_disk(self):
        disks = []
        for part in psutil.disk_partitions():
            try:
                usage = psutil.disk_usage(part.mountpoint)
                inode_total, inode_free, inode_percent = self._get_inode(part.mountpoint)
                disks.append({
                    "device": part.device,
                    "mountpoint": part.mountpoint,
                    "fstype": part.fstype,
                    "total_gb": round(usage.total / 1024**3, 2),
                    "used_gb": round(usage.used / 1024**3, 2),
                    "free_gb": round(usage.free / 1024**3, 2),
                    "percent": usage.percent,
                    "inode_percent": round(inode_percent, 1),
                })
            except (PermissionError, FileNotFoundError):
                continue
        return disks

    def _get_inode(self, path):
        stat = os.statvfs(path)
        total = stat.f_files
        free = stat.f_ffree
        used_percent = (total - free) / total * 100 if total > 0 else 0
        return total, free, used_percent

    def _collect_network(self):
        net = psutil.net_io_counters()
        return {
            "bytes_sent_gb": round(net.bytes_sent / 1024**3, 2),
            "bytes_recv_gb": round(net.bytes_recv / 1024**3, 2),
            "packets_sent": net.packets_sent,
            "packets_recv": net.packets_recv,
            "errin": net.errin,
            "errout": net.errout,
            "dropin": net.dropin,
            "dropout": net.dropout,
            "tcp_connections": len(psutil.net_connections(kind='tcp')),
        }

    def _collect_process(self):
        processes = []
        for proc in psutil.process_iter(['pid', 'name', 'cpu_percent', 'memory_percent', 'status']):
            try:
                info = proc.info
                info['memory_percent'] = round(info['memory_percent'], 1)
                processes.append(info)
            except (psutil.NoSuchProcess, psutil.AccessDenied):
                pass
        # 按CPU排序取Top10
        top_cpu = sorted(processes, key=lambda x: x['cpu_percent'], reverse=True)[:10]
        top_mem = sorted(processes, key=lambda x: x['memory_percent'], reverse=True)[:10]
        # 僵尸进程
        zombies = [p for p in processes if p['status'] == psutil.STATUS_ZOMBIE]
        return {
            "total_count": len(processes),
            "zombie_count": len(zombies),
            "top_cpu": top_cpu,
            "top_mem": top_mem,
        }

    def _collect_boot_time(self):
        boot_time = datetime.fromtimestamp(psutil.boot_time())
        uptime = datetime.now() - boot_time
        return {
            "boot_time": boot_time.strftime("%Y-%m-%d %H:%M:%S"),
            "uptime": str(uptime).split('.')[0],
        }

7.4 告警通知模块

python 复制代码
# notifier.py
import time
import hmac
import hashlib
import base64
import urllib.parse
import requests
import json


class DingTalkNotifier:
    """钉钉机器人告警通知"""

    def __init__(self, webhook, secret):
        self.webhook = webhook
        self.secret = secret

    def _sign(self):
        """生成钉钉加签"""
        timestamp = str(round(time.time() * 1000))
        string_to_sign = f'{timestamp}\n{self.secret}'
        hmac_code = hmac.new(
            self.secret.encode('utf-8'),
            string_to_sign.encode('utf-8'),
            digestmod=hashlib.sha256
        ).digest()
        sign = urllib.parse.quote_plus(base64.b64encode(hmac_code))
        return f"{self.webhook}&timestamp={timestamp}&sign={sign}"

    def send_alert(self, title, alerts):
        """发送告警消息
        alerts: list of dict, 每个包含 level, metric, value, threshold, message
        """
        if not alerts:
            return

        text = f"### {title}\n\n"
        text += f"**告警时间**: {time.strftime('%Y-%m-%d %H:%M:%S')}\n\n"
        text += "**异常项**:\n\n"

        for a in alerts:
            emoji = "严重" if a['level'] == 'critical' else "警告"
            text += f"- **[{emoji}]** {a['metric']}: 当前 {a['value']}, 阈值 {a['threshold']}\n"
            text += f"  - {a['message']}\n"

        data = {
            "msgtype": "markdown",
            "markdown": {
                "title": title,
                "text": text
            },
            "at": {
                "isAtAll": True
            }
        }

        url = self._sign()
        resp = requests.post(url, json=data, timeout=10)
        return resp.json()

7.5 主程序入口

python 复制代码
# monitor.py
import yaml
import time
import logging
from collector import SystemCollector
from notifier import DingTalkNotifier
from report import ReportGenerator

logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)


def load_config(config_path='config.yaml'):
    with open(config_path, 'r', encoding='utf-8') as f:
        return yaml.safe_load(f)


def check_thresholds(data, thresholds):
    """检查指标是否超过阈值,返回告警列表"""
    alerts = []

    # CPU检查
    if data['cpu']['percent'] > thresholds['cpu_percent']:
        alerts.append({
            'level': 'critical',
            'metric': 'CPU使用率',
            'value': f"{data['cpu']['percent']}%",
            'threshold': f"{thresholds['cpu_percent']}%",
            'message': 'CPU使用率过高,可能存在计算密集型任务或异常进程'
        })

    # 负载检查
    if data['cpu']['load_avg'][0] > thresholds['load_avg']:
        alerts.append({
            'level': 'warning',
            'metric': '系统负载(1分钟)',
            'value': f"{data['cpu']['load_avg'][0]:.2f}",
            'threshold': f"{thresholds['load_avg']}",
            'message': '系统负载过高,任务排队严重'
        })

    # 内存检查
    if data['memory']['percent'] > thresholds['memory_percent']:
        alerts.append({
            'level': 'critical',
            'metric': '内存使用率',
            'value': f"{data['memory']['percent']}%",
            'threshold': f"{thresholds['memory_percent']}%",
            'message': '内存使用率过高,可能触发OOM Killer'
        })

    # 磁盘检查
    for disk in data['disk']:
        if disk['percent'] > thresholds['disk_percent']:
            alerts.append({
                'level': 'critical',
                'metric': f"磁盘使用率({disk['mountpoint']})",
                'value': f"{disk['percent']}%",
                'threshold': f"{thresholds['disk_percent']}%",
                'message': f"分区 {disk['mountpoint']} 空间不足"
            })
        if disk['inode_percent'] > thresholds['inode_percent']:
            alerts.append({
                'level': 'warning',
                'metric': f"Inode使用率({disk['mountpoint']})",
                'value': f"{disk['inode_percent']}%",
                'threshold': f"{thresholds['inode_percent']}%",
                'message': f"分区 {disk['mountpoint']} Inode不足,可能无法创建新文件"
            })

    # 僵尸进程检查
    if data['process']['zombie_count'] > 0:
        alerts.append({
            'level': 'warning',
            'metric': '僵尸进程',
            'value': f"{data['process']['zombie_count']}个",
            'threshold': "0个",
            'message': '存在僵尸进程,父进程可能未正确回收子进程'
        })

    return alerts


def main():
    config = load_config()
    collector = SystemCollector()
    notifier = DingTalkNotifier(
        config['alert']['dingtalk_webhook'],
        config['alert']['dingtalk_secret']
    )
    reporter = ReportGenerator(config['report']['output_dir'])

    logger.info("开始采集系统指标...")
    data = collector.collect_all()

    logger.info("检查阈值...")
    alerts = check_thresholds(data, config['thresholds'])

    if alerts and config['alert']['enabled']:
        logger.warning(f"发现 {len(alerts)} 个异常项,发送告警...")
        notifier.send_alert(f"服务器告警 - {data['hostname']}", alerts)
    else:
        logger.info("所有指标正常")

    logger.info("生成巡检报表...")
    report_path = reporter.generate(data, alerts)
    logger.info(f"报表已生成: {report_path}")


if __name__ == '__main__':
    main()

7.6 定时调度

使用crontab实现每小时自动巡检:

bash 复制代码
# 编辑crontab
crontab -e

# 添加以下内容(每小时第0分执行)
0 * * * * cd /opt/server_monitor && /usr/bin/python3 monitor.py >> /var/log/server_monitor.log 2>&1

这样,一个完整的企业级自动巡检告警系统就搭建完成了。它可以在无人值守的情况下,每小时自动检查服务器状态,发现异常立即通过钉钉通知运维人员,同时生成HTML报表留档。

八、竞品对比:psutil vs Glances vs top/htop

选择工具时,了解各方案的优劣势至关重要。下面从多个维度对最常用的几种监控方案进行对比:

对比维度 psutil Glances top/htop Prometheus + Node Exporter
类型 Python库 CLI/Web工具 系统命令 完整监控系统
安装方式 pip install pip/apt 系统自带 二进制部署
跨平台 支持Linux/Windows/macOS 支持Linux/Windows/macOS top全平台,htop主要Linux 全平台
可编程性 极强,可嵌入业务代码 有限,通过API/插件 无,只能解析输出 通过PromQL查询
数据展示 需自行实现 内置CLI/Web UI 终端实时展示 Grafana仪表盘
告警能力 需自行实现 有限,需配合其他工具 无 内置Alertmanager
历史数据 需自行存储 可导出到InfluxDB等 无,仅实时 内置时序数据库
资源消耗 极低(仅采集) 中等 极低 较高
学习成本 低(Python API) 低(命令即用) 极低 高(整套体系)
适用场景 嵌入式监控、自动化脚本、定制化 单机快速查看、远程监控 临时排查、极简环境 大规模集群、长期监控

各工具的适用场景分析

什么时候用psutil?

  • 你需要在Python应用中嵌入系统监控逻辑
  • 你需要写自动化运维脚本,对数据进行定制化处理
  • 你需要跨平台的统一监控代码
  • 你的监控需求比较特殊,现成工具满足不了

什么时候用Glances?

  • 你需要一个开箱即用的全功能监控工具
  • 你需要通过Web界面远程查看服务器状态
  • 你需要快速把数据导出到InfluxDB/Prometheus
  • 你不想写代码,但想要比top更丰富的信息

什么时候用top/htop?

  • 你在一个最小化安装的服务器上,没有额外工具
  • 你只是临时看一下进程和资源状态
  • 你需要极快启动、零依赖的查看方式
  • 资源极其有限的嵌入式环境

什么时候用Prometheus?

  • 你管理的是大规模服务器集群
  • 你需要长期存储历史数据并做趋势分析
  • 你需要复杂的告警规则和多渠道通知
  • 你需要和Kubernetes等容器编排平台集成

一句话总结:psutil是"零件",Glances是"成品工具",top是"瑞士军刀的小刀片",Prometheus是"整套工厂流水线"。根据你的需求选择合适的层级,不要用大炮打蚊子,也不要用螺丝刀拧螺丝帽。

九、常用场景汇总

psutil在实际工作中的应用场景非常广泛,以下是最常见的几类:

9.1 服务器健康检查接口

在微服务架构中,每个服务都应该提供健康检查端点。psutil可以让你的服务同时报告自身健康状态和服务器资源状态:

python 复制代码
from fastapi import FastAPI
import psutil

app = FastAPI()

@app.get("/health")
def health():
    return {
        "status": "ok",
        "server": {
            "cpu": psutil.cpu_percent(interval=0.5),
            "memory": psutil.virtual_memory().percent,
            "disk": psutil.disk_usage("/").percent,
            "uptime": str(psutil.boot_time())
        }
    }

9.2 进程守护与自动重启

监控关键进程是否存活,如果挂了就自动重启:

python 复制代码
import psutil
import subprocess
import time

def is_process_running(process_name):
    for proc in psutil.process_iter(['name']):
        try:
            if process_name.lower() in proc.info['name'].lower():
                return True
        except (psutil.NoSuchProcess, psutil.AccessDenied):
            pass
    return False

def restart_service(service_name):
    subprocess.run(['systemctl', 'restart', service_name], check=True)

while True:
    if not is_process_running('nginx'):
        print("nginx进程不存在,尝试重启...")
        restart_service('nginx')
    time.sleep(30)

9.3 资源限流与降级

在应用中根据系统资源动态调整行为,比如CPU过高时拒绝非核心请求:

python 复制代码
import psutil
from functools import wraps

def system_guard(cpu_threshold=80, mem_threshold=85):
    """系统资源守卫装饰器"""
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            cpu = psutil.cpu_percent(interval=0.1)
            mem = psutil.virtual_memory().percent
            if cpu > cpu_threshold or mem > mem_threshold:
                return {"error": "系统繁忙,请稍后重试", "code": 503}
            return func(*args, **kwargs)
        return wrapper
    return decorator

@app.route("/api/heavy_task")
@system_guard(cpu_threshold=75, mem_threshold=80)
def heavy_task():
    # 执行计算密集型任务
    pass

9.4 日志文件大小监控

监控日志目录大小,超过阈值自动清理旧日志:

python 复制代码
import psutil
import os
import glob
from datetime import datetime, timedelta

def cleanup_logs(log_dir, max_size_gb=10, retain_days=7):
    """清理过期日志,控制目录大小"""
    # 计算目录总大小
    total_size = 0
    log_files = []
    for f in glob.glob(os.path.join(log_dir, "**/*.log"), recursive=True):
        size = os.path.getsize(f)
        mtime = os.path.getmtime(f)
        total_size += size
        log_files.append((f, size, mtime))

    # 超过阈值则按修改时间删除最旧的
    if total_size > max_size_gb * 1024**3:
        log_files.sort(key=lambda x: x[2])
        for f, size, mtime in log_files:
            if total_size <= max_size_gb * 1024**3:
                break
            os.remove(f)
            total_size -= size
            print(f"已删除: {f}")

    # 删除超过保留天数的
    cutoff = (datetime.now() - timedelta(days=retain_days)).timestamp()
    for f, size, mtime in log_files:
        if mtime < cutoff:
            os.remove(f)
            print(f"已删除过期日志: {f}")

9.5 Docker容器资源监控

结合docker模块和psutil,可以监控容器的资源使用:

python 复制代码
import docker
import psutil

client = docker.from_env()
for container in client.containers.list():
    stats = container.stats(stream=False)
    cpu_usage = stats['cpu_stats']['cpu_usage']['total_usage']
    memory_usage = stats['memory_stats']['usage']
    print(f"容器: {container.name}")
    print(f"  CPU使用: {cpu_usage / 1e9:.2f} 核秒")
    print(f"  内存使用: {memory_usage / 1024**3:.2f} GB")

十、面试官高频面试题

面试题一:psutil是什么?它支持哪些平台?

参考答案:

psutil(Process and System Utilities)是一个跨平台的Python第三方库,用于获取系统运行中进程的信息和系统利用率,包括CPU、内存、磁盘、网络、传感器等。它实现了ps、top、free、netstat、ifconfig等经典UNIX命令行工具的功能。

支持的平台包括Linux、Windows、macOS、FreeBSD、OpenBSD、NetBSD、Solaris等。它的最大优势是用统一的Python API屏蔽了不同操作系统的底层差异,让同一份监控代码可以在多个平台运行。

面试题二:cpu_percent(interval=1)中的interval参数是什么原理?为什么第一次调用返回0.0?

参考答案:

CPU使用率的计算原理是:在两个时间点分别采样CPU的总时间和各状态时间(用户态、系统态、空闲等),然后用非空闲时间的增量除以总时间增量,得到使用率百分比。

当interval > 0时,cpu_percent()会阻塞等待interval秒,在等待前后各采样一次,计算这段时间内的平均使用率。当interval = 0或None时,它会比较自上次调用以来的CPU时间。

第一次调用返回0.0的原因是:第一次调用时没有上一次的采样数据作为基准,只能记录当前时间点,无法计算差值,所以返回0.0。第二次调用时,用当前值减去第一次的值,才能算出真实的使用率。

面试题三:virtual_memory()中的available和free有什么区别?

参考答案:

这是最常考的内存概念题。

  • free:完全空闲、没有被使用的物理内存。Linux系统会尽量减少free内存,因为空闲内存是浪费,系统会用空闲内存做文件缓存(cached)和缓冲区(buffers)。
  • available:应用程序实际可使用的内存,它等于free + 可回收的缓存 + 可回收的缓冲区。当应用需要内存时,系统可以立即回收这些缓存来满足需求。

判断服务器内存是否紧张,应该看available而不是free。一台健康的Linux服务器,free可能很小(因为都拿去做缓存了),但available应该比较充足。如果available也很低,才说明内存真的不够用了。

面试题四:如何用psutil排查CPU使用率飙高的问题?

参考答案:

排查步骤如下:

  1. 先看整体CPU使用率和负载:psutil.cpu_percent(interval=1)和psutil.getloadavg(),确认是CPU密集还是IO等待。
  2. 看每个核心的使用率:psutil.cpu_percent(percpu=True),判断是单线程跑满一个核还是全核都高。
  3. 看CPU时间分布:psutil.cpu_times(),如果iowait高说明是磁盘IO瓶颈,如果user高说明是计算密集,如果system高说明是系统调用过多。
  4. 找出占用CPU最高的进程:用psutil.process_iter()遍历所有进程,按cpu_percent()排序取Top进程。
  5. 对可疑进程进一步分析:proc.cpu_times()看用户态和内核态时间,proc.num_threads()看线程数,proc.open_files()看打开的文件。
python 复制代码
import psutil

# 找出Top 5 CPU进程
procs = []
for p in psutil.process_iter(['pid', 'name', 'cpu_percent', 'memory_percent']):
    try:
        procs.append(p.info)
    except (psutil.NoSuchProcess, psutil.AccessDenied):
        pass

top5 = sorted(procs, key=lambda x: x['cpu_percent'], reverse=True)[:5]
for p in top5:
    print(f"PID:{p['pid']} {p['name']} CPU:{p['cpu_percent']}% MEM:{p['memory_percent']:.1f}%")

面试题五:psutil和top、Glances、Prometheus有什么区别?各自的适用场景是什么?

参考答案:

这四个工具处于不同的抽象层级:

  • top/htop:是操作系统自带的命令行工具,零依赖、启动快,但只能实时查看,无法编程、无法存储历史数据、无法告警。适合临时排查问题。
  • psutil:是Python库,只负责数据采集,把展示和告警的灵活性交给开发者。适合嵌入应用、写自动化脚本、做定制化监控。
  • Glances:是基于psutil构建的完整监控工具,内置CLI和Web UI,支持数据导出。适合单机快速查看和远程监控,但不适合大规模集群。
  • Prometheus + Node Exporter:是完整的企业级监控系统,包含数据采集、时序存储、查询引擎、告警管理。适合大规模集群的长期监控,但部署和维护成本高。

简单来说:临时排查用top,写脚本用psutil,单机看全貌用Glances,集群监控用Prometheus。

面试题六:如何设计一个基于psutil的服务器监控告警系统?

参考答案:

一个完整的监控告警系统应该包含以下模块:

  1. 数据采集层:用psutil定时采集CPU、内存、磁盘、网络、进程等指标,采集间隔根据需求设置(如60秒)。
  2. 数据存储层:将采集到的数据存入时序数据库(如InfluxDB)或简单的SQLite/CSV,用于历史趋势分析。
  3. 规则引擎层:定义阈值规则(CPU>85%、内存>90%、磁盘>85%),支持连续N次超过阈值才告警,避免抖动。
  4. 告警通知层:支持钉钉、企业微信、邮件、短信等多渠道通知,支持告警分级(警告/严重)和告警聚合。
  5. 报表展示层:生成HTML巡检报表或对接Grafana做可视化仪表盘。
  6. 调度层:用crontab或APScheduler做定时调度,保证程序异常退出后能自动恢复。

关键设计要点:采集本身不能影响系统性能(psutil很轻量)、告警要有去重和抑制机制、要监控Inode而不仅是磁盘空间、要处理僵尸进程和关键进程存活检测。

面试题七:process_iter()和pids()有什么区别?为什么推荐用process_iter()?

参考答案:

  • psutil.pids()返回当前所有进程PID的列表,然后你需要逐个用Process(pid)去获取信息。
  • psutil.process_iter()直接返回Process对象的迭代器,可以在迭代时指定需要的属性(如attrs=['pid', 'name', 'cpu_percent']),一次性获取。

推荐用process_iter()的原因:

  1. 避免竞态条件 :在pids()返回列表到你逐个Process(pid)之间,进程可能已经退出,导致大量NoSuchProcess异常。process_iter()在迭代时处理了这种情况。
  2. 性能更好 :指定attrs参数后,psutil会一次性获取所需属性,减少系统调用次数。
  3. 代码更简洁:直接迭代Process对象,不需要手动构造。

面试题八:监控磁盘时只看disk_usage().percent够吗?还有什么容易忽略的点?

参考答案:

不够。只监控磁盘空间使用率有两个常见的盲区:

  1. Inode使用率 :在大量小文件的场景(如邮件服务器、会话存储、缓存目录),磁盘空间还很充足,但Inode可能已经耗尽,导致无法创建新文件。需要用os.statvfs()补充监控Inode。
  2. 磁盘IO性能 :空间充足不代表磁盘正常。磁盘可能出现IO等待过高(iowait)、读写速度骤降、坏道等问题。需要用psutil.disk_io_counters()监控读写速率和IO等待时间。

另外还要注意:disk_partitions()默认不包含虚拟文件系统(如proc、sysfs),如果需要完整列表要传all=True参数;某些挂载点可能因为权限问题无法读取,需要异常处理。

十一、总结

psutil是Python生态中系统监控领域的基石级库。它用简洁统一的API,把原本需要十几个命令行工具才能获取的系统信息,封装成了几行Python代码就能搞定的结构化数据。

回顾本文的核心内容:

  1. 是什么:psutil是跨平台的进程与系统工具库,支持CPU、内存、磁盘、网络、进程五大维度的监控。
  2. 为什么用:跨平台一致性、结构化数据无需解析、可编程可嵌入、轻量高效。
  3. 演进历程:从2009年诞生到今天,十七年迭代,从Linux专用到全平台支持,甚至反哺了Python标准库。
  4. 怎么用:从安装到每个API的详细用法,再到企业级自动巡检系统的完整实现。
  5. 竞品对比:psutil是"零件",Glances是"成品",top是"小刀",Prometheus是"工厂",各有适用场景。
  6. 面试考点:interval原理、available vs free、CPU飙高排查、监控系统设计等。

正如《道德经》所言:"天下难事,必作于易;天下大事,必作于细。" 服务器监控看似是运维的基础工作,但真正做到精准、及时、自动化,需要对每一个细节都有深入的理解。psutil就是帮你把这些细节化繁为简的利器。

掌握了psutil,你就掌握了用Python对话操作系统的能力。无论是写自动化运维脚本、构建监控告警系统,还是在应用中嵌入资源保护逻辑,psutil都能成为你手中最可靠的工具。


本文为原创文章,如需转载,请联系作者获得授权,并注明出处。

相关推荐
happylifetree1 小时前
Python22-26:核心语法-流程控制语句-if条件判断
python
程序员清风1 小时前
PydanticAI 实战:用类型安全构建可靠的 Python Agent
开发语言·python·安全
落魄实习生1 小时前
Agent Scope Java 2.x 系列【10】Middleware
java·开发语言·ai
H.莓飛1 小时前
【数据结构】队列_OJ题
linux·数据结构·centos
H.莓飛2 小时前
【数据结构】栈_OJ题
linux·数据结构·算法·centos
梦想画家2 小时前
SQLMesh Python 模型入门(三):前后置语句、蓝图建模与避坑指南
大数据·python·sqlmesh
Ruiery2 小时前
Linux 6.6内核 CPU 深度解析(一):BSP 拉起 — 从实模式到 kernel_init 的完整旅程
linux·运维·服务器
Ramble_Naylor2 小时前
async/await:让一个线程同时等很多件事
开发语言·rust
赵民勇2 小时前
dconf命令详解
linux·运维