alertmanager 源码分析一: 告警的写入

3,285 阅读8分钟

监控告警一般是作为一个整体,包括从采集数据、处理、存储、展示、规则计算、告警消息处理等等。 Alertmanager(以下简称 am) 是一个告警消息管理组件,包括消息路由、静默、抑制、去重等功能,总之其它负责规则计算的组件可以把消息无脑发给 am, 由它来对消息进行处理, 尽可能发出高质量的告警消息。

先看个结构概览图,这个是我基于原开源库里面的架构图画的,原仓库中的架构图有很多跟实际源码出入的地方,所以这个图比原来的更丰富,更准确。

截屏2021-11-13 20.49.21.png

这篇先说第一部分:告警的写入

告警的写入到最终被处理可以抽象成生产-消费模型,生产侧就是 api 接收告警,消费侧就是图中的 dispatcher,中间的 provider.Alerts 作为缓冲区。

下面是写入时的逻辑,主要就是判断告警状态,am 的告警状态是由 alert.StartsAtalert.EndsAt 来判断,而后续还有很多需要这个属性的逻辑,所以这个位置需要把起止时间确认。

func (api *API) insertAlerts(w http.ResponseWriter, r *http.Request, alerts ...*types.Alert) {
	now := time.Now()
	
	api.mtx.RLock()
	resolveTimeout := time.Duration(api.config.Global.ResolveTimeout)
	api.mtx.RUnlock()

	// 确定一个告警消息的起止时间
	// 需要根据起止时间来定义告警的状态, 如果止时间在当前之前就是 Resolved
	for _, alert := range alerts {

		// 新收到的告警标记接收时间, 这样如果有两个告警 label 一致
                // 可以判断出哪个是最新收到的
		alert.UpdatedAt = now

		// Ensure StartsAt is set.
		if alert.StartsAt.IsZero() {
			if alert.EndsAt.IsZero() {
				alert.StartsAt = now
			} else {
				alert.StartsAt = alert.EndsAt
			}
		}
		// 止时间如果没有就需要使用 resolveTimeout 计算一个
		if alert.EndsAt.IsZero() {
			alert.Timeout = true
			alert.EndsAt = now.Add(resolveTimeout)
		}
		if alert.EndsAt.After(time.Now()) {
			api.m.Firing().Inc()
		} else {
			api.m.Resolved().Inc()
		}
	}

	// Make a best effort to insert all alerts that are valid.
	var (
		validAlerts    = make([]*types.Alert, 0, len(alerts))
		validationErrs = &types.MultiError{}
	)

	// 校验 alert, 
        // 比如清理空值的 label, 起止时间, 至少一个label, label中的kv命名规则 等等
	for _, a := range alerts {
		removeEmptyLabels(a.Labels)

		if err := a.Validate(); err != nil {
			validationErrs.Add(err)
			api.m.Invalid().Inc()
			continue
		}
		validAlerts = append(validAlerts, a)
	}
	// 写入 alertsProvider, 这一端相当于生产者
	if err := api.alerts.Put(validAlerts...); err != nil {
		api.respondError(w, apiError{
			typ: errorInternal,
			err: err,
		}, nil)
		return
	}
}

provider.Alerts 是一个interface

// 所有方法都要 groutine-safe
type Alerts interface {
	Subscribe() AlertIterator
	GetPending() AlertIterator
	Get(model.Fingerprint) (*types.Alert, error)
	Put(...*types.Alert) error
}

源码中给出了一个基于内存的实现,所以所有告警的接收都会先写入这个结构,其它过程都从这里获取自己需要的告警,后面会称这个基于内存的实现称为 AlertsProvider

// Alerts 管理结构, 就是架构图中的 Alerts 使用的结构
type Alerts struct {
	cancel context.CancelFunc

	mtx       sync.Mutex
	alerts    *store.Alerts			// 存储 map[fingerprint]*Alert
	listeners map[int]listeningAlerts	// 所有监听者
	next      int				// 监听者计数

	callback AlertStoreCallback
	logger log.Logger
}

先看看 AlertsProviderPut 看看告警是如何写入的

func (a *Alerts) Put(alerts ...*types.Alert) error {
	for _, alert := range alerts {
                // 制作唯一ID, 基于 LabelSets 中的 label name 和 label value
		fp := alert.Fingerprint()

		existing := false

		// 如果已经存在相同的alert, 就是labelSets相同
		if old, err := a.alerts.Get(fp); err == nil {
			existing = true

			// 新旧告警区间有重叠的, 合并, 按照一定的策略使用较新的告警内容
			if (alert.EndsAt.After(old.StartsAt) && alert.EndsAt.Before(old.EndsAt)) ||
				(alert.StartsAt.After(old.StartsAt) && alert.StartsAt.Before(old.EndsAt)) {
				alert = old.Merge(alert)
			}
		}
                // 这个 Set 方法就把当前这个 alert 使用上面制作的 fp 写入 map[fp]*Alert
		if err := a.alerts.Set(alert); err != nil {
			level.Error(a.logger).Log("msg", "error on set alert", "err", err)
			continue
		}
		
		// 发布者广播
		// 程序中其它模块会通过调用 Subscribe 来注册一个 listener 到 AlertsProvider
		// AlertsProvider 每次成功存储一个 alert 都会对所有 listener 广播
                // 这个过程上锁保证所有的 listeners 收到一致的广播
                a.mtx.Lock()
		for _, l := range a.listeners {
			select {
			case l.alerts <- alert:
			case <-l.done:
			}
		}
		a.mtx.Unlock()
	}
	return nil
}

