性能文章>Prometheus时序数据库-报警的计算>

Prometheus时序数据库-报警的计算原创

1年前
552202

前言

在前面的文章中,笔者详细的阐述了Prometheus的数据插入存储查询等过程。但作为一个监控神器,报警计算功能是必不可少的。自然的Prometheus也提供了灵活强大的报警规则可以让我们自由去发挥。在本篇文章里,笔者就带读者去看下Prometheus内部是怎么处理报警规则的。

报警架构

Prometheus只负责进行报警计算,而具体的报警触发则由AlertManager完成。如果我们不想改动AlertManager以完成自定义的路由规则,还可以通过webhook外接到另一个系统(例如,一个转换到kafka的程序)。
image.png
在本篇文章里,笔者并不会去设计alertManager,而是专注于Prometheus本身报警规则的计算逻辑。

一个最简单的报警规则

rules:
	alert: HTTPRequestRateLow
	expr: http_requests < 100
	for: 60s
	labels:
		severity: warning
	annotations:
		description: "http request rate low"

这上面的规则即是http请求数量<100从持续1min,则我们开始报警,报警级别为warning

什么时候触发这个计算

在加载完规则之后,Prometheus按照evaluation_interval这个全局配置去不停的计算Rules。代码逻辑如下所示:

rules/manager.go

func (g *Group) run(ctx context.Context) {
	iter := func() {
		......
		g.Eval(ctx,evalTimestamp)
		......
	}
	// g.interval = evaluation_interval
	tick := time.NewTicker(g.interval)
	defer tick.Stop()
	......
	for {
		......
		case <-tick.C:
			......
			iter()
	}
}

而g.Eval的调用为:

func (g *Group) Eval(ctx context.Context, ts time.Time) {
	// 对所有的rule
	for i, rule := range g.rules {
		......
		// 先计算出是否有符合rule的数据
		vector, err := rule.Eval(ctx, ts, g.opts.QueryFunc, g.opts.ExternalURL)
		......
		// 然后发送
		ar.sendAlerts(ctx, ts, g.opts.ResendDelay, g.interval, g.opts.NotifyFunc)
	}
	......
}

整个过程如下图所示:
image.png
对单个rule的计算
我们可以看到,最重要的就是rule.Eval这个函数。代码如下所示:

func (r *AlertingRule) Eval(ctx context.Context, ts time.Time, query QueryFunc, externalURL *url.URL) (promql.Vector, error) {
	// 最终调用了NewInstantQuery
	res, err = query(ctx,r.vector.String(),ts)
	......
	// 报警组装逻辑
	......
	// active 报警状态变迁
}

这个Eval包含了报警的计算/组装/发送的所有逻辑。我们先聚焦于最重要的计算逻辑。也就是其中的query。其实,这个query是对NewInstantQuery的一个简单封装。

func EngineQueryFunc(engine *promql.Engine, q storage.Queryable) QueryFunc {
	return func(ctx context.Context, qs string, t time.Time) (promql.Vector, error) {
		q, err := engine.NewInstantQuery(q, qs, t)
		......
		res := q.Exec(ctx)
	}
}

也就是说它执行了一个瞬时向量的查询。而其查询的表达式按照我们之前给出的报警规则,即是
**http_requests < 100 **
既然要计算表达式,那么第一步,肯定是将其构造成一颗AST。其树形结构如下图所示:
image.png
解析出左节点是个VectorSelect而且知道了其lablelMatcher是

__name__:http_requests

那么我们就可以左节点VectorSelector进行求值。直接利用倒排索引在head中查询即可(因为instant query的是当前时间,所以肯定在内存中)。
image.png
想知道具体的计算流程,可以见笔者之前的博客《Prometheus时序数据库-数据的查询》 计算出左节点的数据之后,我们就可以和右节点进行比较以计算出最终结果了。具体代码为:

