alertmanager 源码分析二: 告警的处理

1,678 阅读6分钟

上篇说到告警已经写入到内存结构中 AlertsProvider 中,并且 Dispatcher 通过订阅 AlertsProvider 获取一个 chan,能够实时读到新写入的 alert

Dispatcher 的作用就是把 alert 分派给正确的 route 来处理告警,route 是一个树状结构,每个节点有 Matchers 用于判断和当前正在处理的 alert 是否匹配,匹配就对这个 alert 应用这个 route 上的规则,先看看 route 结构:

type Route struct {
	parent *Route
	Routes []*Route
	RouteOpts RouteOpts
	
        // alert 就是由 alert.LabelSet 和 Route.Matchers 两个是否匹配
        // 来决定是否使用这个 Route 来处理
	Matchers labels.Matchers

	// 当前层出现一个 route 匹配 alert 后
        // 是否在同级上继续使用这个 alert 尝试匹配其它 route
	Continue bool
}
// RouteOpts 保存了跟当前 Route 绑定的一些规则
// alert 匹配后就会使用这些规则处理
type RouteOpts struct {
	Receiver string

	GroupBy map[model.LabelName]struct{}

	GroupByAll bool

	GroupWait      time.Duration
	GroupInterval  time.Duration
	RepeatInterval time.Duration

	MuteTimeIntervals []string
}

对树状结构数据,源码中使用了递归来遍历节点

// lset 就是传入的告警的 label 集合
func (r *Route) Match(lset model.LabelSet) []*Route {

	// 当前节点不匹配
	if !r.Matchers.Matches(lset) {
		return nil
	}
	var all []*Route

	for _, cr := range r.Routes {
		matches := cr.Match(lset) // 深度优先

		all = append(all, matches...)

		// 如果已经匹配到同级的一个
                // 且 Continue=false 就结束当前层的匹配
		if matches != nil && !cr.Continue {
			break
		}
	}

	// 如果当前 route 的子 route.Routes 为空
        // 或者 route.Routes 中没有匹配 lset 
        // 那么当前 route 就是匹配的
	if len(all) == 0 {
		all = append(all, r)
	}

	return all
}

找到了所有跟当前告警匹配的 route, 接下来就是处理告警了,看看 processAlert,

func (d *Dispatcher) processAlert(alert *types.Alert, route *Route) {

	// 获取当前 route 上用于分组的标签 kv, 计算一个组的 fp
        // 如果 route 的 groupBy 是 ["a", "b"]
        // 那么 a=1,b="s" 的告警和 a=1,b="e" 的告警就会在一个 route 下的不同分组内
	groupLabels := getGroupLabels(alert, route)
	fp := groupLabels.Fingerprint()

	d.mtx.Lock()
	defer d.mtx.Unlock()
	
	// 没有对应的 routeGroups 就建一个
        // route 对应的是 routeGroups, fp 对应的是 aggrGroup
	routeGroups, ok := d.aggrGroupsPerRoute[route]
	if !ok {
		routeGroups = map[model.Fingerprint]*aggrGroup{}
		d.aggrGroupsPerRoute[route] = routeGroups
	}

	ag, ok := routeGroups[fp]
	if ok {
        // groupBy 中的 label 出现了一个新的取值组合, 直接写入
		ag.insert(alert)
		return
	}

	ag = newAggrGroup(d.ctx, groupLabels, route, d.timeout, d.logger)
	routeGroups[fp] = ag
	d.aggrGroupsNum++
	d.metrics.aggrGroups.Inc()

	// 终于, 新的告警被 Dispatcher 分派到了一个 routeGroup 下的一个 aggrGroup 中
	ag.insert(alert)

	// 每个 aggrGroup 会有个独立的处理当前分组告警的 groutine
        // 这里传给 ag.run 的匿名函数最终会是真正处理每个告警的函数
	go ag.run(func(ctx context.Context, alerts ...*types.Alert) bool {
		// 处理函数就是使用 Dispatcher 的 stage 来处理
		// stage 是在 Dispatcher 新建时编排的流水线
                // 里面都是 stage 接口类型的对象, stage 接口只有一个 Exec 方法
		_, _, err := d.stage.Exec(ctx, d.logger, alerts...)
		// ...
		return err == nil
	})
}

