prometheus-----告警处理源码剖析

一条告警在prometheus中的三种状态切换

prometheus mysql告警规则配置 prometheus告警恢复_lua

prometheus常见参数

# 数据采集间隔
scrape_interval: 15s 

# 评估告警周期
evaluation_interval: 15s 

# 数据采集超时时间默认10s
scrape_timeout: 10s

prometheus对恢复的告警会在内存保存15分钟,期间持续发送给alertmanager(hard code)

// 对于恢复的告警,会在内存保存15分钟后才会删除,这期间会一直重复的发送给alertmanger
// resolvedRetention is the duration for which a resolved alert instance
// is kept in memory state and consequently repeatedly sent to the AlertManager.
const resolvedRetention = 15 * time.Minute

正篇:告警评估与处理

1、prometheus启动一个协程定期去评估所有告警规则是否触发,并对触发的告警规则发送告警

规则组启动流程(Group.run):进入 Group.run 方法后先进行初始化等待,以使规则的运算时间在同一时刻,周期为 g.interval;然后定义规则运算调度方法:iter,调度周期为 g.interval;在 iter 方法中调用 g.Eval 方法执行下一层次的规则运算调度。

规则运算的调度周期:

规则运算的调度周期 g.interval,由 prometheus.yml 配置文件中 global 中的:
[ evaluation_interval: | default = 1m ]指定。
prometheus会有一个协程,每隔g.interval执行一次来判断是否有告警产生:

func (g *Group) run(ctx context.Context) {
	defer close(g.terminated)

    // 告警评估函数
	iter := func() {
		g.metrics.IterationsScheduled.WithLabelValues(GroupKey(g.file, g.name)).Inc()

		start := time.Now()
		g.Eval(ctx, evalTimestamp)
		timeSinceStart := time.Since(start)

		g.metrics.IterationDuration.Observe(timeSinceStart.Seconds())
		g.setEvaluationTime(timeSinceStart)
		g.setLastEvaluation(start)
	}

	// The assumption here is that since the ticker was started after having
	// waited for `evalTimestamp` to pass, the ticks will trigger soon
	// after each `evalTimestamp + N * g.interval` occurrence.
                // 告警评估定时器,周期为g.interval
	tick := time.NewTicker(g.interval)
	defer tick.Stop()
                ...
                ...
	for {
		select {
		case <-g.done:
			return
		default:
			select {
			case <-g.done:
				return
			case <-tick.C:
				missed := (time.Since(evalTimestamp) / g.interval) - 1
				if missed > 0 {
					g.metrics.IterationsMissed.WithLabelValues(GroupKey(g.file, g.name)).Add(float64(missed))
					g.metrics.IterationsScheduled.WithLabelValues(GroupKey(g.file, g.name)).Add(float64(missed))
				}
				evalTimestamp = evalTimestamp.Add((missed + 1) * g.interval)

				useRuleGroupPostProcessFunc(g, evalTimestamp.Add(-(missed+1)*g.interval))
                // 执行告警评估
				iter()
			}
		}
	}
}

