核心指标采集:CPU、内存、磁盘、网络
系统监控这件事,说白了就是回答三个问题:现在机器什么状态?哪里快扛不住了?过去几分钟发生了什么?而回答这些问题,全靠那几个核心指标。
我刚开始做运维那会儿,习惯用 top 和 free 看一眼就完事。后来线上出过一次故障,CPU 空闲还有 30%,但业务就是卡得要死。查了半天才发现是 I/O wait 飙到了 60%。从那以后,我就再也不敢只看 CPU 使用率了。
这一章,咱们把 CPU、内存、磁盘、网络这四个维度的采集原理掰开揉碎,再配上 Python 代码。你跟着写一遍,基本就能搭出一个可用的采集器。
2.1 CPU 指标采集
CPU 指标是所有监控系统的起点。但很多人只盯着 %user 和 %idle,这其实不够。
2.1.1 核心指标清单
| 指标名 | 来源文件 | 含义 | 异常阈值参考 |
|---|---|---|---|
| user | /proc/stat | 用户态进程占用 | > 70% 持续 5 分钟 |
| system | /proc/stat | 内核态系统调用占用 | > 30% 需关注 |
| iowait | /proc/stat | CPU 等待磁盘 I/O 完成 | > 20% 说明磁盘有瓶颈 |
| steal | /proc/stat | 被宿主机偷走的 CPU 时间 | > 10% 说明虚拟机争抢严重 |
| load1/5/15 | /proc/loadavg | 运行队列平均长度 | 超过 CPU 核数 * 0.7 |
这里有个坑,我踩过好几次。iowait 高不代表 CPU 忙,它只是表示 CPU 在等磁盘。你想想看,如果 iowait 到了 40%,但 user 只有 10%,这时候加 CPU 核数根本没用,得去查磁盘。
2.1.2 采集原理
Linux 把 CPU 时间片的使用情况记录在 /proc/stat 里。第一行就是总的 CPU 统计,格式长这样:
cpu user nice system idle iowait irq softirq steal guest guest_nice
这些都是 累计值,单位是 jiffies(通常 10ms)。要算使用率,得取两次快照做差值。
2.1.3 Python 实现
import time
def read_cpu_stat():
with open('/proc/stat', 'r') as f:
line = f.readline().strip().split()
# 跳过 'cpu' 标签,取后续所有数值
values = list(map(int, line[1:]))
return values
def calc_cpu_usage(prev, curr):
# 总时间 = 所有项之和
prev_total = sum(prev)
curr_total = sum(curr)
total_delta = curr_total - prev_total
# 空闲时间 = idle + iowait
prev_idle = prev[3] + prev[4]
curr_idle = curr[3] + curr[4]
idle_delta = curr_idle - prev_idle
usage = (1.0 - idle_delta / total_delta) * 100
return round(usage, 2)
# 使用示例
prev = read_cpu_stat()
time.sleep(1)
curr = read_cpu_stat()
print(f"CPU 使用率: {calc_cpu_usage(prev, curr)}%")
2.2 内存指标采集
内存这块,最容易让人误解的是 available 和 free 的区别。free 只是完全没被用的内存,available 才是真正可分配给新进程的内存。
2.2.1 关键指标
| 指标 | 来源 | 说明 |
|---|---|---|
| MemTotal | /proc/meminfo | 物理内存总量 |
| MemFree | /proc/meminfo | 完全未使用的内存 |
| MemAvailable | /proc/meminfo | 可用于新启动进程的内存(含可回收缓存) |
| Buffers | /proc/meminfo | 块设备读写缓存 |
| Cached | /proc/meminfo | 文件系统页缓存 |
| SwapTotal / SwapFree | /proc/meminfo | 交换分区总量和剩余 |
我曾经遇到过一个案例:某台机器 MemFree 只剩 200MB,但 MemAvailable 还有 6GB。监控告警一直报内存不足,实际上系统运行得很稳。后来我把告警指标从 MemFree 改成了 MemAvailable,世界清净了。
2.2.2 Python 实现
def read_mem_info():
mem = {}
with open('/proc/meminfo', 'r') as f:
for line in f:
parts = line.split(':')
if len(parts) == 2:
key = parts[0].strip()
# 值格式如 "16234568 kB"
value_str = parts[1].strip().split()[0]
mem[key] = int(value_str)
return mem
def calc_mem_usage(mem):
total = mem['MemTotal']
available = mem['MemAvailable']
usage = (1.0 - available / total) * 100
return {
'total_mb': total // 1024,
'available_mb': available // 1024,
'usage_pct': round(usage, 2)
}
mem = read_mem_info()
print(calc_mem_usage(mem))
2.3 磁盘指标采集
磁盘监控有两个维度:空间使用率和I/O 性能。空间用 df 或 /proc/mounts 就能搞定,但 I/O 性能得看 /proc/diskstats。
2.3.1 磁盘空间
import os
def get_disk_usage(path='/'):
stat = os.statvfs(path)
total = stat.f_frsize * stat.f_blocks
free = stat.f_frsize * stat.f_bfree
used = total - free
return {
'total_gb': total / (1024**3),
'used_gb': used / (1024**3),
'usage_pct': round(used / total * 100, 2)
}
print(get_disk_usage('/data'))
2.3.2 磁盘 I/O
/proc/diskstats 每行对应一个磁盘设备,格式如下:
major minor name rio rmerge rsect ruse wio wmerge wsect wuse running use aveq
关键指标:
- r/s, w/s:每秒读写次数(IOPS)
- rkB/s, wkB/s:每秒读写数据量(吞吐量)
- await:平均每次 I/O 的等待时间(毫秒)
- %util:磁盘忙绿百分比(接近 100% 说明有瓶颈)
def read_disk_stats(device='sda'):
with open('/proc/diskstats', 'r') as f:
for line in f:
parts = line.strip().split()
if parts[2] == device:
return {
'rio': int(parts[3]),
'rmerge': int(parts[4]),
'rsect': int(parts[5]),
'ruse': int(parts[6]),
'wio': int(parts[7]),
'wmerge': int(parts[8]),
'wsect': int(parts[9]),
'wuse': int(parts[10]),
'running': int(parts[11]),
'use': int(parts[12]),
'aveq': int(parts[13])
}
return None
2.4 网络指标采集
网络监控的核心是 /proc/net/dev。每行一个网络接口,记录收发包的统计。
2.4.1 关键字段
| 字段 | 含义 | 单位 |
|---|---|---|
| bytes | 收发字节数 | 字节 |
| packets | 收发包数 | 个 |
| errs | 错误包数 | 个 |
| drop | 丢弃包数 | 个 |
| fifo | FIFO 缓冲区溢出 | 个 |
2.4.2 Python 实现
def read_net_dev(interface='eth0'):
with open('/proc/net/dev', 'r') as f:
lines = f.readlines()
# 跳过前两行标题
for line in lines[2:]:
parts = line.strip().split(':')
if len(parts) == 2 and parts[0].strip() == interface:
stats = parts[1].split()
return {
'rx_bytes': int(stats[0]),
'rx_packets': int(stats[1]),
'rx_errs': int(stats[2]),
'rx_drop': int(stats[3]),
'tx_bytes': int(stats[8]),
'tx_packets': int(stats[9]),
'tx_errs': int(stats[10]),
'tx_drop': int(stats[11])
}
return None
def calc_net_speed(prev, curr, interval):
rx_speed = (curr['rx_bytes'] - prev['rx_bytes']) / interval
tx_speed = (curr['tx_bytes'] - prev['tx_bytes']) / interval
return {
'rx_kbps': round(rx_speed / 1024, 2),
'tx_kbps': round(tx_speed / 1024, 2)
}
2.5 知识体系总览
下面这张图把四个维度的采集路径和关键指标串起来了。你写代码的时候可以照着这个结构来组织。
2.6 采集器设计要点
写采集器的时候,有几个设计原则我建议你记住:
- 一次读取,多次计算:比如读一次 /proc/stat,把 CPU、上下文切换、进程数都算出来,别反复打开文件。
- 异常数据要过滤:偶尔会读到 0 或负数,直接跳过,不要往监控系统里塞脏数据。
- 采集间隔要可配置:CPU 和网络可以 10 秒一次,磁盘 I/O 建议 30 秒,内存变化慢,60 秒都行。
- 做好数据缓存:计算速率需要上一次的快照,用字典缓存起来,别每次都重新读。