所以 processAlert 就是把告警分派给某个 route 下的某个 aggrGroup, 而每个 aggrGroup 会有个独立的 groutine 专门处理自己 aggrGroup 内的告警,在继续看每个 aggrGroup 是怎样处理自己组内的告警之前,先了解一下 aggrGroup 的结构和写入时发生了什么。

type aggrGroup struct {
	// 分组使用的 labels,比如上面例子中的 a=1,b="s"
	labels   model.LabelSet
	opts     *RouteOpts
	logger   log.Logger
	routeKey string

	// 这个结构和前面告警写入时存储的结构 AlertsProvider 底层使用的相同
	// 实际上就是一个 mutex.Lock 守护的一个 map[fp]alert
	alerts  *store.Alerts

        // 退出结构
        // aggrGroup 启动后在逻辑上是属于 Dispatcher
        // 所以需要监听 Dispatcher 的退出
        // 这个 ctx 就是 Dispatcher 的 ctx
	ctx     context.Context
	cancel  func()
	done    chan struct{}

	// 这个 timer 新建时使用的周期就是 group_wait, 即一个分组的首次告警等待时间
	// 在这个组首次 flush 之后,会被置为 GroupInterval 即一个更长的等待周期
	next    *time.Timer
	timeout func(time.Duration) time.Duration

	mtx        sync.RWMutex
	hasFlushed bool
}
// 写入 aggrGroup 一个告警
func (ag *aggrGroup) insert(alert *types.Alert) {
	// 和 AlertsProvider 的 Put 时一样
	if err := ag.alerts.Set(alert); err != nil {
		level.Error(ag.logger).Log("msg", "error on set alert", "err", err)
	}

	ag.mtx.Lock()
	defer ag.mtx.Unlock()

	// 写入 aggrGroup 成功后, 如果当前分组在新建后执行过 flush, 
	// 而且当前这个告警的开始时间距离现在大于一个 GroupWait 就触发一次 flush
        // flush 是每个 aggrGroup 用来处理自己分组内告警的函数, 稍后会说
	if !ag.hasFlushed && alert.StartsAt.Add(ag.opts.GroupWait).Before(time.Now()) {
		ag.next.Reset(0)
	}
}

接下来就是 aggrGroup 是如何处理自己组内的告警的

func (ag *aggrGroup) run(nf notifyFunc) {
	defer close(ag.done)
	defer ag.next.Stop()

	for {
		select {

		case now := <-ag.next.C:
			// 如果当前告警没有在 GroupInterval 内完成, 就立即终止
                        // 因为下个 flush 来了
			ctx, cancel := context.WithTimeout(ag.ctx, ag.timeout(ag.opts.GroupInterval))
			// flush 中使用的 now 都是以这个时间为准
			ctx = notify.WithNow(ctx, now)

			// Populate context with information needed along the pipeline.
			ctx = notify.WithGroupKey(ctx, ag.GroupKey())
			ctx = notify.WithGroupLabels(ctx, ag.labels)
			ctx = notify.WithReceiverName(ctx, ag.opts.Receiver)
			ctx = notify.WithRepeatInterval(ctx, ag.opts.RepeatInterval)
			ctx = notify.WithMuteTimeIntervals(ctx, ag.opts.MuteTimeIntervals)

			ag.mtx.Lock()
			// ag.next 这个 timer 在 aggrGroup 新建的时候使用 group_await
			// 首次触发后, 就被重置为 GroupInterval, 一般这个更长一点
			ag.next.Reset(ag.opts.GroupInterval)
			ag.hasFlushed = true
			ag.mtx.Unlock()

			ag.flush(func(alerts ...*types.Alert) bool {
				return nf(ctx, alerts...)
			})

			cancel()

		case <-ag.ctx.Done():
			return
		}
	}
}

