Prometheus時序資料庫-報警的計算

Al發表於2021-03-31

Prometheus時序資料庫-報警的計算

在前面的文章中,筆者詳細的闡述了Prometheus的資料插入儲存查詢等過程。但作為一個監控神器,報警計算功能是必不可少的。自然的Prometheus也提供了靈活強大的報警規則可以讓我們自由去發揮。在本篇文章裡,筆者就帶讀者去看下Prometheus內部是怎麼處理報警規則的。

報警架構

Prometheus只負責進行報警計算,而具體的報警觸發則由AlertManager完成。如果我們不想改動AlertManager以完成自定義的路由規則,還可以通過webhook外接到另一個系統(例如,一個轉換到kafka的程式)。

在本篇文章裡,筆者並不會去設計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)
	}
	......
}

整個過程如下圖所示:

對單個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。其樹形結構如下圖所示:

解析出左節點是個VectorSelect而且知道了其lablelMatcher是

__name__:http_requests

那麼我們就可以左節點VectorSelector進行求值。直接利用倒排索引在head中查詢即可(因為instant query的是當前時間,所以肯定在記憶體中)。

想知道具體的計算流程,可以見筆者之前的部落格《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)
		}
	}
}

如下圖所示:

最後我們的expr輸出即為

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

報警狀態變遷

計算過程講完了,筆者還稍微講一下報警的狀態變遷,也就是最開始報警規則中的rule中的for,也即報警持續for(規則中為1min),我們才真正報警。為了實現這種功能,這就需要一個狀態機了。筆者這裡只闡述下從Pending(報警出現)->firing(真正傳送)的邏輯。

在之前的Eval方法裡面,有下面這段

func (r *AlertingRule) Eval(ctx context.Context, ts time.Time, query QueryFunc, externalURL *url.URL) (promql.Vector, error) {
	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
		}
		......
	
	}
}

上面程式碼邏輯如下圖所示:

總結

Prometheus作為一個監控神器,給我們提供了各種各樣的遍歷。其強大的報警計算功能就是其中之一。瞭解其中告警的計算原理,才能讓我們更好的運用它。

相關文章