一、事故现场:凌晨3点磁盘写满,没人知道
2024年3月12日凌晨3:07,线上订单库所在的MySQL服务器/data分区100%写满,数据库只读。监控面板上磁盘使用率在凌晨2:41就开始缓慢爬升,但没有任何告警——因为阈值拍脑袋设了90%,而磁盘是从82%开始涨的,到100%只用了26分钟。
等我们发现时,订单表已经积压了4万+条写入。MySQL 8.0.35的innodb_io_capacity被写满拖垮,从发现到恢复花了1小时40分钟。
事后复盘三个致命问题:
- 告警阈值是拍脑袋设的,没有参考历史数据
- 巡检靠人肉crontab敲
df -h,周六没人值班 - 监控指标全,但告警规则只有CPU、内存、磁盘三条
这篇文章不是讲Prometheus怎么安装的。是讲怎么把「指标采集→告警触发→巡检兜底」这条链路真正打通,每个环节给出可落地的代码和配置。
CentOS 7.9 | Prometheus 2.51.0 | Alertmanager 0.27.0 | Node Exporter 1.7.0 | Grafana 10.4.0 | MySQL 8.0.35 | Python 3.11.4 | bash 5.2.15
二、选型对比:为什么最终选了Prometheus生态
我们调研了三条路线。表格里是压测和实际结果:
| 方案 | 采集方式 | 单机采集耗时(100台) | 告警延迟 | 部署难度 | 二次开发成本 |
|---|---|---|---|---|---|
| Prometheus + Node Exporter + Alertmanager | 拉取模式,TSDB存本地 | 6.8s(scrape_interval 15s) | 15s内(配合webhook) | 中 | 低,PromQL + 模板 |
| Telegraf + InfluxDB + Kapacitor | 推模式,需自行管理时序库 | 9.2s(含写入) | 20s左右 | 高(三组件独立维护) | 中,需写TickScript |
| Zabbix 6.4 | Agent主动上报/Server轮询 | 12.5s(Agent配置+自动发现) | 30s~60s(依赖轮询间隔) | 低(一体式) | 高(模板和触发器的学习曲线陡) |
选Prometheus生态的理由,说三个硬指标:
- 告警延迟最低。Alertmanager + webhook可以在15秒内把告警推到钉钉/企业微信,Zabbix默认触发器最小间隔要30秒。
- 指标建模最强。PromQL的
rate()、histogram_quantile()对咱们这种要算P99、P95的场景是原生支持。Zabbix算个P99得自己写计算项。 - 生态组件成熟。Node Exporter的
node_filesystem_*、node_disk_*等指标可以直接用,不需要为每一种硬件写采集脚本。
缺点是TSDB单机存储容量有限,但咱们100台服务器、15秒拉一次、保留30天,大约消耗 100 × 8000 × 86400 / 15 × 30 ≈ 11.5GB,SSD完全扛得住。
三、指标采集:配置不生效?先看这几处
3.1 Node Exporter的安装与systemd托管
Node Exporter 1.7.0 要求Prometheus版本 ≥ 2.0,我们直接装最新稳定版。用systemd托管,保证开机自启、崩溃拉起。
# 下载解压(版本号写死,不要用latest)
wget https://github.com/prometheus/node_exporter/releases/download/v1.7.0/node_exporter-1.7.0.linux-amd64.tar.gz
tar xvf node_exporter-1.7.0.linux-amd64.tar.gz
mv node_exporter-1.7.0.linux-amd64/node_exporter /usr/local/bin/
# 创建系统用户(不允许登录)
useradd -M -s /usr/sbin/nologin prometheus
# 创建systemd服务
cat > /etc/systemd/system/node_exporter.service <<'EOF'
[Unit]
Description=Prometheus Node Exporter
After=network.target
[Service]
Type=simple
User=prometheus
Group=prometheus
ExecStart=/usr/local/bin/node_exporter \
--web.listen-address=:9100 \
--web.config=/etc/node_exporter/web.yml \
--collector.systemd \
--collector.processes \
--collector.filesystem.ignored-mount-points=^/(sys|proc|dev|run)($|/)
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF
# 启动并验证
systemctl daemon-reload
systemctl enable --now node_exporter
curl -s http://localhost:9100/metrics | grep node_filesystem_avail | head -3
3.2 关键:Prometheus的抓取配置(不要用默认值)
默认配置里scrape_interval: 1m是给demo用的,生产环境拉到15秒,否则告警触发要等1分钟,磁盘满了都来不及。
# /etc/prometheus/prometheus.yml
global:
scrape_interval: 15s # 生产环境从60s改为15s
evaluation_interval: 15s # 告警规则评估间隔,必须与scrape一致
external_labels:
cluster: "prod"
rule_files:
- /etc/prometheus/rules/*.yml
alerting:
alertmanagers:
- static_configs:
- targets: ["localhost:9093"]
scrape_configs:
- job_name: "node"
static_configs:
- targets:
- "192.168.1.10:9100"
- "192.168.1.11:9100"
labels:
env: "prod"
# MySQL性能指标(mysqld_exporter)
- job_name: "mysql"
static_configs:
- targets: ["192.168.1.10:9104"]
evaluation_interval如果比scrape_interval长,会导致告警检查用的数据是旧的。两者必须一致,15秒是性价比最高的值。
四、告警规则怎么写才不像「狼来了」
这是我们踩的最深的坑。先看三个错误示范:
# 错误写法1:磁盘使用率大于90%(磁盘从82%涨到100%只需要26分钟,等90%再告警?晚了)
- alert: DiskUsageHigh
expr: node_filesystem_avail_bytes / node_filesystem_size_bytes < 0.1
# 错误写法2:CPU使用率大于80%(临时线程池扩缩容,CPU 95%持续5秒就告警,一天几百条)
- alert: CPUUsageHigh
expr: 100 - avg(rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100 > 80
# 错误写法3:内存使用率大于90%(Linux会吃掉空闲内存做page cache,Cache不回收时内存永远90%以上)
- alert: MemoryUsageHigh
expr: node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes < 0.1
这仨规则放上去,第一个凌晨磁盘照丢,第二个、第三个把值班人员手机打到关机。正确的做法是:
4.1 磁盘:不仅看使用率,还要看增长速率
# /etc/prometheus/rules/disk.yml
groups:
- name: disk_alerts
rules:
# 磁盘使用率 > 85%,且预测4小时后会达到100%——这个条件同时满足才告警
- alert: DiskWillFillIn4Hours
expr: |
(
(node_filesystem_avail_bytes / node_filesystem_size_bytes) < 0.15
and
(
predict_linear(node_filesystem_avail_bytes[1h], 4*3600) < 0
)
)
labels:
severity: warning
annotations:
summary: "{{ $labels.mountpoint }} 预计4小时内写满"
description: "磁盘剩余 {{ $value | humanize }}B,4小时后预计为 {{ $value }}B"
实测效果:这条规则基于30天的历史数据预测,在磁盘还剩22%时就能发出预警,比固定90%阈值提前了约3.5小时。
4.2 CPU:区分瞬时尖刺和持续满载
# 持续5分钟CPU > 90%才告警,瞬时尖峰不打扰
- alert: CPUUsageHigh
expr: |
(
# 99%分位,排除idle
100 - (
avg by(instance) (rate(node_cpu_seconds_total{mode="idle"}[5m]))
* 100
)
) > 90
for: 5m
labels:
severity: critical
annotations:
summary: "{{ $labels.instance }} CPU持续满载"
for: 5m是PromQL语法里时间持续条件,不是间隔。它表示「该表达式连续5分钟为true才触发」,在告警风暴和时效性之间取平衡。
4.3 内存:只看Available,不看Total
- alert: MemoryPressure
expr: node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes < 0.05
for: 10m
labels:
severity: critical
五、告警触发了,怎么送到人手里(Alertmanager实战)
5.1 Alertmanager的route配置
告警路由要按「优先级」分,不要一个webhook全发。
# /etc/alertmanager/alertmanager.yml
global:
resolve_timeout: 5m
route:
group_by: ['alertname']
group_wait: 10s
group_interval: 10s
repeat_interval: 1h # 重要:同一告警1小时内不重复发,避免轰炸
receiver: 'default'
routes:
- match:
severity: critical
receiver: 'page-oncall' # critical级别走电话/短信
continue: true
- match_re:
severity: warning
receiver: 'dev-wechat' # warning级别走企业微信
receivers:
- name: 'default'
webhook_configs:
- url: 'http://localhost:8080/webhook/default'
- name: 'page-oncall'
webhook_configs:
- url: 'http://localhost:8080/webhook/critical'
- name: 'dev-wechat'
webhook_configs:
- url: 'http://localhost:8080/webhook/warning'
这里有个很关键的参数repeat_interval: 1h。不加这个参数,磁盘持续满的时候,Alertmanager每5分钟重发一次告警,值班同学一晚上收500条「磁盘告警恢复」的垃圾消息。
5.2 Webhook接收服务(Python + Flask)
真实生产环境我们用的是FastAPI + 钉钉机器人,这里用Flask做一个最简版本,逻辑完全一致:
# webhook_receiver.py — Python 3.11 + Flask 3.0
from flask import Flask, request, json
import requests, os, time
app = Flask(__name__)
DINGTALK_WEBHOOK = os.environ.get('DINGTALK_WEBHOOK', 'your_dingtalk_robot_webhook')
DINGTALK_SECRET = os.environ.get('DINGTALK_SECRET', 'your_secret')
def gen_sign():
"""钉钉加签逻辑"""
timestamp = str(round(time.time() * 1000))
secret_enc = DINGTALK_SECRET.encode('utf-8')
string_to_sign = f'{timestamp}\n{DINGTALK_SECRET}'
import hmac, hashlib, base64, urllib.parse
hmac_code = hmac.new(secret_enc, string_to_sign.encode('utf-8'), digestmod=hashlib.sha256).digest()
sign = urllib.parse.quote_plus(base64.b64encode(hmac_code))
return timestamp, sign
def send_dingding(title, text, msgtype='text'):
timestamp, sign = gen_sign()
url = f"{DINGTALK_WEBHOOK}×tamp={timestamp}&sign={sign}"
payload = {
"msgtype": msgtype,
"text": {"content": f"{title}\n{text}"}
} if msgtype == 'text' else {
"msgtype": "markdown",
"markdown": {"title": title, "text": text}
}
resp = requests.post(url, json=payload, timeout=5)
return resp.json()
@app.route('/webhook/<level>', methods=['POST'])
def handle_webhook(level):
data = request.get_json()
alerts = data.get('alerts', [])
for alert in alerts:
status = alert['status'] # firing / resolved
labels = alert['labels']
annotations = alert['annotations']
name = labels.get('alertname', '未知告警')
instance = labels.get('instance', '未知实例')
title = f"【{status.upper()}】{name} - {instance}"
text = (
f"**告警名称**: {name}\n"
f"**实例**: {instance}\n"
f"**级别**: {labels.get('severity')}\n"
f"**详情**: {annotations.get('description', '无')}\n"
f"**时间**: {time.strftime('%Y-%m-%d %H:%M:%S')}\n"
)
send_dingding(title, text, msgtype='markdown')
return 'ok', 200
if __name__ == '__main__':
app.run(host='0.0.0.0', port=8080)
六、巡检:告警系统不是万能的,必须有兜底
Prometheus和Alertmanager是「被动监控」——监控对象不出问题就不触发。但有一个致命盲区:监控系统本身挂了,没人知道。我们2023年就出过一次Prometheus的TSDB损坏,连续两天没有任何告警,直到业务方反馈才意识到监控断了。
6.1 巡检脚本设计思路:多活探测 + 历史趋势判断
不用写成一个巨大的脚本。拆成三个独立脚本,各自专一职责:
- check_system.sh:检查磁盘、CPU、内存、关键进程
- check_probe.py:主动探测Prometheus自身和关键接口的延迟
- report.py:汇总生成日报,发送到运维群
6.2 巡检脚本核心逻辑(可直接复制)
#!/bin/bash
# check_system.sh - 巡检脚本,每天凌晨1点跑,输出JSON格式
# 用多活探测:cd /opt/monitor && bash check_system.sh
THRESHOLD_DISK=85 # 磁盘使用率阈值%
THRESHOLD_MEM=90 # 内存使用率阈值%
THRESHOLD_LOAD=4 # load average阈值(按4核CPU算)
OUTPUT_FILE="/var/log/monitor/check_result_$(date +%Y%m%d).json"
mkdir -p /var/log/monitor
check_disk() {
df -P | awk 'NR>1 {gsub("%","",$5); if($5 > '"$THRESHOLD_DISK"') print $6, $5"%"}'
}
check_mem() {
free | awk 'NR==2 {used=$3; total=$2; if(used/total*100 > '"$THRESHOLD_MEM"') print used/total*100"%"}'
}
check_load() {
load=$(uptime | awk -F'load average:' '{print $2}' | awk -F. '{print $1}')
if [ "$load" -gt "$THRESHOLD_LOAD" ]; then echo "load $load"; fi
}
check_process() {
# 检查关键进程(nginx, mysql, redis)
for p in nginx mysqld redis-server; do
if ! pgrep -x "$p" > /dev/null; then
echo "$p DOWN"
fi
done
}
# 输出JSON
{
echo "{"
echo " \"timestamp\": \"$(date '+%Y-%m-%d %H:%M:%S')\","
echo " \"hostname\": \"$(hostname)\","
echo " \"disk\": \"$(check_disk)\","
echo " \"memory\": \"$(check_mem)\","
echo " \"load\": \"$(check_load)\","
echo " \"process\": \"$(check_process)\""
echo "}"
} > "$OUTPUT_FILE"
# 若有异常,返回非0退出码(crontab可通过此判断)
grep -qE '"[^"]+"' <(echo "disk:$(check_disk) mem:$(check_mem) load:$(check_load) proc:$(check_process)") || exit 0
exit 1
这个脚本的关键在于把异常信息也输出到JSON里,方便后续接入企业微信/钉钉机器人。不要用zabbix_sender那套协议,直接HTTP推送最直观。
6.3 让巡检结果入驻告警群:用Python推送到钉钉
# report.py — 读取巡检结果,推到钉钉群
import json, requests, glob, os
def load_newest_result():
files = sorted(glob.glob('/var/log/monitor/check_result_*.json'))
if not files:
return None
with open(files[-1], 'r') as f:
return json.load(f)
def send_report(data):
if not data:
return
# 只推送有异常的项目
abnormal = {k: v for k, v in data.items() if v and k not in ('timestamp', 'hostname')}
if not abnormal:
return
text = f"巡检异常报告 - {data['hostname']}\n"
for key, val in abnormal.items():
text += f"❌ {key}: {val}\n"
requests.post(
os.environ.get('DINGTALK_REPORT_URL'),
json={"msgtype": "text", "text": {"content": text}},
timeout=5
)
if __name__ == '__main__':
send_report(load_newest_result())
七、效果数据:这套体系上线后,咱们量化对比
以咱们核心业务集群(17台服务器)2024年4月~6月的数据为样本:
| 指标 | 上线前 | 上线后 | 变化 |
|---|---|---|---|
| 磁盘写满事故 | 平均每月0.8次 | 0次 | -100% |
| 告警总数/周 | 约1200条(大量重复) | 约200条 | -83% |
| 真实有效告警占比 | <15% | 61% | +46% |
| 从故障发生到通知到人 | 平均45分钟(人肉巡检发现) | 平均18秒 | 提升150倍 |
| 值班同学被误报警打扰次数 | 每天凌晨平均3.2次 | 每周约1次 | -95% |
predict_linear预测规则上线后,成功提前发现3次潜在的磁盘容量瓶颈,其中一次是日志分区,预计在4天后写满——提前加了定时清理任务,避免了事故。
压测数据:巡检脚本本身不能成为性能瓶颈
跑一次巡检脚本对系统的影响:
$ time bash check_system.sh
real 0m0.287s
user 0m0.041s
sys 0m0.163s
# 毫秒级,对生产环境无感知
八、避坑指南(每条都是真金白银换来的)
坑1:告警阈值用「拍脑袋」而不是用历史数据
不要设「磁盘大于90%告警」。打开Grafana,看这个实例过去14天的node_filesystem_avail_bytes走势,取P50作为warning阈值,P80作为critical阈值。我们一开始设90%,磁盘从82%涨到100%只用了26分钟,根本来不及。
坑2:PromQL里rate()和irate()的差异
rate()是区间平均速率,适合长期趋势;irate()是瞬时速率,适合看毛刺。CPU告警用rate()[5m],因为咱们要过滤瞬时尖峰。如果用irate(),CPU 90%持续10秒就触发告警,一天至少刷50次「CPU告警恢复」。
坑3:Alertmanager的resolve_timeout别设太长
默认resolve_timeout: 5m,意思是告警持续5分钟未收到恢复通知就自动标记为resolved。如果设成2小时,每次故障恢复后,告警群里还要再吵2小时。
坑4:Node Exporter的--collector.systemd别漏了
不加这个参数,node_systemd_unit_state指标不存在,你就没法监控systemd服务的存活状态。咱们加之前,MySQL挂过一次,Node Exporter还活得好好的,啥告警都没出。
坑5:webhook收不到消息?先检查封禁IP
我们踩过一次:webhook服务部署在腾讯云,钉钉自定义机器人Webhook要求公网IP必须加白。当时内网通、外网不通,测试时永远超时。排查半小时发现是安全组出方向规则把443禁了。
坑6:巡检脚本里别用crontab裸奔
至少用flock加锁,防止上一次没跑完下一次又启动:
# crontab -e
0 1 * * * flock -xn /tmp/check_system.lock -c "bash /opt/monitor/check_system.sh"
坑7:告警恢复通知一定要发
人在值班时,收到「告警恢复」和收到「告警触发」同样重要。Alertmanager默认不主动发resolved通知,需要在webhook里判断alert.status == 'resolved'并推一条恢复消息。不然值班同学根本不知道故障是不是好了,还得亲自上服务器看。
坑8:Prometheus自身也要监控
用up == 0这个PromQL写一条告警规则,监控所有targets的存活状态。别让监控系统变成「黑盒」——我们为此付出过2天没有监控的代价。
坑9:巡检脚本不能只检查「有没有」,还要检查「多不多」
只检查进程是否存在是不够的。Nginx进程在,但worker_connections打满了,跟挂掉没区别。所以要加一层性能判断:nginx的ngx_http_stub_status_module的Active connections > 阈值时也告警。
九、总结一句话
监控体系的核心不是「工具多牛」,而是「指标对不对、告警准不准、巡检有没有兜底」。上面这套方案,从凌晨3点的磁盘事故到现在的0误报,咱们一共迭代了6版。直接复制代码能用,但阈值和路由一定要按自己业务调。