func (ev *evaluator) eval(expr Expr) Value {
	......
	case *BinaryExpr:
	......
		case lt == ValueTypeVector && rt == ValueTypeScalar:
			return ev.rangeEval(func(v []Value, enh *EvalNodeHelper) Vector {
				return ev.VectorscalarBinop(e.Op, v[0].(Vector), Scalar{V: v[1].(Vector)[0].Point.V}, false, e.ReturnBool, enh)
			}, e.LHS, e.RHS)
	.......
}
最后调用的函数即为:
```func (ev *evaluator) VectorBinop(op ItemType, lhs, rhs Vector, matching *VectorMatching, returnBool bool, enh *EvalNodeHelper) Vector {
	// 对左节点计算出来的所有的数据sample
	for _, lhsSample := range lhs {
		......
		// 由于左边lv = 75 < 右边rv = 100,且op为less
		/**
			vectorElemBinop(){
				case LESS
					return lhs, lhs < rhs
			}
		**/
		// 这边得到的结果value=75,keep = true
		value, keep := vectorElemBinop(op, lv, rv)
		......
		if keep {
			......
			// 这边就讲75放到了输出里面,也就是说我们最后的计算确实得到了数据。
			enh.out = append(enh.out.sample)
		}
	}
}

如下图所示:
image.png

最后我们的expr输出即为

sample {
	Point {t:0,V:75}
	Metric {__name__:http_requests,instance:0,job:api-server}
		
}

报警状态变迁

计算过程讲完了,笔者还稍微讲一下报警的状态变迁,也就是最开始报警规则中的rule中的for,也即报警持续for(规则中为1min),我们才真正报警。为了实现这种功能,这就需要一个状态机了。笔者这里只阐述下从Pending(报警出现)->firing(真正发送)的逻辑。

在之前的Eval方法里面,有下面这段

	for _, smpl := range res {
	......
			if alert, ok := r.active[h]; ok && alert.State != StateInactive {
			alert.Value = smpl.V
			alert.Annotations = annotations
			continue
		}
		// 如果这个告警不在active map里面,则将其放入
		// 注意,这里的hash依旧没有拉链法,有极小概率hash冲突
r.active[h] = &Alert{
			Labels:      lbs,
			Annotations: annotations,
			ActiveAt:    ts,
			State:       StatePending,
			Value:       smpl.V,
		}
	}
	......
	// 报警状态的变迁逻辑
	for fp, a := range r.active {
		// 如果当前r.active的告警已经不在刚刚计算的result里面了		if _, ok := resultFPs[fp]; !ok {
			// 如果状态是Pending待发送
			if a.State == StatePending || (!a.ResolvedAt.IsZero() && ts.Sub(a.ResolvedAt) > resolvedRetention) {
				delete(r.active, fp)
			}
			......
			continue
		}
		// 对于已有的Active报警,如果其Active的时间>r.holdDuration,也就是for指定的
		if a.State == StatePending && ts.Sub(a.ActiveAt) >= r.holdDuration {
			// 我们将报警置为需要发送
			a.State = StateFiring
			a.FiredAt = ts
		}
		......
	
	}
}

上面代码逻辑如下图所示:
image.png

总结

Prometheus作为一个监控神器,给我们提供了各种各样的遍历。其强大的报警计算功能就是其中之一。了解其中告警的计算原理,才能让我们更好的运用它。

 

相关阅读

Prometheus时序数据库-报警的计算

Prometheus时序数据库-数据的查询

Prometheus时序数据库-数据的抓取

Prometheus时序数据库-数据的插入

Prometheus时序数据库-磁盘中的存储结构

Prometheus时序数据库-内存中的存储结构

请先登录,再评论

暂无回复,快来写下第一个回复吧~

为你推荐

5G时代,如何彻底搞定海量数据库的设计与实践
5G时代,业务数据越来越丰富,业务使用MySQL数据库作为后台存储,存储引擎使用InnoDB,会带来哪些挑战?如何针对公司业务特点及MySQL数据库特性,制定若干数据库使用规范供一线RD在设计业务时参
Prometheus时序数据库-内存中的存储结构
前言笔者最近担起了公司监控的重任,而当前监控最流行的数据库即是Prometheus。按照笔者打破砂锅问到底的精神,自然要把这个开源组件源码搞明白才行。在经过一系列源码/资料的阅读以及各种Debug之后
Prometheus时序数据库-磁盘中的存储结构
前言之前的文章里,笔者详细描述了监控数据在Prometheus内存中的结构。而其在磁盘中的存储结构,也是非常有意思的,关于这部分内容,将在本篇文章进行阐述。 磁盘目录结构首先我们来看Prometheu
Prometheus时序数据库-数据的插入
前言在之前的文章里,笔者详细的阐述了Prometheus时序数据库在内存和磁盘中的存储结构。有了前面的铺垫,笔者就可以在本篇文章阐述下数据的插入过程。 监控数据的插入在这里,笔者并不会去讨论Promt
Prometheus时序数据库-数据的查询
前言在之前的博客里,笔者详细阐述了Prometheus数据的插入过程。但我们最常见的打交道的是数据的查询。Prometheus提供了强大的Promql来满足我们千变万化的查询需求。在这篇文章里面,笔者
解Bug之路-主从切换"未成功"?
前言数据库主从切换是个非常有意思的话题。能够稳定的处理主从切换是保证业务连续性的必要条件。今天笔者就来讲讲主从切换过程中一个小小的问题。 故障场景最近线上进行主从切换,大部分应用都切过去了,但是某些应
Prometheus时序数据库-报警的计算
前言在前面的文章中,笔者详细的阐述了Prometheus的数据插入存储查询等过程。但作为一个监控神器,报警计算功能是必不可少的。自然的Prometheus也提供了灵活强大的报警规则可以让我们自由去发挥
一个诡异的MySQL查询超时问题,居然隐藏着存在了两年的BUG
这一周线上碰到一个诡异的BUG。线上有个定时任务,这个任务需要查询一个表几天范围内的一些数据做一些处理,每隔十分钟执行一次,直至成功。通过日志发现,从凌晨5:26分开始到5:56任务执行了三次,三次都