高可用性

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"
  • 通知管道中的去重决策

延伸阅读

本页内容