8可观测性工程

基于 SLO 的告警

从症状告警到错误预算

SLOSLI错误预算燃烧率告警多窗口

🎯 AI 时代的学习建议:不要只读理论。理解这一章的设计动机(为什么需要这个概念?)和边界条件(什么情况下会失效?)。当 AI 生成的代码涉及这些概念时,你能判断它是否用对了场景。AI 能帮你总结内容,但无法替代你亲手阅读、思考、建立直觉的过程。

第8章 基于 SLO 的告警

从症状告警到错误预算

章节导读

凌晨三点,你的手机响了。你从床上爬起来,打开电脑,盯着屏幕看了十分钟,发现这只是一次持续了两分钟的流量尖刺,系统早已自愈,没有任何用户受到持续影响。你回到床上,却再也睡不着——而这样的夜晚,这个月已经是第四次。这就是「告警疲劳」(alert fatigue)的日常生活,也是传统告警体系的必然产物:阈值靠猜测、告警基于原因而非症状、每条告警都「看起来重要」于是没有一条真正重要。值班工程师对通知逐渐脱敏,真正的故障混在噪音里被延迟响应——告警体系在形式上存在,在功能上已经死亡。

本章要讲述的是告警领域的范式革命:基于 SLO(Service Level Objective,服务等级目标)的告警,特别是 Google 提出的多窗口多燃烧率(multi-window multi-burn-rate)告警方法。这场革命的逻辑起点是三个概念的精确区分。SLI(Service Level Indicator,服务等级指标)是对服务质量的精确测量——「成功请求占比」「快于 500ms 的请求占比」,它是一个 0 到 1 之间的比率,从可观测性数据中计算(第 4 章的指标体系与第 3 章的宽事件是它的数据源)。SLO 是对 SLI 的目标承诺——「99.9% 的请求在 30 天窗口内快于 500ms」,它是工程团队与用户(以及内部利益相关者)之间的契约:达到 SLO 意味着服务「足够可靠」,可以专注开发新功能;低于 SLO 意味着可靠性债务累积,必须停下功能开发修复。错误预算(Error Budget)是 SLO 的补集——100% 减去 SLO 目标:99.9% 的 SLO 意味着 30 天内允许 0.1% 的请求失败或变慢,这 0.1% 就是「预算」,它可以被有计划的冒险(激进发布、混沌实验)或无计划的故障消耗,但总量有限。

这三个概念如何拯救告警?关键洞察是:告警的本质目的不是「通知系统有异常」,而是「通知有用户正在遭受不可接受的痛苦,且需要人立即行动」。传统阈值告警与这个目的之间存在三重脱节:阈值与用户痛苦脱节(CPU 90% 未必有用户受苦,而 0.5% 的请求失败对大客户可能就是灾难);阈值与业务节奏脱节(促销期流量翻倍,所有基于绝对量的阈值集体误报);告警密度与行动能力脱节(每天 50 条告警,值班者只能麻木)。SLO 告警把三者重新对齐:告警条件直接定义为「错误预算的消耗速度不可持续」——预算是用户视角的(SLO 本身就定义在用户体验指标上)、是相对化的(燃烧率自动适应流量规模)、是行动导向的(预算快速烧完意味着照此速度将在 SLO 周期结束前违约,必须干预)。

多窗口多燃烧率是这个思想的技术成熟形态。朴素的 SLO 告警(「30 天预算用完 90% 时告警」)有一个致命缺陷:30 天窗口的惯性太大——即使系统此刻正在以 100 倍速燃烧预算,30 天窗口的比率也要很久才越过阈值,告警严重滞后。反过来,只用短窗口(「过去 5 分钟错误率 > 1%」)又太敏感——瞬时抖动会触发大量无需行动的告警。Google 在 SRE Workbook 中给出的解法是同时使用多个「燃烧率 × 窗口」组合:1 小时窗口内以 14.4 倍速燃烧(意味着 30 天预算 2 天烧完)触发 page 级告警——这是「不立即行动就会严重违约」的数学表达;6 小时窗口内以 6 倍速燃烧触发 page——稍慢但持续的燃烧;3 天窗口内以 1 倍速燃烧触发 ticket 级告警——按当前速度将恰好违约,需要排期处理。每个燃烧率都配一个短窗口(5-30 分钟)做「二次确认」以过滤瞬时尖刺。这套机制的优雅之处在于:告警的紧急度与「距离违约的时间」精确挂钩,page 告警的每一次触发都对应「数学上必然的用户痛苦累积」——告警疲劳的根源被连根拔起。

从 SLO 告警到可观测性还有一条完整的闭环:告警负责「叫醒正确的人」(检测),被叫醒的人用可观测性平台做探索式诊断(第 7 章的方法论)——告警消息中携带的「起点查询链接」是两者的衔接件。诊断结束后,复盘的 action item 常常回流到 SLO 本身:这次故障暴露的盲区是否需要新的 SLI?SLO 目标是否定得不合理(太松导致用户受苦不告警,太紧导致团队疲于奔命)?错误预算的消耗模式是否揭示了系统性风险?这个「测量 → 目标 → 告警 → 诊断 → 修正目标」的飞轮,是 SRE 体系与可观测性工程的汇合点。

本章还将讨论告警的组织面:分级(page/ticket/仅记录)与路由(哪个团队为哪类症状负责)、告警的可操作性审查(每条 page 告警都应该有明确的「被叫醒后要做什么」)、以及 SLO 的制定过程本身(从用户旅程出发选择 SLI、与利益相关者协商目标值、用历史数据回测 SLO 的合理性)。代码示例将覆盖 SLI 的 PromQL 定义、多窗口燃烧率告警规则的完整实现、Pyrra/SLOth 等 SLO 即代码工具、Alertmanager 路由配置、以及错误预算仪表盘——从数学到 YAML 的完整落地路径。告警是可观测性体系中「主动出击」的部分:数据建设的价值最终要在「正确的时刻叫醒正确的人」这件事上兑现,而 SLO 是把这个兑现从艺术变成工程的方法。

核心概念

  • SLO
  • SLI
  • 错误预算
  • 燃烧率告警
  • 多窗口

要点精析

SLI 的精确设计:从用户旅程到可计算的比率。SLI 是 SLO 体系的地基,地基歪了上面全歪——而 SLI 设计的常见错误恰恰发生在起点:从「系统有什么指标」出发(CPU、内存、QPS)而非从「用户经历什么」出发。正确的推导路径是用户旅程映射:先列出关键用户旅程(电商:浏览商品、搜索、加购、结账、查订单),每条旅程定义「成功」的用户视角标准(结账成功 = 请求返回成功且订单确认页在 2 秒内呈现),然后把这个标准翻译成可计算的比率。SLI 的两大经典形态:可用性 SLI(成功请求数 / 总请求数——「成功」的定义要精心打磨:HTTP 5xx 算失败没有争议,4xx 呢?429(限流)是系统主动拒绝,算失败;404(用户请求了不存在的资源)是用户行为,通常不算系统失败;超时算失败,但「多慢算超时」需要与产品对齐)与延迟 SLI(快于阈值的请求数 / 总请求数——阈值来自用户体验研究或竞品基准,注意这个形态比「P99 < 500ms」更优越:它是比率,天然支持错误预算数学,且对长尾的度量更稳健)。SLI 的数据源选择:优先用服务端测量(负载均衡器/网关的请求日志或宽事件——覆盖全部真实流量、口径统一),客户端测量(RUM 数据)作为补充(能发现服务端看不到的问题如 DNS/CDN/最后一公里,但受采样与客户端环境影响);避免用合成探测(synthetic)作为主 SLI——它测量的是「探测器到系统的旅程」而非真实用户旅程,适合做补充信号与无流量时段的保底。多 SLI 的组合:一个服务通常 2-3 个 SLI(可用性 + 延迟 + 可选的正确性/新鲜度),不要贪多——每个 SLI 都是一个需要维护的契约,SLI 数量爆炸意味着没有一个被认真对待。

