高可用性

Alertmanager 支援透過配置來建立高可用(HA)叢集。本文件介紹了高可用機制的工作原理、設計目標以及運維考量。

設計目標

Alertmanager 的高可用實現圍繞著三個核心原則進行設計:

  1. 單一檢視與管理 - 可以從任何叢集成員檢視和管理靜默(Silences)和告警,從而提供統一的運維體驗
  2. 透過“故障開啟(fail open)”在叢集腦裂中生存 - 在網路分割槽期間,Alertmanager 寧願傳送重複的通知,也不願遺漏關鍵告警
  3. 至少一次交付 - 系統保證通知至少送達一次,這與“故障開啟”理念一致

這些目標將運維可靠性和告警送達率置於嚴格的“僅一次(exactly-once)”語義之上。

架構概述

Alertmanager 叢集由多個透過 Gossip 協議進行通訊的 Alertmanager 例項組成。每個例項:

  • 獨立接收來自 Prometheus 伺服器的告警
  • 參與對等(peer-to-peer)Gossip 網格
  • 將狀態(靜默和通知日誌)複製到其他叢集成員
  • 獨立處理併發送通知
┌──────────────┐    ┌──────────────┐    ┌──────────────┐
│ Prometheus 1 │    │ Prometheus 2 │    │ Prometheus N │
└──────┬───────┘    └──────┬───────┘    └──────┬───────┘
       │                   │                   │
       │ alerts            │ alerts            │ alerts
       │                   │                   │
       ▼                   ▼                   ▼
    ┌────────────────────────────────────────────┐
    │  ┌──────────┐  ┌──────────┐  ┌──────────┐  │
    │  │  AM-1    │  │  AM-2    │  │  AM-3    │  │
    │  │ (pos: 0) ├──┤ (pos: 1) ├──┤ (pos: 2) │  │
    │  └──────────┘  └──────────┘  └──────────┘  │
    │          Gossip Protocol (Memberlist)      │
    └────────────────────────────────────────────┘
              │           │           │
              ▼           ▼           ▼
         Receivers   Receivers   Receivers

Gossip 協議

Alertmanager 使用 Hashicorp 的 Memberlist  庫來實現基於 Gossip 的通訊。Gossip 協議處理以下內容:

成員關係管理

  • 自動節點發現 - 例項可以配置已知節點列表,並將自動發現其他叢集成員
  • 健康檢查 - 定期探測以檢測故障成員(預設:每 1 秒)
  • 故障檢測 - 故障成員會被標記,並可以嘗試重新加入

狀態複製

Gossip 層複製三種類型的狀態:

  1. 靜默(Silences) - 建立、更新和刪除操作會廣播到所有節點
  2. 通知日誌 - 已傳送通知的記錄,用以防止重複傳送
  3. 成員變更 - 加入、離開和故障事件

狀態是最終一致的——在給予足夠的時間和網路連線的情況下,所有叢集成員都將收斂到相同的狀態。

Gossip 穩定

當 Alertmanager 啟動或重新加入叢集時,它會等待 Gossip “穩定”後再處理通知。這可以防止基於不完整的狀態傳送通知。

穩定演算法會一直等待,直到:

  • 節點數量在連續 3 次檢查中保持穩定(預設間隔:push-pull 間隔)
  • 或者發生超時(可透過 context 配置)

在此期間,該例項已經接收並存儲告警,但會推遲通知處理。

高可用模式下的通知管道

通知管道在叢集環境中的執行方式不同,以確保在保持“至少一次交付”的同時進行去重

┌────────────────────────────────────────────────┐
│              DISPATCHER STAGE                  │
├────────────────────────────────────────────────┤
│ 1. Find matching route(s)                      │
│ 2. Find/create aggregation group within route  │
│ 3. Throttle by group wait or group interval    │
└───────────────────┬────────────────────────────┘
                    │
                    ▼
┌────────────────────────────────────────────────┐
│               NOTIFIER STAGE                   │
├────────────────────────────────────────────────┤
│ 1. Wait for HA gossip to settle                │◄─── Ensures complete state
│ 2. Filter inhibited alerts                     │
│ 3. Filter non-time-active alerts               │
│ 4. Filter time-muted alerts                    │
│ 5. Filter silenced alerts                      │◄─── Uses replicated silences
│ 6. Wait according to HA cluster peer index     │◄─── Staggered notifications
│ 7. Dedupe by repeat interval/HA state          │◄─── Uses notification log
│ 8. Notify & retry intermittent failures        │
│ 9. Update notification log                     │◄─── Replicated to peers
└────────────────────────────────────────────────┘

