0%

Prometheus AlertManager 简单聊聊


作为 非常有优秀的时间序列的监控框架,告警功能是必不可少的。Prometheus 本身是没有告警功能的,因此我们需要通过第三方的插件,也就是我这里提到的 AlertManager 来进行告警的。最近因需要在了解这块的内容。

架构分析

alertmanager

我们现在来看这个,从左到右的看:

  • Prometheus 会将警报信息发送到 Alertmanager 的触发器 , 这里的 API 就是 Alertmanager 内部提供的 API ,这个API 的地址为: Alertmanager API 这个 API 是Alertmanager 的openapi , 被我放到了这个 swagger-ui 上面了。

  • 警报会存储在 AlertProvider , 这个 是存放在 本机内存的,本身是一个 map , 同样,这个很容易的就扩展到其他的 Provider .

  • Dispatcher 可以理解为一个订阅,它订阅的是 这个警报信息。简单的说 是一个 goroutine , 它会不断的 去拉新的警报,然后会根据我们的配置好的 路由信息(Routing Tree) 将警报路由分到一个组内;

  • 每一个 分组(Group),会做一个 定时的任务(Flush),间隔时间为参数配置中的 group_interval , Flush 之后,这组的警报会进入到 Notification Pipeline 的链式处理;

  • Notification Pipeline 主要的作用就是:确定告警的目标,执行抑制逻辑,静默逻辑,去重逻辑,发送与重试逻辑,最后是实现警报的投递。

  • Slience 其实就是一个沉默事件。最简单的理解就是 免打扰 模式。

Route

现在,我们需要先看下我们的这个路由的信息,也就是 Routing Tree:
在我们的 alertmanager 的源码里面,我们找到这个:

1
2
3
4
5
6
7
8
9
10
11
12
13
// A Route is a node that contains definitions of how to handle alerts.
type Route struct {
parent *Route
// The configuration parameters for matches of this route.
RouteOpts RouteOpts
// Equality or regex matchers an alert has to fulfill to match
// this route.
Matchers types.Matchers
// If true, an alert matches further routes on the same level.
Continue bool
// Children routes of this route.
Routes []*Route
}

这其实就是一棵树,父节点是 parent , 子节点是 Routes 的数组。

这里针对每一个 Route 的警报的匹配使用的是 深度搜索遍历: (所以别再说 数据结构学了也用不上)

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
26
// Match does a depth-first left-to-right search through the route tree
// and returns the matching routing nodes.
func (r *Route) Match(lset model.LabelSet) []*Route {
if !r.Matchers.Match(lset) {
return nil
}

var all []*Route

for _, cr := range r.Routes {
matches := cr.Match(lset)

all = append(all, matches...)

if matches != nil && !cr.Continue {
break
}
}

// If no child nodes were matches, the current node itself is a match.
if len(all) == 0 {
all = append(all, r)
}

return all
}

警报从 root 节点 开始进行 match , 根据 节点定义的 Matchers 来决定警报和节点是否匹配,匹配的话,我们就 Continue ,否则就 return . 当我们的 DFS 走到了最后一个节点的时候,就会被返回,这就是我们说的最深的 match . 这里有有个情况,就是 如果 每个节点里面的 Continue ,如果这个属性为 true , 那就是 an alert matches further routes on the same level,也就是所谓的 抄送 。这个时候,不会立即返回,而是继续搜索,将支持的警报发送给多方的场景。

这个时候,我们可以看下 Alertmanager 的 Route 的相关的配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
route:
group_by: ['alertname']
group_wait: 10s
group_interval: 10s
repeat_interval: 1h
receiver: 'email'
receivers:
- name: 'email'
email_configs:
- smarthost: 'smtp.163.com:25'
to: 'xxxx@gmail.com'
html: '{{ template "email.html" . }}' # 设定邮箱的内容模板
headers: { Subject: "[WARN] 报警邮件"} # 接收邮件的标题
inhibit_rules:
- source_match:
severity: 'critical'
target_match:
severity: 'warning'
equal: ['alertname', 'dev', 'instance']

可以看到的是,我们可以自由的给每一个警报做分组:

1
2
3
route:
routes:
xxx

