---
title: "Prometheus Alertmanager 电话告警：只为真正紧急的事响铃"
description: "Alertmanager 没有语音接收器。本文介绍如何只在 critical 级别时把 Prometheus 告警变成一通电话：webhook 配置、条件过滤，以及 Watchdog 陷阱。"
date: 2026-09-04
author: Nooc
authorAvatarLink: /images/avatars/nooc.webp
authorLink: https://nooc.me
tags:
  - Prometheus
  - Alertmanager
  - 电话告警
  - Webhook 通知
  - Kubernetes
  - 值班
---

# Prometheus Alertmanager 电话告警：只为真正紧急的事响铃

Alertmanager 没有语音接收器。想在 Prometheus 告警触发时接到电话，只需添加一个 `webhook_configs` 接收器，指向订阅类型设为**来电铃声**的 Echobell 频道。本文给出确切的 YAML 配置、阻止「已恢复」告警打电话的条件、按严重级别路由的方法，以及那个会让你的手机每四小时响一次、永不停歇的 Watchdog 告警。

Prometheus 是过去十年里绝大多数基础设施的默认指标方案，而 [Alertmanager](https://prometheus.io/docs/alerting/latest/alertmanager/) 在真正困难的部分上做得相当出色：告警去重、分组、维护期静默，以及在上游依赖故障时抑制下游噪音。

它唯一不会做的事，就是把人叫醒。

## 为什么 Alertmanager 自己打不了电话

Alertmanager 内置了邮件、Slack、PagerDuty、OpsGenie、Discord、Telegram、Pushover、Webex、MS Teams 等十几种接收器。但它们无一例外都是投递**消息**，而消息会被静音开关、勿扰模式和 iOS 专注模式拦下。到了凌晨三点，这意味着告警到了，却什么都没发生。

它没有 `voice_configs`。人们通常会落到这几个选项上：

- **PagerDuty / OpsGenie / Splunk On-Call** —— 它们确实能打电话，但都是完整的事件管理平台，价格也按席位收。如果你需要排班和升级策略，它们是正确答案；如果你只是想让手机响起来，就太重了。（OpsGenie 正在[逐步停止服务](/zh/blog/opsgenie-end-of-life-alternatives)，这也是眼下这么多团队在重新评估这一层的原因。）
- **短信桥接**，比如 [Sachet](https://github.com/messagebird/sachet) —— 你要多跑一个服务，按条向网关付费，而短信本质上仍是一条消息。在 iOS 上，除非发件人在允许列表里，短信并不会突破专注模式。
- **基于 Twilio 自建** —— 写一个小的 webhook 接收服务、买一个号码、按通话付费，然后你就多了一块生产基础设施，而它唯一的职责是让手机响铃。

通用的 **webhook 接收器**才是那个出口。它会向任意 URL POST 一份有明确文档的 JSON，而这已经足够了。

## 你需要准备

- 一套运行中的 Prometheus + Alertmanager，以及修改 `alertmanager.yml` 的权限
- 安装好 Echobell（[App Store](https://apps.apple.com/app/apple-store/id6743597198?pt=128151925&ct=blog-alertmanager-phone-call-alerts-zh&mt=8) / [Google Play](https://play.google.com/store/apps/details?id=one.echobell.echobellandroid)）
- 十分钟

本文基于 Alertmanager 0.31 撰写。webhook 载荷多年来一直是 `version: "4"`，所以 0.2x 版本的行为完全一致。

你的 Alertmanager 需要能出站访问 `hook.echobell.one`，但**不需要**从公网可达——因此集群内、VPC 内或家庭实验室里的 Alertmanager 都没问题。

## 第一步 —— 创建一个会呼叫你的频道

在 Echobell 中创建一个频道，命名为 `Prometheus Critical` 之类。把它的订阅通知类型设为**来电铃声**。这个设置才是关键：来电类告警会以来电界面的形式出现，能穿透 iOS 专注模式和勿扰模式，而普通推送做不到。

把模板设置为直接读取 Alertmanager 的载荷：

```
Title: 🔴 {{commonLabels.alertname}} on {{commonLabels.instance}}
Body: {{commonAnnotations.summary}}
{{commonAnnotations.description}}
```

再在高级设置里配置一个**链接模板**，让通知记录直接跳转到对应的图表：

```
{{alerts[0].generatorURL}}
```

然后复制频道的 **Webhook URL**：

```
https://hook.echobell.one/t/<channel-token>
```

把这个 URL 当作密钥对待——任何拿到它的人都能让你的手机响起来。

## 第二步 —— 添加 webhook 接收器

在 `alertmanager.yml` 中：

```yaml
route:
  group_by: ["alertname", "cluster", "service"]
  group_wait: 30s
  group_interval: 5m
  repeat_interval: 4h
  receiver: echobell-critical

receivers:
  - name: echobell-critical
    webhook_configs:
      - url: "https://hook.echobell.one/t/<channel-token>"
        send_resolved: false
```

用 `curl -X POST http://localhost:9093/-/reload` 或 `SIGHUP` 重载配置。

注意 `send_resolved: false`。**webhook 接收器的默认值是 `true`**，这一点和 Alertmanager 的大多数其他接收器不同。所以如果不写这一行，服务故障时你的手机会响，服务自愈时还会再响一次。而第二通电话，正是让人开始忽略第一通的原因。第四步会讲如何保留恢复通知、但不带铃声。

## 第三步 —— 搞清楚到底收到了什么

Alertmanager 会先分组，然后每组 POST 一份载荷：

```json
{
  "version": "4",
  "groupKey": "{}:{alertname=\"HighErrorRate\"}",
  "truncatedAlerts": 0,
  "status": "firing",
  "receiver": "echobell-critical",
  "groupLabels": { "alertname": "HighErrorRate" },
  "commonLabels": { "alertname": "HighErrorRate", "severity": "critical" },
  "commonAnnotations": { "summary": "Error rate above 5% for 10m" },
  "externalURL": "http://alertmanager.internal:9093",
  "alerts": [
    {
      "status": "firing",
      "labels": { "alertname": "HighErrorRate", "instance": "api-7d9f:8080" },
      "annotations": { "summary": "Error rate above 5% for 10m" },
      "startsAt": "2026-09-04T02:41:07.351Z",
      "endsAt": "0001-01-01T00:00:00Z",
      "generatorURL": "http://prometheus:9090/graph?g0.expr=...",
      "fingerprint": "a1b2c3d4e5f60718"
    }
  ]
}
```

Echobell 会原样读取 JSON 请求体，因此上面每一个字段都能在模板和条件中使用。嵌套访问两种写法都支持——`{{commonLabels.severity}}` 或 `{{alerts[0].labels["instance"]}}`。

这份载荷有两个特性决定了下面的一切：

**只要组内有**任意**一条告警处于 firing，顶层 `status` 就是 `firing`。** 只有当组内所有告警都恢复后，它才变成 `resolved`。这让它成为一个非常干净的过滤依据。

**`commonLabels` 只包含组内所有告警共有的标签。** 这是最常见的意外。如果 `group_by` 足够宽泛，以致一次 webhook 携带了三个不同实例上的 `HighErrorRate`，那么 `commonLabels.instance` 就不存在，`{{commonLabels.instance}}` 会渲染成空字符串。后文有专门一节讲如何应对。

## 第四步 —— 把恢复通知降级为安静推送

你仍然想知道服务什么时候恢复了——只是不想为此接电话。再创建一个 Echobell 频道 `Prometheus Recovered`，通知类型设为**普通**，模板如下：

```
Title: ✅ {{commonLabels.alertname}} resolved
Body: {{commonAnnotations.summary}}
```

并在高级设置中填入这个**条件**：

```
status == "resolved"
```

条件是在投递任何内容之前求值的表达式。如果表达式为假，Echobell 会接受这个请求，但什么都不发送。

然后让同一个接收器同时指向两个频道——一个接收器可以包含多个 `webhook_configs`：

```yaml
receivers:
  - name: echobell-critical
    webhook_configs:
      # 让手机响铃。只在 firing 时发送。
      - url: "https://hook.echobell.one/t/<calling-channel-token>"
        send_resolved: false
      # 安静推送。频道条件会丢弃 firing 的那一半。
      - url: "https://hook.echobell.one/t/<recovery-channel-token>"
        send_resolved: true
```

恢复频道会同时收到 firing 和 resolved 两种载荷，并丢弃 firing 的那些。结果就是：故障响铃，恢复变成一条你早上再看的推送。

## 第五步 —— 按严重级别路由，而不是全都发

把所有告警都发到来电频道的兜底路由，是一台批量生产「被忽略的电话」的机器。应该在 Alertmanager 里按 severity 分流——路由树本来就该待在那儿：

```yaml
route:
  group_by: ["alertname", "cluster", "service"]
  group_wait: 30s
  group_interval: 5m
  repeat_interval: 4h
  receiver: echobell-warning

  routes:
    # Watchdog 永远不该到达人类。见第六步。
    - matchers:
        - alertname = "Watchdog"
      receiver: "null"

    - matchers:
        - severity = "critical"
      receiver: echobell-critical
      group_wait: 10s
      repeat_interval: 1h

receivers:
  - name: "null"

  - name: echobell-critical
    webhook_configs:
      - url: "https://hook.echobell.one/t/<calling-channel-token>"
        send_resolved: false

  - name: echobell-warning
    webhook_configs:
      - url: "https://hook.echobell.one/t/<normal-channel-token>"
        send_resolved: true
```

路由自上而下求值，**第一个匹配者胜出**——`continue` 默认为 `false`。所以顺序很重要：`Watchdog` 那条路由必须放在任何可能吞掉它的规则之上。

如果你更希望只用一个频道、在 Echobell 侧过滤，等价的条件是：

```
status == "firing" && commonLabels.severity == "critical"
```

通常在 Alertmanager 里做更好，因为这样 `severity` 还能顺带驱动 `group_wait` 和 `repeat_interval`。而当你今天没法合并一个配置变更时，在 Echobell 里做更实际。

## 第六步 —— Watchdog 陷阱

如果你在用 [kube-prometheus-stack](https://github.com/prometheus-operator/kube-prometheus)，你就有一条名为 `Watchdog` 的告警，表达式是 `vector(1)`。它**被设计成永远触发**——它存在的意义，是让外部系统能察觉 Prometheus 本身已经停摆。默认配置会把它路由到一个 `null` 接收器。

一旦你把兜底路由指向来电频道却没有排除它，Watchdog 就会每隔一个 `repeat_interval` 给你打一次电话，从此不停，而且立刻开始。这是人们得出「电话告警根本没法用」这个结论的头号原因。

保留第五步里的 `null` 路由。然后，可选地，用它做一件真正有用的事：把 Watchdog 变成一个真正的死人开关（dead man's switch）。

```yaml
    - matchers:
        - alertname = "Watchdog"
      receiver: deadmansswitch
      group_wait: 0s
      group_interval: 1m
      repeat_interval: 50s

receivers:
  - name: deadmansswitch
    webhook_configs:
      - url: "https://hc-ping.com/<your-check-uuid>"
        send_resolved: false
```

Echobell 本身不能充当死人开关——它在请求**到达**时告警，而不是在请求停止时告警。所以把 Watchdog 的心跳发给专门做静默检测的服务（[Healthchecks.io](https://healthchecks.io)、Cronitor、Dead Man's Snitch），再把**那个服务**的「检查失联」webhook 指向你的 Echobell 来电频道。这样一来，一通电话就意味着「监控系统本身死了」——这是你最希望被叫醒的那条告警，也是几乎没人配置的那条。

## 调参，让它不至于「狼来了」

Alertmanager 里有三个设置承担了大部分工作，Prometheus 里还有一个：

| 设置 | 位置 | 作用 |
| --- | --- | --- |
| `for:` | 告警规则 | 条件必须持续多久才真正触发。这是抵御两秒抖动的第一道防线。 |
| `group_wait` | 路由 | 首次通知前等待更多告警的时间。默认 30s；critical 可降到 `10s`。 |
| `group_interval` | 路由 | 就已有分组中的**新**告警再次通知前的最小间隔。默认 5m。 |
| `repeat_interval` | 路由 | 未恢复的告警多久重复通知一次。**默认 4h**——所以一次夜间故障会在凌晨三点打给你，七点再打一次。 |

`repeat_interval` 是最值得琢磨的一个。四小时对一个坏掉的东西来说太长了；二十分钟则是一台专门让你关掉这个频道的机器。critical 用一小时是个合理的起点。

如果你希望未接来电立刻重试、而不是等到下一个 `repeat_interval`，请在 Echobell 的应用设置中开启**重试失败的通话**。

## 只在非工作时间响铃

工作时间里你多半已经盯着仪表盘了。Echobell 的系统时间变量（全部为 UTC）让一个频道能按小时表现不同，而不需要在 Alertmanager 里再加一条路由：

```
status == "firing" && (hour >= 17 || hour < 9)
```

这样只会在 UTC 09:00–17:00 之外呼叫你。再把一个普通类型的频道指向相反的时段，用于白天的推送：

```
status == "firing" && hour >= 9 && hour < 17
```

加上 `dayOfWeek >= 1 && dayOfWeek <= 5`，就能把周末也算作非工作时间。记住这些值始终按 UTC 计算——请按你自己的时区做偏移。[用 UTC 条件实现时间窗通知](/zh/blog/time-window-notifications-using-utc-conditions)里有更完整的讲解。

## 处理 `commonLabels` 为空的问题

当一个分组包含来自多个实例的告警时，`commonLabels.instance` 会消失，你的通知标题就变成了 `🔴 HighErrorRate on `。

三种解法，按推荐程度排序：

1. **把该标签放进 `group_by`。** 如果 `group_by` 包含 `instance`，那么同组内每条告警都共享它，`commonLabels.instance` 就一定存在。代价是通知变多——从每个告警名一条变成每个实例一条。
2. **改读第一条告警。** `{{alerts[0].labels.instance}}` 一定有值。但它只是可能存在的多条中的一条，所以配上一个计数：`{{alerts[0].labels.instance}}（共 {{alerts.length}} 条）`。
3. **把标签设计成即使为空也读得通。** Echobell 没有默认值运算符——`{{a || "unknown"}}` 会渲染出字面量 `true`，而不是回退值——所以把 `Instance: {{commonLabels.instance}}` 单独写成一行，这样空值一眼就是空值，而不是一句破碎的话。

## 控制载荷大小

一个覆盖上百个 Pod 的分组会产生很大的 JSON 请求体，而 Echobell 会以 HTTP 413 拒绝超过 1 MiB 的触发请求。在 Alertmanager 里限制它：

```yaml
      - url: "https://hook.echobell.one/t/<channel-token>"
        send_resolved: false
        max_alerts: 20
```

Alertmanager 随后最多发送 20 条告警，并把被丢弃的数量写入 `truncatedAlerts`，你可以把它显示在正文里：

```
Body: {{commonAnnotations.summary}}
Alerts: {{alerts.length}}（另有 {{truncatedAlerts}} 条被截断）
```

## 与团队共享告警

Echobell 频道可以通过订阅链接分享，每位订阅者各自选择通知类型。因此同一条路由既能呼叫值班工程师，又能以普通推送落到其他所有人那里——不按席位收费，Alertmanager 里也不用加额外的路由规则。

这也契合许多团队一开始自托管 Prometheus 的理由：指标和告警规则留在你自己的基础设施上，而 Echobell 把通知内容和历史记录保存在设备上，而非它的服务器上。

## 这套方案不提供什么

把边界讲清楚，能让你以后少做一次糟糕的迁移。Echobell 是投递层，不是事件管理平台。它没有：

- 值班排班表或跨时区交接
- 第一个人未接听时自动呼叫第二个人的升级策略
- 事件时间线、确认追踪或事后复盘工具

如果你的团队需要这些，那你需要的是 PagerDuty、Grafana Cloud IRM 之类的产品。这套方案覆盖的是 Alertmanager 留下的那个具体缺口：把一条触发的告警变成一部真正响起来的手机。对独立运维者、小团队和家庭实验室来说，这通常就是全部需求。

## 排查

**完全收不到东西。** 先看 Alertmanager 自己的日志（`level=error component=dispatcher`），再确认路由确实落到了你的接收器上——`amtool config routes test severity=critical alertname=HighErrorRate` 能在不等真实告警的情况下告诉你某条告警会进入哪个接收器。

**Echobell 返回 HTTP 404。** 频道 token 错误，或频道已被删除。未知 token 返回的是 404，而不是静默成功。

**Echobell 返回 200，且 `"notificationTriggered": false`。** 你的条件求值为假。响应体里还带着 `"conditionsMet": false`，这是区分「我的条件写错了」和「我的 webhook 根本没到」最快的方式。请对照 Alertmanager 实际发送的内容检查 `status == "firing"`——注意是顶层 status，不是 `alerts[0].status`。

**HTTP 413。** 载荷超过 1 MiB。按上文设置 `max_alerts`。

**HTTP 405。** 频道启用了 **POST Only**，而某个来源发的是 GET。Alertmanager 用的是 POST，所以这通常意味着你在浏览器里试了这个 URL。

**标题中间空了一块。** 该分组的 `commonLabels` 里没有这个标签。见上文对应小节。

**通知到了，但不响铃。** 该订阅的通知类型是普通或时效性，而不是来电铃声。通知类型是每位订阅者各自选择的，所以请在那台不响的设备上检查。

**在不影响生产的前提下测试。** 添加一条 `expr: vector(1)`、使用独特 `alertname`、带 `severity: critical` 的规则，让它触发一次后删掉。或者手动发一条：

```bash
curl -X POST http://localhost:9093/api/v2/alerts -H 'Content-Type: application/json' -d '[
  {"labels":{"alertname":"EchobellTest","severity":"critical"},
   "annotations":{"summary":"Testing the phone call path"}}
]'
```

## 常见问题

### Prometheus Alertmanager 原生能打电话吗？

不能。Alertmanager 有邮件、Slack、PagerDuty、OpsGenie 等许多接收器，但没有语音或短信接收器。要实现电话告警，需要把通用 webhook 接收器路由到一个能拨打电话的服务（比如 Echobell），或者付费使用事件管理平台。

### 电话告警能绕过勿扰模式吗？

可以。Echobell 的来电铃声通知类型以来电形式呈现，能穿透 iOS 专注模式和勿扰模式。细节和相关设置见[如何让关键告警绕过 iOS 专注模式](/zh/blog/how-to-bypass-ios-focus-mode-for-critical-alerts)。

### 防火墙后面或 Kubernetes 里的 Alertmanager 能用吗？

可以。这个 webhook 是从 Alertmanager 发出的出站 HTTPS 请求，只需要能访问 `hook.echobell.one`。你的 Alertmanager 不需要公网地址，也不需要 Ingress。

### 怎样让告警恢复时不再打电话？

在指向来电频道的那个 webhook 配置上设置 `send_resolved: false`。webhook 接收器的默认值是 `true`，这一点与 Alertmanager 的大多数其他接收器不同，所以这是「选择退出」而非「选择加入」。若仍想安静地收到恢复通知，可再建一个频道，条件设为 `status == "resolved"`。

### 为什么同一条告警每四小时就让我的手机响一次？

那是 `repeat_interval`，默认值 `4h`。Alertmanager 会按这个节奏对仍在触发的告警重复通知。请按路由分别设置——critical 用 `1h` 是常见选择。如果这些电话是在你加了兜底路由之后立刻开始的，那罪魁祸首更可能是永远触发的 `Watchdog` 告警，见第六步。

### 同一条告警能呼叫多个人吗？

可以。把频道分享给同事，每位订阅者选择自己的通知类型。所有订阅了来电频道的人都会被呼叫，且不按席位收费。

### 严重级别过滤应该放在 Alertmanager 还是 Echobell 条件里？

优先放在 Alertmanager：在那里路由还能让你为不同 severity 设置各自的 `group_wait` 和 `repeat_interval`，而且路由树会和其余配置一起纳入版本控制。当你改不了 Alertmanager 配置时，或者要做 Alertmanager 没有对应概念的过滤（比如按一天中的时段）时，再用 Echobell 条件。

## 小结

整套配置就是一个接收器、一行 `send_resolved: false`，以及一棵把 `severity: critical` 以外的一切都挡在来电频道之外的路由树。它完全不改动你现有的告警规则、分组、静默和抑制配置，却补上了「Prometheus 发现了」和「有人发现了」之间的那道缝。

[在 iPhone 上下载 Echobell](https://apps.apple.com/app/apple-store/id6743597198?pt=128151925&ct=blog-alertmanager-phone-call-alerts-zh&mt=8) 或[在 Google Play 获取](https://play.google.com/store/apps/details?id=one.echobell.echobellandroid)，然后先发一次上面那条 `EchobellTest` 告警，再把任何真正重要的事情托付给这条链路。

---

## 相关内容

- [Prometheus 集成文档](/zh/docs/developer/prometheus)
- [频道条件参考](/zh/docs/conditions)
- [Grafana 电话告警](/zh/blog/grafana-call-notification)
- [Uptime Kuma 电话告警](/zh/blog/uptime-kuma-phone-call-alerts)
- [开发者的告警疲劳修复指南](/zh/blog/fix-alert-fatigue-developer-guide)