高可用特定階段

1. 等待 Gossip 穩定

在傳送來自某個組的第一個通知之前,例項會等待 Gossip 穩定。這確保了:

  • 靜默已完全複製
  • 通知日誌包含來自其他例項的近期傳送記錄
  • 叢集成員關係穩定

實現peer.WaitReady(ctx)

2. 基於節點位置的等待

為了防止所有叢集成員同時傳送通知,每個例項會根據其在排序後的節點列表中的位置進行等待

wait_time = peer_position × peer_timeout

例如,如果有 3 個例項且節點超時(peer timeout)為 15 秒:

  • 例項 am-1(位置 0):等待 0 秒
  • 例項 am-2(位置 1):等待 15 秒
  • 例項 am-3(位置 2):等待 30 秒

這種交錯的時間安排允許:

  • 第一個例項傳送通知
  • 後續例項檢視通知日誌條目
  • 進行去重以防止重複傳送

實現cmd/alertmanager/main.go:594 中的 clusterWait()

位置是透過按字母順序對所有節點名稱進行排序來決定的

func (p *Peer) Position() int {
    all := p.mlist.Members()
    sort.Slice(all, func(i, j int) bool {
        return all[i].Name < all[j].Name
    })
    // Find position of self in sorted list
}

3. 透過通知日誌去重

DedupStage 查詢通知日誌以確定是否應該傳送通知

// Check notification log for recent sends
entry := nflog.Query(receiver, groupKey)
if entry.exists && !shouldNotify(entry, alerts, repeatInterval) {
    // Skip: already notified recently
    return nil
}

去重檢查:

  • 正在觸發的告警發生變化? 如果是,傳送通知
  • 已恢復的告警發生變化? 如果是,且 send_resolved: true,傳送通知
  • 重複時間間隔已過? 如果是,傳送通知
  • 否則:跳過通知(去重)

通知日誌透過 Gossip 進行復制,因此所有叢集成員共享相同的傳送歷史記錄。

腦裂處理(故障開啟)

在網路分割槽期間,叢集可能會分裂成多個無法通訊的組。Alertmanager 的“故障開啟”設計可確保告警仍能送達

場景:網路分割槽

Before partition:
┌────────┬────────┬────────┐
│  AM-1  │  AM-2  │  AM-3  │
└────────┴────────┴────────┘
    Unified cluster

After partition:
┌────────┐       │       ┌────────┬────────┐
│  AM-1  │       │       │  AM-2  │  AM-3  │
└────────┘       │       └────────┴────────┘
 Partition A     │        Partition B

分割槽期間的行為

在分割槽 A 中(僅有 AM-1)

  • AM-1 將自己視為位置 0
  • 等待 0 × 超時 = 0 秒
  • 傳送通知(沒有來自 AM-2/AM-3 的去重)

在分割槽 B 中(AM-2,AM-3)

  • AM-2 為位置 0,AM-3 為位置 1
  • AM-2 等待 0 秒,傳送通知
  • AM-3 看到 AM-2 的通知日誌條目,進行去重

結果:傳送了重複的通知(一個來自分割槽 A,另一個來自分割槽 B)

這是刻意為之的——與遺漏告警相比,Alertmanager 更傾向於傳送重複通知。

分割槽恢復後

當網路分割槽恢復時:

  1. Gossip 協議再次檢測到所有節點
  2. 通知日誌被合併(透過帶有時間戳的類 CRDT 合併)
  3. 未來的通知將在所有例項之間被正確去重
  4. 在任一分割槽中建立的靜默都會被複制到所有節點

高可用模式下的靜默管理

靜默是叢集中的一等複製狀態。

靜默建立與更新

當在任何例項上建立或更新靜默時:

  1. 本地儲存 - 靜默儲存在本地狀態對映中
  2. 廣播 - 靜默被序列化(protobuf)並透過 Gossip 進行廣播
  3. 接收時合併 - 其他例項接收併合並該靜默
    // Merge logic: last-write-wins based on UpdatedAt timestamp
    if !exists || incoming.UpdatedAt > existing.UpdatedAt {
        accept_update()
    }
  4. 索引 - 更新靜默匹配器快取,以便進行快速告警匹配

靜默過期

靜默包含:

  • StartsAt, EndsAt - 生效時間範圍
  • ExpiresAt - 何時進行垃圾回收(EndsAt + 保留期)
  • UpdatedAt - 用於合併期間的衝突解決

