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