AlertManager 中的几个容易混淆的参数

首先在 Prometheus 中有两个全局的参数 scrape_intervalevaluation_interval

scrape_interval

参数表示的是 Prometheus 从各种 metrics 接口抓取指标数据的时间间隔

evaluation_interval

参数表示的是 Prometheus 对报警规则进行评估计算的时间间隔。

group_by

为了避免连续发送类似的告警通知,可以将相关告警分到同一组中进行告警。分组机制可以将详细的告警信息合并成一个通知,在某些情况下,比如由于系统宕机导致大量的告警被同时触发,在这种情况下分组机制可以将这些被触发的告警合并为一个告警通知,避免一次性接受大量的告警通知:

1
group_by: ['alertname', 'job']

group_wait

当一个新的报警分组被创建后,需要等待至少 group_wait 时间来初始化告警。

这样实际上就缓冲了从 Prometheus 发送到 AlertManager 的告警,将告警按相同的标签分组,而不必全都发送:

1
2
group_by: ['alertname', 'job']
group_wait: 45s # 通常设置成0s ~ 几分钟

group_interval

但是这可能也导致了接收到的告警通知的等待时间更长了。另外一个问题是下次对告警规则进行评估的时候,我们将再次收到相同的分组告警通知,这个时候我们可以使用 group_interval 参数来进行配置,当上一个告警通知发送到一个 group 后,我们在等待 group_interval 时长后,然后再将触发的告警以及已解决的告警发送给 receiver:

1
2
3
group_by: ['instance', 'job']
group_wait: 45s
group_interval: 10m # 通常设置成5分钟以上

repeat_interval

1
2
3
4
group_by: ['instance', 'job']
group_wait: 45s
group_interval: 10m
repeat_interval: 4h # 对于比较重要紧急的可以将改参数设置稍微小点,对于不太紧急的可以设置稍微大点

除此之外还有一个 repeat_interval 参数,该参数主要是用于配置告警信息已经发送成功后,再次被触发发送的时间间隔,一般不同类型的告警业务改参数配置不太一样,对于比较重要紧急的可以将改参数设置稍微小点,对于不太紧急的可以设置稍微大点。 上面这些都是在 Prometheus 或者 AlertManager 中配置的一些全局的参数,对于具体的告警规则还有时间可以配置,如下所示的告警规则:

告警规则说明

1
2
3
4
5
6
7
8
9
10
11
groups:
- name: test-node-mem
rules:
- alert: NodeMemoryUsage
expr: (node_memory_MemTotal_bytes - (node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes)) / node_memory_MemTotal_bytes * 100 > 90
for: 1m
labels:
team: node
annotations:
summary: "{{$labels.instance}}: High Memory usage detected"
description: "{{$labels.instance}}: Memory usage is above 90% (current value is: {{ $value }}"

上面我们定义了一个名为 test-node-mem 的报警规则分组,一条报警规则主要由以下几部分组成:

  • alert:告警规则的名称
  • expr:是用于进行报警规则 PromQL 查询语句
  • for:评估等待时间(Pending Duration),用于表示只有当触发条件持续一段时间后才发送告警,在等待期间新产生的告警状态为pending
  • labels:自定义标签,允许用户指定额外的标签列表,把它们附加在告警上
  • annotations:指定了另一组标签,它们不被当做告警实例的身份标识,它们经常用于存储一些额外的信息,用于报警信息的展示之类的

其中的 for 字段同样会影响到我们的告警到达时间,该参数用于表示只有当触发条件持续一段时间后才发送告警,在等待期间新产生的告警状态为pending,这个参数主要用于降噪,很多类似响应时间这样的指标都是有抖动的,通过指定 Pending Duration,我们可以过滤掉这些瞬时抖动,可以让我们能够把注意力放在真正有持续影响的问题上。

所以有的情况下计算我们的监控图表上面已经有部分指标达到了告警的阈值了,但是并不一定会触发告警规则,比如我们上面的规则中,设置的是1分钟的 Pending Duration,对于下图这种情况就不会触发告警,因为持续时间太短,没有达到一分钟:

