监控告警通路测试规则
在 Prometheus / VictoriaMetrics 监控体系中,告警规则成百上千,但「通路本身是否还活着」常常被忽略——Alertmanager 挂了、Webhook 失效、企业微信 token 过期、邮件服务器异常,都可能让真实告警悄悄丢失。
最朴素的做法是配一个 vector(1) 的常驻告警,但这会持续刷屏。更合理的方案是「定时心跳」:
- 每 N 小时主动触发一条 info 级别告警
- 如果收到,说明通路正常
- 如果超过预期时间未收到,需要人工介入排查
本规则在此基础上加了一个智能优化:如果过去 6 小时已经有其他告警触发过,则跳过本次心跳——毕竟通路已经被实战验证过了,再发一条纯属噪音。
一、完整规则
重点参考 spec.groups.rules 下面的规则即可
下面是以VMRule这个 CRD 示例,也可以适配PrometheusRule CRD(修改下apiVersion、kind即可),以什么方式应用不是重点
apiVersion: operator.victoriametrics.com/v1beta1
kind: VMRule
metadata:
name: alert-alive
namespace: monitor
labels:
project: base
spec:
groups:
- name: alert-alive
rules:
# 每6小时触发一次,用于测试告警通路是否正常(UTC+8)
# 如果过去6小时内有其他告警触发过,则跳过本次测试
- alert: AlertAlive
expr: ((hour() + 8) % 6 == 0) and (minute() < 5) and vector(1) unless on() (max_over_time(ALERTS{alertname!="AlertAlive", alertstate="firing"}[6h]))
for: 0s
labels:
severity: info
owner: system
annotations:
summary: "告警通路测试(每6小时触发,近6小时无告警时发送)"
description: "此告警为自动触发的告警通路测试,若过去6小时内已有其他告警触发则跳过,无需处理。"
二、表达式逐段拆解
整条 expr 由 4 个部分通过布尔运算符串联而成:
((hour() + 8) % 6 == 0) and (minute() < 5) and vector(1)
unless on()
(max_over_time(ALERTS{alertname!="AlertAlive", alertstate="firing"}[6h]))
1. (hour() + 8) % 6 == 0 —— 时间窗口(每 6 小时)
hour()返回 UTC 时间的小时数(0–23)。+ 8将其偏移为北京时间(UTC+8)。% 6 == 0表示偏移后的小时能被 6 整除。
满足条件的 UTC 小时为 4, 10, 16, 22,对应北京时间 00:00、06:00、12:00、18:00。
VictoriaMetrics / Prometheus 默认按 UTC 评估,所有
hour()minute()都是 UTC。本规则通过+8模拟本地时区,避免依赖服务端时区配置。
2. minute() < 5 —— 触发窗口
只在每小时的前 5 分钟内「可能」触发,配合 for: 0s(无等待)让告警在窗口开始后立即 firing。
为什么留 5 分钟?因为告警从触发到送达要经过多个环节(vmalert 的评估周期、Alertmanager 的 group_wait 分组等待等),留个缓冲;同时让告警在窗口结束前有足够时间完成分组、抑制、发送。
3. vector(1) —— 永真占位
PromQL 中 and 是集合运算,要求两边都有样本才保留。(hour() + 8) % 6 == 0 和 minute() < 5 在条件不满足时返回空向量,会让整个 and 链断掉。
加 vector(1) 确保「时间条件满足时一定有样本」,否则前两个条件虽然成立,但表达式整体仍可能为空。
4. unless on() (max_over_time(ALERTS{...}[6h])) —— 智能跳过
这是规则最核心的部分。
ALERTS是 Prometheus / VMAlert 内置指标,记录所有告警的当前状态:alertstate="firing"—— 已触发alertstate="pending"—— 等待中
alertname!="AlertAlive"—— 排除自身,避免心跳告警影响下一次判断。max_over_time(...[6h])—— 过去 6 小时内是否存在过 firing 状态的告警。max_over_time把范围向量降为瞬时向量,有值则返回 1。unless on()—— 左边有但右边没有时保留。on()空标签列表表示按「无标签」匹配,即整条规则只剩下一个样本。- 效果:过去 6 小时有真实告警 firing 过 → unless 成立 → 不输出心跳告警;反之 → 输出心跳告警。
三、关键技术点
3.1 ALERTS 内置指标
ALERTS 是 vmalert / Prometheus 自动生成的时序指标,每条告警规则对应若干时间序列:
ALERTS{alertname="<规则名>", alertstate="firing|pending", <rule labels>}
- 仅在告警 firing / pending 期间存在样本。
- firing 结束后会写入一个
alertstate="firing"但值为 0 的样本(用于表达「刚恢复」)。 - 因此
max_over_time(ALERTS{...}[6h])能正确反映过去 6 小时是否有过 firing 事件。
3.2 unless 与 on() 修饰符
A unless B:返回在 A 中但不在 B 中的样本。on()指定匹配用的标签列表。空的on()表示按零标签匹配,等价于「不关心标签,只要 B 有任意样本就抑制 A」。
如果不加 on(),unless 会按所有标签匹配,导致「过去 6h 内某个具体告警的样本」无法抑制「当前 AlertAlive 这一条」,跳过逻辑失效。
3.3 vector(1) 的必要性
省略 vector(1) 时,(hour() + 8) % 6 == 0 and (minute() < 5) 在条件成立时返回空向量(因为 and 需要双方都有样本),整条 expr 永远为空,告警永远不会触发。
3.4 for: 0s 的作用
通常 for: 1m 表示条件持续 1 分钟才 firing。这里设为 0s 让告警在表达式成立的瞬间就进入 firing,避免窗口(5 分钟)不够长导致 pending 来不及转 firing。
3.5 时区处理的取巧之处
PromQL 没有原生时区函数,hour() 永远是 UTC。+ 8 是常见但脆弱的写法:
- 优点:不依赖服务端时区配置,规则可移植。
- 缺点:夏令时国家不适用(中国无夏令时,所以没问题)。
- 替代方案:在 vmalert 容器设置
TZ=Asia/Shanghai环境变量,让hour()直接返回本地时间,表达式可简化为(hour() % 6 == 0) and (minute() < 5) and vector(1)。
四、部署与验证
4.1 应用规则
kubectl apply -f alert-alive-vmrule.yaml
确认 VMRule 被 vmoperator 识别:
kubectl get vmrule -n monitor alert-alive
4.2 验证 vmalert 已加载
可以在 vmalert 前端页面中查看告警规则
4.3 手动验证 expr
直接在 vmselect 查询接口验证表达式返回是否正确:
- 可以通过页面访问 vmselect 服务的 UI 页面来手动验证 expr,
<ingress-domain>/select/0/vmui- 如果在 Grafana 中添加了 vmselect 地址作为数据源,也可以在 Grafana 的 Explore 页面查询
# 验证时间条件(当前不在窗口内应返回空)
((hour() + 8) % 6 == 0) and (minute() < 5) and vector(1)
# 验证过去 6h 是否有其他告警 firing
max_over_time(ALERTS{alertname!="AlertAlive", alertstate="firing"}[6h])
五、踩坑记录
alertname!="AlertAlive"必须排除自身:否则心跳自身 firing 后,下一个窗口会以为「过去 6h 有过告警」而跳过。for: 0s比for: 1m更稳:窗口只有 5 分钟,for: 1m可能让 firing 滞后到窗口外。