去年我们线上有一台核心业务机高峰期明明还有 30% 多的空闲 CPU 没用完接口响应却隔三差五往上飙。运维同事第一反应是“加机器”但几个老手盯着 dashboard 看了半天总觉得哪里不对劲CPU 不高、内存够用负载却像过山车。后来我花了两天时间用一套服务器性能数据分析脚本把事件前后的 CPU 队列、上下文切换、磁盘 await、网络软中断全部拉出来对齐才定位到是两块网卡的硬中断全压在一个 CPU 核上触发了调度器局部热点。这件事给我的教训很直接性能数据的价值不在于“采集”而在于“关联分析”。裸跑 top 能看到当下但看不到趋势、对不齐事件、找不出瓶颈之间的因果关系。这篇文章就分享我整理的一套服务器性能数据分析脚本思路从数据采集、指标解读到报警设计、可视化输出尽量讲清楚每个环节背后的“为什么”并提供可直接复用的代码骨架。无论你是刚接触 Linux 性能排查的开发者还是已经在生产环境摸爬滚打的运维希望能帮你省掉一些我踩过的坑。1. 脚本方案的总体设计先想清楚要采集什么再谈怎么采集很多人写性能采集脚本上来就top -b -n 1把一堆输出丢进文件然后就没有然后了。数据分析脚本跟监控系统不同它的目标往往很具体要么是复盘某一次故障要么是找出某个时间窗口内的瓶颈特征。所以第一步不是写代码而是想清楚三个问题看多细、看多久、看哪些指标。1.1 三次数据采集架构实时快照、趋势序列、细粒度样本我习惯把整个方案分成三层实时快照层用于“此刻发生了什么”对应top -n 1、vmstat 1 2、free -h这类即时命令。趋势序列层用于“过去一段时间怎么变化”按固定间隔持续采样并落盘比如每秒或每 5 秒一条记录。细粒度事件层用于“具体是哪个进程/哪个线程导致的”对应pidstat、perf、/proc/*/status中的上下文切换计数等。这套脚本的核心在第二层和第三层。第一层只能回答“有没有问题”第二层回答“问题如何演变”第三层回答“根因是什么”。生产环境排障时三层缺一不可。1.2 命名一套可以落地的性能指标清单我把常见的关键指标整理成了一张表按子系统分类子系统核心指标Linux 工具备注CPUus/sy/wa/id、load average、run queue、上下文切换top、vmstat、sar注意区分用户态、内核态和 iowait内存total/used/buff/cache、swap in/out、缺页中断free、vmstat、sar -r别只看 free 的可用内存磁盘await、svctm、util、IOPS、读写吞吐iostat、sar -dutil 高不代表一定饱和网络带宽、pps、软中断、丢包、TCP 重传sar -n DEV、ss、netstat结合撷包工具更准进程CPU、内存占用、线程数、上下文切换、打开文件数pidstat、top -H、ps若有容器需关注 cgroup 隔离真正写脚本时我不会把所有指标一股脑全采而是根据场景选择。日常巡检只采核心趋势参数故障复盘才会启动细粒度采集合集。这也是脚本设计的第一个原则按需采集不要制造新的性能负担。2. 数据采集实现用 shell 和 Python 各司其职明确了指标清单接下来就是写采集。我的做法是shell 负责现场采集Python 负责解析和关联分析各干各擅长的。2.1 shell 采集脚本骨架带时间戳落盘shell 脚本适合做定时采样因为它调用系统命令最直接而且不依赖第三方库。核心逻辑很简单循环采样写入带时间戳的 CSV 或文本文件。#!/bin/bash # perf_collect.sh —— 服务器性能数据采集脚本 # 用法: bash perf_collect.sh [间隔秒] [采样次数] INTERVAL${1:-5} COUNT${2:-720} OUTPUT_DIR./perf_data mkdir -p $OUTPUT_DIR for ((i0; iCOUNT; i)); do TS$(date %Y-%m-%d %H:%M:%S) # 采集 CPU、负载、内存摘要 echo $TS $(top -bn1 | awk NR1 || NR3 {printf %s | , $0}) $OUTPUT_DIR/top_summary.txt # 采集 vmstat 关键列 vmstat 1 2 | tail -1 | awk -v ts$TS {printf %s us%s sy%s wa%s id%s st%s cs%s si%s so%s\n, ts, $13, $14, $16, $15, $17, $12, $7, $8} $OUTPUT_DIR/vmstat.txt # 采集磁盘 IO iostat -x 1 1 | tail -1 | awk -v ts$TS {printf %s util%s await%s svctm%s w_await%s\n, ts, $NF, $(NF-2), $(NF-1), $(NF-5)} $OUTPUT_DIR/iostat.txt sleep $INTERVAL done这个脚本里有两个易错点vmstat 1 2的含义是“先采一次丢弃因为首个样本是自启动以来的均值再采第二次作为当前值”。直接用一次采样的结果往往会偏大这个坑我踩过好几回。iostat -x 1 1同理需要一次预热。否则第一个样本统计的是自系统启动以来的平均值无法反映当前 IO。2.2 进程级细粒度采集pidstat 的妙用趋势序列能定位到子系统但要揪出具体进程还需要pidstat。它可以按进程维度输出 CPU、内存、IO、上下文切换等数据。pidstat -d -l -p ALL 5 $OUTPUT_DIR/pidstat_io.txt pidstat -w -l -p ALL 5 $OUTPUT_DIR/pidstat_ctxsw.txt 示例输出Linux 5.4.0-xxx (hostname) 2024-01-15 10:30:01 x86_64 10:30:01 UID PID %usr %system %guest %wait %CPU CPU Command 10:30:01 1000 12051 12.00 8.00 0.00 0.00 20.00 3 java%wait这一列很有用它表示线程等待 CPU 的时间占比。如果 CPU 使用率不高但%wait很高说明存在锁竞争或调度器延迟问题。2.3 采集频率和持续时间的确定方法采样频率不是拍脑袋定的。从信号分析的直觉出发采样频率至少要高于目标现象频率的两倍否则会丢失关键特征。日常排查中如果是短期故障且正在发生用1 秒间隔采样 10~30 分钟。如果是复盘历史问题用5 秒间隔采集 1~2 小时。如果是性能压测用1 秒间隔贯穿整个压测过程。同时注意采集脚本自身的开销。top -bn1这类命令会 fork 进程采集密度越高干扰越大。在极端场景比如 I/O 已经饱和下脚本本身可能成为压死骆驼的最后一根稻草。所以我通常把采样脚本的 nice 值调高并限制其 CPU 占用nice -n 10 bash perf_collect.sh 1 6002.4 数据落盘格式首选 CSV拒绝无脑文本早期我图省事直接把命令输出丢进日志文件结果分析时痛苦不堪要对齐时间戳、要处理多行格式、要手动数空格。后来统一改成 CSVtimestamp,cpu_us,cpu_sy,cpu_wa,cpu_id,load1,load5,load15,mem_used_kb,mem_free_kb,mem_buff_kb,mem_cache_kb,io_await,io_util,net_rx_bytes,net_tx_bytesCSV 可以直接用 pandas 读取也方便 Excel 查看。这个格式层面的调整让后续分析效率提升了不止一倍。3. 数据分析怎么从一堆数字里定位真瓶颈采集只是手段分析才是目的。数据到手后最忌讳的就是“看到啥说啥”。比如一看到wa高就断定磁盘慢结果 igos 抓包却发现瓶颈在锁。所以这一节我按子系统逐一拆解关键指标的解读方法。3.1 CPU 维度utilization 只是表面runqueue 才是核心很多时候 CPU 使用率看起来不高但系统已经卡得不行。原因在于调度队列积压。Linux 的 load average 本质上就是“可运行进程数 不可中断睡眠进程数”的指数移动平均但它反映的是趋势不是瞬时状态。所以我会额外看/proc/loadavg里的可运行进程数也就是第一个数去掉小数后的整数部分。如果这个值长期大于 CPU 核数的 2~3 倍说明 CPU 已经过载with open(/proc/loadavg) as f: fields f.read().split() runnable int(float(fields[0])) cores os.cpu_count() if runnable cores * 2.5: print(f警告: runqueue{runnable} 核数{cores}调度积压)另一个常被忽略的是steal列。在虚拟化环境里st占比高说明宿主机 CPU 资源争抢激烈你就算在虚拟机里调优也白搭。这类问题要靠宿主侧解决。3.2 内存维度free 显示剩余很多为什么还 swap 剧烈这个场景我遇到过不止一次。free -m看着还有几 GB available但 swap 的 si/so 一直在跳动。原因通常是内存回收的阈值触发点不对也就是/proc/sys/vm/min_free_kbytes或swappiness设置得不够合理导致内核过早开始回收页缓存。分析内存时我特别推荐用/proc/pressure/memory和 PSI 指标。some avg10、full avg10能直接反映内存回收给业务带来的等待压力cat /proc/pressure/memory如果full avg10长期大于某个阈值比如 5说明内存瓶颈已经严重影响调度了不能只看剩余内存大小。这个指标很多老手都不常用但排查内存诡异问题非常有效。3.3 磁盘维度await 高不代表磁盘坏了也可能是 IO 调度排队iostat 的await是 IO 请求从发起到完成的平均耗时包括排队时间和服务时间。如果util接近 100% 且await高基本可以判断磁盘饱和。但如果await高util却不怎么高这时就要怀疑IO 调度队列过长或上层文件系统锁/日志刷盘了。另外iostat的svctm列在较新版本中已被标记为废弃因为它只能近似计算且 RAID 和 SSD 场景下失真。我更建议关注r_await与w_await的差值以及aqu-sz平均队列长度。队列长度持续大于磁盘并发能力的 2~3 倍就是排队过深。3.4 网络维度带宽没跑满但卡成狗先看软中断网络性能分析比 CPU/磁盘更隐蔽。带宽利用率不高但应用却感觉网络延迟很大常见原因包括单队列网卡导致的硬中断集中在单个 CPU。软中断softirq处理缓慢/proc/net/softnet_stat里的dropped列持续增长。TCP 重传率上升ss -s或netstat -s里的retrans计数异常。我在开头提到的那个事故最后就是用/proc/interrupts看各 CPU 上的中断分布才定位到的。eth0的中断几乎全部落在 CPU0 上而 CPU0 还要处理定时器和部分内核线程最后导致调度延迟。后来通过设置irqbalance或手动配置smp_affinity才解决。3.5 趋势对齐用 Python 把多个数据源合并到同一时间轴单看某个指标很容易误判。真正有价值的是把 CPU、内存、磁盘、网络按同一时间轴对齐。我一般用 pandasimport pandas as pd cpu_df pd.read_csv(vmstat.csv, parse_dates[timestamp]) io_df pd.read_csv(iostat.csv, parse_dates[timestamp]) net_df pd.read_csv(net.csv, parse_dates[timestamp]) merged cpu_df.merge(io_df, ontimestamp, howouter) merged merged.merge(net_df, ontimestamp, howouter) merged merged.sort_values(timestamp).fillna(methodffill)对齐后就可以直接在故障事件前后加一个“事件标记列”对比各指标的变化节奏。事件前后 CPU 先飙升、还是 IO 先飙升、还是网络先出现重传这个顺序本身就是非常重要的排障线索。4. 报警与自动分析逻辑让脚本主动告诉你“哪里可能出问题了”采集和分析做完后第三步是让脚本具备“初级判断力”。这里的核心思路不是固定阈值而是动态基线 极值检测。4.1 为什么固定阈值不靠谱不同业务机器的画像差异比如某台机器平时 CPU 就 60%某天突然 80%看起来还行另一台机器平时 10%某天到了 40%已经算异常。固定阈值 80% 会漏报前者也会对后者无感。所以我采用了两层策略硬阈值比如磁盘 util 超过 95%swap 持续落盘超过阈值这类属于严重问题必须告警。动态基线采集近 7 天的数据按小时计算均值和标准差当实时指标偏离均值超过 3 倍标准差时判定为异常。import numpy as np def detect_anomaly(series, window24): rolling_mean series.rolling(window).mean() rolling_std series.rolling(window).std() z_score (series - rolling_mean) / (rolling_std 1e-9) return z_score.abs() 3.0这套逻辑同样适用于 CPU、IO、网络等各类指标。z-score 的优势在于它不关心机器本身的资源余量只关心“和它自己过往的行为相比是否异常”。4.2 报警消息要带着证据链不能只发一句“性能异常”报警不是目的定位才是。所以我的脚本在触发异常时会打包一个“证据包”触发时间、异常指标、触发值、基线值。对应时间点的 top 前 10 进程快照。异常前后两分钟的 vmstat 摘要。系统日志中对应时间窗口的 dmesg 片段。这样收到通知的人不用再登录机器翻半天直接能看到初步线索。格式上我采用 JSON 纯文本两种JSON 给自动化系统纯文本发到钉钉/企业微信机器人都方便。alert { title: 服务器性能异常, host: web-03, time: event_time, metric: cpu_runqueue, value: runqueue_value, baseline: baseline_value, evidence: { top_process: top_procs[:10], vmstat_window: vmstat_window, dmesg_tail: dmesg_tail } }4.3 自动生成趋势图matplotlib 就够了别搞太重可视化方面我坚持“够用就好”。服务器端分析不需要上大屏系统直接用 matplotlib 输出 PNG 图片附在报告里即可。我常用的绘图逻辑import matplotlib.pyplot as plt fig, axes plt.subplots(4, 1, figsize(12, 10), sharexTrue) axes[0].plot(cpu_df.timestamp, cpu_df.cpu_wa, labeliowait) axes[1].plot(io_df.timestamp, io_df.io_util, labeldisk util) axes[2].plot(net_df.timestamp, net_df.tcp_retrans, labeltcp retrans) axes[3].plot(load_df.timestamp, load_df.load1, labelload1) plt.savefig(perf_report.png, dpi100)有人会推荐用 Grafana Prometheus 整套监控体系那是长期建设的路径。如果只是临时排查或轻量自建脚本输出趋势图往往更高效尤其是在内网隔离环境、不方便装额外组件的情况下。5. 常见问题与排查技巧实录脚本跑了一段时间实际生产环境中会遇到各种稀奇古怪的问题。这里挑几个我亲身踩过坑、且非常有代表性的分享。5.1 top 显示 wa 很高但 iostat 说 util 不高这个现象第一次遇到时很懵。后来发现是top的wa和iostat的util统计口径不同wa包含所有 CPU 核的加权 wait IO 时间而iostat的 util 是按设备维度的。更常见的原因是对应的 IO 落在文件系统缓存回写上设备层显示忙碌程度低但文件系统层已经因为日志、锁等原因阻塞了。排查建议用iostat -x -m 1同时看%util、aqu-sz、r_await/w_await再用pidstat -d看是哪个进程在写。很多时候问题反而不在磁盘硬件而在应用层的 fsync 频率过高。5.2 采集脚本本身成了性能瓶颈脚本如果写得粗糙高频调用top、awk、grep每秒钟 fork 十几个进程那么它自身就能把 CPU 消耗好几个百分点。在性能排查的关键时刻这非常致命。我后来的做法是尽量用/proc直接读取避免频繁 forkvmstat和iostat这类命令每次只跑一次预热 一次采样采集脚本的调度间隔默认拉大到 5 秒。另外跑完一批数据后立即kill掉后台采集进程别让它一直在那空转。5.3 日志文件膨胀与循环写入长时间跑采集脚本一天下来日志可能轻松突破几个 GB。所以我给采集脚本内置了“滚动保留”逻辑单文件超过 500MB 自动切割。只保留最近 3 天或最近 30 个文件。可选 gzip 压缩历史文件。find $OUTPUT_DIR -name *.csv -mtime 3 -delete find $OUTPUT_DIR -name *.log -mtime 3 -delete5.4 时间戳对齐误差多个采集命令分别采样时时间戳总会有几百毫秒到几秒的偏差。如果不同子系统指标在时间轴上错开做关联分析就会得出错误结论。解决方案有三点一是统一由脚本主进程生成基准时间戳而不是每个子命令各自取时间二是采集线程尽量同步启动三是分析时做对齐和插值避免直接按原始时间戳对比。5.5 /proc 文件直接读取的小技巧很多关键数据直接用/proc读比跑命令更快而且格式稳定CPU 总时间/proc/stat第一行。内存状态/proc/meminfo。负载/proc/loadavg。软中断统计/proc/softnet_stat。中断计数/proc/interrupts。进程 IO/proc/pid/io。举个例子解析 CPU 使用率的 Python 片段def read_cpu_usage(): with open(/proc/stat) as f: fields f.readline().split() user, nice, system, idle, iowait map(int, fields[1:6]) idle_all idle iowait total sum(map(int, fields[1:])) return user, system, idle_all, total连续两次采样计算差值就能得到这一时间窗内的 CPU 使用率。这种方式比反复调 top 高效得多。6. 这套脚本还能怎么扩展上面讲的是一套相对完整但轻量的服务器性能数据分析闭环。如果你已经在生产环境使用可能还会遇到容器化、云主机等场景有几个方向可以继续扩展。6.1 容器场景cgroup 视角的采集物理机指标在容器场景下会失真。比如宿主机 CPU 很高但你只能看到自己容器内的配额使用情况。此时需要读/sys/fs/cgroup/cpu.stat、/sys/fs/cgroup/memory.stat并且区分“宿主机总体压力”和“容器自身压力”。PSI 指标在容器内同样可用但要确认内核版本支持。6.2 对接现有监控系统把数据格式转换为后端接口我的做法是保留脚本的分析能力但单独抽出一个“数据发送器”把异常判定结果 POST 到内部的监控告警平台。脚本本身不需要把整套告警体系重写一遍只需要遵循平台的 webhook 格式。6.3 引入机器学习做趋势预测可以但先别急很多技术文章一上来就鼓励用机器学习做性能预测但根据我的经验线性回归和简单的滑动平均在绝大多数场景下已经足够。比如根据过去 24 小时 CPU 趋势预测未来 1 小时是否将达到瓶颈scikit-learn里的LinearRegression就能干。先把基线和阈值做扎实再考虑更复杂的模型否则就是给自己挖坑。from sklearn.linear_model import LinearRegression import numpy as np X np.arange(len(cpu_series)).reshape(-1, 1) y cpu_series.values model LinearRegression().fit(X, y) future np.array([[len(cpu_series)]]).reshape(-1, 1) predicted model.predict(future)[0]7. 一些个人的实操体会写这套脚本的时候我最大的感受是性能分析没有银弹工具链再多最后拼的还是对系统原理的理解。脚本帮你把数据整合好了但如果看不懂 runqueue 和 iowait 之间的关系、分不清await和svctm的区别再漂亮的图表也是摆设。从工程落地角度看我建议先从一个最小闭环做起——先只采集 CPU、内存、磁盘三个最核心维度跑通脚本、出报告、对齐时间戳再逐步加网络、进程和报警。不要一上来就追求“大而全”否则脚本本身的维护成本就会吞掉你排查故障的效率。另外一个小技巧每次故障处理完把最终定位到的根因和对应的指标特征记录到脚本内置的“案例库”里比如“wa 高但 util 低 → 文件系统锁”“CPU 不高但 load 高 → 不可中断 D 进程”。时间长了这套脚本就不再只是采集工具而是你自己的排障知识库助手。下次遇到类似现象脚本会直接提示你过去是怎么解决的这一点我认为价值最大。