程序中其它部分(dispatcher, Inhibitor)都是通过调用 Subscribe 来监听新写入的告警消息

func (a *Alerts) Subscribe() provider.AlertIterator {
	// groutine-safe
	a.mtx.Lock()
	defer a.mtx.Unlock()

	var (
		done   = make(chan struct{})
                // 获取所有的alerts
		alerts = a.alerts.List()
                // 创建一个 buffer chan, 保证容量要么盈余, 要么恰好
		ch     = make(chan *types.Alert, max(len(alerts), alertChannelLength)) 
	)
	// 把调用时已经存在的 alerts 写入一个 buffered chan
        // 实际上从 Alerts 开始接收到其它组件subscribe都是在程序启动期间就完成
        // 这中间接收到告警的可能性很小,即使接收到也不会很多
	for _, a := range alerts {
		ch <- a
	}

	// 为 AlertsProvider 新建一个 listener, 结构就是 buffered chan 和一个关闭信号 chan
        // 很明显 buffered chan 就是调用方获取告警的, 关闭信号 chan 就是调用方用来监听结束信号的
	// 使用 next 作为计数, 当前共有 next 个 listener
	a.listeners[a.next] = listeningAlerts{alerts: ch, done: done}
	a.next++
	
        // 这里把 buffered chan 和关闭信号 chan 重新打包成了 alertIterator 返回给调用方
	return provider.NewAlertIterator(ch, done, nil)
}

所以调用方就要使用 alertIterator 提供的一些方法了

type alertIterator struct {
	ch   <-chan *types.Alert
	done chan struct{}
	err  error
}
func (ai alertIterator) Next() <-chan *types.Alert { return ai.ch }
func (ai alertIterator) Err() error { return ai.err }
func (ai alertIterator) Close()     { close(ai.done) }

alertIterator 的实现了迭代器协议,先看一下Dispatcher 如何使用的

// 首先由 main.go 实例化 Dispatcher 并启动
go disp.Run()

// 其次, Dispatcher 中可导出的 Run 就从 AlertsProvider 通过调用 Subscribe 获取了一个 alertIterator
func (d *Dispatcher) Run() {
	d.done = make(chan struct{})

	d.mtx.Lock()
	d.aggrGroupsPerRoute = map[*Route]map[model.Fingerprint]*aggrGroup{}
	d.aggrGroupsNum = 0
	d.metrics.aggrGroups.Set(0)
	d.ctx, d.cancel = context.WithCancel(context.Background())
	d.mtx.Unlock()

	d.run(d.alerts.Subscribe())
	close(d.done)
}