如果告警规则超过阈值的持续时间超过了 Pending Duration 那么就会触发告警了,告警产生后,还要经过 Alertmanager 的分组、抑制处理、静默处理、去重处理和降噪处理最后再发送给接收者。所以从一条告警规则被评估到触发告警再到发送给接收方,中间会有一系列的各种因素进行干预,所以有时候在监控图表上看到已经达到了阈值而最终没有收到监控报警也就不足为奇了

状态说明

当一条告警规则评估后,它的状态可能是 inactivepending 或者 firing 中的一种。评估之后,状态将被发送到关联的 AlertManager 以进行潜在地开始或者停止告警通知的发送。

Alert对象有三个状态

  • Pending:活跃但是还未发送给 Alertmanager,是所有 Alert 对象的初始状态。
  • Firing:告警发送中,特指发送到 Alertmanager。
  • Inactive:未激活,这类告警会保留 resolved_retension 规定的时间 (程序常量 15 分钟),而不是马上删除。
  • 虚拟状态“被删除”,处于 Inactive 状态的告警超过 resolved_retention 规定的时间之后,就会被删除。

三种状态的迁移逻辑:

Pending -> Firing

Alert 对象当前处于 Pending,Eval 结果是 true,且距离初次活跃时间(Alert.ActiveAt)超过<for>AlertingRule.holdDuration)的时长,那么这个Alert对象就会变成 Firing 状态。

Pending -> 被删除

Alert 对象当前处于 Pending 状态,Eval 结果是 false,那么这个 Alert 对象就直接被删除。

Firing -> Inactive

Alert 对象当前处于 Firing 状态,Eval 结果是 false,那么这个 Alert 对象会变成 Inactive状态

Inactive -> Pending

Alert 对象当前处于 Inactive状态,Eval 结果是 true,那么这个 Alert 对象会重置为 Pending 状态。

Inactive -> 被删除

Alert对象当前处于Inactive状态,且保持超过了 resolved_retention (p8s里写死 15 分钟),则被删除。

告警发送的逻辑

AlertingRule 执行之后,会把 Firing / Inactive 状态的 Alert 发送出去,逻辑如下:

1
2
3
4
5
6
7
8
9
10
11
12
func (a *Alert) needsSending(ts time.Time, resendDelay time.Duration) bool {
if a.State == StatePending {
return false
}

// if an alert has been resolved since the last send, resend it
if a.ResolvedAt.After(a.LastSentAt) {
return true
}

return a.LastSentAt.Add(resendDelay).Before(ts)
}

代码中的连个参数:

  • ts,是当前时间
  • resendDelay,是程序启动参数 --rules.alert.resend-delay 规定的,默认 1m

Alert 发送之后会更新 LastSentAtValidUntil 字段:

1
2
Alert.LastSentAt = ts
Alert.ValidUntil = ts + max([check_interval], [resend_delay]) * 4

ValidUntil 字段是一个预估的告警有效时间,超过这个时间点告警会被认为已经解除,具体逻辑见下文。

Prometheus -> Alertmanager 机制

当告警变成 Firing 时,发送给 Alertmanager 的消息如下,可以看到 startsAt 就是当前时间,而 endsAt 则是 ValidUntil

1
2
3
4
5
6
7
8
9
10
ts = 2022-06-08 14:41:14.199515 +0800 
[
{
"annotations": { ... },
"startsAt": "2022-06-08T06:41:14.185Z",
"endsAt": "2022-06-08T06:45:14.185Z",
"generatorURL": "...",
"labels": { ... }
}
]

当告警 Inactive 后,发送给 Alertmanager 的消息如下, endsAt 就是当前时间,而 startsAt 和原来一样:

1
2
3
4
5
6
7
8
9
10
ts = 2022-06-08 14:41:29.195836 +0800 
[
{
"annotations": { ... },
"startsAt": "2022-06-08T06:41:14.185Z",
"endsAt": "2022-06-08T06:41:29.185Z",
"generatorURL": "...",
"labels": { ... }
}
]

如果告警一直 Firing,那么 Prometheus 会在 resend_delay 的间隔重复发送,而 startsAt 保持不变, endsAt 跟着 ValidUntil 变:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
ts = 2022-06-08 14:48:34.197001 +0800
[
{
"annotations": { ... },
"startsAt": "2022-06-08T06:48:34.185Z",
"endsAt": "2022-06-08T06:52:34.185Z",
"generatorURL": "...",
"labels": { ... }
}
]
ts = 2022-06-08 14:49:39.195611 +0800
[
{
"annotations": { ... },
"startsAt": "2022-06-08T06:48:34.185Z",
"endsAt": "2022-06-08T06:53:39.185Z",
"generatorURL": "...",
"labels": { ... }
}
]