SLO 目标值的设定艺术:不是越高越好,而是「恰好够用的可靠」。SLO 数值设定是工程、产品、财务的三方协商,最常见的错误是「直觉性追求高目标」——「我们的服务当然应该 99.99% 可靠!」。为什么这是错的?三个理由。第一,可靠性有边际成本:从 99.9% 到 99.99%,错误预算从每月 43 分钟缩到 4.3 分钟——意味着任何变更窗口、任何实验、任何单点故障都立即违约,工程上需要多活架构、更快的故障转移、更保守的发布策略,成本可能翻数倍;而用户未必感知到差异(家庭 WiFi 的可用性可能都不到 99.9%——你的服务比用户的网络更可靠时,额外的可靠性对用户不可见)。第二,SLO 应该锚定用户期望与竞品水平:用户「感知不到劣化」的阈值是设定依据——如果历史上服务达到 99.95% 时投诉率没有变化,那么 99.9% 的 SLO 加上「违约即行动」的纪律,可能比「99.99% 的 SLO 加上长期违约的麻木」更健康。第三,SLO 是承诺不是成就:定得太高导致团队长期处于违约状态,错误预算永远为负,「预算耗尽冻结发布」的机制失去意义(反正一直冻着),最终 SLO 被无视——比没有 SLO 更糟,因为它制造了「我们有可靠性管理」的幻觉。设定流程的最佳实践:先用历史数据回测(过去 90 天,候选 SLO 值会被违反几次、每次持续多久、违约时的用户投诉量如何——数据会告诉你「用户实际容忍线」在哪);从略松的目标起步(99.5% 或 99.9%),运行一个季度后根据「预算消耗模式」调整(预算总是剩很多 → 可以收紧或把余量用于更激进的发布;预算总是烧穿 → 要么放松要么投入可靠性工程);对外承诺(SLA,合同层面)永远低于内部 SLO——SLO 是工程目标,SLA 是法律承诺,两者之间要留出缓冲带。记住 SLO 的真正功能不是「达到某个数字」而是「让可靠性与迭代速度的权衡变得显式、可讨论、可数据化」——它是工程决策的框架而非考核的鞭子。

错误预算:把可靠性与速度的权衡变成可执行的机制。错误预算 = 1 - SLO:99.9% 的 30 天 SLO 意味着 43.2 分钟的「允许不可靠时间」或等价的失败请求配额。这个概念的革命性在于它把「质量 vs 速度」的永恒争吵转化为一个双方都接受的仲裁机制。预算充足时(比如还剩 80%):团队被明确授权激进迭代——频繁发布、feature flag 大胆放量、混沌实验排期执行,因为「消耗预算换取学习速度」是划算的;此时如果有人以「稳定性」为由阻止发布,需要论证的是「这次发布的预期预算消耗超过其价值」而非抽象的风险厌恶。预算告急时(剩余 < 25%):发布节奏自动放缓,非关键变更延期,可靠性工作(修复 flaky 组件、补冗余、加测试)优先级提升。预算耗尽时(≤ 0):触发预设的「预算耗尽流程」——冻结功能发布(只允许可靠性修复与安全补丁),直到下个周期或预算恢复;这个流程必须在预算耗尽之前就书面约定好(谁有权豁免、豁免的审批链),否则耗尽时刻的现场谈判必然被业务压力击穿。预算的消耗分析同样有价值:每月回顾「预算花在哪了」——计划内消耗(实验、演练、激进发布)vs 计划外消耗(故障)的比例是团队工程文化的体温计:健康的团队计划内消耗占相当比例(说明预算在被主动投资于学习),病态的团队预算全部被重复性故障消耗(说明可靠性债务在累积)。Google 的经典案例:某团队预算连续三个月被同一类故障烧穿,错误预算机制强制该团队停下功能开发做了一个季度的可靠性加固——没有 SLO 体系时,这种「停下来修地基」的决策在业务压力下几乎不可能做出。错误预算的本质是给「长期主义」一个可执行的制度载体。

燃烧率(Burn Rate):错误预算消耗速度的数学与告警语义。燃烧率定义为「当前错误消耗速度 / SLO 允许的消耗速度」:burn_rate = 实际错误率 / (1 - SLO)。燃烧率 = 1 意味着按当前速度,预算恰好在 SLO 周期(如 30 天)结束时用完——可持续的临界线;燃烧率 = 14.4 意味着 30 天的预算将在 30/14.4 ≈ 2.08 天烧完——两天内必然违约,必须立即行动;燃烧率 = 0.5 意味着消耗速度只有允许值的一半——预算会有富余。这个「燃烧率 → 违约倒计时」的换算是多窗口告警的数学核心:每个燃烧率阈值都对应一个明确的「不干预则违约」的时间承诺,告警的紧急度不再是主观判断而是算术结果。燃烧率的计算细节:分子是窗口内的实际错误率(1 - SLI 窗口值),分母是预算率(1 - SLO 目标);窗口选择决定燃烧率的灵敏度与噪音——5 分钟窗口的燃烧率剧烈波动(一次部署的瞬时错误就能打出 50 倍燃烧率),1 小时窗口平滑但滞后。为什么燃烧率优于直接告警错误率?两个原因:它自动归一化了 SLO 差异(99.9% 服务的「错误率 0.5%」是 5 倍燃烧,99% 服务的同样错误率只有 0.5 倍燃烧——同一个告警阈值语义在不同 SLO 的服务间自动适配);它把「多严重」表达为「多快违约」——值班工程师看到「14.4x burn rate」立即理解「这是两天内违约级别的紧急事态」,而「错误率 1.4%」需要心算与 SLO 的距离。燃烧率也是错误预算报表的自然语言:「本月平均燃烧率 0.7」一句话概括可靠性状态,比「本月可用性 99.93%」更直接地表达「预算在积累还是消耗」。

多窗口多燃烧率告警:Google 方法的完整机制与设计原理。单窗口告警的两难:短窗口灵敏但吵(瞬时尖刺触发无需行动的 page),长窗口安静但钝(真实危机要等窗口比率爬升很久才越过阈值)。Google SRE Workbook 的多窗口多燃烧率方案用「组合」破解两难:每个告警级别由「长窗口 + 短窗口」的双重条件定义,长窗口确认「燃烧是持续的」,短窗口确认「燃烧此刻仍在进行」(防止长窗口内的历史错误在问题已恢复后继续维持告警)。标准配置(30 天 SLO 周期):page 级第一档——1 小时窗口燃烧率 > 14.4 且 5 分钟窗口燃烧率 > 14.4(预算 2 天烧完,立即行动);page 级第二档——6 小时窗口 > 6 且 30 分钟窗口 > 6(预算 5 天烧完,当班处理);ticket 级——3 天窗口 > 1 且 6 小时窗口 > 1(按当前速度将恰好违约,排期处理)。三档告警覆盖了从「两天内违约」到「月底前违约」的完整紧急度谱系,每一档的触发都对应明确的行动预期与响应时限。这套机制的统计性质值得理解:14.4 = 30天/2.08天 的换算保证「1 小时窗口内观测到 14.4 倍燃烧」在统计上意味着「若持续则 2 天违约」——1 小时窗口足够长以过滤分钟级噪音,足够短以在「2 天违约」的事态中留出 47 小时的响应余量;5 分钟二次确认窗口过滤「1 小时内的一次 10 分钟尖刺后已恢复」的场景(长窗口仍显示高燃烧但短窗口已回落 → 不告警,因为危机已过)。实施注意:所有窗口的燃烧率应该用 recording rules 预计算(第 4 章示例 3 的 L2 层)——告警规则直接引用预计算序列,评估快且口径统一;多档告警的路由要区分(14.4x 档 page 到值班电话,1x 档建 ticket 到团队 backlog);SLO 变更时所有燃烧率阈值联动更新(用 SLO 即代码工具生成规则,避免手改遗漏)。这套机制在 Google 内部经过大规模验证,其告警量比阈值告警低一个数量级而漏报率更低——「每条 page 都值得被叫醒」从口号变成数学保证。

