Prometheus 告警事件中的 $value
表示當前告警觸發時的值,但是在告警恢復時,Resolved 事件中的 $value
仍然是最新告警時的值,並非是恢復時的值,這是什麼原因和原理?是否有辦法來解決呢?
不廢話,先說原理。
原理
告警規則是配置在 prometheus.yaml 中的,由 Prometheus 負責做規則判定。Prometheus 規則判定的邏輯也很簡單,就是週期性的,拿著 promql 去查詢資料,如果查到了資料,並且連續多次滿足 for
指定的時長,就觸發告警事件,如果查不到資料,就認為指標是正常健康的狀態。比如:
cpu_usage_idle < 5
如上例,告警規則的 promql 中是帶有閾值(< 5
)的,所以只要查到了資料,就說明當前的值小於 5,只要沒查到資料,就說明當前的值大於等於 5,即當前資料是健康的狀態。注意,查不到資料的時候,時序庫不返回資料,換句話說,資料正常的時候,因為時序庫不返回資料,上層就拿不到正常狀態時的值,既然拿不到正常狀態時的值,也就沒法在恢復時展示當前最新的值啦。
實際上,恢復時的事件,是 Alertmanager 根據 resolve_timeout 生成的,而不是 Prometheus 生成的。Alertmanager 生成恢復事件時,會把上次告警的標籤和註解帶過去,值呢?就是上次告警時的值,Alertmanager 不會再去查詢 Prometheus 拿到最新的值。
Alertmanager 可以拿到恢復時的值麼?
坦白講,很難。Alertmanager 需要根據上次告警的標籤和註解去查詢 Prometheus 拿到上次告警時的值,Alertmanager 不會這麼幹的,核心是:
- 從職能上,Alertmanager 去查詢 Prometheus,就反向依賴了,Alertmanager 是告警的分發中心,不止是接收 Prometheus 推送過來的事件,還會接收其他告警源推送過來的事件,如果 Alertmanager 要去查詢 Prometheus,那就耦合太過嚴重。
- Prometheus 的告警規則中可以附加標籤,和監控指標的標籤一起,作為事件的標籤集發給 Alertmanager,Alertmanager 需要根據這些標籤去查詢 Prometheus,拿到原始資料,這在某些場景下是不可行的。一個是要把標籤中的附加標籤剔除,只留資料標籤,Alertmanager 沒有辦法做到;其次,有些 promql 查詢結果壓根就沒有標籤,根本沒法查;再次,Alertmanager 需要解析 promql,把閾值的部分拿掉,而有些 promql 壓根就不是數字閾值。
如果你想透過修改 Alertmanager 達成所願,放棄吧。
有沒有辦法解決?
有。通常的解決辦法有兩種:
- 告警規則裡,順便配置恢復時的 promql
- 把閾值從 promql 中拿掉,promql 只用於查詢原始資料,然後在上層做閾值判定,不管監控指標當前是否健康,都會查到原始資料
下面我以一些監控產品為例,具體來說明。
告警規則裡配置恢復時的 promql
使用這個方法的產品,以夜鶯(Nightingale)監控為例,來講解具體做法。核心是配置兩個地方,一個是在告警規則裡配置恢復時的 promql,另一個是在告警模板裡配置恢復時的值的渲染。
比如我有一個告警規則用來偵測 HTTP 地址探測失敗:
需要在告警規則最下面的自定義欄位裡,增加 recovery_promql 的配置,如下:
要理解這個工作邏輯,我們先來看看 http_response_result_code 這個指標的資料長什麼樣子:
從上圖可以看出,這個指標包含兩個 series,其中 agent_hostname 和 method 欄位相同,target 欄位可以區分開這倆 series。告警規則 http_response_result_code != 0 如果觸發,告警事件中一定會帶有 target 標籤,所以,如果告警事件恢復的時候,我們用高警時的那個 target 標籤去查詢,一定就可以準確查到恢復時的值了。所以 recovery_promql 的配置中引用了 target 標籤,其值是變數,這個變數就是告警事件中的 target 標籤值。
然後,我們在告警模板裡,增加一個恢復時的值的渲染,以釘釘模板為例:
#### {{if .IsRecovered}}<font color="#008800">💚{{.RuleName}}</font>{{else}}<font color="#FF0000">💔{{.RuleName}}</font>{{end}}
---
{{$time_duration := sub now.Unix .FirstTriggerTime }}{{if .IsRecovered}}{{$time_duration = sub .LastEvalTime .FirstTriggerTime }}{{end}}
- **告警級別**: {{.Severity}}級
{{- if .RuleNote}}
- **規則備註**: {{.RuleNote}}
{{- end}}
{{- if not .IsRecovered}}
- **當次觸發時值**: {{.TriggerValue}}
- **當次觸發時間**: {{timeformat .TriggerTime}}
- **告警持續時長**: {{humanizeDurationInterface $time_duration}}
{{- else}}
{{- if .AnnotationsJSON.recovery_value}}
- **恢復時值**: {{formatDecimal .AnnotationsJSON.recovery_value 4}}
{{- end}}
- **恢復時間**: {{timeformat .LastEvalTime}}
- **告警持續時長**: {{humanizeDurationInterface $time_duration}}
{{- end}}
- **告警事件標籤**:
{{- range $key, $val := .TagsMap}}
{{- if ne $key "rulename" }}
- `{{$key}}`: `{{$val}}`
{{- end}}
{{- end}}
這裡最為關鍵的邏輯是判斷 .AnnotationsJSON.recovery_value
的邏輯:
{{- if .AnnotationsJSON.recovery_value}}
- **恢復時值**: {{formatDecimal .AnnotationsJSON.recovery_value 4}}
{{- end}}
如果 .AnnotationsJSON 中包含 recovery_value 就展示,展示的時候把 recovery_value 保留 4 位小數。這個 .AnnotationsJSON 是夜鶯告警規則中的自定義欄位部分,如果告警事件中有恢復時的值,就會在這個欄位中體現。
最終效果如下:
把閾值從 promql 中拿掉,promql 只用於查詢原始資料
使用這個做法的產品,以 FlashDuty 舉例,FlashDuty 不但支援類似夜鶯這樣的配置 recovery_promql 的方式,還支援 promql 中不帶閾值的方式。我們重點講解 promql 中不帶閾值的方式。
以 Memcached 的某個告警規則舉例,查詢條件裡不寫閾值,判定規則裡寫閾值,如下圖所示:
這種方式需要先查到當前值,再拿著當前值去做判定,所以不管是告警時還是恢復時,都可以拿到當前值。這種方式非常直觀,大部分場景都適用。對於一個查詢條件過濾到很多時序的場景,這種方式會查到特別多的資料,對告警引擎也是個壓力。需要斟酌。
如果你也想讓自己的監控系統支援在恢復時拿到恢復時的值,可以參考上述兩種方式。