1. 程式人生 > >Prometheus時序資料庫-報警的計算

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

# Prometheus時序資料庫-報警的計算 在前面的文章中,筆者詳細的闡述了Prometheus的資料插入儲存查詢等過程。但作為一個監控神器,報警計算功能是必不可少的。自然的Prometheus也提供了靈活強大的報警規則可以讓我們自由去發揮。在本篇文章裡,筆者就帶讀者去看下Prometheus內部是怎麼處理報警規則的。 ## 報警架構 Prometheus只負責進行報警計算,而具體的報警觸發則由AlertManager完成。如果我們不想改動AlertManager以完成自定義的路由規則,還可以通過webhook外接到另一個系統(例如,一個轉換到kafka的程式)。 ![](https://oscimg.oschina.net/oscnet/up-66fea7ae49f9ad663511fdb0381b815650f.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) } ...... } ``` 整個過程如下圖所示: ![](https://oscimg.oschina.net/oscnet/up-6e8f9b9a7caf0eddebfd3be0b7a4dd52e1f.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。其樹形結構如下圖所示: ![](https://oscimg.oschina.net/oscnet/up-6d2d0df21a05dda8316f31c400275d63b81.png) 解析出左節點是個VectorSelect而且知道了其lablelMatcher是 ``` __name__:http_requests ``` 那麼我們就可以左節點VectorSelector進行求值。直接利用倒排索引在head中查詢即可(因為instant query的是當前時間,所以肯定在記憶體中)。 ![](https://oscimg.oschina.net/oscnet/up-b409c8950485e16ce78206848806397d852.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) } } } ``` 如下圖所示: ![](https://oscimg.oschina.net/oscnet/up-05d4a71b5931f57ed08682b2681f4d8b5d5.png) 最後我們的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 } ...... } } ``` 上面程式碼邏輯如下圖所示: ![](https://oscimg.oschina.net/oscnet/up-83a00965989fe9f3a5e6f5e93f49343fb5d.png) ## 總結 Prometheus作為一個監控神器,給我們提供了各種各樣的遍歷。其強大的報警計算功能就是其中之一。瞭解其中告警的計算原理,才能讓我們更好的運用它。 ![](https://oscimg.oschina.net/oscnet/up-0124e4cdd8e9cecb13071dad7b6544ebb71.png)