症状告警 vs 原因告警的完整分工:SLO 告警体系里还有没有阈值告警的位置?第 2 章确立了「基于症状告警」的原则,SLO 燃烧率告警是症状告警的成熟形态——但一个完整的告警体系并非只有 SLO 告警,三类告警各有明确分工。第一类,SLO 燃烧率告警(page 主力):覆盖所有「用户正在受苦且需要立即行动」的场景,是叫醒人的默认机制。第二类,前瞻性原因告警(ticket 为主,少量 page):针对「尚未影响用户但即将影响」的资源饱和——磁盘 7 天后写满(按增长速率预测)、证书 14 天后过期、连接池利用率持续 > 85%、Kafka lag 持续增长。这类告警的价值在于「在症状出现前行动」,但必须严格控制为「可预测、有明确行动、低误报」的场景——「CPU > 90%」不是好的前瞻告警(CPU 高本身不预测用户痛苦),「磁盘按当前增速 7 天内满」才是(有明确的时间承诺与行动:扩容或清理)。第三类,调试与审计告警(不通知人,仅记录):部署事件、配置变更、异常日志模式——它们进事件流供诊断时查询(第 7 章的「查变更」板斧依赖它们),但不产生通知。分工的边界案例:「数据库主从延迟 30 秒」属于哪类?如果从库服务于用户读请求且 SLO 覆盖读路径——它最终会反映为 SLO 燃烧,不需要独立告警(SLO 是终极仲裁);如果从库只服务于内部报表——它是 ticket 级的前瞻告警(影响内部用户)。判断框架:能映射到某个用户旅程 SLI 的问题交给 SLO 告警;不能映射但会「确定性地」演化为资源耗尽的问题用前瞻告警;其余一律降级为记录。这个框架的推论是:随着 SLO 覆盖度提高(更多用户旅程被 SLI 度量),传统阈值告警应该持续缩减——如果你的告警清单里阈值告警仍占大头,说明 SLO 覆盖不足或 SLI 设计没有对准真实用户旅程。

告警的可操作性审查:每条 page 告警的「被叫醒测试」。可操作性(actionability)是告警质量的核心标准:一条 page 告警应该满足——被叫醒的工程师能够且需要立即采取行动,行动能缓解用户痛苦。Google 的「被叫醒测试」:如果一条告警在凌晨三点把你叫醒,你打开电脑后发现「什么都不需要做」(系统已自愈)或「什么都做不了」(没有可执行的缓解手段),这条告警就不该是 page 级。审查的具体维度:行动明确性——告警的 runbook 是否给出了「第一步做什么」(而不是「调查一下」);缓解可用性——是否存在能立即降低用户痛苦的动作(回滚、切流、扩容、降级开关),如果唯一「行动」是等待自愈,降级为 ticket;紧急性——延迟 6 小时处理与立即处理的用户痛苦差异是否显著,不显著则不该 page;频率——过去 90 天该告警的触发次数与「触发后实际行动」次数的比值,比值 < 50% 说明告警在训练值班者忽略它。审查的执行机制:季度性告警审计(第 2 章的审计脚本提供数据)+ 事故复盘的告警回顾(这次事故中,告警是帮忙还是添乱?有没有该响没响的?)+ 新告警的准入门槛(新增 page 告警需要回答四个测试问题并通过平台团队评审)。告警删除的文化障碍要正视:工程师倾向于「留着这条告警以防万一」——防御性保留制造噪音,噪音摧毁整个体系的可信度。对抗手段是把「告警精简」变成有荣誉感的工程活动:公开表彰「删除了 N 条噪音告警且零漏报」的团队、把「page 告警的行动率」纳入团队健康指标。一个成熟的告警体系的量化画像:每服务 page 告警规则 < 10 条、月均 page 次数个位数、行动率 > 80%、从告警到开始诊断 < 5 分钟(靠 annotations 里的起点链接)——达不到任何一项,都说明体系需要本章方法论的手术。

SLO 即代码:用 Pyrra/Sloth 生成告警规则与仪表盘。手写多窗口燃烧率的 PromQL 规则(每个 SLO 3 档告警 × 每档 2 个窗口条件 + 预算仪表盘查询)不仅繁琐而且易错——燃烧率公式的分子分母、窗口组合、阈值换算(14.4 从哪来)任何一处手误都会产生「静默失效的告警」(规则存在但永不触发或永远误触发)。SLO 即代码工具把「SLO 定义」与「规则生成」分离:你声明意图(SLI 查询、目标值、窗口周期),工具生成全部 PromQL 规则与仪表盘。Pyrra(pyrra.dev,开源 + SaaS)的 CRD 形态:一个 Slo 自定义资源声明 sli 的 events/error 查询、objective: 99.9、window: 30d,Pyrra operator 自动生成 recording rules(燃烧率序列)、告警规则(多窗口多燃烧率全套)、Grafana 仪表盘(预算消耗曲线、燃烧率热图);SLO 变更 = 修改 CR,规则自动联动更新。Sloth(Go 实现,CLI 形态)思路相同:YAML 定义 SLI 事件查询与目标,sloth generate 输出 Prometheus 规则文件进 GitOps 流程。两个工具都实现了 Google 多窗口方法的标准化参数(燃烧率档位与窗口组合按 SRE Workbook 默认值,可定制)。采用 SLO 即代码的三个收益:正确性(燃烧率数学由工具保证,且工具本身被社区大规模验证)、一致性(全组织所有 SLO 的告警语义统一——「14.4x/1h」在每个团队含义相同,值班跨团队支援时无需重新理解)、可维护性(几十个 SLO 的规则文件由生成器管理,人工只维护声明层)。落地路径:平台团队部署 Pyrra/Sloth 并提供 SLI 查询模板库(网关可用性、服务 RED、批处理新鲜度等常见形态的声明模板),业务团队按模板填空声明自己的 SLO;生成的规则与仪表盘统一进 GitOps 仓库,变更走 PR 评审。注意事项:工具的 SLI 查询性能要验证(events/error 两个查询在每个评估周期都执行,对高流量服务确认查询成本);自定义燃烧率档位时理解换算数学(阈值 = 窗口周期 / 目标响应时间,改档位前算清楚「这档告警意味着几天内违约」)。

Alertmanager 路由与告警治理:让正确的告警到达正确的人。告警规则产生告警,Alertmanager 决定告警的命运:分组(grouping)、抑制(inhibition)、静默(silence)、路由(routing)。分组把相关告警合并为一条通知——一次数据库故障可能同时触发 20 个依赖服务的 SLO 告警,group_by: [alertname, slo] 加上合理的 group_wait/group_interval 让值班者收到一条聚合通知而非 20 条轰炸;分组键的设计要反映「同一根因的告警共享什么标签」。抑制处理告警间的因果层级——「基础设施层告警触发时抑制其上所有服务层告警」(inhibit_rules 用 source_matchers/target_matchers 表达层级关系):机房网络故障时,你只需要知道网络故障,不需要同时收到 200 条服务 SLO 告警——抑制规则把「告警风暴」压缩为「根因告警 + 静默的衍生告警」。静默处理计划内维护——部署窗口、演练期间预先创建 silence(带过期时间与创建人),避免已知操作触发告警噪音;静默的纪律是「必须有过期时间」(永久静默是告警体系腐化的标志——被静默的告警等于被删除,但比删除更危险因为它制造「有告警覆盖」的幻觉)。路由树把告警按标签分发到接收者——severity=page 走值班电话链(PagerDuty/OpsGenie 集成,含升级策略:15 分钟无确认自动升级到二线)、severity=ticket 走工单系统、团队标签决定路由到哪个值班组;路由设计的组织含义是「告警所有权」:每条告警规则必须有明确的 owner 团队标签,无主告警(路由到默认接收者「ops-all」)是责任稀释的温床——第 11 章的组织设计在告警层的投影。治理的例行机制:月度告警报表(每团队的告警量、行动率、静默清单审查)、季度路由审计(接收者是否还在职、升级链是否有效)、静默到期自动提醒(防止「临时静默」永久化)。