每個例項都獨立地:

  • 根據當前時間評估靜默狀態(待處理/處於活動狀態/已過期)
  • 垃圾回收超過其保留期的已過期靜默
  • GC 僅在本地進行(不透過 Gossip),因為所有例項都會收斂到相同的決策

統一檢視

使用者可以與叢集中的任何 Alertmanager 例項進行互動

  • 檢視靜默 - 所有例項都擁有相同的靜默狀態(最終一致性)
  • 建立/更新靜默 - 在任何例項上所做的更改都會傳播到所有節點
  • 刪除靜默 - 實現為“立即過期” + Gossip 廣播

這提供了一個統一的運維體驗,無論您訪問的是哪一個例項。

運維考量

配置

要配置叢集,每個 Alertmanager 例項需要:

# alertmanager.yml
global:
  # ... other config ...

# No cluster config in YAML - use CLI flags

命令列標誌

alertmanager \
  --cluster.listen-address=0.0.0.0:9094 \
  --cluster.peer=am-1.example.com:9094 \
  --cluster.peer=am-2.example.com:9094 \
  --cluster.peer=am-3.example.com:9094 \
  --cluster.advertise-address=192.0.2.1:9094 \
  --cluster.peer-timeout=15s \
  --cluster.gossip-interval=200ms \
  --cluster.pushpull-interval=60s

