Alertmanager 踩坑筆記:不用 Prometheus,也能用 5 行 bash 發告警
事情的起點是某天早上,手機被 Telegram 轟了六則告警。查完之後發現:三則是真的(服務 OOM 崩潰),三則是自己嚇自己(正常停機被當成崩潰)。
追這件事的過程中,把 Alertmanager 從頭理解了一遍,也踩了幾個坑。這篇記錄下來。
(同一套 stack 的 Prometheus / Grafana / MLflow 部分,我在 vLLM 推論服務監控實戰 寫過,這篇專講告警。)
最大的誤解:Alertmanager 不是 Prometheus 的一部分
大部分人第一次接觸 Alertmanager,都是透過這條路:
Prometheus 評估 alerting rules → 推給 Alertmanager → 送 Telegram / Slack
看久了很容易以為 Alertmanager 是 Prometheus 的下游附屬品。它不是。
Alertmanager 是一個獨立的服務,跑自己的 process、開自己的 port(預設 9093)。它的 /api/v2/alerts 是公開的 REST endpoint,任何東西都能直接推告警進去 —— Prometheus 可以,你的 bash 腳本也可以。
這個認知差別很重要,因為它直接決定了你要花多少力氣。
為什麼這件事重要:不是所有問題都是 metrics
假設你要監控一個背景 worker:它的 systemd unit 死了沒?主迴圈卡住了沒?
這兩件事不是時序資料。要走「正統」的 Prometheus 路線,你得先架一個 exporter,把「服務活著沒」變成一個 up metric,讓 Prometheus 定時抓、寫進 TSDB、再跑 rule 判斷、才推給 Alertmanager。
繞一大圈,而且你多了一個常駐的 exporter 要顧 —— 它自己也會掛,然後你需要監控你的監控。
直接推 Alertmanager 就沒這些事。 一個 curl 就結束了:
curl -sf -XPOST http://127.0.0.1:9093/api/v2/alerts \
-H 'Content-Type: application/json' -d '[{
"labels": {
"alertname": "MyWorkerDown",
"severity": "critical",
"instance": "'$(hostname)'"
},
"annotations": {
"summary": "worker 掛了",
"description": "查:journalctl -u my-worker -e"
}
}]'
Code language: PHP (php)推進去之後,Alertmanager 那端的分組、節流、靜音、Telegram 推送全部照常運作 —— 因為對它來說,這則告警跟 Prometheus 推的沒有任何區別。
技巧:用 endsAt 讓告警自動恢復,免寫 recovery 邏輯
這是我覺得最漂亮的一招。
Alertmanager 眼中的告警不是一則訊息,而是一個有生命週期的狀態:firing → resolved。每則告警有 startsAt / endsAt,過了 endsAt 沒人續命,就自動變成 resolved。
利用這點,你的監控腳本可以完全不寫「恢復偵測」:
- 每次推告警時,把
endsAt設成 +10 分鐘 - 用 cron / systemd timer 每 3 分鐘跑一次檢查
- 問題還在 → 繼續推,
endsAt一直被延後,告警維持 firing - 問題好了 → 沒人推了 → 10 分鐘後 Alertmanager 自己 resolve
搭配 receiver 的 send_resolved: true,恢復通知也是自動的。你一行 recovery 邏輯都沒寫,卻能正確發出「已恢復」。
ends_at=$(date -u -d '+10 minutes' +%Y-%m-%dT%H:%M:%SZ)
# 把 "endsAt": "${ends_at}" 加進 payload 就好
Code language: PHP (php)等於把狀態管理外包給 Alertmanager 的 TTL 機制。這是純 Telegram Bot API 給不了的 —— 自己 call Telegram 的話,「什麼時候該說恢復了」得你自己記。
完整範例:一個 systemd 服務的看門狗
把上面兩件事組合起來,一個實用的看門狗大概長這樣。它檢查兩件事:unit 活著沒,以及心跳有沒有卡住(服務每跑一圈 touch 一個檔案)。
#!/usr/bin/env bash
set -uo pipefail # 注意:不加 -e,因為 is-active 對非 active 會回非零
UNIT="my-worker"
BEAT="$HOME/.cache/my-worker.beat"
AM_URL="http://127.0.0.1:9093"
STALE_SEC=300
now=$(date +%s)
mtime() { stat -c %Y "$1" 2>/dev/null || echo 0; }
state=$(systemctl --user is-active "$UNIT" 2>/dev/null || true)
reason=""
if [[ "$state" != "active" ]]; then
reason="unit 狀態=${state}"
else
beat_age=$(( now - $(mtime "$BEAT") ))
if (( beat_age > STALE_SEC )); then
reason="心跳 ${beat_age}s 沒更新(行程在,但主迴圈沒動)"
fi
fi
[[ -z "$reason" ]] && exit 0
ends_at=$(date -u -d '+10 minutes' +%Y-%m-%dT%H:%M:%SZ)
curl -sf -m 10 -XPOST "$AM_URL/api/v2/alerts" \
-H 'Content-Type: application/json' -d "[{
\"labels\": {\"alertname\": \"MyWorkerDown\", \"severity\": \"critical\", \"instance\": \"$(hostname)\"},
\"annotations\": {\"summary\": \"${reason}\"},
\"endsAt\": \"${ends_at}\"
}]" || logger -t my-watchdog "告警送不進 Alertmanager:$reason"
Code language: PHP (php)心跳檔那段是重點,它能抓到 systemd 抓不到的狀況:行程還在、PID 還活著,但主迴圈已經卡死。systemd 只知道 process 在不在,不知道它有沒有在做事。
配上 timer 每 3 分鐘巡邏,再加上 service 的 OnFailure 立即觸發:
# my-worker.service
[Unit]
OnFailure=my-watchdog.service # 崩潰當下立刻告警,不用等下一輪
StartLimitIntervalSec=300
StartLimitBurst=5 # 5 分鐘內崩 5 次就停手,別無限重啟
[Service]
Restart=always
RestartSec=5
Code language: PHP (php)# my-watchdog.timer
[Timer]
OnBootSec=3min
OnUnitActiveSec=3min
Code language: PHP (php)整套零依賴 —— 純 bash、systemd、curl,沒有 monit、沒有 supervisor、沒有 exporter。watchdog 自己不是常駐 process,所以它不會變成下一個需要被監控的東西。
坑 1:Grafana 接 Alertmanager 有兩個方向,選錯什麼都看不到
這是最容易卡住的一個。
Alertmanager 內建的 web UI(:9093)很陽春,而且沒有任何認證 —— 裸露出去等於任何人都能進去按 Silence 把你的告警全部靜音,而且不會有人通知你。所以通常會想接進 Grafana(至少有帳密)。
問題來了:Grafana 裡有兩個地方都寫著 Alertmanager,方向完全相反。
| 設定位置 | 方向 | 用途 |
|---|---|---|
| Administration → External Alertmanagers | Grafana → AM | 把 Grafana 自己的 rules 推出去 |
| Connections → Data sources → Alertmanager | Grafana ← AM | 讀取外部 AM 的告警與 silence |
想在 Grafana 裡看既有 Alertmanager 的告警,要的是下面那個(data source)。選錯上面那個,畫面永遠是空的。
正確做法是加一個 Alertmanager 型別的 data source。用 provisioning 的話:
apiVersion: 1
datasources:
- name: Alertmanager
uid: alertmanager
type: alertmanager
access: proxy
url: http://alertmanager:9093
jsonData:
implementation: prometheus
handleGrafanaManagedAlerts: false # Grafana 自己的 rules 不往這裡推
Code language: PHP (php)接好之後還有第二層陷阱:進到 /alerting/groups 或 /alerting/silences,頁面上方有一個下拉選單,預設選的是 Grafana(內建那套,通常是空的)。要手動切成 Alertmanager,才看得到你的告警。
我就是卡在這裡,明明 API 測試全部 200、資料也讀得到,UI 卻空白 —— 因為選單沒切。
驗證的話,直接打 Grafana UI 自己在用的那個 API 最快:
curl -s -u admin:密碼 \
'http://localhost:3000/api/datasources/proxy/uid/alertmanager/api/v2/alerts'
Code language: JavaScript (javascript)回 200 就是通的。
坑 2:Alertmanager 沒有歷史,resolve 就消失
我一開始以為告警列表是空的代表壞了,其實空白才是正常 —— 那個 UI 只顯示「此刻正在燒」的告警。
這代表:已經恢復的告警,你在 Alertmanager 和 Grafana 裡都查不到了。 想知道「上週三到底燒過沒」,那兩個 UI 都給不了答案。
歷史得自己留,實務上有幾個地方:
- 通知管道的聊天記錄(Telegram / Slack)—— 最直接,但難搜尋
- 你自己腳本裡的
logger—— 我在 watchdog 加了logger -t my-watchdog,結果這變成最有用的一份,journalctl -t my-watchdog --since '30 days ago'直接把整個月的事件撈出來,後來就是靠它還原現場的 - Prometheus 的
ALERTS時序 —— 只涵蓋 Prometheus rules 觸發的告警,你用 curl 直推的它不知道
所以如果你打算直推 Alertmanager,務必在腳本裡加一行 logger,成本一行,價值很高。
坑 3:systemd 的 143 —— 正常停機被當成崩潰
這是我那六則告警裡「假警報」的來源,也是最陰的一個。
Stopping my-worker.service...
my-worker.service: Main process exited, code=exited, status=143/n/a
my-worker.service: Failed with result 'exit-code'.
my-worker.service: Triggering OnFailure= dependencies. ← 告警發出去了
Code language: JavaScript (javascript)143 = 128 + 15 = SIGTERM。也就是說:這是一次正常的 systemctl stop,服務乖乖收信號退出。
但 systemd 認為「非零退出碼 = 失敗」,於是 Failed → 觸發 OnFailure= → 看門狗發告警。每次你正常停機維護,就收到一則假崩潰通知。
修法一行:
[Service]
SuccessExitStatus=143
或者更根本地,讓程式自己攔 SIGTERM 然後 exit 0。
**判讀速查**:
status=143= 被 SIGTERM(正常 stop)、status=137= 被 SIGKILL(-9 或 OOM killer)、status=1/FAILURE= 程式自己異常退出(**這種才是真的崩了**,要去看它的 stderr)。
我那次就是混了兩種:143 是維護停機(假警報),1/FAILURE 才是真的 CUDA OOM。分不清楚的話,會浪費很多時間查錯方向。
坑 4:Compose 的 inline config 改了不會重建容器
這個坑我在上一篇寫過,結果這次又踩了一次,所以再提一句。
用 configs: 的 content: 寫 inline 設定時,改了內容重新 up,Docker 不認為需要重建容器,你的新設定不會生效。
# 確認容器內拿到的是不是新版
docker exec <容器> cat /path/to/config.yml
# 沒換的話,砍掉重建(具名 volume 的資料會留著)
docker rm -f <容器> && docker compose up -d
Code language: HTML, XML (xml)判斷方法:docker inspect <容器> --format '{{.Created}}',如果建立時間是好幾天前,那它用的就是舊設定。
小結
| 重點 | 一句話 |
|---|---|
| Alertmanager 的定位 | 獨立的告警匯流排,不是 Prometheus 的附屬品 |
| 什麼時候直推 | 監控對象不是 metrics(服務死活、心跳、檔案狀態)時,別為它架 exporter |
endsAt + 定期續火 |
免寫 recovery 邏輯,恢復通知自動來 |
| 要留歷史 | Alertmanager 沒有,靠 logger 自己記 |
| 接 Grafana | 要 data source(讀),不是 External Alertmanagers(推);記得切下拉選單 |
| systemd 143 | 是正常 stop,不是崩潰,加 SuccessExitStatus=143 |
整套下來最大的心得是:「偵測問題」和「通知人類」本來就該分開。 Alertmanager 把後者做成了一個誰都能接的服務 —— 你的 5 行 bash 和正牌的 Prometheus,在它眼裡地位完全相同。認清這點,很多監控需求就不用繞遠路了。