从告警到诊断再到 SLO 修正:完整的反馈飞轮。SLO 告警体系不是静态配置而是学习系统,其完整循环有五个环节。环节一,告警触发:多窗口燃烧率告警 page 到值班者,通知中携带三件套——SLO 上下文(哪个 SLI、当前燃烧率、预算剩余、预计违约时间)、起点查询链接(预置时间窗与过滤条件的可观测性平台查询)、runbook 链接(该 SLO 的标准诊断路径)。环节二,诊断与缓解:值班者用第 7 章的方法论定位根因、执行缓解——诊断过程中的每个查询与发现记录进事故日志(假设登记表)。环节三,复盘的三问:这次事故中告警体系表现如何(触发是否及时?有没有更早的燃烧率档位本可以预警?通知里的起点链接是否有效)?诊断暴露了哪些数据缺口(哪些步骤因埋点不足而猜测——补埋点 action)?SLO 本身是否需要修正(这次违约是「SLO 定得太紧」还是「系统真的不够可靠」?受影响的旅程是否根本不在 SLI 覆盖内——补 SLI action)?环节四,修正落地:告警参数调整(窗口/档位)、埋点补充、SLI 扩展、runbook 更新——全部进 backlog 且有 owner。环节五,周期性 SLO 评审:季度回顾每个 SLO 的预算消耗历史——长期富余的 SLO 讨论收紧或释放预算给迭代速度、长期烧穿的 SLO 分析是目标不合理还是可靠性投入不足、新上线服务的 SLO 从「观察期默认值」转正。这个飞轮的终极产出不是「更少的故障」而是「更精确的可靠性管理」:团队对「我们的系统多可靠、用户需要什么可靠、我们愿意为可靠性付出什么」三个问题的回答越来越数据化。SLO 体系成熟度的标志:可靠性讨论中不再出现「我觉得系统挺稳定的」这类无数据陈述——每个论断都有燃烧率、预算、SLI 分布的支撑;以及「冻结发布」不再需要管理层拍板——错误预算耗尽自动触发预设流程,制度替代了政治。

SLO 的组织落地路径:从第一个 SLO 到全面覆盖的渐进策略。SLO 体系的失败大多不是技术性的(PromQL 谁都会写)而是组织性的(推不动、没人信、沦为形式)。渐进落地路径分四阶段。阶段一(1-2 个月):选一个「痛点最明确的服务」做试点——最近有重大故障、或投诉最多、或团队自己最想要 SLO 的服务;只定义 1-2 个 SLI(可用性 + 延迟),用历史数据回测设定目标(不要凭空定 99.9%——回测会告诉你过去 90 天这个目标会被违反几次,从「略优于历史表现」的目标起步);配置多窗口告警但先只发到一个观察频道(不 page 人),运行两周校准噪音水平。阶段二(2-3 个月):试点 SLO 转正(告警接值班)、发布错误预算报表(月度回顾预算消耗)、执行第一次「预算驱动决策」(用预算数据支持一次发布节奏或可靠性投入的讨论——让组织看到 SLO 的决策价值而不只是告警价值)。阶段三(一个季度+):横向推广——平台团队提供 SLO 即代码工具与 SLI 模板(Pyrra CRD 模板、网关层自动 SLI),各团队为自己的关键服务定义 SLO;推广的抓手是「用户旅程覆盖」而非「服务覆盖率」——先保证每条关键旅程(登录、结账、搜索)端到端有 SLO,再逐步覆盖次级服务。阶段四(持续):SLO 进入组织决策流程——季度业务评审包含错误预算回顾、发布冻结机制被尊重(至少一次「预算耗尽真的冻结了发布」的公开案例——制度的可信度来自它第一次被执行时的表现)、对外 SLA 谈判以内部 SLO 数据为输入。全程的三个组织纪律:SLO 是团队自己的承诺而非上级考核指标(考核化的 SLO 会被博弈——挑容易的 SLI、定宽松的目标,体系名存实亡);工程与产品共同拥有 SLO(目标值协商需要双方在场,纯工程定的 SLO 常与用户期望脱节,纯产品定的常无视工程成本);从简开始拒绝完美(第一个 SLO 粗糙没关系——「成功请求比率,99.5%,30 天窗口」就够启动飞轮,精细化的 SLI 定义随复盘迭代)。

代码示例

/* 示例 1:SLI 的 PromQL 定义——可用性、延迟与正确性三种形态 */

/* --- SLI 1:可用性(成功请求比率)---
   事件定义:分母 = 全部有效请求;分子 = 成功请求
   「成功」的口径:非 5xx 且非 429(限流是系统主动拒绝,算失败)
   404/400 等用户侧错误不算系统失败 */
/* 错误事件(分子的反面) */
sum(rate(checkout_http_requests_total{status_class="5xx"}[5m]))
  +
sum(rate(checkout_http_requests_total{status="429"}[5m]))
/* 总事件 */
sum(rate(checkout_http_requests_total[5m]))
/* SLI = 1 - 错误/总数 */

/* --- SLI 2:延迟(快于 500ms 的请求比率)---
   依赖 Histogram 的 le="0.5" 桶——桶设计必须对齐 SLO 阈值(第 4 章) */
sum(rate(checkout_http_request_duration_seconds_bucket{le="0.5"}[5m]))
  /
sum(rate(checkout_http_request_duration_seconds_count[5m]))

/* --- SLI 3:数据新鲜度(批处理/管道类服务)---
   「成功」= 数据延迟在承诺内;适合 ETL、索引、同步类服务 */
/* 事件 = 每次同步周期检查;错误 = lag 超过 15 分钟的检查 */
sum(rate(pipeline_sync_checks_total[5m]))
  -
sum(rate(pipeline_sync_checks_total{lag_seconds > 900}[5m]))

/* --- 固化为 recording rules(SLO 计算的统一口径) --- */
groups:
  - name: sli_definitions
    rules:
      - record: sli:checkout_availability:ratio_rate5m
        expr: |
          1 - (
            (sum(rate(checkout_http_requests_total{status_class="5xx"}[5m]))
             + sum(rate(checkout_http_requests_total{status="429"}[5m])))
            /
            sum(rate(checkout_http_requests_total[5m]))
          )
      - record: sli:checkout_latency_fast:ratio_rate5m
        expr: |
          sum(rate(checkout_http_request_duration_seconds_bucket{le="0.5"}[5m]))
            /
          sum(rate(checkout_http_request_duration_seconds_count[5m]))