關鍵標誌

  • --cluster.listen-address - 用於叢集通訊的繫結地址(預設:0.0.0.0:9094
  • --cluster.peer - 節點地址列表(可重複指定)
  • --cluster.advertise-address - 廣播給節點的 IP 地址(不能是主機名),格式為 <ip>:<port>(如果省略,則自動檢測)
  • --cluster.peer-timeout - 去重時每個節點位置的等待時間(預設:15s
  • --cluster.gossip-interval - Gossip 通訊的頻率(預設:200ms
  • --cluster.pushpull-interval - 完整狀態同步間隔(預設:60s
  • --cluster.probe-interval - 節點健康檢查間隔(預設:1s
  • --cluster.settle-timeout - 等待 Gossip 穩定的最大時間(預設:context 超時)

Prometheus 配置

重要提示:配置 Prometheus 將告警傳送到所有 Alertmanager 例項,而不是透過負載均衡器。

# prometheus.yml
alerting:
  alertmanagers:
    - static_configs:
        - targets:
            - am-1.example.com:9093
            - am-2.example.com:9093
            - am-3.example.com:9093

這能確保:

  • 冗餘性 - 如果一個 Alertmanager 宕機,其他例項仍能接收告警
  • 獨立處理 - 每個例項都獨立評估路由、分組和去重
  • 無單點故障 - 負載均衡器會引入單點故障

叢集規模考量

由於 Alertmanager 使用無需法定人數(quorum)或投票的 Gossip 協議,任何 N 個例項都可以承受多達 N-1 次故障——只要有一個例項存活,通知就會被髮送。

然而,叢集規模需要權衡利弊:

更多例項的好處

  • 對併發故障(硬體、網路、資料中心中斷)具有更強的容錯能力
  • 即使在維護視窗期內也能繼續執行

更多例項的成本

  • 在發生網路分割槽的情況下,重複通知的數量將會增加
  • 更多的 Gossip 流量

典型部署方案

  • 2-3 個例項 - 單資料中心生產部署的常用方案
  • 4-5 個例項 - 多資料中心或高關鍵性環境

注意:與基於共識的系統(etcd、Raft)不同,奇數與偶數的叢集規模沒有區別——因為這裡沒有投票或法定人數機制。

監控叢集健康狀態

要監控的關鍵指標

# Cluster size
alertmanager_cluster_members

# Peer health
alertmanager_cluster_peer_info

# Peer position (affects notification timing)
alertmanager_peer_position

# Failed peers
alertmanager_cluster_failed_peers

# State replication
alertmanager_nflog_gossip_messages_propagated_total
alertmanager_silences_gossip_messages_propagated_total

安全性

預設情況下,叢集 communication 是未加密的。對於生產環境部署,尤其是跨廣域網(WAN)的部署,請使用雙向 TLS(mutual TLS)

alertmanager \
  --cluster.tls-config=/etc/alertmanager/cluster-tls.yml

有關雙向 TLS 配置,請參閱 Gossip 流量部分

持久化

每個 Alertmanager 例項都會持久化:

  • 靜默 - 儲存在快照檔案中(預設:data/silences
  • 通知日誌 - 儲存在快照檔案中(預設:data/nflog

重新啟動時:

  1. 例項從磁碟載入靜默和通知日誌
  2. 加入叢集並與節點進行 Gossip 通訊
  3. 合併從節點接收的狀態(較新的時間戳勝出)
  4. 在 Gossip 穩定後開始處理通知

注意:告警本身是進行持久化的——Prometheus 會定期重新發送正在觸發的告警。

常見陷阱

  1. Prometheus → Alertmanager 的負載均衡

    • ❌ 不要使用負載均衡器
    • ✅ 在 Prometheus 中配置所有例項
  2. 未等待 Gossip 穩定

    • 可能導致啟動時遺漏靜默或傳送重複通知
    • --cluster.settle-timeout 標誌用於控制此項
  3. 網路 ACL 阻塞叢集埠

    • 確保所有例項之間已開放 9094 埠(或您的 --cluster.listen-address 埠)
    • 預設情況下同時使用 TCP 和 UDP(如果使用 TLS 傳輸,則僅使用 TCP)
  4. 無法路由的廣播(advertise)地址

    • 如果未設定 --cluster.advertise-address,Alertmanager 會嘗試自動檢測
    • 該值必須是 <ip>:<port> 格式的 IP 地址——不支援主機名。使用主機名會記錄一條警告,並且啟動過程將繼續(但廣播地址會處於未設定或不正確狀態),從而導致叢集通訊失敗
    • 對於雲/NAT 環境,請顯式設定可路由的 IP 地址(例如 --cluster.advertise-address=192.0.2.1:9094
  5. 叢集配置不匹配

    • 所有例項都應具有相同的 --cluster.peer-timeout 和 Gossip 設定
    • 不匹配可能會導致不必要的重複或遺漏通知

工作原理:端到端示例

場景:包含 3 個例項的叢集,新的告警組

  1. 告警從 Prometheus 送達至所有 3 個例項
  2. 排程器(Dispatcher) 建立聚合組,等待 group_wait(例如,30 秒)
  3. 在 group_wait 之後:
    • 每個例項準備傳送通知
  4. 通知階段:
    • 所有例項等待 Gossip 穩定(如果剛剛啟動)
    • AM-1(位置 0):等待 0 秒,檢查通知日誌(為空),傳送通知,記錄到 nflog
    • AM-2(位置 1):等待 15 秒,檢查通知日誌(看到 AM-1 的條目),跳過通知
    • AM-3(位置 2):等待 30 秒,檢查通知日誌(看到 AM-1 的條目),跳過通知
  5. 結果:剛好傳送了一條通知(由 AM-1 傳送)

場景:AM-1 發生故障

  1. 告警僅送達 AM-2 和 AM-3
  2. 排程器 建立組,等待 group_wait
  3. 通知階段:
    • AM-1 不在叢集中(探測失敗)
    • AM-2 現在處於位置 0:等待 0 秒,傳送通知
    • AM-3 現在處於位置 1:等待 15 秒,看到 AM-2 的條目,跳過通知
  4. 結果:通知仍能傳送(故障開啟)

場景:通知期間發生網路分割槽

  1. 告警送達所有例項
  2. 網路分割槽將 AM-1 與 AM-2/AM-3 分開
  3. 在分割槽 A 中(AM-1)
    • 處於位置 0,等待 0 秒,傳送通知
  4. 在分割槽 B 中(AM-2,AM-3)
    • AM-2 處於位置 0,等待 0 秒,傳送通知
    • AM-3 處於位置 1,等待 15 秒,進行去重
  5. 結果:傳送了兩條通知(每個分割槽各一條)——展現故障開啟行為

問題排查

檢查叢集狀態

# View cluster members via API
curl http://am-1:9093/api/v2/status

# Check metrics
curl http://am-1:9093/metrics | grep cluster

診斷腦裂問題

如果您懷疑存在腦裂:

  1. 在每個例項上檢查 alertmanager_cluster_members
    • 應與總叢集規模相匹配
  2. 檢查 alertmanager_cluster_peer_info{state="alive"}
    • 應顯示所有節點均為活躍狀態(alive)
  3. 檢查例項之間的網路連線狀況

除錯重複通知問題

發生重複通知的原因可能是:

  1. 網路分割槽(預期內,由於故障開啟)
  2. Gossip 未穩定 - 檢查 --cluster.settle-timeout
  3. 時鐘偏移 - 確保所有例項上都配置了 NTP
  4. 通知日誌未複製 - 檢查 Gossip 相關指標

啟用除錯日誌記錄

alertmanager --log.level=debug

尋找以下資訊:

  • "Waiting for gossip to settle..."
  • "gossip settled; proceeding"
  • 通知管道中的去重決策

延伸閱讀

本頁內容