一句話結論:小規模叢集可以把 Alertmanager 關掉,改用 Grafana 內建的告警引擎,用同一套規則、同一套通知策略處理指標與日誌。代價是 Prometheus 自己那一百多條預設規則,不會再通知任何人,要的規則得在 Grafana 裡重寫。

遇到的問題

kube-prometheus-stack 預設的告警路徑是這樣:Prometheus 評估規則,把觸發的告警送給 Alertmanager,再由 Alertmanager 分組、路由、發通知。

指標告警這樣做沒問題。但叢集裡通常還有日誌,想對「log 出現某個錯誤關鍵字」告警,就變成另一條路:要有 Loki 自己的 ruler,或另外想辦法。

結果是:規則寫在 PrometheusRule 的 CRD 裡,路由寫在 Alertmanager 的設定檔裡,日誌告警又在別的地方。小規模的叢集,光是維護這三處就嫌多。

另一個選擇是:只用 Grafana。Grafana 本身就內建告警引擎,規則可以同時查 Prometheus 和 Loki,通知策略也在同一個地方設定。

這篇用實測帶一遍怎麼做,也把關掉 Alertmanager 的代價講清楚。

手把手步驟

前提與測試版本

手邊要有一個用 kube-prometheus-stack 安裝的 Grafana 與 Prometheus。要測日誌告警的話,還需要一個 Loki 資料來源(uid 為 loki),沒有 Loki 的話,把下面的日誌規則略過即可。

下面的 values,是在這些版本上實測的:

元件 版本
kube-prometheus-stack chart 92.10.1
內含的 Grafana chart 14.0.1
grafana/loki chart 7.3.0

叢集是 k3s v1.31.5(單節點)。版本不同的話,欄位可能不一樣,請對一下 chart 的 values.yaml。

步驟一:關掉 Alertmanager

在 kube-prometheus-stack 的 values 加上:

alertmanager: enabled: false defaultRules: disabled: # 沒有 Alertmanager 時這條一定會亮 PrometheusNotConnectedToAlertmanagers: true 

第二段很容易漏掉。chart 有一條預設規則 PrometheusNotConnectedToAlertmanagers,內容就是「Prometheus 沒有連到任何 Alertmanager」,等待時間 10 分鐘。

關掉 Alertmanager 又不關這條規則,10 分鐘後它就會一直亮著,而且永遠不會好。

步驟二:用 chart values 定義聯絡點與通知策略

Grafana 的 chart 支援用 alerting: 直接把告警資源寫進 values,部署時就會載入。放在 kube-prometheus-stack 的 grafana: 底下:

grafana: alerting: contactpoints.yaml: apiVersion: 1 contactPoints: - orgId: 1 name: demo-webhook receivers: - uid: demo-webhook type: webhook settings: url: http://receiver.monitoring.svc.cluster.local:8080/alert httpMethod: POST policies.yaml: apiVersion: 1 policies: - orgId: 1 receiver: demo-webhook group_by: ["alertname"] group_wait: 10s group_interval: 1m repeat_interval: 4h 

聯絡點

聯絡點就是「通知要送去哪裡」。這裡用最簡單的 webhook,後面會放一個接收端來驗證。實際使用時,換成 email、聊天工具等其他類型即可。

通知策略

策略決定哪些告警送給哪個聯絡點。這裡只有一條預設策略,全部都送給 demo-webhook,並以 alertname 分組。

group_wait: 10s 是為了測試時快一點看到結果,平常用預設值就好。

步驟三:用 values 定義告警規則

