概述
你大概率经历过这种场景:凌晨三点被告警吵醒,爬起来看 Grafana,发现 CPU 使用率飙到 85%。你紧张了半天,查了一圈,最后发现——这台数据库服务器每天凌晨 2:50 到 3:10 都会跑定时备份任务,CPU 本来就该到这个数。
问题出在哪?你不知道这台机器"正常"是什么样子。没有基线,85% 是高还是低,你根本判断不了。
性能基线(Performance Baseline)解决的就是这个问题:在系统健康的时候,把关键指标记录下来,形成一份"体检报告"。以后出了异常,拿当前数据跟基线一对比,是真故障还是正常波动,一眼就能看出来。
这篇文章讲的是怎么从零建立一套可落地的 Linux 性能基线体系。不是泛泛而谈"监控很重要",而是从方法论选择、指标采集、基线建模、异常识别到自动化落地,每一步都给你能直接用的配置和代码。
为什么静态阈值不靠谱
大多数团队的告警配置是这样的:CPU > 80% 告警,内存 > 90% 告警,磁盘 > 85% 告警。这就是静态阈值——一个拍脑袋定的数字,挂在所有机器上。
静态阈值有三个致命问题:
第一,不同业务、不同时段的正常值不一样。 一台跑批处理任务的服务器,CPU 峰值在工作时间冲到 95% 是正常的;一台 API 网关,CPU 持续 70% 就得警惕了。用同一个 80% 阈值套上去,前者天天误报,后者出了事还不报。
第二,绝对值掩盖趋势。 某台机器 CPU 从 30% 缓慢爬到 60%,持续两周。静态阈值看不到——没到 80% 嘛。但这种缓慢上升往往是内存泄漏或连接堆积的前兆,等真到 80% 的时候已经炸了。
第三,无法识别"不正常的好"。 某个核心服务的 QPS 突然从 5000 掉到 200,CPU 使用率跟着降到 10%。静态阈值觉得一切正常——CPU 低着呢。但实际上上游可能挂了,流量根本没进来。
基线解决这些问题的思路很简单:不设固定阈值,而是让系统自己学什么算正常。当实际表现偏离了历史学到的"正常模式",才触发告警。
USE 方法论:基线采集的骨架
建立基线之前,先得知道采什么指标。乱采一通,采了 200 个指标,80% 没人看,纯属浪费。
Brendan Gregg 提出的 USE 方法(Utilization、Saturation、Errors)是目前最实用的资源检查框架。核心思路:对每一类资源,检查利用率、饱和度和错误三个维度。
| 资源 | Utilization(利用率) | Saturation(饱和度) | Errors(错误) |
|---|---|---|---|
| CPU | CPU 使用率百分比 | 运行队列长度、load average | 上下文切换异常飙升 |
| 内存 | 已用内存占比 | 换页率(page in/out)、OOM 触发次数 | OOM kill 事件 |
| 磁盘 I/O | %util(设备繁忙百分比) | iostat 的 await、avgqu-sz | 磁盘 I/O 错误计数 |
| 网络 | 带宽使用率 | 网卡丢弃包数(dropped) | 网卡错误包数(errs) |
| 文件系统 | inode 使用率 | 队列等待时间 | 文件系统错误(ext4 errors) |
USE 方法的好处是简单粗暴。Brendan 自己说过:“USE 方法能解决 80% 的服务器性能问题,但只需要 5% 的努力。” 不需要一上来就搞火焰图、eBPF 这些重武器,先把四大资源的 U、S、E 采全了,基本就能覆盖日常 90% 的场景。
基线指标清单
基于 USE 方法,以下是建立基线必须采集的核心指标清单。别贪多,先采这些:
CPU 子系统:
cpu_usage_user/cpu_usage_system/cpu_usage_idle— 分别记录用户态、内核态、空闲时间占比load_avg_1/load_avg_5/load_avg_15— 1/5/15 分钟平均负载context_switches— 每秒上下文切换次数run_queue_length— 运行队列长度
内存子系统:
mem_available_pct— 可用内存百分比(看 available,不是 free)swap_used_pct— Swap 使用率page_in/page_out— 每秒换入/换出页面数oom_kill_count— OOM kill 累计次数
磁盘 I/O 子系统:
disk_util— 每块磁盘的 %utildisk_await— I/O 请求平均等待时间(毫秒)disk_iops_read/disk_iops_write— 每秒读写 IOPSdisk_avgqu_sz— 平均队列长度
网络子系统:
net_rx_bytes/net_tx_bytes— 每秒收发字节数net_rx_drop/net_tx_drop— 网卡丢包计数net_rx_errs/net_tx_errs— 网卡错误包计数tcp_retransmit_rate— TCP 重传率
采集工具选型:从手动到自动化
基线采集的核心矛盾是:你需要长期、连续、低开销地采集数据。用 top 手动看一眼不算基线,那叫"瞥一眼"。
下面按三个层次介绍采集工具。
第一层:内核态轻量采集——sar
sar(System Activity Reporter)是 sysstat 包里的经典工具,几乎所有 Linux 发行版都能装。它的原理是后台跑一个 cron 每 10 分钟采集一次系统指标,写到二进制文件里,你随时可以查历史数据。
# 安装 sysstat(包含 sar)
# CentOS/RHEL
yum install -y sysstat
# Ubuntu/Debian
apt install -y sysstat
# 启用数据采集(编辑 /etc/default/sysstat 或 /etc/sysconfig/sysstat)
# Ubuntu/Debian:
sed -i 's/ENABLED="false"/ENABLED="true"/' /etc/default/sysstat
systemctl restart sysstat
systemctl enable sysstat
# CentOS/RHEL:
sed -i 's/false/true/' /etc/sysconfig/sysstat
systemctl restart sysstat
systemctl enable sysstat
sar 采集的数据默认存在 /var/log/sa/ 目录下,按天滚动(sa28 表示 28 号的数据)。你可以回溯查看任意一天的历史性能数据:
# 查看今天的 CPU 使用率历史
sar -u
# 查看 8 月 28 日的内存使用历史
sar -r -f /var/log/sa/sa28
# 查看磁盘 I/O 历史
sar -d -p
# 查看网络流量历史
sar -n DEV
# 每秒采集一次,持续 60 秒(实时模式)
sar -u 1 60
sar 的优势是极其轻量、零依赖、数据可回溯。劣势是默认 10 分钟采集间隔太粗,而且只覆盖基础指标,没有进程级别的数据。
第二层:定制化采集脚本——collectd / 自研脚本
当你需要更细粒度的采集(比如每分钟),或者需要采集 sar 不覆盖的指标(如 TCP 重传率、进程级数据),自研采集脚本更灵活。
下面是一个用 Bash 写的轻量级基线采集脚本,可以 cron 每分钟跑一次,输出 JSON 格式,方便后续入库:
#!/bin/bash
# perf-baseline-collector.sh
# 每分钟采集一次系统性能基线数据,输出 JSON
TIMESTAMP=$(date '+%Y-%m-%dT%H:%M:%S%z')
HOSTNAME=$(hostname)
# --- CPU 指标 ---
CPU_LINE=$(cat /proc/stat | grep '^cpu ' | awk '{print $2,$3,$4,$5,$6,$7,$8}')
read USER NICE SYSTEM IDLE IOWAIT IRQ SOFTIRQ <<< "$CPU_LINE"
TOTAL=$((USER + NICE + SYSTEM + IDLE + IOWAIT + IRQ + SOFTIRQ))
CPU_USAGE=$(awk "BEGIN {printf \"%.2f\", ($TOTAL - $IDLE) / $TOTAL * 100}")
LOAD_AVG=$(cut -d' ' -f1-3 /proc/loadavg)
# 运行队列长度
RUNNABLE=$(grep -c 'R' /proc/*/stat 2>/dev/null || echo 0)
# 上下文切换次数
CTXT=$(grep ctxt /proc/stat | awk '{print $2}')
# --- 内存指标 ---
MEMINFO=$(cat /proc/meminfo)
MEM_TOTAL=$(echo "$MEMINFO" | grep MemTotal | awk '{print $2}')
MEM_AVAIL=$(echo "$MEMINFO" | grep MemAvailable | awk '{print $2}')
MEM_AVAIL_PCT=$(awk "BEGIN {printf \"%.2f\", $MEM_AVAIL / $MEM_TOTAL * 100}")
SWAP_TOTAL=$(echo "$MEMINFO" | grep SwapTotal | awk '{print $2}')
SWAP_FREE=$(echo "$MEMINFO" | grep SwapFree | awk '{print $2}')
SWAP_USED_PCT=0
if [ "$SWAP_TOTAL" -gt 0 ]; then
SWAP_USED_PCT=$(awk "BEGIN {printf \"%.2f\", ($SWAP_TOTAL - $SWAP_FREE) / $SWAP_TOTAL * 100}")
fi
# 换页统计
PAGE_IN=$(grep pgpgin /proc/vmstat | awk '{print $2}')
PAGE_OUT=$(grep pgpgout /proc/vmstat | awk '{print $2}')
# --- 磁盘 I/O 指标 ---
# 取第一块非 loop 设备
DISK_DEV=$(ls /sys/block/ | grep -v 'loop\|ram\|sr' | head -1)
DISK_STATS=$(cat /sys/block/$DISK_DEV/stat)
read DISK_IOS_READ DISK_SECT_READ DISK_IOS_WRITE DISK_SECT_WRITE DISK_TICKS <<< "$DISK_STATS"
# --- 网络指标 ---
NET_DEV=$(ip route get 8.8.8.8 2>/dev/null | grep -oP 'dev \K\S+' || echo "eth0")
NET_STATS=$(cat /proc/net/dev | grep "$NET_DEV:")
read RX_BYTES RX_PACKETS RX_ERRS RX_DROP _ _ _ _ TX_BYTES TX_PACKETS TX_ERRS TX_DROP <<< "$NET_STATS"
# TCP 重传统计
TCP_RETRANS=$(grep -c retrans /proc/net/snmp 2>/dev/null || echo 0)
# --- 输出 JSON ---
cat <<EOF
{
"timestamp": "$TIMESTAMP",
"hostname": "$HOSTNAME",
"cpu": {
"usage_pct": $CPU_USAGE,
"load_avg": "$(echo $LOAD_AVG | tr ' ' ',')",
"runnable_tasks": $RUNNABLE,
"ctxt_per_sec": $CTXT
},
"memory": {
"available_pct": $MEM_AVAIL_PCT,
"swap_used_pct": $SWAP_USED_PCT,
"page_in": $PAGE_IN,
"page_out": $PAGE_OUT
},
"disk": {
"device": "$DISK_DEV",
"io_read_ops": $DISK_IOS_READ,
"io_write_ops": $DISK_IOS_WRITE
},
"network": {
"interface": "$NET_DEV",
"rx_bytes": $RX_BYTES,
"tx_bytes": $TX_BYTES,
"rx_drop": $RX_DROP,
"tx_drop": $TX_DROP,
"rx_errs": $RX_ERRS,
"tx_errs": $TX_ERRS,
"tcp_retrans": $TCP_RETRANS
}
}
EOF
这个脚本读的全是 /proc 和 /sys 下的文件,不依赖任何外部包,开销极低(单次执行 < 10ms)。配到 cron 里每分钟跑一次,输出追加到文件或推送到时序数据库:
# crontab 配置:每分钟采集一次
* * * * * /opt/scripts/perf-baseline-collector.sh >> /var/log/perf-baseline/$(date +\%Y\%m\%d).jsonl
第三层:时序数据库 + 可视化
采集到的数据最终要进时序数据库,才能做长期存储、聚合分析和异常检测。这里推荐两种方案:
方案一:Prometheus + node_exporter
这是最主流的方案。node_exporter 已经覆盖了 CPU、内存、磁盘、网络的基础指标,你不需要自己写采集脚本:
# node_exporter 启动参数(推荐开启 textfile collector 自定义指标)
node_exporter --collector.textfile.directory=/var/node_exporter/textfile \
--collector.cpu \
--collector.meminfo \
--collector.diskstats \
--collector.netdev \
--collector.filesystem \
--collector.tcpstat \
--no-collector.infiniband \
--no-collector.ipvs
Prometheus 侧的采集配置:
scrape_configs:
- job_name: 'node'
static_configs:
- targets: ['localhost:9100']
scrape_interval: 60s # 基线数据 60 秒粒度够用
scrape_timeout: 10s
方案二:直接用 sar 数据 + Python 脚本入库
如果你的环境不允许装 Prometheus(比如某些严格隔离的合规环境),可以用 Python 脚本把 sar 的二进制数据解析后推到 InfluxDB 或直接存 CSV:
#!/usr/bin/env python3
"""
将 sar 历史数据导出为 CSV,用于基线分析和异常检测建模。
依赖: sadf (sysstat 包自带)
"""
import subprocess
import csv
import sys
from datetime import datetime
def export_sar_to_csv(date_str, output_file):
"""导出指定日期的 sar 数据为 CSV"""
cmd = [
'sadf', '-d', '--', '-u', '-r', '-d', '-n', 'DEV',
f'/var/log/sa/sa{date_str}'
]
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode != 0:
print(f"Error reading sar data for {date_str}: {result.stderr}")
return
with open(output_file, 'w', newline='') as f:
reader = csv.reader(result.stdout.strip().split('\n'), delimiter=';')
writer = csv.writer(f)
for row in reader:
if row and not row[0].startswith('#'):
writer.writerow(row)
print(f"Exported {date_str} sar data to {output_file}")
# 批量导出最近 30 天数据
if __name__ == '__main__':
from datetime import datetime, timedelta
end_date = datetime.now()
start_date = end_date - timedelta(days=30)
current = start_date
while current <= end_date:
day_str = current.strftime('%d')
output = f'/var/log/perf-baseline/sar-{current.strftime("%Y%m%d")}.csv'
export_sar_to_csv(day_str, output)
current += timedelta(days=1)
怎么定义"正常":基线建模方法
数据采到了,下一个问题是:什么算正常?
这听起来像废话——“正常不就是平时那样吗”。但"平时那样"到底怎么量化?下面三种方法,从简单到复杂,按需选择。
方法一:分位数法(最实用)
最简单也最实用的方法。取历史数据的 P50(中位数)作为基线值,P95 和 P99 作为波动上限。
举个例子:过去 30 天的 CPU 使用率数据,按从小到大排列:
- P50 = 42% — 一半时间 CPU 低于这个值
- P95 = 68% — 95% 的时间 CPU 低于这个值
- P99 = 75% — 几乎所有时间 CPU 都低于这个值
那基线就是:正常 CPU 在 42% 左右波动,偶尔冲到 68%,极端情况到 75%。超过 P99 才考虑告警。
这个方法的好处是简单、不需要机器学习、结果直观可解释。坏处是没法区分时段——凌晨 3 点和下午 3 点的 CPU 基线本来就不一样,混在一起算分位数会把基线"摊平"。
用 PromQL 实现分位数基线:
# CPU 使用率的 30 天 P50 基线
quantile_over_time(0.50, (100 - avg(rate(node_cpu_seconds_total{mode="idle"}[5m]) * 100))[30d:1m])
# CPU 使用率的 30 天 P95 基线(上界)
quantile_over_time(0.95, (100 - avg(rate(node_cpu_seconds_total{mode="idle"}[5m]) * 100))[30d:1m])
# 当前值超过 P95 基线
(100 - avg(rate(node_cpu_seconds_total{mode="idle"}[5m]) * 100)) >
quantile_over_time(0.95, (100 - avg(rate(node_cpu_seconds_total{mode="idle"}[5m]) * 100))[30d:1m])
方法二:时段分桶法(推荐)
把一天切成若干时段,每个时段单独算基线。比如把一天分成 24 小时,每个小时单独算 P50 和 P95。
这样凌晨 3 点的基线就跟下午 3 点不一样了。批处理任务在工作时间 CPU 飙到 90% 是正常的,凌晨 3 点飙到 90% 就该查了。
# 按小时分桶的 CPU 基线
# 使用 avg_over_time 配合 hour() 函数实现时段基线
avg_over_time(
(100 - avg(rate(node_cpu_seconds_total{mode="idle"}[5m]) * 100))[30d:1m]
) * on() group_left() (hour(time()) == 14) # 14 点时段
更优雅的做法是用 Recording Rule 预计算每个小时的基线:
# prometheus rules - perf-baseline.yml
groups:
- name: performance_baseline
rules:
# 每小时 CPU 基线(P50)
- record: cpu_usage_baseline_p50
expr: |
quantile_over_time(0.50,
(100 - avg by (instance) (rate(node_cpu_seconds_total{mode="idle"}[5m]) * 100))
[30d:1m])
# 每小时 CPU 基线(P95)
- record: cpu_usage_baseline_p95
expr: |
quantile_over_time(0.95,
(100 - avg by (instance) (rate(node_cpu_seconds_total{mode="idle"}[5m]) * 100))
[30d:1m])
# 偏离基线告警
- alert: CpuUsageAboveBaseline
expr: |
(100 - avg by (instance) (rate(node_cpu_seconds_total{mode="idle"}[5m]) * 100))
>
cpu_usage_baseline_p95 * 1.2
for: 5m
labels:
severity: warning
annotations:
summary: "{{ $labels.instance }} CPU 超过基线 P95 的 1.2 倍"
方法三:动态基线(进阶)
如果你有时间序列分析的能力(比如有机器学习平台),可以用更高级的方法。核心思路是用滑动窗口预测下一个时间点的预期值,如果实际值偏离预测值超过一定范围,就判定为异常。
一个轻量级的实现是用 Python 的 statsmodels 库做季节性分解:
#!/usr/bin/env python3
"""
基于 STL 季节性分解的动态基线异常检测。
适合有明显日/周周期性的指标(如 QPS、CPU、内存)。
依赖: pip install statsmodels pandas numpy
"""
import numpy as np
import pandas as pd
from statsmodels.tsa.seasonal import seasonal_decompose
from datetime import datetime, timedelta
def build_dynamic_baseline(metric_series, period=1440, window_days=30):
"""
构建动态基线。
参数:
metric_series: pd.Series, 带时间索引的指标序列
period: 季节周期(1440 = 1天的分钟数)
window_days: 使用多少天历史数据建模
返回:
baseline: 基线值(预期值)
upper_bound: 上限(基线 + 3 * 残差标准差)
lower_bound: 下限
"""
# 取最近 N 天数据
cutoff = metric_series.index[-1] - timedelta(days=window_days)
data = metric_series[metric_series.index >= cutoff]
# 去掉缺失值
data = data.dropna()
if len(data) < period * 3:
# 数据不够 3 个周期,退回到简单统计
baseline = data.rolling(window=period, min_periods=1).median()
std = data.rolling(window=period, min_periods=1).std()
return baseline, baseline + 3 * std, baseline - 3 * std
# STL 分解:趋势 + 季节性 + 残差
decomposition = seasonal_decompose(data, model='additive', period=period)
# 基线 = 趋势 + 季节性
baseline = decomposition.trend + decomposition.seasonal
# 残差的标准差作为波动范围
resid_std = decomposition.resid.std()
upper_bound = baseline + 3 * resid_std
lower_bound = baseline - 3 * resid_std
return baseline, upper_bound, lower_bound
def detect_anomalies(metric_series, baseline, upper_bound, lower_bound):
"""检测异常点"""
df = pd.DataFrame({
'actual': metric_series,
'baseline': baseline,
'upper': upper_bound,
'lower': lower_bound
})
df['is_anomaly'] = (df['actual'] > df['upper']) | (df['actual'] < df['lower'])
df['deviation'] = abs(df['actual'] - df['baseline'])
anomalies = df[df['is_anomaly']].copy()
return anomalies
# 使用示例
if __name__ == '__main__':
# 模拟数据:生成 30 天的 CPU 使用率,每天有周期性
np.random.seed(42)
dates = pd.date_range(start='2026-08-01', end='2026-08-30', freq='1min')
# 日周期 + 周周期 + 噪声
daily_pattern = 30 * np.sin(2 * np.pi * np.arange(len(dates)) / 1440)
weekly_pattern = 5 * np.sin(2 * np.pi * np.arange(len(dates)) / (1440 * 7))
noise = np.random.normal(0, 5, len(dates))
cpu_values = 50 + daily_pattern + weekly_pattern + noise
series = pd.Series(cpu_values, index=dates)
# 构建基线
baseline, upper, lower = build_dynamic_baseline(series)
# 检测异常
anomalies = detect_anomalies(series, baseline, upper, lower)
print(f"基线建模完成,检测到 {len(anomalies)} 个异常点")
if len(anomalies) > 0:
print("\n异常点示例:")
print(anomalies[['actual', 'baseline', 'upper']].head(10))
说实话,方法三在大部分场景下属于"杀鸡用牛刀"。除非你有几十万台机器、告警量巨大、简单分位数法已经扛不住了,才值得上动态基线。方法一和方法二能解决 80% 的问题,先把这两个做扎实。
从基线到告警:异常识别的工程化
基线建好了,怎么用?关键是把"偏离基线"转化为可执行的告警规则。
告警规则设计原则
基于基线的告警,跟静态阈值告警的设计思路不一样。核心区别是:告警条件不是"绝对值超阈",而是"相对偏离超阈"。
设计原则有三条:
- 持续时间过滤:基线偏离必须持续一段时间才告警。瞬时抖动(GC 停顿、瞬时网络抖动)不该触发告警。建议
for: 5m起步。 - 偏离倍数:不是"超过基线就告警",而是"超过基线的 N 倍"或"超过基线 + K 倍标准差"才告警。N 通常取 1.5-2,K 取 3。
- 多指标联动:单个指标偏离不一定是问题,多个关联指标同时偏离才是真问题。比如 CPU 偏离 + 网络重传率偏离 = 可能是网络引起的中断。
下面是一个完整的 Prometheus 基线告警规则集:
# baseline-alerts.yml
groups:
- name: baseline_anomaly_alerts
rules:
# CPU 使用率偏离基线
- alert: CpuDeviationFromBaseline
expr: |
(
(100 - avg by(instance)(rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100)
-
quantile_over_time(0.50, (100 - avg by(instance)(rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100))[30d:1m]
)
>
3 * stddev_over_time((100 - avg by(instance)(rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100))[30d:1m]
for: 10m
labels:
severity: warning
category: baseline-deviation
annotations:
summary: "{{ $labels.instance }} CPU 使用率偏离基线超过 3 倍标准差"
description: "当前 CPU {{ $value }} 偏离历史 P50 基线,持续时间超过 10 分钟"
# 磁盘 I/O 等待时间偏离基线
- alert: DiskIoAwaitDeviation
expr: |
rate(node_disk_io_time_weighted_seconds_total[5m])
/
rate(node_disk_io_time_seconds_total[5m])
>
quantile_over_time(0.95,
rate(node_disk_io_time_weighted_seconds_total[5m]) / rate(node_disk_io_time_seconds_total[5m])
)[30d:5m] * 1.5
for: 15m
labels:
severity: critical
category: baseline-deviation
annotations:
summary: "{{ $labels.instance }} 磁盘 I/O 等待时间偏离基线"
# 网络丢包率偏离基线
- alert: NetworkDropDeviation
expr: |
rate(node_network_receive_drop_total[5m]) + rate(node_network_transmit_drop_total[5m])
>
quantile_over_time(0.99,
rate(node_network_receive_drop_total[5m]) + rate(node_network_transmit_drop_total[5m])
)[30d:5m]
for: 5m
labels:
severity: critical
category: baseline-deviation
annotations:
summary: "{{ $labels.instance }} 网络丢包率超过 30 天 P99 基线"
# 内存可用率突降(偏离基线)
- alert: MemoryAvailableDeviation
expr: |
node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes * 100
<
quantile_over_time(0.05,
node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes * 100
)[30d:5m] * 0.5
for: 10m
labels:
severity: critical
category: baseline-deviation
annotations:
summary: "{{ $labels.instance }} 可用内存低于历史 P5 基线的 50%"
注意看上面几个规则的共同点:全部用了 quantile_over_time 或 stddev_over_time 这类范围函数,计算的是历史 30 天的统计量,然后跟当前值比较。没有任何一行写了 > 80 这样的固定数字。
告警降噪:别让基线告警变成新的噪声
基线告警有自己的坑。最大的坑是:基线本身会"漂移"。
假设你的服务一直在缓慢泄漏内存,过去 30 天的内存使用率从 50% 慢慢爬到 80%。如果你用这 30 天的数据建基线,基线也会跟着往上漂,到头来 80% 的内存使用率反而被认为是"正常"的。
解决方案:
1. 用更长的时间窗口建基线,但用更短的时间窗口检测偏离。
基线用 30 天甚至 90 天的数据建,这样短期的恶化趋势会被拉长到时间轴上看。但检测偏离时用最近 5 分钟的数据。这样基线反映的是长期平均水位,偏离反映的是近期突变。
2. 定期人工校准基线。
每个季度人工 Review 一次基线数据,确认基线是否仍然代表"健康状态"。如果业务发生了重大变更(比如换了新版本、加了新功能、改了部署架构),基线需要重建。
3. 加入变化率告警。
光看绝对偏离不够,还得看变化速度。一个指标在过去 1 小时内的变化量超过历史变化量的 P99,本身就是异常信号,不管它有没有超过基线绝对值。
# CPU 使用率 1 小时变化量超过历史 P99
abs(
(100 - avg(rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100)
-
(100 - avg(rate(node_cpu_seconds_total{mode="idle"}[5m] offset 1h)) * 100)
)
>
quantile_over_time(0.99,
abs(
(100 - avg(rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100)
-
(100 - avg(rate(node_cpu_seconds_total{mode="idle"}[5m] offset 1h)) * 100)
)
)[30d:1h]
60 秒快速基线检查清单
不是所有场景都有时间等 30 天数据积累。有时候你刚接手一台新机器,需要快速摸底。Brendan Gregg 在 LISA'19 演讲中提到过一份"60 秒检查清单",我做了本地化调整:
| 序号 | 命令 | 看什么 | 异常信号 |
|---|---|---|---|
| 1 | uptime | load average 三列 | 1min > 5min 说明在上升;load > CPU 核数说明饱和 |
| 2 | `dmesg -T | tail -20` | 最近内核日志 |
| 3 | vmstat 1 5 | r 列、si/so 列、wa 列 | r > CPU 核数 = CPU 饱和;si/so > 0 = 在用 swap;wa > 20% = I/O 瓶颈 |
| 4 | mpstat -P ALL 1 3 | 各 CPU 核使用率 | 单核 100% 但整体低 = 单线程瓶颈 |
| 5 | pidstat 1 5 | 进程级 CPU 占用 | 找出吃 CPU 最多的进程 |
| 6 | iostat -xz 1 3 | %util、await、r/s、w/s | %util > 80% 或 await > 20ms = 磁盘瓶颈 |
| 7 | free -h | available 列 | available < 总量 10% = 内存告急 |
| 8 | sar -n DEV 1 3 | 各网卡 rxkB/s、txkB/s | 带宽接近上限 = 网络瓶颈 |
| 9 | ss -s | TCP 连接数和状态 | TIME-WAIT 过多 = 连接复用问题 |
| 10 | `top -b -n 1 | head -20` | 系统概览 + top 进程 |
这份清单不是基线,但它是建立基线之前的第一步摸底。跑完这 10 条命令,你对一台机器的"健康底色"就有了基本认识。之后把 sar 或采集脚本挂上,等数据积累 3-7 天,就能形成初步基线。
生产级落地:一份完整的基线自动化方案
把前面的碎片拼起来,这里给出一份完整的、可以直接落地到生产的基线自动化方案。
架构设计
整体架构分四层:
┌─────────────────────────────────────────────────────┐
│ 告警与可视化层 │
│ Grafana 基线仪表盘 + Prometheus Alerting Rules │
├─────────────────────────────────────────────────────┤
│ 基线计算层 │
│ Recording Rules (P50/P95/P99) + Python 异常检测 │
├─────────────────────────────────────────────────────┤
│ 数据存储层 │
│ Prometheus TSDB (短期) + 长期存储 (Thanos/VM) │
├─────────────────────────────────────────────────────┤
│ 采集层 │
│ node_exporter + 自定义 textfile collector │
│ + sar (兜底) + 采集脚本 (/proc 直读) │
└─────────────────────────────────────────────────────┘
完整的 Recording Rules
# /etc/prometheus/rules/baseline-recording-rules.yml
groups:
- name: cpu_baseline
interval: 1m
rules:
# CPU 使用率(减去 idle)
- record: instance:cpu_usage:ratio
expr: 1 - avg by(instance)(rate(node_cpu_seconds_total{mode="idle"}[5m]))
# 30 天 P50 基线
- record: instance:cpu_usage_baseline:p50
expr: quantile_over_time(0.50, instance:cpu_usage:ratio[30d:1m])
# 30 天 P95 基线
- record: instance:cpu_usage_baseline:p95
expr: quantile_over_time(0.95, instance:cpu_usage:ratio[30d:1m])
# 30 天 P99 基线
- record: instance:cpu_usage_baseline:p99
expr: quantile_over_time(0.99, instance:cpu_usage:ratio[30d:1m])
# 30 天标准差
- record: instance:cpu_usage_baseline:stddev
expr: stddev_over_time(instance:cpu_usage:ratio[30d:1m])
- name: memory_baseline
interval: 1m
rules:
- record: instance:mem_available:ratio
expr: node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes
- record: instance:mem_available_baseline:p50
expr: quantile_over_time(0.50, instance:mem_available:ratio[30d:1m])
- record: instance:mem_available_baseline:p05
expr: quantile_over_time(0.05, instance:mem_available:ratio[30d:1m])
- name: disk_baseline
interval: 1m
rules:
- record: instance:disk_io_util:ratio
expr: rate(node_disk_io_time_seconds_total[5m])
- record: instance:disk_io_util_baseline:p95
expr: quantile_over_time(0.95, instance:disk_io_util:ratio[30d:1m])
- record: instance:disk_io_util_baseline:p50
expr: quantile_over_time(0.50, instance:disk_io_util:ratio[30d:1m])
- name: network_baseline
interval: 1m
rules:
- record: instance:net_drop_rate
expr: rate(node_network_receive_drop_total[5m]) + rate(node_network_transmit_drop_total[5m])
- record: instance:net_drop_baseline:p99
expr: quantile_over_time(0.99, instance:net_drop_rate[30d:1m])
Grafana 基线仪表盘
在 Grafana 里建一个基线仪表盘,核心是把当前值和基线范围画在一起,一眼就能看出当前表现是否在基线区间内。
推荐的面板配置:
| 面板 | 指标 | 可视化方式 | 说明 |
|---|---|---|---|
| CPU 使用率 | 当前值 + P50 + P95 + P99 | 折线图 | 四条线,当前值越过 P95 标红 |
| 内存可用率 | 当前值 + P50 + P05 | 折线图 | 当前值低于 P05 标红 |
| 磁盘 I/O | 当前 %util + P50 + P95 | 折线图 | 当前值超过 P95 标红 |
| 网络丢包 | 当前值 + P99 | 柱状图 | 超过 P99 的柱子标红 |
| 偏离倍数 | (当前 - P50) / stddev | 折线图 | 超过 3 标红线 |
一键部署脚本
#!/bin/bash
# deploy-baseline-monitoring.sh
# 一键部署基线监控方案(Prometheus + node_exporter + Recording Rules)
set -euo pipefail
PROMETHEUS_VERSION="2.55.0"
NODE_EXPORTER_VERSION="1.9.0"
ARCH=$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/')
echo "=== 1. 安装 node_exporter ==="
cd /tmp
wget -q "https://github.com/prometheus/node_exporter/releases/download/v${NODE_EXPORTER_VERSION}/node_exporter-${NODE_EXPORTER_VERSION}.linux-${ARCH}.tar.gz"
tar xzf "node_exporter-${NODE_EXPORTER_VERSION}.linux-${ARCH}.tar.gz"
cp "node_exporter-${NODE_EXPORTER_VERSION}.linux-${ARCH}/node_exporter" /usr/local/bin/
chmod +x /usr/local/bin/node_exporter
# 创建 systemd 服务
cat > /etc/systemd/system/node-exporter.service <<'UNIT'
[Unit]
Description=Node Exporter
After=network.target
[Service]
ExecStart=/usr/local/bin/node_exporter \
--collector.textfile.directory=/var/node_exporter/textfile \
--collector.tcpstat \
--collector.processes
Restart=always
RestartSec=5
User=node_exporter
[Install]
WantedBy=multi-user.target
UNIT
useradd -r -s /bin/false node_exporter 2>/dev/null || true
mkdir -p /var/node_exporter/textfile
chown -R node_exporter:node_exporter /var/node_exporter
systemctl daemon-reload
systemctl enable --now node-exporter
echo "=== 2. 创建 Recording Rules ==="
mkdir -p /etc/prometheus/rules
# ... (此处放上面定义的 baseline-recording-rules.yml 内容)
# 实际部署时将 YAML 内容写入文件
echo "=== 3. 创建 Alert Rules ==="
# ... (此处放上面定义的 baseline-alerts.yml 内容)
echo "=== 4. 验证部署 ==="
sleep 2
if curl -s http://localhost:9100/metrics | grep -q "node_cpu_seconds"; then
echo "✓ node_exporter 正常运行"
else
echo "✗ node_exporter 启动失败,检查日志: journalctl -u node-exporter"
exit 1
fi
echo ""
echo "=== 部署完成 ==="
echo "node_exporter: http://$(hostname -I | awk '{print $1}'):9100/metrics"
echo "下一步: 在 Prometheus 配置中添加本机为 scrape target"
echo "基线数据需要积累 3-7 天后才能形成有效基线"
踩坑实录:基线方案落地的 5 个教训
教训一:别在变更窗口期建基线。
有一回在发版窗口建基线,新版本正好引入了一个内存缓慢泄漏的 bug。基线建出来内存使用率的 P95 是 85%——这根本不是"正常",是"带病运行"。等 bug 修了,基线反而成了误报源。
正确做法:在系统稳定运行至少 7 天后才开始建基线。如果刚做完大变更,等一周再说。
教训二:基线窗口不是越长越好。
30 天是个比较好的窗口。试过用 90 天的窗口,结果发现基线对短期变化的敏感度太低——某台机器的磁盘 I/O 从 200 IOPS 涨到 800 IOPS,基线花了两周才"追上去",这段时间全是漏报。
反过来用 7 天窗口也不行,周末和工作日的差异太大,基线波动剧烈。
实测下来,30 天窗口 + 1 分钟采集间隔是性价比最高的组合。
教训三:有些指标不适合建基线。
错误计数类的指标(OOM kill 次数、磁盘错误计数、网卡 hardware error)不适合用分位数法建基线。因为这些指标在正常情况下应该是 0 或者极低值,它们的"正常基线"就是 0,任何非零值都值得注意。
对这类指标,直接用"非零即告警"的策略比基线法更有效:
# 错误类指标:非零即告警
- alert: OomKillDetected
expr: increase(node_vmstat_oom_kill[5m]) > 0
for: 0m
labels:
severity: critical
annotations:
summary: "{{ $labels.instance }} 发生 OOM Kill"
教训四:基线告警必须有收件人路由。
基线告警的语义跟传统阈值告警不一样——它说的是"这东西跟平时不一样",而不是"这东西超了危险线"。运维收到这两类告警的处置方式不同。
建议在 Alertmanager 里给基线告警打上 category: baseline-deviation 标签,路由到独立的接收渠道,或者先走低优先级通知(如 IM 群消息而非电话):
# alertmanager.yml
route:
receiver: default
group_by: ['instance', 'category']
routes:
- matchers:
- category = "baseline-deviation"
receiver: baseline-alerts-channel
group_wait: 10m # 基线告警等 10 分钟聚合
repeat_interval: 4h # 4 小时不重复
receivers:
- name: default
webhook_configs:
- url: 'https://hooks.slack.com/...'
- name: baseline-alerts-channel
webhook_configs:
- url: 'https://hooks.slack.com/...'
教训五:基线不是建完就不管了。
基线是个活的东西。业务在变、流量在变、架构在变,基线也得跟着变。建议建立一个季度基线 Review 流程:
- 每季度拉出各机器的基线数据,人工 Review 是否合理
- 如果发现基线漂移(比如 CPU 基线从 40% 漂到了 60%),调查原因
- 确认原因合理后,用最近 30 天数据重建基线
- 如果原因不合理(比如内存泄漏),先修问题再重建基线
总结
性能基线不是什么新概念,但很多团队一直没做起来。原因不是技术难——分位数法 + Prometheus Recording Rules 就能跑——而是没有形成"建基线 → 用基线 → 调基线"的闭环习惯。
实操路径建议这样走:
- 先跑 60 秒检查清单摸底,确认当前系统有没有明显问题
- 装好 node_exporter + sar,开始采集数据,至少积累 7 天
- 上分位数基线 Recording Rules(方法一),先做 CPU 和内存两个子系统
- 配基线告警规则,用低优先级通道收告警,观察一周看误报率
- 逐步扩展到磁盘 I/O、网络、TCP 指标
- 跑稳一个月后,考虑上时段分桶法(方法二),区分工作时间和非工作时间
- 机器规模超过 500 台或告警量超过日均 100 条时,再考虑动态基线(方法三)
核心思想就一句话:让数据告诉你什么算正常,而不是拍脑袋定阈值。这件事前期投入不大(node_exporter + 几条 Recording Rules),但长期收益极高——告警少了,MTTR 降了,凌晨三点的电话也不那么频繁了。
参考资料与致谢
本文在撰写过程中参考了以下资料,感谢原作者的贡献:
- Performance Analysis Methodology — Brendan Gregg,USE 方法论的原始定义和多种性能分析方法概述
- Linux Systems Performance (LISA'19) — Brendan Gregg / USENIX,Linux 性能分析六大领域的系统性总结
- perf-tools (GitHub) — Brendan Gregg,基于 perf_events 和 ftrace 的性能分析工具集
- 第6 篇|系统性能基线建立与异常识别 — CSDN/zhoucoolqi,从"救火队员"到"健康管理师"的性能基线实践
- What Is AIOps? Guide to Artificial Intelligence for IT Operations — phoenixNAP,AIOps 异常检测中的动态基线能力说明
- How to Optimize Linux Device Performance in 2026 — fosslinux/Liam,建立基线后再做优化的实践建议