Alertmanager -> webhook 机制

第一次收到 Firing 的告警消息,Alertmanager 发给 webhook 的消息如下,则可以看到 status=firing,当前时间戳比 startsAt 晚一些,endsAt 没有提供(忽略了 Prometheus 提供的信息):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
ts = 2022-06-08 14:55:49.201768 +0800 
{
"receiver": "webhook",
"status": "firing",
"alerts": [
{
"status": "firing",
"labels": { ... },
"annotations": { ... },
"startsAt": "2022-06-08T06:55:44.185Z",
"endsAt": "0001-01-01T00:00:00Z",
"generatorURL": "...",
"fingerprint": "3ec2d9fb9c4f7f1a"
}
],
"groupLabels": {
"alertname": "mock2"
},
"commonLabels": { alerts 数组里的共同 label },
"commonAnnotations": { alerts 数组里的共同 annotation },
"externalURL": "...",
"version": "4",
"groupKey": "{}:{alertname=\"mock2\"}",
"truncatedAlerts": 0
}

收到 Inactive 的消息(endsAt <= 当前时间),Alertmanager 发给 webhook 的消息如下,可以看到 status=resolved,当前时间戳比 endsAt 晚一些,startsAt 则保持不变:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
ts = 2022-06-08 14:56:19.201334 +0800 
{
"receiver": "webhook",
"status": "resolved",
"alerts": [
{
"status": "resolved",
"labels": { ... },
"annotations": { ... },
"startsAt": "2022-06-08T06:55:44.185Z",
"endsAt": "2022-06-08T06:56:04.185Z",
"generatorURL": "...",
"fingerprint": "3ec2d9fb9c4f7f1a"
}
],
"groupLabels": {
"alertname": "mock2"
},
"commonLabels": { alerts 数组里的共同 label },
"commonAnnotations": { alerts 数组里的共同 annotation },
"externalURL": "<alertmanager 的URL>",
"version": "4",
"groupKey": "{}:{alertname=\"mock2\"}",
"truncatedAlerts": 0
}

注意:Alertmanager 里必须有 Inactive 消息所对应的告警,否则是会被忽略的。换句话说如果一个告警在 Alertmanager 里已经解除了,再发同样的 Inactive 消息,Alertmanager 是不会发给 webhook 的。

Prometheus 需要 持续 地将 Firing 告警发送给 Alertmanager,遇到以下一种情况,Alertmanager 会认为告警已经解决,发送一个 resolved:

  1. Prometheus 发送了 Inactive 的消息给 Alertmanager,即 endsAt=当前时间
  2. Prometheus 在上一次消息的 endsAt 之前,一直没有发送任何消息给 Alertmanager

不用担心 Alertmanager 会将告警消息重复发送给 webhook,route.repeat_interval (文档) 会避免这个问题。

对于第二种情况,Alertmanager 发送给 webhook 的消息如下,status=resolved,当前时间戳比 endsAt 稍晚一些,startsAt 则保持不变::

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
ts = 2022-06-08 15:34:27.167246 +0800 
{
"receiver": "webhook",
"status": "resolved",
"alerts": [
{
"status": "resolved",
"labels": { ... },
"annotations": { ... },
"startsAt": "2022-06-08T07:25:58Z",
"endsAt": "2022-06-08T07:34:00Z",
"generatorURL": "...",
"fingerprint": "3ec2d9fb9c4f7f1a"
}
],
"groupLabels": {
"alertname": "mock2"
},
"commonLabels": { alerts 数组里的共同 label },
"commonAnnotations": { alerts 数组里的共同 annotation },
"externalURL": "<alertmanager 的URL>",
"version": "4",
"groupKey": "{}:{alertname=\"mock2\"}",
"truncatedAlerts": 0
}

另外两个细节:

  • 如果 startsAt 没有提供,则自动等于当前时间
  • 如果 endsAt 没有提供,则自动等于 startsAt + resolve_timeout(默认 5m)