跳过正文
如何设计一个好的告警体系

如何设计一个好的告警体系

·570 字·3 分钟·
目录
SRE 实战手册 - 这篇文章属于一个选集。
§ : 本文

运维了一年多的 EKS 集群,有段时间钉钉群的告警消息一天能超过 200 条。到最后,所有人都对告警视而不见——因为大多数都是噪音。直到某天真正的故障来临,值班工程师因为习惯性地忽略告警,足足延误了 20 分钟才响应。

那次之后我们开始认真重构告警体系。这篇文章是我们在这个过程中的一些思考和实践。


告警噪音是怎么来的
#

回顾那段时间的告警,归结起来有几类:

1. 阈值设置太敏感

# 错误的设置:CPU 超过 70% 就告警,几乎每天都触发
- alert: HighCPU
  expr: cpu_usage > 70
  for: 1m

服务正常运行时 CPU 在 60–80% 之间波动,这个阈值没有任何实际意义。

2. 告警没有 for 缓冲

# 瞬时抖动就触发告警,30 秒后自己好了
- alert: PodRestartTooFrequent
  expr: kube_pod_container_status_restarts_total > 3

Pod 偶尔重启一两次是正常的,没有时间窗口的告警会在每次 Pod 启动时都触发。

3. 原因告警而非症状告警

一台节点的磁盘写入延迟高,触发了:

  • 节点磁盘告警
  • 该节点上所有 Pod 的请求延迟告警
  • 依赖这些服务的上游告警

三层级联,同一个根因产生了十几条告警。

4. 告警没有优先级,全部发同一个群

P0 和 P3 的告警混在一起,P0 告警出现时被淹没了。


好告警的标准
#

Google SRE 书里提到的告警原则,我认为用四个词概括最准确:可操作、及时、准确、上下文充足

可操作:每一条告警触发后,值班工程师应该知道下一步要做什么。如果一条告警触发后,工程师需要先去查另外三个系统才能判断是不是真正的问题,这条告警的设计就有问题。

及时:告警应该在用户感知到问题之前触发(或者至少同时)。一条告警在故障发生 30 分钟后才触发,已经没有意义了。

准确:告警应该精确反映真实问题,误报和漏报都是失败。误报会导致告警疲劳,漏报会导致故障扩大。

上下文充足:告警消息要包含足够的信息,让值班工程师不需要额外查询就能判断严重程度和初步方向。


SLI/SLO 与告警的关系
#

这是我们重构告警最重要的思维转变:基于症状告警,而不是基于原因告警

什么是 SLI 和 SLO
#

SLI(Service Level Indicator):衡量服务健康的指标,通常是:

  • 可用性:成功请求 / 总请求
  • 延迟:P95/P99 响应时间
  • 错误率:5xx / 总请求

SLO(Service Level Objective):SLI 的目标值,例如:

  • 可用性 ≥ 99.9%(30 天内最多允许 43.2 分钟不可用)
  • P99 延迟 ≤ 500ms
  • 错误率 ≤ 0.1%

基于 SLO 的告警设计
#

传统做法:监控各种基础设施指标(CPU、内存、磁盘),指标异常就告警。

SLO 做法:直接监控用户体验指标(错误率、延迟),用户感受到的问题才是告警的依据。

# 传统告警:原因导向
- alert: HighCPU
  expr: container_cpu_usage_seconds_total > 0.8

# SLO 告警:症状导向(用户真正感受到的)
- alert: ErrorRateTooHigh
  expr: |
    sum(rate(http_requests_total{status=~"5.."}[5m])) /
    sum(rate(http_requests_total[5m])) > 0.01
  for: 5m
  labels:
    severity: critical
  annotations:
    summary: "{{ $labels.service }} 错误率超过 1%"
    description: "当前错误率: {{ $value | humanizePercentage }},超过 SLO 阈值 0.1%"
    runbook: "https://wiki.internal/runbooks/high-error-rate"

错误预算告警
#

更进一步,可以基于错误预算消耗速率告警:

# 30 天错误预算,如果当前速率继续,1 小时内会消耗 2% 的月度预算
- alert: ErrorBudgetBurnRateCritical
  expr: |
    (
      sum(rate(http_requests_total{status=~"5.."}[1h])) /
      sum(rate(http_requests_total[1h]))
    ) > 14.4 * 0.001   # 14.4 倍于 SLO 阈值
  for: 2m
  labels:
    severity: page   # 需要立即叫醒人

告警分级设计
#

我们目前使用四级告警:

级别定义响应时间通知方式
P0核心功能完全不可用,用户大规模受影响立即(5 分钟内)电话 + IM
P1核心功能降级,部分用户受影响15 分钟内IM(@人)
P2非核心功能异常,有应急方案1 小时内IM 群通知
P3低影响问题,需要关注但不紧急工作时间内告警看板

分级不是贴标签,而是真正指导响应行为。P0 必须有人立刻看,P3 允许明天上班再处理。


告警规则设计原则
#

1. 使用 for 时长过滤抖动
#

# 没有 for:瞬时抖动就触发
- alert: HighMemory
  expr: memory_usage > 0.9

# 有 for:持续 5 分钟才触发
- alert: HighMemory
  expr: memory_usage > 0.9
  for: 5m   # 持续 5 分钟才触发告警

for 时长的选择:

  • 需要立即感知的(错误率、服务不可用):for: 2m
  • 资源类(CPU、内存):for: 10m
  • 容量预警(磁盘将满):for: 30m

2. 避免高基数 label
#

# 差:user_id 是高基数 label,会产生大量时间序列
- alert: UserHighLatency
  expr: request_latency_seconds{user_id=~".+"} > 1

# 好:按服务聚合
- alert: ServiceHighLatency
  expr: histogram_quantile(0.99, rate(request_duration_seconds_bucket[5m])) > 1