这段代码展示了三种 SLI 形态的定义与两个关键工程决策。第一个决策是「成功/失败」口径的精确化:可用性 SLI 的分母是「全部有效请求」——5xx 与 429 算系统失败(5xx 是系统错误没有争议;429 是系统主动拒绝服务,从用户视角「我请求了但系统不服务我」,算失败;把 429 排除在失败外是常见的口径错误,会让限流风暴期间的 SLO 虚假健康),而 4xx 中的 404/400 不算(用户请求了不存在/非法的资源,系统正确地告知了错误——这是系统正常工作)。这个口径必须在 SLI 定义处注释清楚并进入团队 wiki——口径的模糊是 SLO 争议的最大来源(「为什么昨天可用性 99.2% 但没人投诉?」——因为口径把某类用户错误算成了失败)。第二个决策是延迟 SLI 的形态选择:「快于 500ms 的请求比率」而非「P99 < 500ms」——两者数学上相关但性质不同:比率形态天然可加(30 天窗口的比率 = 窗口内所有快请求/所有请求),直接支持错误预算计算;P99 形态是分位数,跨窗口聚合语义复杂(30 天的 P99 不等于每日 P99 的任何简单组合)。比率形态的实现依赖 Histogram 的 le="0.5" 桶——回看第 4 章的桶设计纪律:SLO 阈值处必须有桶边界,否则这个 SLI 无法计算或精度崩坏。第三种形态(新鲜度 SLI)覆盖非请求类服务:ETL/同步管道的用户旅程是「数据是新的」,SLI 定义为「lag 在承诺内的检查占比」——每个服务类型都有自己的「成功」定义,SLI 设计的通用框架是「用户旅程的成功标准 → 可计算比率」,而不是「所有服务都套可用性模板」。最后的 recording rules 固化是口径统一的机制保证:所有燃烧率计算、仪表盘、告警都引用 sli:* 序列而非各自重写查询——SLI 口径修改时只改一处,全体系联动。
/* 示例 2:多窗口多燃烧率告警规则——Google 方法的完整 PromQL 实现 */
/* 前提 recording rules(燃烧率预计算,30 天 SLO,预算率 = 0.001) */
groups:
  - name: slo_burn_rates
    rules:
      /* 各窗口燃烧率 = 窗口错误率 / 预算率(0.001) */
      - record: slo:checkout_error_budget:burn_rate5m
        expr: (1 - sli:checkout_availability:ratio_rate5m) / 0.001
      - record: slo:checkout_error_budget:burn_rate30m
        expr: (1 - avg_over_time(sli:checkout_availability:ratio_rate5m[30m])) / 0.001
      - record: slo:checkout_error_budget:burn_rate1h
        expr: (1 - avg_over_time(sli:checkout_availability:ratio_rate5m[1h])) / 0.001
      - record: slo:checkout_error_budget:burn_rate6h
        expr: (1 - avg_over_time(sli:checkout_availability:ratio_rate5m[6h])) / 0.001
      - record: slo:checkout_error_budget:burn_rate3d
        expr: (1 - avg_over_time(sli:checkout_availability:ratio_rate5m[3d])) / 0.001

  - name: slo_multiburn_alerts
    rules:
      /* 档位 1:14.4x 燃烧(30 天预算 2 天烧完)——立即 page */
      - alert: CheckoutSLOBudgetBurnCritical
        expr: |
          slo:checkout_error_budget:burn_rate1h > 14.4
          and
          slo:checkout_error_budget:burn_rate5m > 14.4
        for: 2m
        labels:
          severity: page
          slo: checkout-availability
          team: payments
        annotations:
          summary: "checkout 可用性 SLO 以 14.4 倍速燃烧:2 天内违约"
          description: "1h 燃烧率 {{ $value }},预算剩余将在 ~2 天内耗尽"
          runbook: "https://wiki.internal/runbooks/checkout-slo"
          start_query: "https://grafana.internal/explore?query=checkout-errors-by-version-region"

      /* 档位 2:6x 燃烧(5 天烧完)——当班 page */
      - alert: CheckoutSLOBudgetBurnHigh
        expr: |
          slo:checkout_error_budget:burn_rate6h > 6
          and
          slo:checkout_error_budget:burn_rate30m > 6
        for: 5m
        labels:
          severity: page
          slo: checkout-availability
        annotations:
          summary: "checkout SLO 以 6 倍速燃烧:5 天内违约"

      /* 档位 3:1x 燃烧(恰好月底违约)——ticket 排期 */
      - alert: CheckoutSLOBudgetBurnSustained
        expr: |
          slo:checkout_error_budget:burn_rate3d > 1
          and
          slo:checkout_error_budget:burn_rate6h > 1
        for: 30m
        labels:
          severity: ticket
          slo: checkout-availability
        annotations:
          summary: "checkout SLO 持续 1 倍速燃烧:本周期将违约,需排期处理"
这份规则文件是 Google 多窗口多燃烧率方法的逐行实现,三个设计层次值得拆解。第一层,燃烧率预计算:五档窗口(5m/30m/1h/6h/3d)的燃烧率全部固化为 recording rules——告警规则引用预计算序列而非现场计算,收益是评估性能(每个评估周期只做阈值比较而非重算比率)与口径统一(燃烧率公式只写一次,分母的 0.001 即 1-99.9% 的预算率集中可见——SLO 目标调整时只改这一处)。注意 avg_over_time 包裹 5m 基础比率的写法:长窗口燃烧率 = 窗口内基础比率的平均,这是「窗口错误率」的 Prometheus 惯用实现(等价于直接对窗口做 ratio 计算但复用已有序列更高效)。第二层,双窗口条件的 and 逻辑:每档告警都是「长窗口 > 阈值 AND 短窗口 > 阈值」——14.4x 档用 1h+5m:1h 窗口确认燃烧是持续性的(过滤单次尖刺:10 分钟的错误风暴在 1h 平均后燃烧率被稀释,若未达 14.4 不触发),5m 窗口确认燃烧仍在进行(防止历史错误的惯性:40 分钟前的严重故障已恢复,1h 窗口仍显示高燃烧,但 5m 窗口已回落 → and 条件不满足 → 不告警,因为无需行动)。for: 2m 是第三重去抖:条件持续 2 分钟才触发,过滤评估周期边界的毛刺。第三层,告警语义的分级路由:14.4x(2 天违约)与 6x(5 天违约)是 page——响应时限不同(前者立即、后者当班)但都需要人;1x(月底违约)是 ticket——排期处理,不叫醒人;三档的阈值换算全部来自「窗口周期/违约倒计时」的数学(14.4 ≈ 30d/2.08d,6 = 30d/5d,1 = 30d/30d),annotations 的 summary 直接写出换算结果(「2 天内违约」)——值班者不需要理解燃烧率数学也能感知紧急度。annotations 中的 start_query 是第 7 章「告警到诊断的衔接件」:预置了「按 version/region 切片的错误查询」链接,被叫醒后的第一个动作从「打开电脑想从哪查起」变成「点击链接看数据」。
/* 示例 3:Pyrra SLO 即代码——声明式定义自动生成全套规则 */
/* pyrra-slo-checkout.yaml:一个 CRD 声明替代数百行手写 PromQL */
apiVersion: pyrra.dev/v1alpha1
kind: ServiceLevelObjective
metadata:
  name: checkout-availability
  namespace: observability
  labels:
    team: payments
    service: checkout
spec:
  target: "99.9"              /* SLO 目标:99.9% */
  window: 30d                 /* SLO 周期:30 天滚动窗口 */
  indicator:
    ratio:
      /* 错误事件(分子):5xx 与 429 */
      errors:
        selector: checkout_http_requests_total{status_class="5xx"}
                or checkout_http_requests_total{status="429"}
      /* 总事件(分母) */
      total:
        selector: checkout_http_requests_total
      grouping:
        - route               /* 按路由细分 SLO:每条路由独立的预算与告警 */
  /* Pyrra 自动生成:
     1. SLI recording rules(5m 基础比率 + 各窗口燃烧率)
     2. 多窗口多燃烧率告警(14.4x/1h+5m、6x/6h+30m、3x/3d+2h、1x/3d+6h 四档,
        按 Google SRE Workbook 标准参数)
     3. Grafana 仪表盘(预算消耗曲线、燃烧率热图、按 route 的 SLO 明细)
     4. Prometheus API 可查询的 slo:* 元指标(预算剩余、当前燃烧率) */
---
/* 使用侧:查询当前预算剩余 */
/* curl http://prometheus:9090/api/v1/query?query= \
     pyrra_slo_budget_remaining{identifier="checkout-availability"} */