这里关于配置啥的,就不做过多的说明了,大家可以参考 Alertmanager 的官方文档:

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
global:
# The smarthost and SMTP sender used for mail notifications.
smtp_smarthost: 'localhost:25'
smtp_from: 'alertmanager@example.org'

# The root route on which each incoming alert enters.
route:
# The root route must not have any matchers as it is the entry point for
# all alerts. It needs to have a receiver configured so alerts that do not
# match any of the sub-routes are sent to someone.
receiver: 'team-X-mails'

# The labels by which incoming alerts are grouped together. For example,
# multiple alerts coming in for cluster=A and alertname=LatencyHigh would
# be batched into a single group.
#
# To aggregate by all possible labels use '...' as the sole label name.
# This effectively disables aggregation entirely, passing through all
# alerts as-is. This is unlikely to be what you want, unless you have
# a very low alert volume or your upstream notification system performs
# its own grouping. Example: group_by: [...]
group_by: ['alertname', 'cluster']

# When a new group of alerts is created by an incoming alert, wait at
# least 'group_wait' to send the initial notification.
# This way ensures that you get multiple alerts for the same group that start
# firing shortly after another are batched together on the first
# notification.
group_wait: 30s

# When the first notification was sent, wait 'group_interval' to send a batch
# of new alerts that started firing for that group.
group_interval: 5m

# If an alert has successfully been sent, wait 'repeat_interval' to
# resend them.
repeat_interval: 3h

# All the above attributes are inherited by all child routes and can
# overwritten on each.

# The child route trees.
routes:
# This routes performs a regular expression match on alert labels to
# catch alerts that are related to a list of services.
- match_re:
service: ^(foo1|foo2|baz)$
receiver: team-X-mails

# The service has a sub-route for critical alerts, any alerts
# that do not match, i.e. severity != critical, fall-back to the
# parent node and are sent to 'team-X-mails'
routes:
- match:
severity: critical
receiver: team-X-pager

- match:
service: files
receiver: team-Y-mails

routes:
- match:
severity: critical
receiver: team-Y-pager

# This route handles all alerts coming from a database service. If there's
# no team to handle it, it defaults to the DB team.
- match:
service: database

receiver: team-DB-pager
# Also group alerts by affected database.
group_by: [alertname, cluster, database]

routes:
- match:
owner: team-X
receiver: team-X-pager

- match:
owner: team-Y
receiver: team-Y-pager


# Inhibition rules allow to mute a set of alerts given that another alert is
# firing.
# We use this to mute any warning-level notifications if the same alert is
# already critical.
inhibit_rules:
- source_match:
severity: 'critical'
target_match:
severity: 'warning'
# Apply inhibition if the alertname is the same.
# CAUTION:
# If all label names listed in `equal` are missing
# from both the source and target alerts,
# the inhibition rule will apply!
equal: ['alertname']


receivers:
- name: 'team-X-mails'
email_configs:
- to: 'team-X+alerts@example.org, team-Y+alerts@example.org'

- name: 'team-X-pager'
email_configs:
- to: 'team-X+alerts-critical@example.org'
pagerduty_configs:
- routing_key: <team-X-key>

- name: 'team-Y-mails'
email_configs:
- to: 'team-Y+alerts@example.org'

- name: 'team-Y-pager'
pagerduty_configs:
- routing_key: <team-Y-key>

- name: 'team-DB-pager'
pagerduty_configs:
- routing_key: <team-DB-key>

Notification Pipeline

前面有说道 , Alertmanager 在经过 Dispatch 的拉取后,会进入到这个 Notification Pipeline 序列。

也就是说 , 在 Routing Tree 的分组后,警报会触发 Notification Pipeline :

  • 当一个 AlertGroup 新建后,它会等待一段时间(group_wait 参数),再触发第一次 Notification Pipeline ;

  • 假如这个 AlertGroup 持续存在,那么之后每隔一段时间(group_interval 参数),都会触发一次 Notification Pipeline;

现在来看这个 Notification Pipeline , 每一次触发,AlertGroup 都会将 Alert 作为一个 列表 传给 Pipeline , Notification Pipeline 本身是按照责任链模式设计的接口, MultiStage 这个实现会执行所有的 Stage:

notify.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// A Stage processes alerts under the constraints of the given context.
type Stage interface {
Exec(ctx context.Context, l log.Logger, alerts ...*types.Alert) (context.Context, []*types.Alert, error)
}

// A MultiStage executes a series of stages sequentially.
type MultiStage []Stage

// Exec implements the Stage interface.
func (ms MultiStage) Exec(ctx context.Context, l log.Logger, alerts ...*types.Alert) (context.Context, []*types.Alert, error) {
var err error
for _, s := range ms {
if len(alerts) == 0 {
return ctx, nil, nil
}

ctx, alerts, err = s.Exec(ctx, l, alerts...)
if err != nil {
return ctx, nil, err
}
}
return ctx, alerts, nil
}

这块没看明白。。。。虽然是跟着大神一步一步走的,但在这块还是有点懵逼的。看下大神的解释是这样的:

MultiStage 里塞的就是开头架构图里画的 InhibitStage、SilenceStage…这么一条链式处理的流程,这里要提一下,官方的架构图画错了,RoutingStage 其实处在整个 Pipeline 的首位,不过这个顺序并不影响逻辑。 要重点说的是DedupStage和NotifySetStage它俩协同负责去重工作,具体做法是:

  • NotifySetStage 会为发送成功的警报记录一条发送通知,key 是’接收组名字’+’GroupKey 的 key 值’,value 是当前 Stage 收到的 []Alert (这个列表和最开始进入 Notification Pipeline 的警报列表有可能是不同的,因为其中有些 Alert 可能在前置 Stage 中已经被过滤掉了)

  • DedupStage 中会以’接收组名字’+’GroupKey 的 key 值’为 key 查询通知记录,假如:

    • 查询无结果,那么这条通知没发过,为这组警报发送一条通知;
    • 查询有结果,那么查询得到已经发送过的一组警报 S,判断当前的这组警报 A 是否为 S 的子集:
      • 假如 A 是 S 的子集,那么表明 A 和 S 重复,这时候要根据 repeat_interval 来决定是否再次发送:
        • 距离 S 的发送时间已经过去了足够久(repeat_interval),那么我们要再发送一遍;
        • 距离 S 的发送时间还没有达到 repeat_interval,那么为了降低警报频率,触发去重逻辑,这次我们就不发了;
      • 假如 A 不是 S 的子集,那么 A 和 S 不重复,需要再发送一次; 上面的表述可能有些抽象,最后表现出来的结果是:
  • 假如一个 AlertGroup 里的警报一直发生变化,那么虽然每次都是新警报,不会被去重,但是由于 group_interval (假设是5分钟)存在,这个 AlertGroup 最多 5 分钟触发一次 Notification Pipeline,因此最多也只会 5 分钟发送一条通知;

  • 假如一个 AlertGroup 里的警报一直不变化,就是那么几条一直 FIRING 着,那么虽然每个 group_interval 都会触发 Notification Pipeline,但是由于 repeate_interval(假设是1小时)存在,因此最多也只会每 1 小时为这个重复的警报发送一条通知; 再说一下 Silence 和 Inhibit,两者都是基于用户主动定义的规则的:

  • Silence Rule:静默规则用来关闭掉部分警报的通知,比如某个性能问题已经修复了,但需要排期上线,那么在上线前就可以把对应的警报静默掉来减少噪音;

  • Inhibit Rule:抑制规则用于在某类警报发生时,抑制掉另一类警报,比如某个机房宕机了,那么会影响所有上层服务,产生级联的警报洪流,反而会掩盖掉根本原因,这时候抑制规则就有用了; 因此 Notification Pipeline 的设计意图就很明确了:通过一系列逻辑(如抑制、静默、去重)来获得更高的警报质量,由于警报质量的维度很多(剔除重复、类似的警报,静默暂时无用的警报,抑制级联警报),因此 Notification Pipeline 设计成了责任链模式,以便于随时添加新的环节来优化警报质量

这是打赏的地方...

本文标题:Prometheus AlertManager 简单聊聊

文章作者:Mr.Sun

发布时间:2020年06月17日 - 09:08:10

最后更新:2020年06月17日 - 17:35:16

原始链接:http://www.blog.sun-iot.xyz/posts/77a48a2c

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

---------Thanks for your attention---------