监控告警一般是作为一个整体,包括从采集数据、处理、存储、展示、规则计算、告警消息处理等等。 Alertmanager(以下简称 am) 是一个告警消息管理组件,包括消息路由、静默、抑制、去重等功能,总之其它负责规则计算的组件可以把消息无脑发给 am, 由它来对消息进行处理, 尽可能发出高质量的告警消息。
先看个结构概览图,这个是我基于原开源库里面的架构图画的,原仓库中的架构图有很多跟实际源码出入的地方,所以这个图比原来的更丰富,更准确。
这篇先说第一部分:告警的写入
告警的写入到最终被处理可以抽象成生产-消费模型,生产侧就是 api 接收告警,消费侧就是图中的 dispatcher,中间的 provider.Alerts 作为缓冲区。
下面是写入时的逻辑,主要就是判断告警状态,am 的告警状态是由 alert.StartsAt 和 alert.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
}
先看看 AlertsProvider 的 Put 看看告警是如何写入的
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 的监听,可以看出这个发布-订阅模式:
- 每个订阅者订阅时,发布者会新建一个
buffered chan,并把现在发布者已经有的消息写入buffered chan - 发布者把这个
buffered chan存入自己listeners中,同时返回给订阅者这个buffered chan - 订阅者监听返回的
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, 这个时候等待一个 AlertsProvider 的 gc 周期就会被 callback 函数清理掉。这里要先 delete(a.listeners, i) 再 close(l.alerts),否则先关闭的话,在广播时仍能看到这个被关闭的 listener,出现发送已关闭通道的 panic,当然清理 listeners 和广播这两个过程使用 mutex.Lock() 保护了,所以即使出现上面的顺序颠倒也不会有这个 panic.
还有个极端情况下也是安全的: 在订阅者已经退出,对应的 listener 在等待 AlertsProvider 的 gc 周期时,来了很多告警,再看看这个广播的过程:
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 的可以重复接收的特性
总结上面的退出过程:
- 发布者的关闭是安全的。
- 订阅者的关闭需要双向通信结构
listener,listener结构就是buffered chan和一个done chan,其中buffered chan是从发布者向订阅者传送数据,而done chan是订阅者把关闭信号发给发布者,让发布者清理该订阅者的注册信息。
现在告警已经被写入内存的 AlertsProvider 中, 并且 Dispatcher 通过监听订阅时获取的 chan 来实时处理告警消息,并匹配正确的 route 来处理这个告警,后面接着匹配 route 和 处理 alert 说。