标签: SLO/SLI

  • 深入 Prometheus 告警陷阱排查:静态阈值引发的 On-Call 疲劳雪崩与 Multi-Window 燃烧率 SLO 降噪实战

    抛弃所有基于 CPU、内存利用率的静态阈值告警。基础设施指标飙升不代表业务受损,海量无意义的 Warning 只会带来 On-Call 疲劳,最终导致真实的 P0 故障被淹没。本文直接给出基于 Prometheus (v2.45.0) 的多时间窗口(Multi-Window)Error Budget 燃烧率告警落地代码,将告警噪音降低 80% 以上,实现仅在用户体验真实受损时才触发 Page。

    为什么静态阈值与基础设施告警一定会摧毁 On-Call 体系?

    在近期的某次系统重构排查中,我看到业务线的 Prometheus 里堆满了这类告警规则:

    - alert: NodeCPUUsageHigh
      expr: 100 - (avg by(instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100) > 85
      for: 3m
    

    这种告警逻辑在生产环境简直是一场灾难。Go Runtime GC、大促期间的瞬时突发流量、甚至是某个后台定时批处理任务,都会导致 CPU 短暂飙升。结果就是 On-Call 工程师在深夜被频繁唤醒,看一眼面板发现“已经恢复了”,默默点个 Ack 接着睡。 狼来了的故事上演三次后,墨菲定律就会生效: 当 CPU 飙升是因为死锁或真实的流量雪崩引起时,疲惫的工程师习惯性地认为“等 5 分钟自己就降下去了”,从而错失最佳止血时机。

    监控的本质是度量系统提供服务的质量,而不是度量系统的健康状态。用户根本不在乎你的 CPU 是 90% 还是 10%,他们在乎的是:请求有没有报错(Error Rate)?响应够不够快(Latency)?

    告别直觉:SLI 建模与 Error Budget 燃烧率(Burn Rate)

    真正的防御性告警设计,必须围绕 SLO(服务等级目标)展开。假设我们定了一个核心 API 的 SLO 为 99.9%(30 天为一个周期),这意味着我们在 30 天内允许的错误预算(Error Budget)为 0.1%。

    如果错误率恰好保持在 0.1%,那么预算将在 30 天后耗尽,此时的燃烧率(Burn Rate)就是 1。 如果错误率飙升到 1%,是允许值的 10 倍,燃烧率就是 10。预算将在 30天 / 10 = 3天 内耗尽。

    为了兼顾告警的灵敏度(快速发现大范围故障)与精确度(不被瞬时抖动误导),业界标准做法是引入 Multi-Window(多时间窗口)判断逻辑: 必须同时满足长窗口(评估故障规模)和短窗口(确认故障正在持续)的燃烧率阈值,才触发告警。

    Multi-Window 燃烧率告警在 Prometheus 的落地实战

    不要在告警规则里写几百个字符的复杂 PromQL,这会导致 Prometheus 每次评估告警时产生严重的 CPU 开销和查询超时。必须通过 Recording Rules 提前聚合 SLI。

    1. 定义 SLI 聚合规则 (Recording Rules)

    基于 Prometheus v2.45.0,我们为核心服务配置以下 Recording Rules。这里以 5m, 30m, 1h, 2h, 6h 五个窗口的错误率为例:

    groups:
    - name: sli_http_error_rate
      interval: 1m
      rules:
      # 预聚合 5 分钟窗口错误率
      - record: job:http_requests:error_rate5m
        expr: >
          sum by(job) (rate(http_requests_total{status=~"5.."}[5m]))
          /
          sum by(job) (rate(http_requests_total[5m]))
    
      # 预聚合 1 小时窗口错误率
      - record: job:http_requests:error_rate1h
        expr: >
          sum by(job) (rate(http_requests_total{status=~"5.."}[1h]))
          /
          sum by(job) (rate(http_requests_total[1h]))
    
      # (30m, 2h, 6h 的规则同理,此处省略以保持排版整洁)
    

    2. 构建多窗口燃烧率告警 (Alerting Rules)

    目标:SLO = 99.9%(允许错误率 0.001)。 级别分类:

    • P0 (Page, 电话呼叫): 14.4x 燃烧率(预算将在 2 天内耗尽),长窗口 1h,短窗口 5m。

    • P1 (Ticket, 企微/钉钉/Jira): 6x 燃烧率(预算将在 5 天内耗尽),长窗口 6h,短窗口 30m。

    groups:
    - name: slo_burn_rate_alerts
      rules:
      # P0 级别告警:短时间大量燃烧,必须立即人工介入 (14.4x)
      # 条件:(1h错误率 > 0.001 * 14.4) AND (5m错误率 > 0.001 * 14.4)
      - alert: HighErrorRate_Page
        expr: >
          (
            job:http_requests:error_rate1h > (1 - 0.999) * 14.4
          and
            job:http_requests:error_rate5m > (1 - 0.999) * 14.4
          )
        labels:
          severity: page
        annotations:
          summary: "High Error Budget Burn Rate (14.4x)"
          description: "Service {{ $labels.job }} error budget is burning rapidly. 2% of 30d budget consumed in 1h."
    
      # P1 级别告警:慢性燃烧,不需要半夜起床,但上班后需首要处理 (6x)
      # 条件:(6h错误率 > 0.001 * 6) AND (30m错误率 > 0.001 * 6)
      - alert: HighErrorRate_Ticket
        expr: >
          (
            job:http_requests:error_rate6h > (1 - 0.999) * 6
          and
            job:http_requests:error_rate30m > (1 - 0.999) * 6
          )
        labels:
          severity: ticket
        annotations:
          summary: "Significant Error Budget Burn Rate (6x)"
    

    3. Alertmanager (v0.25.0) 的防御性路由配置

    有了精准的标签打标(severity: page vs severity: ticket),Alertmanager 的路由配置就能彻底隔离噪音,保护 On-Call 人员的睡眠。

    route:
      receiver: 'default_slack'
      group_by: ['job', 'alertname']
      group_wait: 30s
      group_interval: 5m
      repeat_interval: 4h
      routes:
        # 彻底静默没有任何业务影响的基础设施 Warning
        - matchers:
            - severity="warning"
          receiver: 'blackhole'
    
        # 真实的 P0,触发 PagerDuty/电话
        - matchers:
            - severity="page"
          receiver: 'pagerduty_voice'
          group_wait: 10s   # P0 不等聚合,立刻发
          repeat_interval: 1h
    
        # P1 生成 Jira 工单或发工作群,不弹窗、不打电话
        - matchers:
            - severity="ticket"
          receiver: 'jira_and_slack'
    

    经过这套重构,系统告警量通常能直接断崖式下跌。你不再需要去猜测“MySQL Load 飙到 50 到底要不要起夜”,只要 HighErrorRate_Page 没响,业务吞吐和正确率就是稳的,天塌下来也等天亮再排查。

    常见问题

    Q1: 对于异步消费 MQ 的后台任务,没有 HTTP 状态码,怎么定义 SLI 和燃烧率? 不能用队列深度(Queue Depth)作为核心 SLI,因为消费速率和产生速率是动态变化的,队列深不一定影响业务最终时效。 应使用 端到端延迟 (End-to-End Latency) 或 处理成功率 (Pipeline Success Rate)。在入口处记录消息产生的时间戳,在消费者端计算 CurrentTime - ProduceTime。SLI 定义为:“99% 的消息必须在 5 分钟内处理完毕”。用 Histogram 抓取延迟,计算超出 5 分钟的比例作为“Error Rate”来计算燃烧率。

    Q2: 触发了 Page,排查后发现是因为下游第三方 API 挂了导致的,我的 SLO 受损但不是我的锅怎么办? 这体现了容错架构的缺失。你的 SLI 衡量的是你对客户提供的服务质量,下游挂了导致你的用户报错,这就是你的问题。正确的做法不是把告警转给下游,而是进行防御性编程:为下游依赖增加 Circuit Breaker(断路器),并在降级策略中返回默认值或缓存值。只要成功执行了降级返回 200 OK,你的系统就守住了 SLO。

    Q3: 每次发版发布(Deployment)时,Pod 重启总会引发瞬时的 5xx 错误,导致 5m 短窗口的燃烧率报警,怎么处理这种误报? 这是 Multi-Window 的典型副作用。解法有二:

    1. 联动 CI/CD 抑制: 部署流水线在开始前调用 Alertmanager API 注入一个带 matcher: service=XXX 的 Silence,部署结束后删除。

    2. 调优短窗口容忍度: 如果系统实在做不到无损发布(比如没配置 K8s preStop hook 和优雅下线),可以将针对短窗口 5m 的判断改为 job:http_requests:error_rate5m_offset5m,或者直接在 PromQL 中附加部署事件的规避逻辑。但这治标不治本,根本解法还是治理应用层的优雅启停机制。

  • 深入 SRE 告警治理:告别资源阈值风暴,基于多窗口 SLO 燃烧率与 Alertmanager 抑制实战

    生产环境绝大多数告警风暴源于粗放的“资源阈值”触发器。要真正给 On-Call 工程师减负,必须抛弃 CPU/内存使用率等原因导向告警,转向基于用户体验的 SLO(服务级别目标)现象导向告警。本文直接给出基于 Prometheus 的多窗口多燃烧率(Multi-Window Burn Rate)实现方案,结合 Alertmanager 路由抑制,彻底过滤瞬态抖动噪音。

    现场还原:被“阈值告警”淹没的真正故障

    近期排查过一个典型案例:某个核心交易链路出现 504 Gateway Timeout 雪崩。但在故障发生时的前 5 分钟内,On-Call 工程师的 Slack 和邮箱瞬间涌入 400 多条告警。

    其中 95% 的告警长这样:

    [FIRING] K8sNodeCpuHigh
    Severity: warning
    Summary: Node 10.x.x.x CPU usage is > 85%
    Description: CPU usage is at 92% for more than 3m.
    

    工程师的注意力完全被 Kubernetes 节点的 CPU 和 Pod 的重启告警吸引,试图去扩容 Node。但底层根因其实是:DB 连接池因慢查询耗尽,导致上游网关堆积请求,线程阻塞打满 CPU。高 CPU 只是结果,而非原因。 真正有价值的告警——“支付接口 P99 延迟突破 2s”被淹没在无穷无尽的资源告警噪音中。

    这种传统的告警配置策略(如 CPU > 80% 告警),在现代微服务和云原生架构中,除了消耗 SRE 的精力,毫无价值。

    为什么我们必须彻底抛弃静态资源利用率告警?

    传统的监控思路是自底向上的(Bottom-Up):监控机器 -> 监控 OS -> 监控 DB -> 监控应用。但在 K8S 集群中,Pod 随时在漂移,HPA(Horizontal Pod Autoscaler)会根据负载自动扩缩容。一个节点 CPU 跑到 90% 完全是资源利用率高的健康表现,只要服务的 RT(响应时间)和错误率达标,用户根本不关心你的 CPU 是 10% 还是 99%。

    防御性运维的核心思想是面向症状告警(Symptom-based Alerting)。 我们需要围绕 SLI(服务级别指示器)来构建监控体系,通常只关注四个黄金信号:延迟、流量、错误、饱和度。当且仅当错误预算(Error Budget)被快速消耗时,才触发 P1 级别 On-Call 呼叫。

    SLO 燃烧率告警核心架构与 PromQL 落地实战

    基于 Google SRE 实践,我们采用多时间窗口多燃烧率(Multi-Window, Multi-Burn-Rate)模型。

    假设我们的 SLO 是:API 过去 30 天的可用性达到 99.9%。 这意味着 30 天(730 小时)内的错误预算(Error Budget)为 0.1%。

    如果我们在 1 小时内消耗了整个月 2% 的错误预算,燃烧率(Burn Rate)计算如下: (2% / 100%) / (1h / 730h) ≈ 14.6(通常工程上取 14.4)。

    为了防止低频抖动触发告警(Flapping),我们引入双窗口:长窗口(1h)用于触发,短窗口(5m)用于确认当前故障仍在持续。只有当两个窗口的燃烧率同时超标时,才发出告警。

    1. 预计算 Recording Rules (Prometheus 2.45+)

    直接在告警规则中跑高基数(High Cardinality)的原始指标聚合会导致 Prometheus 评估超时。必须先使用 Recording Rules 将 SLI 降维。

    groups:
      - name: slo_sli_recordings
        interval: 1m
        rules:
          # 计算过去 5 分钟的错误率 SLI
          - record: job:request_error_rate5m
            expr: |
              sum by (job) (rate(http_requests_total{status=~"5.."}[5m]))
              /
              sum by (job) (rate(http_requests_total[5m]))
    
          # 计算过去 1 小时的错误率 SLI
          - record: job:request_error_rate1h
            expr: |
              sum by (job) (rate(http_requests_total{status=~"5.."}[1h]))
              /
              sum by (job) (rate(http_requests_total[1h]))
    

    2. 多窗口燃烧率告警规则

    在上述预计算指标的基础上,配置 14.4 燃烧率告警(严重告警,即刻 Page On-Call):

    groups:
      - name: slo_burn_rate_alerts
        rules:
          - alert: API_HighErrorBurnRate_Page
            # 条件:1小时的燃烧率 > 14.4 且 5分钟的燃烧率 > 14.4
            # SLO=99.9%, Budget=0.1% (0.001)
            # 14.4 * 0.001 = 0.0144 (即 1.44% 的绝对错误率阈值)
            expr: |
              (
                job:request_error_rate1h > 0.0144
                and
                job:request_error_rate5m > 0.0144
              )
            labels:
              severity: critical
              pager: "true"
            annotations:
              summary: "API 错误预算极速消耗 (Burn Rate > 14.4)"
              description: "服务 {{ $labels.job }} 在过去1小时内消耗了 2% 的月度错误预算,请立即介入排查。"
    

    通过这种多窗口机制,若只是 1 分钟的网络抖动,5m 窗口会很快回落,告警自动解除,On-Call 工程师根本不会被打扰;而如果是持续的底层熔断,1h 窗口和 5m 窗口同时达标,立刻触发电话告警。

    Alertmanager 高级减噪机制:Inhibit 与 Grouping

    即使有了 SLO 告警,在机房级网络割接或交换机故障时,仍会产生“服务级 SLO 全部崩塌”的并发告警。此时必须利用 Alertmanager (v0.26+) 的 group_by 和 inhibit_rules 机制。

    1. 分组折叠 (Grouping)

    不要让每个容器的报错发一条消息,按服务或集群聚合:

    route:
      receiver: 'slack-oncall'
      group_by: ['job', 'cluster']
      group_wait: 30s      # 等待30秒收集同类告警
      group_interval: 5m   # 每5分钟发送一批新告警
      repeat_interval: 4h  # 未解决告警4小时后才重发
    

    2. 拓扑抑制 (Inhibition)

    底层基础组件宕机时,静默其上层所有应用的告警。例如:所在宿主机 NodeDown,则直接抑制该宿主机上所有 Pod 触发的 SLO 告警。

    inhibit_rules:
      - source_matchers:
          - alertname = "NodeDown"
          - severity = "critical"
        target_matchers:
          - severity =~ "warning|critical|info"
        # 只要 target 告警的 instance/node 标签和 source 匹配,就将其丢弃
        equal: ['node', 'cluster']
    

    通过抑制链设计:DatacenterDown -> 抑制 ClusterDown -> 抑制 NodeDown -> 抑制 AppSLOAlert,在灾难性故障现场,On-Call 工程师只会收到唯一一条最顶层的根因告警。

    常见问题

    Q:既然抛弃了静态资源告警,数据库磁盘满了或者证书过期这类问题怎么监控? A:不要陷入极端。基于症状的 SLO 告警针对的是用户请求链路。对于确定性的、必然导致宕机且有充足时间提前干预的“饱和度/容量指标”(如磁盘使用率 > 85%、TLS 证书 7 天后过期),依然需要配置静态阈值告警,但这部分告警级别通常设为 Warning,走工单或 IM 推送,白天处理即可,绝不能 Page 深夜的 On-Call。

    Q:对于流量极低的服务(比如每分钟只有几个请求),SLO 燃烧率计算会剧烈抖动,如何解决? A:低频服务的指标在计算 rate() 时极易出现“分母为0”或“1个错误=100%错误率”的噪音。解决方案是在 PromQL 中加入绝对流量过滤条件,例如 and sum by (job) (rate(http_requests_total[5m])) > 10,确保样本量具备统计学意义时才评估错误率。

    Q:如何定义异步消息队列(如 Kafka/RocketMQ 消费端)的 SLI? A:异步服务的核心用户体验不是“同步响应时间”,而是“消息堆积延迟”。SLI 可以定义为:过去 5 分钟内,99% 的消息从发送到被消费的端到端延迟(End-to-End Latency)小于 5 秒,或者更直白地以 Consumer Group 的 Lag 积压绝对值作为 SLI 指标,结合消费速率评估剩余处理时间(Time-to-critical)。