/* 结果 0.73 = 本周期预算还剩 73% */

/* SLO 调整 = 修改 target 字段重新 apply,全部规则与仪表盘自动联动 */
这个 Pyrra CRD 展示了 SLO 即代码的完整形态:30 行声明替代了示例 2 的数百行手写规则,且正确性由工具保证。声明层的每个字段对应一个 SLO 设计决策:target 与 window 定义契约本身(99.9%/30 天——修改这两个值是最常见的 SLO 迭代动作,声明式定义让它成为一行 diff 而非「找到所有引用 0.001 和窗口参数的规则逐一修改」的高危操作);indicator.ratio 的 errors/total selector 定义 SLI 口径(示例 1 的口径决策在这里以声明形式表达——or 组合两个 selector 实现「5xx 或 429」的失败定义);grouping: [route] 是一个强大而常被忽略的特性:SLO 按路由维度细分——/v1/orders 和 /v1/browse 各自有独立的 SLI 序列、独立的预算、独立的告警,这解决了「整体 SLO 健康但关键路由已违约」的聚合掩盖问题(与第 2 章的局部故障盲区同构),代价是告警数量随路由数增长(Pyrra 的告警带 route 标签,路由层可按重要性分级——核心路由 page、边缘路由 ticket)。生成层的四件套覆盖了 SLO 运营的全部工件:recording rules(口径统一的预计算)、四档燃烧率告警(比示例 2 手写的三档多了 3x/3d+2h 档——Google Workbook 的完整参数)、Grafana 仪表盘(预算消耗曲线是月度回顾的数据源、燃烧率热图是「可靠性体温」的可视化)、slo:* 元指标(预算剩余可被其他系统消费——比如发布系统查询预算剩余 < 20% 时自动要求发布审批加一级,错误预算机制与 CI/CD 的自动化集成)。选型对比:Pyrra 是 CRD/operator 形态(Kubernetes 原生、声明即状态、适合平台团队统一管理),Sloth 是 CLI 形态(YAML 进、规则文件出、适合 GitOps 流程、不依赖 K8s);两者的燃烧率数学与告警参数同源(Google Workbook),选择取决于基础设施形态。共同的纪律:生成的规则文件不要手改(改动会在下次生成时丢失)——所有定制通过声明层的配置项表达,这是「代码生成」类工具的通用契约。
/* 示例 4:Alertmanager 完整路由配置——分组、抑制、静默与升级 */
/* alertmanager.yml */
global:
  resolve_timeout: 5m
  pagerduty_url: https://events.pagerduty.com/v2/enqueue

route:
  receiver: default-ticket          /* 兜底:未匹配任何路由的告警进工单(不丢) */
  group_by: ["alertname", "slo", "team"]
  group_wait: 30s                   /* 首条告警等 30s 聚合同组 */
  group_interval: 5m                /* 同组新告警 5m 内合并追加 */
  repeat_interval: 4h               /* 未解决告警每 4h 提醒一次 */
  routes:
    /* SLO page 告警 -> 值班电话链(PagerDuty 含升级策略) */
    - matchers: [ 'severity="page"', 'slo!=""' ]
      receiver: oncall-pagerduty
      group_by: ["slo", "team"]
      repeat_interval: 1h           /* page 级未确认则每小时重发 */
      continue: false
    /* 前瞻性资源告警 -> 工单 */
    - matchers: [ 'severity="ticket"' ]
      receiver: jira-ticket
      repeat_interval: 24h
    /* 特定团队的路由覆盖 */
    - matchers: [ 'team="data-platform"' ]
      receiver: data-oncall-slack

receivers:
  - name: oncall-pagerduty
    pagerduty_configs:
      - routing_key: <pd_key>
        severity: critical
        details:
          runbook: '{{ .CommonAnnotations.runbook }}'
          start_query: '{{ .CommonAnnotations.start_query }}'
  - name: jira-ticket
    webhook_configs:
      - url: http://alert-bridge:8080/jira   /* 告警转工单的桥接服务 */
  - name: data-oncall-slack
    slack_configs:
      - api_url: <slack_webhook>
        channel: "#data-oncall"
  - name: default-ticket
    webhook_configs:
      - url: http://alert-bridge:8080/jira

/* 抑制规则:基础设施层故障时抑制服务层 SLO 告警(保留根因告警) */
inhibit_rules:
  - source_matchers: [ 'alertname="ClusterNetworkPartition"' ]
    target_matchers: [ 'slo!=""' ]
    equal: ["cluster"]
  - source_matchers: [ 'alertname="DatabasePrimaryDown"' ]
    target_matchers: [ 'severity="page"' ]
    equal: ["cluster"]
/* 静默通过 amtool/API 管理,纪律:必须带过期时间与创建原因 */
/* amtool silence add --duration=2h --comment="计划内数据库维护" \
     alertname=~".*Database.*" */
这份 Alertmanager 配置实现了告警治理关键点的全部四个机制。路由树的设计逻辑:顶层 default-ticket 是「不丢告警」的兜底(未匹配任何规则的告警自动进工单——告警体系的静默丢失比噪音更危险,兜底路由保证每条告警至少有人类可见的落点);SLO page 告警走 PagerDuty(电话叫醒 + 升级策略:15 分钟无确认自动升级二线值班——升级链是「值班者本人失能」场景的保险);repeat_interval 的分级(page 1h、ticket 24h)表达「未解决告警的提醒频率与紧急度成正比」;group_by 含 slo 与 team 标签让同一 SLO 的多路由告警(Pyrra grouping 生成的按 route 细分告警)聚合为一条通知——一次全局故障触发 20 条路由级 SLO 告警时,值班者收到一条聚合通知而非 20 条。receivers 段的细节:PagerDuty 的 details 透传 runbook 与 start_query——告警通知(短信/电话的附带的富文本)直接携带诊断入口,「被叫醒 → 看到上下文 → 点击起点查询」的路径在手机端完成;alert-bridge 是告警转工单的桥接服务(Alertmanager 的 webhook 格式转 Jira API——工单自动带上告警的完整 annotations 与首次触发时间,解决「ticket 告警靠人肉转录」的常见混乱)。抑制规则是告警风暴的压缩器:ClusterNetworkPartition(集群网络分区)触发时,同 cluster 的所有 SLO 告警被抑制——网络分区必然导致大面积 SLO 燃烧,200 条服务告警的信息量为零,根因告警一条就够;equal: ["cluster"] 限定抑制范围(A 集群的网络故障不抑制 B 集群的告警)。抑制规则的设计纪律:只在「因果关系确定」的层级间建立抑制(基础设施 → 服务、数据库主节点 → 依赖它的服务),模糊的抑制(「某类告警多了就抑制另一类」)会制造漏报。静默的管理纪律在注释中:amtool 创建时必须带 duration 与 comment——「永久静默」在制度上被禁止(工具层面可以加校验拒绝无过期时间的静默请求),每月静默清单审查(第 9 条关键点的治理机制)清理过期未删与「临时转永久」的静默。
/* 示例 5:错误预算仪表盘与报表查询——SLO 运营的数据视图 */

/* --- 查询 1:当前预算剩余比例(30 天滚动窗口) --- */
/* 30 天内实际错误量 / 允许错误量,剩余 = 1 - 该比值 */
1 - (
  sum(increase(checkout_http_requests_total{status_class="5xx"}[30d]))
  /
  (0.001 * sum(increase(checkout_http_requests_total[30d])))
)
/* 结果 0.73 = 预算还剩 73%;< 0 = 已违约 */

/* --- 查询 2:预算消耗的时间分布(哪几天烧掉的) --- */
sum by () (
  increase(checkout_http_requests_total{status_class="5xx"}[1d])
) / (0.001 * sum(increase(checkout_http_requests_total[1d])))
/* 按天画柱状图:一眼看出预算是被一次大故障烧掉还是持续小额消耗 */

