从Zabbix/CloudWatch迁移到Prometheus:为什么你的告警规则成了技术债?
迁移不是"配置翻译",而是"观测范式重构"
去年这个时候,我刚把公司最后一台Zabbix Server关机。看着 Grafana 上漂亮的 Prometheus 仪表盘,本以为功德圆满,结果接下来两周,我们团队被告警风暴折腾得几乎神经衰弱——不是因为Prometheus不好,而是我们把Zabbix的思维硬生生塞进了Prometheus的壳子里。
如果你正在计划或执行类似的迁移,请停下来问自己一个问题:你只是在把旧系统的阈值"翻译"成PromQL,还是在重新思考"什么才是真正需要告警的"?
这是两个完全不同的维度。前者是配置搬运工,后者是SRE工程师。
陷阱一:把"固定阈值"当成真理
在Zabbix或CloudWatch时代,我们习惯了这样的逻辑:
# Zabbix风格的思维
if CPU使用率 > 85% for 5 minutes then 告警
if 内存使用率 > 90% then 告警
这种基于绝对数值的阈值在Prometheus里当然能写:
# 灾难性的直接翻译
100 - (avg(irate(node_cpu_seconds_total{mode="idle"}[5m])) * 100) > 85
问题在哪? 这种规则忽略了业务上下文。一个批处理服务器的85% CPU可能是正常的,而一个API网关的50% CPU伴随延迟飙升才是灾难。
正确的重构思路是基于SLO(服务等级目标)的SLI(服务等级指标)设计:
# 错误率SLI(基于业务影响)
(sum(rate(http_requests_total{status=~"5.."}[5m]))
/ sum(rate(http_requests_total[5m]))) > 0.001 # 0.1%错误预算
# 延迟SLI(关注长尾,而非平均值)
histogram_quantile(0.99,
sum(rate(http_request_duration_seconds_bucket[5m])) by (le)
) > 0.5 # P99延迟超过500ms
关键区别: 旧系统监控的是"资源健康",新系统应该监控的是"用户体验"。旧规则清单只是你的需求参考源,告诉你"以前大家担心什么",而不是配置模板告诉你"应该抄什么"。
陷阱二:忽视指标类型的语义差异
CloudWatch里的CPUUtilization和Prometheus的node_cpu_seconds_total不仅仅是格式不同,它们代表了完全不同的观测哲学。
- Zabbix/CloudWatch:定期采样点值,关注的是"此刻是多少"
- Prometheus:Counter/Gauge/Histogram/Summary,关注的是"趋势和分布"
最危险的迁移垃圾:把Histogram当Gauge用
很多人把应用埋点的http_request_duration_seconds直接取avg()就拿来告警,这完全浪费了Histogram的价值。正确的做法是使用分位数和速率:
# 垃圾写法(丢失了分布信息)
avg(http_request_duration_seconds) > 0.3
# 正确写法(关注99分位线的趋势)
histogram_quantile(0.99,
sum(rate(http_request_duration_seconds_bucket[5m])) by (le, job)
) > 0.3
特别注意rate()和increase()的使用: 旧系统的"当前连接数"可能直接对应node_netstat_Tcp_CurrEstab,但如果你想告警"连接数突增",必须用:
deriv(node_netstat_Tcp_CurrEstab[10m]) > 100 # 变化率告警,而非绝对值
陷阱三:Alertmanager路由的"复制粘贴"灾难
这是最容易产生"告警噪音"的环节。Zabbix的Action配置和Alertmanager的route机制有着本质差异:
Zabbix思维(扁平化):
- 触发器 → Action → 发送给某个用户组
Prometheus思维(分层路由):
- 告警标签 → 路由树 → 分组(group) → 抑制(inhibition) → 接收器
典型的反模式配置:
# 错误的:为每个旧Zabbix规则创建独立路由
routes:
- match: {alertname: "HighCPU"}
receiver: "ops_team"
- match: {alertname: "HighMemory"}
receiver: "ops_team"
# ... 100个类似的路由
正确的映射策略应该是基于 severity 和 service 的矩阵:
route:
receiver: 'default'
group_by: ['alertname', 'cluster', 'service']
group_wait: 30s
group_interval: 5m
repeat_interval: 12h
routes:
# 关键路径:立即通知,不分组
- match:
severity: critical
team: backend
receiver: 'pagerduty-backend'
group_by: ['alertname', 'instance'] # 关键告警需要快速定位到具体实例
continue: false
# 非关键:聚合通知,减少噪音
- match:
severity: warning
receiver: 'slack-monitoring'
group_by: ['cluster', 'service'] # 按服务聚合,避免单实例抖动轰炸
group_interval: 30m # 降低通知频率
# 抑制规则:关键告警已触发时,抑制低级别告警
inhibit_rules:
- source_match:
severity: 'critical'
target_match:
severity: 'warning'
equal: ['cluster', 'service', 'alertname']
核心原则:
- 分组(group_by):解决"单故障多点告警"问题,比如数据库主库挂了,相关的连接池告警应该被聚合
- 抑制(inhibit_rules):解决"级联告警"问题,网络不可达时,抑制所有依赖该网络的下游服务告警
- 路由(routes):解决"谁应该在什么时间收到什么"的问题,而非简单的人员映射
实战迁移检查清单
如果你正在准备迁移,按这个顺序执行,能避免90%的坑:
第一阶段:语义梳理(不要碰代码)
- 审计旧告警:列出Zabbix/CloudWatch所有触发器,标记哪些是"资源监控",哪些是"业务监控"
- 定义SLO:为每个服务定义错误率、延迟、吞吐量的SLO(如99.9%可用性,P99<200ms)
- 分级标签:为每条旧规则打上
severity: critical|warning|info和team: backend|dba|sre标签
第二阶段:指标映射(关键转换)
| 旧系统概念 | Prometheus对应 | 注意点 |
|---|---|---|
| CPU使用率 | 100 - rate(node_cpu_seconds_total{mode="idle"}[5m]) * 100 |
用rate而非原始值 |
| 磁盘使用率 | node_filesystem_avail_bytes / node_filesystem_size_bytes |
关注可用空间比例,而非使用率 |
| 网络流量 | rate(node_network_receive_bytes_total[5m]) |
必须取rate,原始Counter无意义 |
| 应用延迟 | histogram_quantile(0.99, rate(...)) |
不要用avg(),必须用分位数 |
| 黑盒探测 | probe_success |
比ping更可靠的端点健康检查 |
第三阶段:灰度验证
- 双跑期:Prometheus和Zabbix并行运行2周,但不要直接对比告警数量,而是对比"漏报"和"误报"事件
- 降噪调优:观察Alertmanager的
group_interval和repeat_interval,确保非关键告警不会在夜间打电话 - 标签规范化:建立
cluster、service、team的标签字典,这是路由的基础
结语:从"监控工具迁移"到"可观测性升级"
迁移到Prometheus不是终点,而是**从"监控"(Monitoring)转向"可观测性"(Observability)**的起点。当你不再纠结于"如何把Zabbix的CPU阈值改写成PromQL",而是思考"这个指标是否真正反映用户体验"时,你才真正完成了这次迁移。
那些直接复制过来的旧规则,最好的归宿是git commit -m "legacy rules for reference"然后永远不要启用。毕竟,技术债最好的偿还方式,有时候是承认它存在,然后勇敢地重写。