同樣放在 grafana.alerting 底下,再加一個 rules.yaml。這裡放兩條規則:一條查 Prometheus,一條查 Loki。

 rules.yaml: apiVersion: 1 groups: - orgId: 1 name: demo folder: demo interval: 30s rules: # 指標規則:容器重啟(Prometheus 資料來源) - uid: container-restarts title: ContainerRestarts condition: C for: 0s noDataState: OK execErrState: Error labels: severity: warning annotations: summary: "有容器在最近 5 分鐘內重啟過" data: - refId: A relativeTimeRange: {from: 300, to: 0} datasourceUid: prometheus model: refId: A expr: 'sum by (namespace, pod) (increase(kube_pod_container_status_restarts_total{namespace="demo"}[5m]))' instant: true - refId: B datasourceUid: __expr__ model: refId: B type: reduce expression: A reducer: last datasource: {type: __expr__, uid: __expr__} - refId: C datasourceUid: __expr__ model: refId: C type: threshold expression: B conditions: - evaluator: {type: gt, params: [0]} datasource: {type: __expr__, uid: __expr__} # 日誌規則:log 裡出現 ERROR_DEMO(Loki 資料來源) - uid: error-logs title: ErrorLogs condition: C for: 0s noDataState: OK execErrState: Error labels: severity: critical annotations: summary: "最近 1 分鐘內 log 出現錯誤關鍵字" data: - refId: A relativeTimeRange: {from: 120, to: 0} datasourceUid: loki model: refId: A expr: 'sum(count_over_time({namespace="demo"} |= "ERROR_DEMO" [1m]))' queryType: instant - refId: B datasourceUid: __expr__ model: refId: B type: reduce expression: A reducer: last datasource: {type: __expr__, uid: __expr__} - refId: C datasourceUid: __expr__ model: refId: C type: threshold expression: B conditions: - evaluator: {type: gt, params: [0]} datasource: {type: __expr__, uid: __expr__} 

規則的結構三段式,兩條規則都一樣。

  1. A 是查詢:對資料來源下 PromQL 或 LogQL。
  2. B 是縮減(reduce):把查詢結果壓成單一數字。
  3. C 是閾值(threshold):B 大於 0 就算觸發。

condition: C 指的是以 C 的結果當作這條規則的判斷依據。

兩條規則查的是不同資料來源,但規則的寫法、通知策略、聯絡點完全一樣,這就是「一套引擎管指標與日誌」的樣子。

datasourceUid 要對上資料來源的 uid。kube-prometheus-stack 預設的 Prometheus 資料來源 uid 是 prometheus,Loki 是自己加的,這裡設成 loki。

步驟四:套用並確認載入

helm upgrade monitoring prometheus-community/kube-prometheus-stack \ --version 92.10.1 -n monitoring -f prom-values.yaml --wait 

Grafana 有提供 provisioning API,可以直接問它載入了什麼。先 port-forward,再用 admin 帳號查:

curl -s -u admin:<密碼> http://127.0.0.1:3000/api/v1/provisioning/alert-rules curl -s -u admin:<密碼> http://127.0.0.1:3000/api/v1/provisioning/contact-points curl -s -u admin:<密碼> http://127.0.0.1:3000/api/v1/provisioning/policies 

實測看到的是:兩條規則(ContainerRestarts、ErrorLogs)、一個 demo-webhook 聯絡點,以及預設策略的 receiver 是 demo-webhook、group_by 是 alertname。

也可以確認 Prometheus 真的沒有連 Alertmanager:

kubectl get --raw \ "/api/v1/namespaces/monitoring/services/monitoring-kube-prometheus-prometheus:9090/proxy/api/v1/alertmanagers" 

實測結果 activeAlertmanagers 與 droppedAlertmanagers 都是 0。

步驟五:放一個接收端,製造事件驗證

先放一個會把收到的 POST 印出來的小接收端:

apiVersion: v1 kind: Pod metadata: {name: receiver, namespace: monitoring, labels: {app: receiver}} spec: containers: - name: r image: python:3.12-alpine command: [python, -u, -c] args: - | from http.server import BaseHTTPRequestHandler, HTTPServer class H(BaseHTTPRequestHandler): def do_POST(self): n = int(self.headers.get('content-length', 0)) print('POST', self.path, self.rfile.read(n).decode()[:2500], flush=True) self.send_response(200); self.end_headers() def log_message(self, *a): pass HTTPServer(('', 8080), H).serve_forever() --- apiVersion: v1 kind: Service metadata: {name: receiver, namespace: monitoring} spec: selector: {app: receiver} ports: [{port: 8080, targetPort: 8080}] 

再製造兩種事件:一個會一直重啟的容器,和一個一直印出錯誤關鍵字的容器。

kubectl create namespace demo kubectl -n demo run crasher --image=busybox:1.36 --restart=Always -- \ sh -c 'echo starting; sleep 15; exit 1' kubectl -n demo run noisy --image=busybox:1.36 --restart=Always -- \ sh -c 'while true; do echo "ERROR_DEMO something broke"; sleep 5; done' 