/* --- 查询 3:按 route 的预算明细(Pyrra grouping 的运营视图) --- */
pyrra_slo_budget_remaining{slo="checkout-availability"}
/* 每条路由的独立预算:/v1/orders 剩 20%(危险)而 /v1/browse 剩 95% */

/* --- 查询 4:月度报表——预算的计划内/计划外消耗归因 --- */
/* 计划外:事故期间的消耗(与事故记录系统的时间窗 join) */
/* 计划内:部署窗口 + 混沌实验期间的消耗(与 deploy_events 关联) */
sum(increase(checkout_http_requests_total{status_class="5xx"}[30d]))
/* 拆分方法:用事故/部署/实验的时间窗过滤事件流分别求和——
   这依赖第 7 章示例 4 的变更事件遥测:没有变更记录就无法归因预算消耗 */

/* --- 查询 5:SLO 违约的「用户视角」翻译(给管理层的报表) --- */
/* 30 天内受影响的独立用户数与请求数(宽事件查询,非 PromQL) */
/* SELECT uniqExact(user_id) AS affected_users, count() AS affected_requests
   FROM checkout_events
   WHERE success = false AND event_time > now() - INTERVAL 30 DAY */
这组查询构成 SLO 运营的完整数据视图,从工程仪表盘到管理层报表。查询 1 是预算剩余的标准算法:30 天滚动窗口的「实际错误增量 / 允许错误增量」——注意 increase 在 30 天窗口上的成本(大窗口聚合查询较重,应该用 recording rule 预计算并降采样支撑,而非仪表盘每次刷新现场计算)。查询 2 把预算消耗按天展开:这个视图区分两种截然不同的可靠性模式——「一次大故障烧掉 60% 预算」(应对:该故障的深度复盘与针对性加固)与「每天均匀小额消耗」(应对:系统性问题,可能是 flaky 组件或容量边缘运行,需要专项治理);两种模式的行动方向完全不同,只看「剩余 73%」的单一数字无法区分。查询 3 是 route 级预算明细的运营用法:核心路由(/v1/orders)预算告急而边缘路由(/v1/browse)预算富余时,「整体 SLO 健康」的表象下核心业务已在违约边缘——route 级 SLO(Pyrra grouping)的价值在这个视图中兑现,路由重要性分级(核心路由更严的 SLO 或更低的告警阈值)是下一步治理动作。查询 4 是预算归因——SLO 体系成熟度的高级标志:把预算消耗拆分为「计划内」(部署、实验、演练的合理代价)与「计划外」(故障),健康的团队计划内消耗占显著比例(预算被主动投资于学习与速度);这个拆分依赖变更事件遥测(deploy_events)与事故记录的时间窗关联——再次印证「变更遥测是投入产出比最高的可观测性建设」。查询 5 是面向非工程受众的翻译层:「预算剩余 27%」对管理层是抽象数字,「30 天内 12,847 个独立用户遭遇 34,291 次失败请求」是具体的用户痛苦——SLO 报表应该同时包含工程视角(燃烧率、预算)与用户视角(受影响用户数、失败请求数),后者用宽事件的 uniqExact(user_id) 计算(高基数查询能力在 SLO 运营中的复用)。这五个查询建议固化为月度 SLO 评审会的标准材料:预算趋势、消耗归因、路由明细、用户影响四张图,支撑「收紧/放松 SLO、投入/释放可靠性资源」的季度决策。
/* 示例 6:预算驱动的发布门禁——错误预算与 CI/CD 的自动化集成 */
#!/usr/bin/env bash
/* release-gate.sh:发布流水线的预算检查步骤 */
set -euo pipefail
SERVICE=$1
PROM="http://prometheus:9090"

/* 查询该服务的预算剩余与当前燃烧率 */
BUDGET=$(curl -sG "$PROM/api/v1/query" --data-urlencode \
  "query=pyrra_slo_budget_remaining{slo=\"$SERVICE-availability\"}" \
  | jq -r '.data.result[0].value[1] // "unknown"')
BURN=$(curl -sG "$PROM/api/v1/query" --data-urlencode \
  "query=slo:${SERVICE}_error_budget:burn_rate1h" \
  | jq -r '.data.result[0].value[1] // "0"')

echo "服务: $SERVICE | 预算剩余: $BUDGET | 当前 1h 燃烧率: $BURN"

gate_decision() {
  /* 规则 1:预算已耗尽 -> 阻断发布(只允许带 hotfix 标签的可靠性修复) */
  if (( $(echo "$BUDGET <= 0" | bc -l) )); then
    if [ "${RELEASE_KIND:-feature}" = "hotfix" ]; then
      echo "GATE: 预算耗尽,但本次为 hotfix —— 放行(需二人审批已在流水线前置)"
      return 0
    fi
    echo "GATE: 错误预算耗尽,功能发布被阻断。恢复路径:可靠性修复或等待窗口重置。"
    return 1
  fi
  /* 规则 2:预算 < 20% 且正在燃烧(>1x)-> 要求额外审批 */
  if (( $(echo "$BUDGET < 0.2" | bc -l) )) && (( $(echo "$BURN > 1" | bc -l) )); then
    echo "GATE: 预算告急(<20%)且仍在燃烧 —— 需要 SRE 审批标签"
    [ "${HAS_SRE_APPROVAL:-false}" = "true" ] || return 1
  fi
  /* 规则 3:当前燃烧率 > 6 -> 任何发布暂停(正在事故中,别添乱) */
  if (( $(echo "$BURN > 6" | bc -l) )); then
    echo "GATE: 当前 6 倍速燃烧(疑似事故进行中)—— 发布暂停"
    return 1
  fi
  echo "GATE: 预算充足($(echo "scale=0; $BUDGET*100/1" | bc)%),放行"
  return 0
}
gate_decision
这个脚本把错误预算从「报表上的数字」变成「发布流程的自动闸门」,是错误预算机制「可执行化」的关键一步——第 3 条关键点说过,预算制度的可信度来自它第一次被真正执行时的表现,而人工执行的预算制度几乎必然在业务压力下被击穿(「这次发布很重要,预算的事下次再说」),自动化门禁把「例外审批」的成本显式化,让绕过制度变成需要主动决策的行为而非默认路径。三条门禁规则的设计逻辑各有依据。规则 1(预算耗尽 → 阻断功能发布)是错误预算制度的核心承诺:预算烧完意味着「本周期用户已经承受了超出承诺的不可靠」,继续激进迭代表示制度无效——hotfix 例外通道保证「修复可靠性的发布」永远不被阻断(阻断可靠性修复会形成死锁),但要求二人审批(例外的成本显式化)。规则 2(预算 < 20% 且仍在燃烧 → SRE 审批)是预警带:预算告急但未耗尽时,发布仍可继续但需要可靠性专家的额外审查——「仍在燃烧」的条件很重要:预算低但燃烧率 < 1(系统已稳定,预算是历史故障的遗产)时不应过度限制,预算低且正在燃烧说明系统处于脆弱期,新发布风险放大。规则 3(当前 6x 燃烧 → 一切发布暂停)是事故保护:6 倍燃烧率意味着 page 告警大概率已触发、事故可能正在进行——此时发布(哪怕是无关服务的)会污染事故现场(变更时间线出现新条目,干扰「查变更」板斧)并分散工程注意力;「正在事故中不做无关变更」是事故响应的基本纪律,门禁把它自动化。集成形态:这个脚本作为 CI/CD 流水线的一个 stage(部署前执行),RELEASE_KIND/HAS_SRE_APPROVAL 从流水线的标签/审批系统注入;门禁决策与数据(预算、燃烧率)写入发布记录——事后审计「那次发布时预算状态如何」有据可查。推广注意:门禁上线前与所有团队沟通规则与例外流程(制度的合法性来自事先共识而非事后强制),第一个月用「警告模式」(输出决策但不阻断)收集反馈校准阈值,然后切换为强制模式——第 11 章「平台团队的产品思维」在发布治理中的应用。