3. 告警消息要包含操作指引
#

annotations:
  summary: "{{ $labels.namespace }}/{{ $labels.deployment }} 副本数不足"
  description: |
    期望副本数: {{ $value }}
    当前可用副本数: {{ query "kube_deployment_status_replicas_available" | first | value }}
    
    可能原因:
    1. Pod 调度失败(检查节点资源)
    2. 容器镜像拉取失败
    3. 健康检查失败
    
    排查命令:
    kubectl describe deployment {{ $labels.deployment }} -n {{ $labels.namespace }}
    kubectl get events -n {{ $labels.namespace }} --sort-by=.lastTimestamp
  runbook: "https://wiki.internal/runbooks/deployment-unavailable"

4. 避免告警风暴:使用 inhibit 规则
#

当高级别告警触发时,抑制相关的低级别告警:

# alertmanager.yml
inhibit_rules:
  # 节点宕机时,抑制该节点上所有 Pod 的告警
  - source_match:
      severity: critical
      alertname: NodeDown
    target_match:
      severity: warning
    equal: [node]
  
  # 服务完全不可用时,抑制相关的延迟告警
  - source_match:
      alertname: ServiceDown
    target_match:
      alertname: HighLatency
    equal: [service]

告警路由设计
#

不同级别、不同服务的告警应该路由到不同的人和渠道:

# alertmanager.yml
route:
  receiver: default
  group_by: [alertname, cluster, service]
  group_wait: 30s       # 同组告警等待 30s 再发送(合并)
  group_interval: 5m    # 同组告警最短 5 分钟发一次
  repeat_interval: 4h   # 持续告警每 4 小时重复一次

  routes:
    # P0 告警:立即通知,电话告警
    - match:
        severity: critical
      receiver: pagerduty
      group_wait: 0s
      repeat_interval: 1h

    # P1 告警:钉钉 @相关人
    - match:
        severity: high
      receiver: dingtalk-oncall
      group_wait: 1m

    # 数据库相关告警路由给 DBA
    - match:
        component: database
      receiver: dba-team

    # 夜间静默非关键告警(22:00 - 08:00)
    - match_re:
        severity: "warning|info"
      mute_time_intervals:
        - night-hours
      receiver: dingtalk-noncritical

time_intervals:
  - name: night-hours
    time_intervals:
      - times:
          - start_time: "22:00"
            end_time: "08:00"
        weekdays: [monday:friday]
      - weekdays: [saturday, sunday]

告警复盘
#

告警风暴后,我们会做一次告警质量复盘,分析:

1. 误报率:过去 7 天内,有多少告警触发后被手动 resolve,没有实际操作?

# 在 Alertmanager API 中查询已 resolve 的告警
curl 'http://alertmanager:9093/api/v2/alerts?silenced=false&active=false' | \
  jq '[.[] | select(.status.state == "unprocessed")] | length'

2. 无效告警:有多少告警在 for 时间内就自动消失(说明只是抖动)?

3. 响应时延:P0 告警从触发到有人 ack,平均需要多少分钟?

4. 遮盖问题:有多少告警同时触发,互相遮盖?

基于复盘结果,我们会:

  • 提高误报告警的 for 时长
  • 删除 3 个月内从未触发的告警规则(可能是误配置或场景不存在)
  • 优化告警分组规则,减少风暴

一些实际教训
#

教训 1:先建 SLO,再建告警

我们之前的告警大多是"能想到什么就告警什么",没有体系。重新梳理后,先定义核心服务的 SLO,再基于 SLO 设计告警,告警数量从 80+ 条缩减到 23 条,但覆盖的真实问题反而更全了。

教训 2:告警要有 runbook

一条没有 runbook 的告警,工程师收到后第一反应是"这是什么?我该怎么办?"。每条告警都应该附上处理链接,哪怕只是一个简单的内部 wiki 页面。

教训 3:不要把 metrics 展示图当告警

“这个指标看起来重要,告警一下吧”——这是告警噪音的来源。告警的触发条件必须是"用户受影响"或"将要受影响",纯粹的信息展示放在 Grafana dashboard,不发告警。

教训 4:夜间静默要真的设置好

值班工程师的精力是有限的,夜间频繁的非关键告警会导致告警疲劳,反而让真正的 P0 告警被忽略。我们现在夜间只有 P0/P1 告警会触发,P2/P3 等工作时间统一处理。


告警体系不是一次设计好的,是被一场场告警风暴打磨出来的。我们现在的铁律就一条:告警必须能驱动行动。一条告警进群,值班人能立刻知道下一步做什么——否则不如不发。

Wenzhuo Huang
作者
Wenzhuo Huang
搞运维的工程师,写代码的运维人。专注 Kubernetes、AWS、GitOps 与基础设施可靠性。这个博客既是我的技术笔记本,也是我踩过的坑的受害者档案。
SRE 实战手册 - 这篇文章属于一个选集。
§ : 本文

相关文章

SLO/SLI/Error Budget 从理论到落地:SRE 可靠性工程实战

·1096 字·6 分钟
从 SLI 指标选取到 Error Budget 消耗速率告警,系统讲解 SRE 可靠性工程体系的落地实践,包括 Prometheus recording rules 计算 SLI、多窗口 burn rate 告警规则配置、SLO 违规复盘流程,以及与开发团队的协作策略。

Kubernetes 集群升级策略:零停机升级的完整实践指南

·855 字·5 分钟
K8s 集群升级听起来简单,实际操作中坑很多:API 弃用导致的 Helm 失败、Admission Webhook 拦截升级流量、PDB 配置不当导致服务中断。这篇文章从真实的升级经验出发,给出一套可复用的零停机升级方案。