// 最后, Dispatcher 的不可导出的 run, 
// 就是一个 for-select 结构同时监听 AlertsProvider 中新收到的 alert, gc 信号和退出信号
func (d *Dispatcher) run(it provider.AlertIterator) {
	cleanup := time.NewTicker(30 * time.Second)
	defer cleanup.Stop()

	defer it.Close()

	for {
		select {
		// alertIterator 的 Next() 方法返回一个 chan
                // 也就是上面 AlertsProvider 中提供的 buffered chan
		case alert, ok := <-it.Next():
			// 接下来就是 Dispatcher 在接收到新的告警该如何处理的问题了
			if !ok {
				// Iterator exhausted for some reason.
				if err := it.Err(); err != nil {
					level.Error(d.logger).Log("msg", "Error on alert update", "err", err)
				}
				return
			}

			level.Debug(d.logger).Log("msg", "Received alert", "alert", alert)

			// Log errors but keep trying.
			if err := it.Err(); err != nil {
				level.Error(d.logger).Log("msg", "Error on alert update", "err", err)
				continue
			}
			
			// 从 Dispatcher 中找到哪些 router 跟这个 alert 匹配
                        // 可能有多个 router 匹配
			// 使用每个 router 来处理这个 alert
			now := time.Now()
			for _, r := range d.route.Match(alert.Labels) {
				d.processAlert(alert, r)
			}
			d.metrics.processingDuration.Observe(time.Since(now).Seconds())

		case <-cleanup.C:
			// Dispatcher 会有个 gc 过程,主要清理一些不用的内存容器
			d.mtx.Lock()
			for _, groups := range d.aggrGroupsPerRoute {
				for _, ag := range groups {
					if ag.empty() {
						ag.stop()
						delete(groups, ag.fingerprint())
						d.aggrGroupsNum--
						d.metrics.aggrGroups.Dec()
					}
				}
			}
			d.mtx.Unlock()

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

综合 Put 中的广播过程、Subscribe 方法、alertIterator 的设计还有 Dispatcher 的监听,可以看出这个发布-订阅模式:

  1. 每个订阅者订阅时,发布者会新建一个 buffered chan,并把现在发布者已经有的消息写入 buffered chan
  2. 发布者把这个 buffered chan 存入自己 listeners 中,同时返回给订阅者这个 buffered chan
  3. 订阅者监听返回的 buffered chan,发布者每次收到消息把 listeners 中所有的 buffered chan 都广播,这样每个订阅者都会收到消息

再看一下这个结构如何退出,先看一下 AlertsProvider 新建时

// AlertsProvider 的运行工作比较简单,就是 GC 和 等待退出
func (a *Alerts) Run(ctx context.Context, interval time.Duration) {
	t := time.NewTicker(interval)
	defer t.Stop()
	for {
		select {
		case <-ctx.Done():
			return
		case <-t.C:
			a.gc()
		}
	}
}
// gc 的工作也很简单, 就是把已经解决的告警挑出来
// 这里还没有删除而是把它们都传给了 callback 函数
func (a *Alerts) gc() {
	a.Lock()
	defer a.Unlock()

	var resolved []*types.Alert
	for fp, alert := range a.c {
		if alert.Resolved() {
			delete(a.c, fp)
                        // 收集这些已经处理的为了传给回调函数
			resolved = append(resolved, alert)
		}
	}
	a.cb(resolved)
}

func NewAlerts(ctx context.Context, m types.Marker, intervalGC time.Duration, alertCallback AlertStoreCallback, l log.Logger) (*Alerts, error) {
	...
	ctx, cancel := context.WithCancel(ctx)
	a := &Alerts{ 省略各种初始化 }

	// 这里就是写 callback 函数的地方, 接收的就是已经解决的告警的 list
	a.alerts.SetGCCallback(func(alerts []*types.Alert) {
		
		// 从内存中清理 resolved 告警
		for _, alert := range alerts {
			m.Delete(alert.Fingerprint())
			a.callback.PostDelete(alert)
		}
		
		// 清理 listeners
		a.mtx.Lock()
		for i, l := range a.listeners {
			select {
			case <-l.done:
				delete(a.listeners, i)
				close(l.alerts)
			default:
				// listener is not closed yet, hence proceed.
			}
		}
		a.mtx.Unlock()
	})
	go a.alerts.Run(ctx, intervalGC)

	return a, nil
}

发布者关闭是安全的,因为是 chan 的发送者。

而订阅者的退出要把自己获取的 buffered chan 从发布者的 listeners 中去掉,Dispatcher.run 中退出时使用 defer 调用了 it.Close(),这个就是会关闭 listener 中的 done chan, 这个时候等待一个 AlertsProvidergc 周期就会被 callback 函数清理掉。这里要先 delete(a.listeners, i)close(l.alerts),否则先关闭的话,在广播时仍能看到这个被关闭的 listener,出现发送已关闭通道的 panic,当然清理 listeners 和广播这两个过程使用 mutex.Lock() 保护了,所以即使出现上面的顺序颠倒也不会有这个 panic.

还有个极端情况下也是安全的: 在订阅者已经退出,对应的 listener 在等待 AlertsProvidergc 周期时,来了很多告警,再看看这个广播的过程:

a.mtx.Lock()
for _, l := range a.listeners {
    select {
    case l.alerts <- alert:
    case <-l.done:
    }
}
a.mtx.Unlock()

这时订阅者已经使用 defer 调用了 it.Close() 退出,l.done 一定是成立的,l.alerts<-alert 也是成立的,所以有一定概率会往 buffered chan 中发送告警的,但是订阅者已经不接收,所以这个 case 有一定概率会执行直到这个 buffered chan 满了阻塞,再往后就是走 l.done 了,这个位置和清理 listeners 的位置优雅的使用了已关闭 chan 的可以重复接收的特性

总结上面的退出过程:

  1. 发布者的关闭是安全的。
  2. 订阅者的关闭需要双向通信结构 listenerlistener 结构就是 buffered chan 和一个 done chan,其中 buffered chan 是从发布者向订阅者传送数据,而 done chan 是订阅者把关闭信号发给发布者,让发布者清理该订阅者的注册信息。

截屏2021-10-30 17.29.54.png

现在告警已经被写入内存的 AlertsProvider 中, 并且 Dispatcher 通过监听订阅时获取的 chan 来实时处理告警消息,并匹配正确的 route 来处理这个告警,后面接着匹配 route 和 处理 alert 说。