实验笔记

【实验 8:从零构建 SLO 告警体系——定义、回测、多窗口告警与预算门禁】

一、实验目标

为一个演示服务完整走一遍 SLO 生命周期:SLI 口径设计 → 历史数据回测定目标 → Pyrra/手写多窗口告警 → Alertmanager 路由 → 预算仪表盘 → 发布门禁。完成后你应当能够:(1) 为任意服务定义可用性+延迟双 SLI 并论证口径决策;(2) 手工推导并验证 14.4x/6x/1x 燃烧率档位的数学;(3) 配置并触发一次真实的多窗口 SLO 告警。

二、环境准备

Docker Compose:演示服务(checkout,可注入错误率与延迟劣化)、Prometheus + Alertmanager、Pyrra operator(或用手写规则替代)、Grafana、PagerDuty 开发者账号(或用 Slack webhook 模拟 page 通道)。演示服务支持故障注入:ERROR_RATE=0.005(0.5% 错误率,约 5x 燃烧)、ERROR_RATE=0.02(20x 燃烧)、LATENCY_P99=800(延迟 SLI 劣化)。变更事件遥测(第 7 章示例 4)已接入。

三、实验步骤

步骤 1(SLI 定义与口径论证):为 checkout 写可用性 SLI(5xx+429 为失败)与延迟 SLI(<500ms 为快)的 recording rules。写一段 200 字的口径说明:为什么 429 算失败而 404 不算?为什么延迟用比率形态而非 P99 形态?

步骤 2(历史回测):正常流量跑 2 小时后,用 increase 查询计算「若 SLO 定为 99.9%/99.5%,过去 2 小时窗口折算的燃烧率各是多少」。注入 ERROR_RATE=0.005 再跑 1 小时,重新计算:当前错误率下 99.9% 目标的燃烧率(预期约 5x)。据此讨论:这个服务的合理 SLO 目标应该是多少?

步骤 3(多窗口告警部署):用 Pyrra CRD(或手写示例 2 的规则)部署四档燃烧率告警。验证 recording rules 生成的燃烧率序列存在且数值正确(手工计算对照:错误率 0.5% / 预算率 0.1% = 5x)。

步骤 4(告警触发实验):依次注入三档故障并观察告警行为:(a) ERROR_RATE=0.02 持续 10 分钟——预期 14.4x 档在 1h+5m 双条件满足后触发 page(注意:1h 窗口需要时间累积,观察触发延迟并解释);(b) 恢复后注入 ERROR_RATE=0.005 持续 3 小时——预期 6x 不触发(5x < 6x)但 1x/3d 档在窗口累积后触发 ticket;(c) 注入 10 分钟的 ERROR_RATE=0.05 然后恢复——观察 5m 短窗口条件如何在恢复后阻止长窗口惯性触发(双窗口机制的防误报验证)。记录每档告警的实际触发时间与理论预期的差异。

步骤 5(路由与通知):配置 Alertmanager:page → Slack #oncall 频道(模拟 PagerDuty)、ticket → 本地文件接收器。验证告警通知中包含 summary(含违约倒计时翻译)、runbook、start_query 三个字段且链接可点击。配置一条抑制规则(模拟「数据库主节点宕机」告警抑制 checkout SLO 告警)并验证。

步骤 6(预算仪表盘与门禁):在 Grafana 建预算仪表盘:预算剩余曲线(30d 窗口,用 2h 数据外推演示)、按天消耗柱状图、燃烧率热图。部署示例 6 的发布门禁脚本,分别在预算充足、预算 < 20%、燃烧率 > 6 三种状态下运行,验证三种门禁决策。

四、检查清单

□ SLI 口径说明能回答「429 为什么算失败」

□ 燃烧率手工计算与 recording rule 输出一致(误差 < 5%)

□ 三档告警的触发行为与多窗口理论一致,能解释 1h 窗口的触发延迟

□ 短窗口的防误报机制被实验 (c) 验证

□ 告警通知三件套(summary/runbook/start_query)完整可用

□ 门禁脚本三种状态的决策正确

五、进阶挑战

(1) 把 SLO 周期从 30d 改为 7d,重新推导各档燃烧率阈值(14.4x 变成多少?)并修改规则验证——理解「阈值 = 周期/违约倒计时」的换算本质;(2) 为延迟 SLI 配置独立的燃烧率告警,注入 LATENCY_P99=800 验证「可用性 SLO 健康但延迟 SLO 燃烧」的场景,讨论两个 SLO 的告警应该 page 到同一个人吗;(3) 实现「预算耗尽自动冻结」的完整闭环:门禁脚本的决策写入一个 K8s ConfigMap,ArgoCD/Flux 同步策略读取该 ConfigMap 拒绝部署——把实验 6 的 GitOps 与本章的预算机制串联成端到端自动化。

延伸阅读

  • Google SRE Book 第 4 章 "Service Level Objectives" 与 SRE Workbook 第 2-5 章(免费在线:sre.google)——SLO/SLI/错误预算概念的原始出处与工程展开。Workbook 的 "Implementing SLOs" 与 "Alerting on SLOs" 两章是多窗口多燃烧率方法的权威定义,本章示例 2 的规则参数直接源于此,必读。
  • Alex Hidalgo, "Setting SLOs: a practical guide" 与 "The How and Why of SLOs"(Google Cloud 博客/YouTube 演讲)——SRE Workbook 作者之一对 SLO 设定流程的实操指南:用户旅程映射、SLI 口径打磨、历史回测方法。比原书更聚焦「第一次做 SLO 该怎么动手」,适合作为团队工作坊材料。
  • Pyrra 文档(pyrra.dev)与 Sloth 文档(sloth.dev)——两大 SLO 即代码工具的完整参考:CRD/YAML 声明格式、生成的规则结构、自定义燃烧率参数的方法。部署前重点读「multi-burn-rate 参数表」与「grouping 细分 SLO」章节。
  • Prometheus Alertmanager 官方文档(prometheus.io/docs/alerting)——路由树、分组、抑制、静默四大机制的权威说明。重点精读 inhibition 的 equal 语义与 silences 的 matcher 规则,配置错误导致的漏报/风暴大多源于对这两个机制的误解。
  • 《The Site Reliability Workbook》第 10 章 "Postmortem Culture" 与 John Lunney 等的 SLO 实践案例——复盘与 SLO 修正的闭环流程:事故后如何判断「SLO 定错了」还是「系统不行」,预算消耗归因的运营实践。与第 7 章的诊断复盘内容衔接成完整的事故学习体系。
  • Liz Fong-Jones, "SLOs, Alerts, and Observability" 系列演讲(YouTube)——本书作者之一对「SLO 告警与可观测性探索如何分工衔接」的论述:告警负责叫醒、可观测性负责诊断、start_query 是两者的关节。理解这个分工能避免「用告警规则做诊断」与「用探索查询做告警」两类常见错位。
  • PagerDuty/OpsGenie 的告警响应流程文档(incident response workflow)——升级策略、值班轮换、告警确认机制的产品化实践参考。SLO 告警的组织侧落地(谁被叫醒、多久升级、如何交接)需要这类工具的机制支撑,选型时对照其「多团队路由」与「升级链」能力。
  • Evan Jones, "SLOs are not enough: measuring user happiness" 类批判性文章(及各 SRE 会议对 SLO 局限的讨论)——SLO 体系的边界与批评:SLI 覆盖不到的用户痛苦(正确性问题、数据丢失)、比率形态对「少数用户深度受伤」的钝感。读批判性材料防止把 SLO 当成可靠性的全部——它是必要框架而非充分答案。
8/12基于 S
0%