一句話結論:小規模叢集可以把 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__} 規則的結構三段式,兩條規則都一樣。
- A 是查詢:對資料來源下 PromQL 或 LogQL。
- B 是縮減(reduce):把查詢結果壓成單一數字。
- 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 比較簡單。
延伸閱讀
- Grafana:Alert rule types:Grafana 管理的規則與資料來源管理的規則有什麼不同,以及 Import 功能。
- Grafana:Alertmanager:Grafana 內建的 Alertmanager,以及評估與通知的分工。
- Grafana:File provisioning of alerting resources:規則、聯絡點、通知策略的檔案格式。
- Prometheus:Alertmanager:Alertmanager 做的事:去重、分組、路由、靜音、抑制。
- kube-prometheus-stack 的 chart 說明:
alertmanager.enabled與defaultRules的設定。
這篇的 values 在上面列的版本實測過。換版本的話,記得先看 chart 的 values.yaml 欄位有沒有改名。