func (ag *aggrGroup) flush(notify func(...*types.Alert) bool) {
	if ag.empty() { return }
	var (
		alerts      = ag.alerts.List()
		alertsSlice = make(types.AlertSlice, 0, len(alerts))
		now         = time.Now()
	)
	for _, alert := range alerts {
		a := *alert
		// 没解决的就把结束时间置零, 不然后续流程如果很长
                // 可能就处理到一半 EndsAt 就到期了
		if !a.ResolvedAt(now) {
			a.EndsAt = time.Time{}
		}
		alertsSlice = append(alertsSlice, &a)
	}
	sort.Stable(alertsSlice)

	// 这个 notify(alertsSlice...) 就是消息处理流水线入口, 
	// if body 中的逻辑就是跑完流水线并且没有错误再执行的逻辑
	if notify(alertsSlice...) {
		for _, a := range alertsSlice {
			fp := a.Fingerprint()
			got, err := ag.alerts.Get(fp)
			if err != nil {
				// This should never happen.
				level.Error(ag.logger).Log("msg", "failed to get alert", "err", err, "alert", a.String())
				continue
			}
			// 从当前的 aggrGroup 中清理已经解决的告警
			if a.Resolved() && got.UpdatedAt == a.UpdatedAt {
				if err := ag.alerts.Delete(fp); err != nil {
					level.Error(ag.logger).Log("msg", "error on delete alert", "err", err, "alert", a.String())
				}
			}
		}
	}
}

上面两个地方提到了 aggrGroup 触发 flush 的周期,总结一下:

  1. 新写入告警时,如果 aggrGroup 是新建的,一定是有新的告警导致新建的,组内一定是有告警的,这时会等待 GroupWait 再去执行 flush.
  2. 新写入告警时,aggrGroup 已经存在的,那么如果没执行过 flush, 说明还在首次等待 GroupWait 的周期内,这个时候要决定是继续等待 GroupWait, 还是立即触发一次,这里的判断是告警的开始时间距离现在已经超过 GroupWait, 那就立即触发一次,否则就继续等待 GroupWait,当然如果执行过 flush,就要等待 GroupInterval.

到这个位置告警已经被处理掉了,实际上就是跑完流水线后告警的状态变了,aggrGroup 中清理告警只是清理告警的一个指针,还有其它地方的组件还存储者这个告警的指针,比如 AlertsProvider 中,不过那边就由 AlertsProviderGC 函数处理了。

上面的流水线的入口函数被传递了很多层,我把它收集在一起就看的比较清楚:

// 最初是 aggrGroup 启动时定义, 传给 aggrGroup.run 函数
// 实际上这个函数是一个闭包, 因为引用了 d.logger, d.stage
go ag.run(func(ctx context.Context, alerts ...*types.Alert) bool {
	_, _, err := d.stage.Exec(ctx, d.logger, alerts...)
	return err == nil
})
// aggrGroup.run 对它进一步封装, 也是返回一个闭包
// 把 aggrGroup.run 中计算好的 ctx 传给这个函数, 增加了闭包中的信息
func (ag *aggrGroup) run(nf notifyFunc) {
	for {
		select {
		case now := <-ag.next.C:
			ag.flush(func(alerts ...*types.Alert) bool {
				return nf(ctx, alerts...)
			})
			cancel()
		}
	}
}
// 最终是在 flush 中使用本组的 alert 调用这个函数
func (ag *aggrGroup) flush(notify func(...*types.Alert) bool) {
	if notify(alertsSlice...) {}
}

总结下来,整个过程就是为了调用 d.stage.Exec(ctx, d.logger, alerts...),每一层都通过闭包向下一层传递数据,最终是使用了 Dispatcher.logger, aggrGroupctxalerts 来调用 Dispatcher.stage.Exec 函数。这样Dispatcher里面的每个分组就共用了相同的流水线,而每个分组调用时传入的ctx都是当前分组的信息。

截屏2021-11-19 17.44.02.png

现在告警从 api 写入到 AlertsProvider, 再被发布到 Dispatcher, 然后根据路由被分派到一个 aggrGroup, 每个 aggrGroup 会周期的调用 d.stage.Exec 函数来处理这些告警,处理完会从中清理完成的告警,d.stage 就是一个流水线, 静默、抑制、去重等等都是在流水线中完成的,后面就接着看 d.stage.Exec 中的逻辑。