// 告警评估函数
// Eval runs a single evaluation cycle in which all rules are evaluated sequentially.
func (g *Group) Eval(ctx context.Context, ts time.Time) {
	var samplesTotal float64
    // 对每条规则进行评估
	for i, rule := range g.rules {
		select {
		case <-g.done:
			return
		default:
		}

		func(i int, rule Rule) {
			ctx, sp := otel.Tracer("").Start(ctx, "rule")
			sp.SetAttributes(attribute.String("name", rule.Name()))
			defer func(t time.Time) {
				sp.End()

				since := time.Since(t)
				g.metrics.EvalDuration.Observe(since.Seconds())
				rule.SetEvaluationDuration(since)
				rule.SetEvaluationTimestamp(t)
			}(time.Now())

			g.metrics.EvalTotal.WithLabelValues(GroupKey(g.File(), g.Name())).Inc()
           // 这里是执行对每条规则评估函数
			vector, err := rule.Eval(ctx, ts, g.opts.QueryFunc, g.opts.ExternalURL, g.Limit())
			if err != nil {
				rule.SetHealth(HealthBad)
				rule.SetLastError(err)
				sp.SetStatus(codes.Error, err.Error())
				g.metrics.EvalFailures.WithLabelValues(GroupKey(g.File(), g.Name())).Inc()

				// Canceled queries are intentional termination of queries. This normally
				// happens on shutdown and thus we skip logging of any errors here.
				var eqc promql.ErrQueryCanceled
				if !errors.As(err, &eqc) {
					level.Warn(g.logger).Log("name", rule.Name(), "index", i, "msg", "Evaluating rule failed", "rule", rule, "err", err)
				}
				return
			}
			rule.SetHealth(HealthGood)
			rule.SetLastError(nil)
			samplesTotal += float64(len(vector))

			if ar, ok := rule.(*AlertingRule); ok {
                // 这里是告警规则发送函数,发送评估后需要发送的告警
				ar.sendAlerts(ctx, ts, g.opts.ResendDelay, g.interval, g.opts.NotifyFunc)
			}
            ...
            ...
}

2、对每条告警规则的评估方法

// Eval evaluates the rule expression and then creates pending alerts and fires
// or removes previously pending alerts accordingly.
// 处理告警
func (r *AlertingRule) Eval(ctx context.Context, ts time.Time, query QueryFunc, externalURL *url.URL, limit int) (promql.Vector, error) {
	res, err := query(ctx, r.vector.String(), ts)
	if err != nil {
		return nil, err
	}

	// Create pending alerts for any new vector elements in the alert expression
	// or update the expression value for existing elements.
	resultFPs := map[uint64]struct{}{}

	var vec promql.Vector
	alerts := make(map[uint64]*Alert, len(res))
	for _, smpl := range res {
		// Provide the alert information to the template.
		l := make(map[string]string, len(smpl.Metric))
		for _, lbl := range smpl.Metric {
			l[lbl.Name] = lbl.Value
		}

		tmplData := template.AlertTemplateData(l, r.externalLabels, r.externalURL, smpl.V)
		// Inject some convenience variables that are easier to remember for users
		// who are not used to Go's templating system.
		defs := []string{
			"{{$labels := .Labels}}",
			"{{$externalLabels := .ExternalLabels}}",
			"{{$externalURL := .ExternalURL}}",
			"{{$value := .Value}}",
		}

		expand := func(text string) string {
			tmpl := template.NewTemplateExpander(
				ctx,
				strings.Join(append(defs, text), ""),
				"__alert_"+r.Name(),
				tmplData,
				model.Time(timestamp.FromTime(ts)),
				template.QueryFunc(query),
				externalURL,
				nil,
			)
			result, err := tmpl.Expand()
			if err != nil {
				result = fmt.Sprintf("<error expanding template: %s>", err)
				level.Warn(r.logger).Log("msg", "Expanding alert template failed", "err", err, "data", tmplData)
			}
			return result
		}

		lb := labels.NewBuilder(smpl.Metric).Del(labels.MetricName)

		for _, l := range r.labels {
			lb.Set(l.Name, expand(l.Value))
		}
		lb.Set(labels.AlertName, r.Name())

		annotations := make(labels.Labels, 0, len(r.annotations))
		for _, a := range r.annotations {
			annotations = append(annotations, labels.Label{Name: a.Name, Value: expand(a.Value)})
		}

		lbs := lb.Labels(nil)
		h := lbs.Hash()
		resultFPs[h] = struct{}{}

		if _, ok := alerts[h]; ok {
			return nil, fmt.Errorf("vector contains metrics with the same labelset after applying alert labels")
		}

		alerts[h] = &Alert{
			Labels:      lbs,
			Annotations: annotations,
			ActiveAt:    ts,
			State:       StatePending,
			Value:       smpl.V,
		}
	}

	r.activeMtx.Lock()
	defer r.activeMtx.Unlock()
               
                // 对于发生的告警,检测是否在当前的活跃告警集合中存在,且状态不等于非活跃,则更新告警的value和annotation
	for h, a := range alerts {
		// Check whether we already have alerting state for the identifying label set.
		// Update the last value and annotations if so, create a new alert entry otherwise.
		if alert, ok := r.active[h]; ok && alert.State != StateInactive {
			alert.Value = a.Value
			alert.Annotations = a.Annotations
			continue
		}

		r.active[h] = a
	}

	var numActivePending int
	// Check if any pending alerts should be removed or fire now. Write out alert timeseries.
          
	for fp, a := range r.active {
		if _, ok := resultFPs[fp]; !ok {
                                     
			// If the alert was previously firing, keep it around for a given
			// retention time so it is reported as resolved to the AlertManager.
                                                // 对于当前每条活跃的告警,判断是否在本次的告警中,如果不在的话,则证明此条活跃的告警的指标已经恢复了
                                                // 对于已经恢复的告警指标,如果之前是pending或者之前的ResolvedAt非空,且在resolvedRetention(15m)之前的,则删除此告警
                                                // 因为已经没必要发送通知了
			if a.State == StatePending || (!a.ResolvedAt.IsZero() && ts.Sub(a.ResolvedAt) > resolvedRetention) {
				delete(r.active, fp)
			}
                                                // 否则更新告警的状态为恢复,且恢复的时间为当前时间
			if a.State != StateInactive {
				a.State = StateInactive
				a.ResolvedAt = ts
			}
			continue
		}
		numActivePending++
                                // 对于当前每条活跃的告警,也在本次的告警中,且之前的状态是pending,且持续的时间大于holdDuration(也就是告警规则里for的时间)
                                // 更新状态为触发,触发时间为当前
		if a.State == StatePending && ts.Sub(a.ActiveAt) >= r.holdDuration {
			a.State = StateFiring
			a.FiredAt = ts
		}

		if r.restored.Load() {
			vec = append(vec, r.sample(a, ts))
			vec = append(vec, r.forStateSample(a, ts, float64(a.ActiveAt.Unix())))
		}
	}

	if limit > 0 && numActivePending > limit {
		r.active = map[uint64]*Alert{}
		return nil, fmt.Errorf("exceeded limit of %d with %d alerts", limit, numActivePending)
	}

	return vec, nil
}

3、评估后,对触发的告警判断是否需要发送

// 发送告警
func (r *AlertingRule) sendAlerts(ctx context.Context, ts time.Time, resendDelay, interval time.Duration, notifyFunc NotifyFunc) {
	alerts := []*Alert{}
	r.ForEachActiveAlert(func(alert *Alert) {
        // 判断是否需要发送
		if alert.needsSending(ts, resendDelay) {
			alert.LastSentAt = ts
			// Allow for two Eval or Alertmanager send failures.
			delta := resendDelay
			if interval > resendDelay {
				delta = interval
			}
            // ValidUntil 字段是一个预估的告警有效时间,超过这个时间点告警会被认为已经解除
            // ValidUntil = ts + max([check_interval], [resend_delay]) * 4
            // resendDelay,是程序启动参数 --rules.alert.resend-delay 规定的,默认 1m
            // interval,是我们配置的采集间隔
			alert.ValidUntil = ts.Add(4 * delta)
			anew := *alert
			alerts = append(alerts, &anew)
		}
	})
	notifyFunc(ctx, r.vector.String(), alerts...)
}

// 判断一条规则是否需要发送
func (a *Alert) needsSending(ts time.Time, resendDelay time.Duration) bool {
    // 状态是pending则不需要
	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
	}
    // 距离上一次发送的时间是否大于1分钟,否则不发送,避免频繁发送
	return a.LastSentAt.Add(resendDelay).Before(ts)
}

// sendAlerts implements the rules.NotifyFunc for a Notifier.
func sendAlerts(s sender, externalURL string) rules.NotifyFunc {
	return func(ctx context.Context, expr string, alerts ...*rules.Alert) {
		var res []*notifier.Alert

		for _, alert := range alerts {
			a := ¬ifier.Alert{
				StartsAt:     alert.FiredAt,
				Labels:       alert.Labels,
				Annotations:  alert.Annotations,
				GeneratorURL: externalURL + strutil.TableLinkForExpression(expr),
			}
            // 如果告警的ResolvedAt不空,则EndsAt = .ResolvedAt,否则等于ValidUntil
            // 也就是说如果告警的ResolvedAt不空,证明是采集到了恢复的情况,EndAt代表实际的恢复实际
            // 如果告警的ResolvedAt为空,则还没有恢复,那么设置上一个ValidUntil,就是告警的有效时间,
            // 就是说如果持续了ValidUntil之后,没有收到新的firing,则当作恢复来处理
			if !alert.ResolvedAt.IsZero() {
				a.EndsAt = alert.ResolvedAt
			} else {
				a.EndsAt = alert.ValidUntil
			}
			res = append(res, a)
		}

		if len(alerts) > 0 {
			s.Send(res...)
		}
	}
}

总结:

1、Prometheus配置的’scrape_interval’定义的时间间隔,定期采集目标主机上监控数据,每次采集的超时时间为scrape_timeout

2、 Prometheus同时根据配置的"evaluation_interval"的时间间隔,定期(默认1min)的对Alert Rule进行评估,当到达评估周期的时候,发现触发告警规则,激活Alert

具体步骤如下:
      * 对于已经发送的告警,之前没有在活跃告警集合里面的,加入活跃告警集合
      * 对于已经发送的告警,之前在活跃告警集合里面的,更新value和annotation
      * 对于当前每条活跃的告警,判断是否在本次的告警中,
        如果不在的话,则证明此条活跃的告警的指标已经恢复了
      * 对于已经恢复的告警指标,如果之前是pending或者之前的ResolvedAt非空,且
        在resolvedRetention(15m)之前的,则删除此告警;
        否则更新告警的状态为恢复,且恢复的时间为当前时间
      * 对于当前每条活跃的告警,同时也在本次的告警中,且之前的状态是pending,
        且持续的时间大于holdDuration(也就是告警规则里for的时间);
        更新状态为触发,触发时间为当前
     * 对告警进行判断是否需要发送  
           对于pending的不需要发送
           距离上一次发送的时间是否大于1分钟,否则不发送,避免频繁发送
           恢复时间是大于上次发送告警的时间,证明恢复是在告警后发生的,那么已经恢复了,需发送恢复
      * 设置告警的ValidUntil,如果这条告警过了ValidUntil的话,还没收到新的firing,则代表恢复:
         ValidUntil = ts + max([check_interval], [resend_delay]) * 4
      * 发送前设置告警EndAt
         如果告警的ResolvedAt不空,则EndsAt = ResolvedAt,否则等于ValidUntil
         也就是说如果告警的ResolvedAt不空,证明是采集到了恢复的情况,EndAt代表实际的恢复实际
          如果告警的ResolvedAt为空,则还没有恢复,设置其为一个ValidUntil,就是告警的有效时间,
          就是说如果持续了ValidUntil之后,没有收到新的firing,则当作恢复来处理
      * 发送告警

定时评估,带来的告警的发送延迟:

evaluation_interva默认为1分钟,也就是说你假设for为30s,那你最快也要判断pending+firing两个周期,默认需要2分钟后才能激活告警,并发给alertmanager
假如不配置for或for设为0,则alert被激活后会立即变为firing状态,同时发送相关报警信息给alertmanager。
只有在评估周期期间,警报才会从当前状态转移到另一个状态。

3、当下一个alert rule的评估周期到来的时候,发现告警为真,然后判断警报Active的时间是否已经超出rule里的‘for’ 持续时间,如果未超出,则进入下一个评估周期;
如果时间超出,则alert的状态变为“FIRING”;同时调用Alertmanager接口,发送相关报警数据。

4、startsAt和endsAt

这两个字段,这两个字段分别表示告警的起始时间和终止时间,不过两个字段都是可选的。当AlertManager收到告警实例之后,会分以下几类情况对这两个字段进行处理:

1、两者都存在:不做处理
2、两者都未指定:startsAt指定为当前时间,endsAt为当前时间加上告警持续时间,默认为5分钟
3、只指定startsAt:endsAt指定为当前时间加上默认的告警持续时间
4、只指定endsAt:将startsAt设置为endsAt

即:如果 endsAt 没有提供,则自动等于 startsAt + resolve_timeout(默认 5m)

AlertManager一般以当前时间和告警实例的endsAt字段进行比较用以判断告警的状态:
* 若当前时间位于endsAt之前,则表示告警仍然处于触发状态(firing)
* 若当前时间位于endsAt之后,则表示告警已经消除(resolved)

告警时间线的合并压缩:

另外,当Prometheus Server中配置的告警规则被持续满足时,默认对于一条告警,每隔一分钟发一次。
显然,这些实例除了startsAt和endsAt字段以外都完全相同
(其实Prometheus Server会将所有实例的startsAt设置为告警第一次被触发的时间)。
最终,这些实例都会进行压缩去重,多条最终labels相同的告警最终被压缩聚合为一条告警。
当我们进行查询时,只会得到一条起始时间为最开始第一条告警通知的开始时间,
结束时间为最后一条告警通知的恢复时间

为什么一条持续触发的告警不会触发恢复,而采集不到数据时会触发恢复

如果告警一直 Firing,那么 Prometheus 会在 resend_delay 的间隔重复发送,
而 startsAt 保持不变, endsAt 跟着 ValidUntil 变。
这也就是为啥一直firing的规则不会被认为恢复,而不发firting则会认为恢复。
因为一直firing的告警消息中, endsAt 跟着 ValidUntil 变,一直在后延。
而如果没收到,就会导致alertmanger那边在过了告警的endAt时间后,
没收到恢复或者新firing,则认为恢复

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

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

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

alertmanager的resolve_timeout对prometheus无效

alertmanager的resolve_timeout是指多久没收到新firing则认为一条老的告警已经恢复,这个选项对prometheus无效,因为prometheus的消息一直都会带着endAt