等大約一兩分鐘,看接收端的 log:

kubectl logs -n monitoring receiver 

會看到兩筆 firing 的 POST,內容是 Grafana 的 webhook 格式,重點欄位長這樣:

{"receiver":"demo-webhook","status":"firing","alerts":[{"status":"firing", "labels":{"alertname":"ErrorLogs","grafana_folder":"demo","severity":"critical"}, "annotations":{"summary":"最近 1 分鐘內 log 出現錯誤關鍵字"}, ... 

另一筆是 ContainerRestarts,label 多了 namespace="demo" 和 pod="crasher",因為規則的查詢有 sum by (namespace, pod)。

步驟六:確認恢復通知

把兩個測試 Pod 刪掉:

kubectl -n demo delete pod noisy crasher 

等約三分鐘,接收端會收到 ErrorLogs 的 resolved。

ContainerRestarts 在我測的這三分鐘內還沒有恢復,這是正常的:它的查詢是 increase(...[5m]),窗口有 5 分鐘,要等重啟的紀錄滑出窗口才會變成 0。

踩到的坑

1. 關掉 Alertmanager,但沒關那條「沒連到 Alertmanager」的規則

這條規則在 chart 裡是預設就有的(等待 10 分鐘,severity 是 warning)。我特地比對過:關掉 Alertmanager 之後它仍然會被渲染出來,要在 defaultRules.disabled 裡明確關掉,渲染結果裡才會消失。

不關的話,10 分鐘後它就會一直亮著。

2. 預設那一百多條規則還在跑,但不會通知任何人

這是關掉 Alertmanager 最大的代價,一定要知道。

實測:Prometheus 載入了 148 條告警規則,但 Alertmanager 數量是 0。其中 Watchdog 這種永遠觸發的規則,在 Prometheus 裡是 firing,但接收端一次都沒收到。

也就是說,kube-prometheus-stack 預設附的 KubePodCrashLooping、NodeFilesystemSpaceFillingUp 這類規則,狀態會在 Prometheus 的 Alerts 頁亮起來,卻沒有任何通知出去。

要這些規則的通知,就得在 Grafana 裡自己寫(或匯入)。Grafana 文件有 Import to Grafana-managed rules 這個功能,可以把資料來源管理的規則轉成 Grafana 管理的規則,這部分我沒有實測。

3. values 裡的雙大括號要跳脫

Grafana chart 會把 alerting: 底下的內容當成 Helm 模板處理。如果規則的 annotation 裡想寫 Grafana 的樣板語法(例如用 {{ $labels.pod }} 帶出 Pod 名稱),那組雙大括號會先被 Helm 當成它自己的模板。

chart 的說明有寫:不是要給 Helm 處理的雙大括號必須跳脫。本篇為了簡單,annotation 全部寫成固定文字,避開這個問題,要用樣板語法的話,請照 chart 的說明處理跳脫。

為什麼這樣選

先說一個誠實的點:省的記憶體很少。Alertmanager 在這個叢集大約只吃 38 MiB,不是為了省資源才關它。

真正的理由是這兩個:

一個地方管指標和日誌

Grafana 官方文件說,Grafana 管理的告警規則可以查很多種資料來源,甚至在單一規則裡同時用多個資料來源。所以指標和日誌用同一套寫法、同一個 UI、同一套通知策略。

規則可以跟著 values 一起進版本控制

規則、聯絡點、策略都寫在 values 裡,跟著 Helm 部署,改動走 Git,不需要登入 UI 手動設定。

什麼情況應該保留 Alertmanager

Prometheus 官方文件對 Alertmanager 的定位,是接收 Prometheus 之類的用戶端送來的告警,負責去重、分組、路由到各種通知管道,也處理靜音與抑制。

如果符合下面任一種情況,保留它比較合適:

  • 手上有很多現成的 PrometheusRule(或直接使用 kube-prometheus-stack 的預設規則),不想重寫。
  • 需要 Alertmanager 的功能,例如 inhibition。
  • 多個團隊共用 Prometheus,每個團隊有自己的路由設定。

反過來,單一叢集、規則不多、又想同時管日誌告警的話,只用 Grafana 比較簡單。

延伸閱讀

這篇的 values 在上面列的版本實測過。換版本的話,記得先看 chart 的 values.yaml 欄位有沒有改名。