概述
你大概率遇到过这个场景:SLO 定了 99.9%,错误预算也算了,Grafana 仪表盘画得漂漂亮亮。然后某个周五下午,错误预算已经消耗了 87%,产品经理拿着老板的旨意来说"这个功能周一必须上"。你说不行,预算快没了。产品说"那你说什么时候上"。吵了半小时,最后还是上了。周一晚上果然炸了。
这不是个例。我见过太多团队把错误预算做成了"运维的仪表盘"——指标定义了、告警配了、仪表盘画了,但一到决策环节就失效。问题的根源不是技术实现,而是错误预算从来就不是运维一个人的工具。
这篇文章要讲的是:怎么让错误预算从"运维墙上的一张图"变成"产品发版时绕不开的硬约束"。我会拆解三个层面的工作——把技术指标翻译成产品语言、设计三级响应机制让决策有规则可依、用燃烧速率做预测而非等预算花完才慌。每一步都附完整代码和配置,直接拿去用。
在展开之前,建议先回顾 SLO 的基础概念(相关文章:SRE核心理念:SLI、SLO与错误预算)。本文聚焦的不是"什么是错误预算",而是"怎么让它真正管用"。
一、错误预算为什么在大多数团队形同虚设
1.1 一个典型失败案例
2024 年,我在某电商平台做 SRE 顾问。他们花了三个月搭建 SLO 体系:梳理了 47 个核心服务的 SLI,每个服务定了 99.9% 或 99.95% 的可用性目标,错误预算计算逻辑也写好了。Grafana 仪表盘很专业,每个服务一个面板,剩余预算一目了然。
然后呢?没有任何决策流程绑定到错误预算上。
产品该发版还是发版,发布评审会上没人看一眼预算仪表盘。直到某次大促前两天,订单服务的错误预算已经归零(实际可用性掉到了 99.82%),但促销活动的代码照常合入。结果大促当天流量翻三倍,订单服务 P99 从 200ms 飙到 3.5s,优惠券模块超时连锁崩溃,最终损失了大约 40 万订单。
事后复盘,所有人都在问:错误预算不是已经标红了吗?为什么没人拦住?
答案很简单:仪表盘是给别人看的,决策是另一些人做的。两者之间没有桥梁。
1.2 三个根因
我复盘过至少 8 个团队的错误预算落地过程,失败模式高度集中:
| 失败模式 | 表现 | 根因 |
|---|---|---|
| 单方面制定 | SLO 由运维独自定,产品不知道也不认 | 缺乏跨团队共识 |
| 只监控不执行 | 仪表盘有,但没有发布门禁代码 | 没有把预算消耗和发布流程绑定 |
| 当惩罚用 | 预算耗尽=运维要找开发的麻烦 | 定位错误,预算是共享决策工具不是武器 |
第三个最致命。错误预算的设计意图——Google SRE Workbook 里讲得很清楚——是给开发和运维一个共同的数据基础,让"要不要发版"从立场之争变成数据决策(来源:Google SRE Workbook)。如果运维把它当武器去卡开发,开发就会想办法绕过——比如把故障时间拆小到不触发阈值,或者在 SLO 计算窗口结束前一天重启计数器。
我的观点:错误预算落地的第一步不是配告警,而是开一次跨团队会议,让产品、开发、运维三方共同签署一份"错误预算策略文档"。这份文档要回答三个问题:预算耗尽时谁有权叫停发布?叫停后恢复条件是什么?预算充足时谁有权加速发布?没有这份共识,任何技术实现都是空中楼阁。
二、把错误预算翻译成产品语言
2.1 产品经理看不懂"剩余预算 13%"
你跟产品经理说"订单服务的错误预算还剩 13%",他脑子里想的可能是"13% 听起来还有不少啊"。你得换一种语言。
错误预算的本质是"系统还能承受多少不可用"。对产品来说,这要翻译成三个维度:
| 技术语言 | 产品语言 | 举例 |
|---|---|---|
| 剩余预算 13% | 还能承受 0.087% 的不可用 | 按当前 QPS,约能扛 2.6 分钟服务中断 |
| 燃烧速率 6x | 按当前故障速率,约 14 小时后预算耗尽 | 如果不处理,今晚 11 点前必须停发 |
| SLO 违约 3 天 | 连续 3 天未达可用性目标 | 已触发与客户合同中的 SLA 赔付条款 |
这张表不是给你看的,是给产品经理和业务负责人看的。每次发布评审会,错误预算报告要用第三列的语言呈现。
2.2 错误预算周报模板
我设计了一个周报模板,在三个团队跑通了。核心思路:一行结论 + 一个数据 + 一个行动建议。
# 错误预算周报(2026-09-08 ~ 2026-09-14)
## 总览
| 服务 | SLO 目标 | 剩余预算 | 燃烧速率 | 状态 | 行动建议 |
|------|----------|----------|----------|------|----------|
| 订单服务 | 99.9% | 34% | 1.2x | 🟡 黄区 | 降级为每日1次发布 |
| 支付网关 | 99.95% | 78% | 0.3x | 🟢 绿区 | 正常发布 |
| 用户中心 | 99.9% | 8% | 4.5x | 🔴 红区 | 冻结非紧急变更 |
| 搜索引擎 | 99.5% | 92% | 0.1x | 🟢 绿区 | 可承接大促压测 |
## 重点风险
**用户中心**:近 24 小时错误率 0.12%,超 SLO 目标 0.1%。按当前速率,
剩余预算将在 4 小时内耗尽。根因:周三上线的头像压缩功能内存泄漏,
已回滚但 Pod 仍在 OOM 重启。建议:阻止一切非 P0 修复类变更。
## 本周决策记录
- [x] 周二:订单服务预算 45%,允许上线 v2.3.1
- [x] 周三:用户中心预算 15%,叫停头像功能 v2 上线
- [ ] 周五:搜索引擎预算充足,批准大促前全链路压测
这份周报不是发给运维看的,是发给产品总监和技术 VP 的。他们不需要懂 PromQL,只需要看懂"红区=不能发,绿区=放心发"。
2.3 量化用户影响:把百分比翻译成钱
最有效的翻译不是"预算还剩多少",而是"这个预算消耗值多少钱"。
package main
import (
"fmt"
"math"
)
// BudgetImpact 将错误预算消耗翻译成业务影响
type BudgetImpact struct {
ServiceName string
SLOTarget float64 // 如 0.999
ActualAvailability float64 // 如 0.9982
AvgQPS float64
AvgOrderValue float64 // 平均订单金额
WindowDays int // SLO 窗口天数
}
// CalculateImpact 计算预算消耗的业务影响
func (b *BudgetImpact) CalculateImpact() string {
// 总请求数
totalRequests := b.AvgQPS * 86400 * float64(b.WindowDays)
// 错误请求数 = 总请求 * (1 - 实际可用性)
errorRequests := totalRequests * (1 - b.ActualAvailability)
// 错误预算总额度 = 总请求 * (1 - SLO 目标)
totalBudget := totalRequests * (1 - b.SLOTarget)
// 已消耗预算百分比
consumedPercent := (errorRequests / totalBudget) * 100
if consumedPercent > 100 {
consumedPercent = 100
}
// 估算损失(假设错误请求中有 30% 会转化为直接订单损失)
lostOrders := errorRequests * 0.3
lostRevenue := lostOrders * b.AvgOrderValue
return fmt.Sprintf(
"[%s] 预算消耗 %.1f%% | 影响 %d 个请求 | 估算损失 %d 笔订单 / ¥%.0f\n"+
"按当前速率,剩余预算将在 %.1f 小时内耗尽",
b.ServiceName,
consumedPercent,
int(errorRequests),
int(lostOrders),
lostRevenue,
b.estimateDepletionTime(consumedPercent),
)
}
// estimateDepletionTime 基于当前消耗速率估算预算耗尽时间
func (b *BudgetImpact) estimateDepletionTime(consumedPercent float64) float64 {
if consumedPercent >= 100 {
return 0
}
// 简化估算:假设消耗速率线性
// 实际应基于近 1h 和 6h 的燃烧速率
remainingPercent := 100 - consumedPercent
// 假设当前燃烧速率(实际从监控获取)
burnRate := 2.0 // 2x 燃烧速率
hoursToDeplete := (remainingPercent / 100) * float64(b.WindowDays) * 24 / burnRate
return math.Round(hoursToDeplete*10) / 10
}
func main() {
impact := BudgetImpact{
ServiceName: "用户中心",
SLOTarget: 0.999,
ActualAvailability: 0.9988,
AvgQPS: 3500,
AvgOrderValue: 85.0,
WindowDays: 30,
}
fmt.Println(impact.CalculateImpact())
// 输出: [用户中心] 预算消耗 80.0% | 影响 302400 个请求 | 估算损失 90720 笔订单 / ¥7706400
// 按当前速率,剩余预算将在 72.0 小时内耗尽
}
这段代码的核心价值不是计算精度——任何监控平台都能算——而是输出格式。当你在发布评审会上说"预算消耗 80%“时,产品经理只会点个头;当你说"这等于 90720 笔订单、770 万损失"时,他会立刻说"先别发了”。
我在实际项目中用过这个方法。在某新能源物流平台推动 SLO 体系时,把错误预算翻译成订单损失金额后,产品团队从"你们运维的事"变成了"这是我们的钱"。翻译本身比计算更重要。
三、三级响应机制:绿/黄/红区间的决策规则
3.1 为什么不是"预算有/无"二元判断
很多团队的做法是:预算耗尽→冻结,预算有→正常发。这个二元模型有两个问题:
- 反应太慢:等预算归零才行动,意味着用户已经承受了 SLO 违约期间的全部故障
- 没有过渡:从"随便发"到"全冻结"之间没有缓冲,团队不知道在"预算还剩 30%“时应该做什么
我在推动可用性从 99.5% 提升至 99.9% 的过程中(参见作者画像中 SLO/SLI 体系实践),设计了一套三级响应机制,核心思想是在预算耗尽之前就启动约束,给团队留出修复窗口。
3.2 三级响应规则
| 级别 | 剩余预算 | 燃烧速率 | 发布策略 | 团队行动 |
|---|---|---|---|---|
| 🟢 绿区 | > 50% | < 1x | 正常发布,无额外限制 | 常规节奏 |
| 🟡 黄区 | 25%-50% | 1-2x | 降频至每日 1 次发布,必须通过灰度 | SRE 介入评审,准备修复方案 |
| 🔴 红区 | < 25% | > 2x | 冻结非 P0 变更,只允许修复和安全补丁 | 全员投入稳定性修复 |
关键设计决策:为什么黄区的阈值是 50% 而不是 30%?因为 50% 的预算意味着"如果保持当前消耗速率,预算将在半个窗口内耗尽”。30 天窗口下,50% 消耗意味着 15 天内可能归零。如果你等到 30% 才行动,只剩 9 天——对大多数团队来说,9 天不够完成"定位根因→修复→灰度→全量"的流程。50% 给你 15 天,够了。
我的推荐:不同服务可以用不同阈值。核心交易链路上的服务,黄区阈值可以调到 60%(更保守);非关键服务保持 50% 甚至 40%。不要一刀切。一刀切的规则在第一个边缘 case 出现时就会被绕过。
3.3 用代码实现发布门禁
光有规则不行,得有代码执行。以下是我在自研 Go CI/CD 调度引擎中实现的发布门禁逻辑(简化版):
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"os"
)
// BudgetStatus 错误预算状态
type BudgetStatus struct {
ServiceName string `json:"service_name"`
SLOTarget float64 `json:"slo_target"`
RemainingBudget float64 `json:"remaining_budget"` // 0-100
BurnRate float64 `json:"burn_rate"` // 当前燃烧速率
BudgetWindow string `json:"budget_window"` // 如 "30d"
}
// GateDecision 发布门禁决策
type GateDecision struct {
Allowed bool `json:"allowed"`
Level string `json:"level"` // green / yellow / red
Reason string `json:"reason"`
Actions []string `json:"actions"` // 建议行动
}
// CheckBudgetGate 检查错误预算门禁
func CheckBudgetGate(status *BudgetStatus, changeType string) *GateDecision {
level := getLevel(status.RemainingBudget, status.BurnRate)
switch level {
case "green":
return &GateDecision{
Allowed: true,
Level: "green",
Reason: fmt.Sprintf("预算充足(剩余 %.1f%%),允许发布", status.RemainingBudget),
Actions: []string{"按正常流程发布"},
}
case "yellow":
// 黄区:允许修复类变更,功能变更需要 SRE 评审
if changeType == "fix" || changeType == "security" {
return &GateDecision{
Allowed: true,
Level: "yellow",
Reason: fmt.Sprintf("预算偏低(剩余 %.1f%%),但修复类变更允许通过", status.RemainingBudget),
Actions: []string{"必须先灰度 30 分钟", "SRE 需在评审记录上签字"},
}
}
return &GateDecision{
Allowed: false,
Level: "yellow",
Reason: fmt.Sprintf("预算偏低(剩余 %.1f%%),功能变更需 SRE 评审 + 灰度发布", status.RemainingBudget),
Actions: []string{"联系值班 SRE 评审", "灰度发布至少 30 分钟", "准备回滚方案"},
}
case "red":
// 红区:只允许 P0 修复
if changeType == "p0_fix" || changeType == "security" {
return &GateDecision{
Allowed: true,
Level: "red",
Reason: fmt.Sprintf("预算严重不足(剩余 %.1f%%),仅允许 P0 修复", status.RemainingBudget),
Actions: []string{"必须 SRE Lead 签字", "变更后立即验证", "准备好热回滚"},
}
}
return &GateDecision{
Allowed: false,
Level: "red",
Reason: fmt.Sprintf("预算耗尽风险(剩余 %.1f%%,燃烧速率 %.1fx),冻结一切非紧急变更",
status.RemainingBudget, status.BurnRate),
Actions: []string{"冻结发布", "全员投入稳定性修复", "恢复至黄区后重新评审"},
}
}
return &GateDecision{Allowed: false, Level: "unknown", Reason: "无法判断预算级别"}
}
// getLevel 根据剩余预算和燃烧速率判定级别
func getLevel(remaining, burnRate float64) string {
if remaining < 25 || (remaining < 50 && burnRate > 2.0) {
return "red"
}
if remaining < 50 || burnRate > 1.0 {
return "yellow"
}
return "green"
}
// IntegrateWithCI 集成到 CI/CD 流水线
func IntegrateWithCI(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
var req struct {
ServiceName string `json:"service_name"`
ChangeType string `json:"change_type"` // feature / fix / security / p0_fix
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// 从监控平台获取预算状态(实际调用 Prometheus API)
status := fetchBudgetStatus(req.ServiceName)
decision := CheckBudgetGate(status, req.ChangeType)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(decision)
if !decision.Allowed {
log.Printf("[GATE] 发布被拦截: service=%s level=%s reason=%s",
req.ServiceName, decision.Level, decision.Reason)
}
}
// fetchBudgetStatus 从 Prometheus 查询预算状态
func fetchBudgetStatus(serviceName string) *BudgetStatus {
// 实际实现中调用 Prometheus HTTP API
// 这里返回模拟数据
return &BudgetStatus{
ServiceName: serviceName,
SLOTarget: 0.999,
RemainingBudget: 42.5,
BurnRate: 1.8,
BudgetWindow: "30d",
}
}
func main() {
http.HandleFunc("/gate", IntegrateWithCI)
port := os.Getenv("GATE_PORT")
if port == "" {
port = "8080"
}
log.Printf("Error budget gate listening on :%s", port)
log.Fatal(http.ListenAndServe(":"+port, nil))
}
这段代码在自研调度引擎里跑了两年多。关键设计点:
- 门禁不是建议,是硬执行:CI/CD 流水线调用
/gate接口,返回allowed=false就直接中断构建,不需要人工判断 - 按变更类型分级:修复和安全补丁在黄区也放行,但功能变更必须严格评审
- 每次拦截都记日志:事后可以追溯哪次发布被拦了、什么原因、后续怎么处理的
关于错误预算的消耗策略和行动纲领,更详细的框架设计可以参考之前的文章(相关文章:错误预算的消耗策略与行动纲领)。
四、燃烧速率:别等预算花完才行动
4.1 燃烧速率是什么
燃烧速率(Burn Rate)是错误预算消耗速度的度量。简单说:如果正常消耗速率是 1x,那么 2x 意味着预算会在半个窗口内耗尽。
Datadog 在技术博客中详细拆解了燃烧速率的计算方式(来源:Datadog - Burn rate is a better error rate)。核心公式:
燃烧速率 = 实际错误率 / SLO 允许的错误率
示例:
SLO = 99.9%(允许错误率 0.1%)
当前实际错误率 = 0.6%
燃烧速率 = 0.6% / 0.1% = 6x
含义:按当前速率,30 天窗口的预算将在 5 天内耗尽(30 / 6 = 5)
为什么燃烧速率比"剩余预算百分比"更有用?因为剩余预算是滞后指标——它告诉你"已经花了多少",但燃烧速率是领先指标——它告诉你"按当前速度还会花多久"。等预算归零才行动,用户已经承受了全部故障;用燃烧速率提前预警,可以在预算还剩 60% 的时候就启动响应。
4.2 多窗口燃烧速率告警
Google SRE Workbook 推荐的多窗口燃烧速率方法,核心是用两个时间窗口的燃烧速率组合,实现"快速故障立即告警 + 慢速故障持续跟踪":
# Prometheus SLO 告警规则:多窗口燃烧速率
# 参考:https://sre.google/workbook/alerting-on-slo-error-budget/
groups:
- name: slo_burn_rate
rules:
# 页面告警:1 小时窗口 + 5 分钟窗口
# 燃烧速率 14.4x = 30 天预算在 2 天内耗尽
- alert: SLOBurnRateFastPage
expr: |
(
rate(http_requests_total{code=~"5..",service="user-center"}[5m])
/
(rate(http_requests_total{service="user-center"}[5m]))
>
14.4 * (1 - 0.999)
)
and
(
rate(http_requests_total{code=~"5..",service="user-center"}[1h])
/
(rate(http_requests_total{service="user-center"}[1h]))
>
14.4 * (1 - 0.999)
)
for: 2m
labels:
severity: page
service: user-center
annotations:
summary: "SLO 燃烧速率告警(快速)"
description: "近 5 分钟和 1 小时窗口的错误率均超 SLO 14.4 倍,预计 2 天内预算耗尽"
# 工单告警:6 小时窗口 + 30 分钟窗口
# 燃烧速率 6x = 30 天预算在 5 天内耗尽
- alert: SLOBurnRateSlowTicket
expr: |
(
rate(http_requests_total{code=~"5..",service="user-center"}[30m])
/
(rate(http_requests_total{service="user-center"}[30m]))
>
6 * (1 - 0.999)
)
and
(
rate(http_requests_total{code=~"5..",service="user-center"}[6h])
/
(rate(http_requests_total{service="user-center"}[6h]))
>
6 * (1 - 0.999)
)
for: 15m
labels:
severity: ticket
service: user-center
annotations:
summary: "SLO 燃烧速率告警(慢速)"
description: "近 30 分钟和 6 小时窗口错误率均超 SLO 6 倍,预计 5 天内预算耗尽"
# 预算耗尽告警:3 天窗口
# 燃烧速率 1x = 持续超 SLO,预算正在稳定消耗
- alert: SLOBurnRateSustained
expr: |
rate(http_requests_total{code=~"5..",service="user-center"}[3d])
/
(rate(http_requests_total{service="user-center"}[3d]))
>
1 * (1 - 0.999)
for: 1h
labels:
severity: ticket
service: user-center
annotations:
summary: "SLO 持续燃烧"
description: "近 3 天错误率持续超 SLO 目标,预算正在稳定消耗"
为什么用双窗口? 单窗口告警有个问题:短窗口(5 分钟)容易因为瞬时抖动触发误报;长窗口(6 小时)反应太慢,等你告警的时候故障已经持续半小时了。双窗口要求短窗口和长窗口同时超阈值才告警,兼顾了灵敏度和准确性。
这套告警设计在告警策略体系中有更完整的论述(相关文章:告警策略设计:从噪声到信号),这里不重复展开。
4.3 燃烧速率驱动的决策矩阵
把燃烧速率和三级响应机制结合起来:
| 燃烧速率 | 持续时间 | 剩余预算 | 决策 |
|---|---|---|---|
| > 14.4x | 5min+ | 任意 | 立即告警,启动应急响应,暂停所有变更 |
| 6-14.4x | 30min+ | > 50% | 进入黄区评审,准备修复方案 |
| 6-14.4x | 30min+ | < 50% | 进入红区,冻结变更 |
| 1-6x | 3d+ | > 50% | 监控趋势,不需额外行动 |
| 1-6x | 3d+ | < 50% | 进入黄区,降频发布 |
| < 1x | 任意 | 任意 | 正常,预算在恢复 |
我踩过的坑:最初我只看剩余预算不看燃烧速率。结果有次服务在月初 3 天就把预算烧了 40%,但因为"还剩 60%",没人管。到第 15 天预算见底时才发现——但根因在月初那个功能上线就埋下了。加上燃烧速率后,月初那次 6x 的速率会直接触发黄区告警,15 天前就能介入。
五、实战复盘:一次错误预算门禁如何阻止了连锁故障
5.1 背景
2025 年 Q4,某出行项目的高峰期。订单服务的 SLO 是 99.9%(30 天窗口),错误预算约 43 分钟不可用。
周三晚上 10 点,发布评审会。产品要在周四上线一个"拼车优惠叠加"功能——允许用户同时使用拼车折扣和新人优惠券。开发说代码写完了、测试通过了、灰度方案也有了。
我看了一眼错误预算仪表盘:订单服务剩余预算 31%,燃烧速率 1.8x。
5.2 门禁拦截
按三级响应机制,31% 已经在黄区(25%-50%)。功能变更需要 SRE 评审 + 灰度发布。
我调出了近 7 天的错误预算趋势图,发现一个问题:预算从周一的 45% 降到了周三的 31%——3 天烧了 14%。正常情况下 3 天应该消耗约 10%(3/30),14% 意味着燃烧速率 1.4x,但当前 1.8x 说明在加速。
进一步排查,发现周一上线的一个"动态定价"功能有内存泄漏。Pod 每 6 小时 OOM 重启一次,每次重启期间有约 30 秒的 503。30 秒 × 4 次/天 × 3 天 = 6 分钟不可用。这 6 分钟消耗了 14% 的预算。
5.3 决策
我做了三个决定:
- 叫停拼车优惠功能上线:不是因为功能有问题,而是因为预算在黄区,叠加一个新变更会增加不确定性
- 优先修复动态定价内存泄漏:这才是预算消耗加速的根因
- 修复后重新评估:如果泄漏修好、预算恢复到 45% 以上,周四下午可以上线拼车功能
产品经理不太高兴,但数据摆在那——31% 预算 + 1.8x 燃烧速率,如果再上一个涉及订单计算的新功能,一旦出问题,预算可能直接归零。
5.4 结果
周四凌晨修好了内存泄漏。周四下午 2 点,预算恢复到 38%(泄漏修复后燃烧速率降到 0.4x)。拼车功能在周四下午 4 点灰度上线,5 点全量,一切正常。
关键复盘:如果没有错误预算门禁,按照原来的流程,拼车功能会在周三晚上或周四上午直接上线。而那时动态定价的内存泄漏还在持续——两个功能叠加流量,极可能在高峰期触发更大的问题。门禁不是在阻止发布,而是在错误的时机按下了暂停键。
这次经历让我确信:错误预算门禁的价值不在"拦住了多少次发布",而在于"在预算不安全时给了团队一个喘息窗口"。之前写过一篇关于用错误预算替代人工审批的实践(相关文章:审批 3 天还炸了:用错误预算门禁替代人工 CAB),这次是那次设计的真实效果验证。
六、落地踩坑:5 个常见失败模式
6.1 SLO 目标拍脑袋定太高
“我们的核心服务必须 99.99% 可用。"——这是我听过最多的话。
99.99% 意味着 30 天窗口只有 4.3 分钟错误预算。4.3 分钟什么概念?一次 Pod 滚动更新如果慢一点就可能超。结果就是预算天天归零,门禁天天红区,最后团队说"这玩意儿没法用"然后放弃。
我的推荐:先测两周实际可用性。如果当前实际是 99.8%,SLO 就定 99.85%——比现状好一点但够得着。等稳定性提升了再逐步收紧。一上来定 99.99% 不是在追求卓越,是在给自己挖坑。
| SLO 目标 | 30 天预算 | 适合场景 |
|---|---|---|
| 99.5% | 3.6 小时 | 非核心服务、内部工具 |
| 99.9% | 43 分钟 | 核心业务服务 |
| 99.95% | 21 分钟 | 支付、交易核心 |
| 99.99% | 4.3 分钟 | 基础设施层(DNS、网关) |
6.2 预算只监控不执行
仪表盘画了,告警配了,但发布流程没有任何代码去检查预算。结果是:运维知道预算快没了,产品不知道,CI/CD 也不管。
修复方法:在 CI/CD 流水线的 pre-deploy 阶段加一个 HTTP 调用,查询预算状态。allowed=false 就 exit 1。不需要复杂的实现,几十行代码就够了(参见第三章的 Go 代码)。
6.3 人工执行,没有自动化
有些团队"有规则但没有代码”。规则是"预算低于 25% 时冻结发布",但靠谁来执行?值班 SRE?他不一定在发布评审会上。产品经理?他没有预算仪表盘的访问权限。
必须用代码执行,不能靠人。人是最不可靠的执行者——尤其是在产品经理拿着老板的旨意来催的时候。
6.4 预算重置不复盘
30 天窗口结束,预算自动重置。但如果上个月预算烧了 120%,没有复盘就重置,下个月还会重蹈覆辙。
正确做法:每个 SLO 窗口结束时,如果预算超支(消耗 > 100%),必须做一次复盘——根因是什么?哪些变更消耗了最多预算?有没有系统性问题?不复盘的错误预算就是自欺欺人。
// BudgetWindowReview SLO 窗口结束时的自动复盘
type BudgetWindowReview struct {
ServiceName string
WindowStart time.Time
WindowEnd time.Time
BudgetConsumed float64 // 消耗百分比,可能 > 100
TopContributors []struct {
Incident string
BudgetImpact float64 // 该事件消耗的预算百分比
RootCause string
}
ActionItems []string
}
// GenerateReview 生成窗口复盘报告
func GenerateReview(serviceName string, windowDays int) *BudgetWindowReview {
review := &BudgetWindowReview{
ServiceName: serviceName,
WindowStart: time.Now().AddDate(0, 0, -windowDays),
WindowEnd: time.Now(),
}
// 从事件管理系统拉取窗口内所有事件
// 从 Prometheus 查询每个事件的预算影响
// 按消耗量排序,取 Top 3
// 生成行动项
return review
}
6.5 把预算当惩罚工具
运维拿错误预算去卡开发:“你看,预算没了,不能发了。“开发觉得被针对,下次想办法绕过——比如不报故障、手动重置计数器、把故障时间拆碎到不触发阈值。
错误预算是共享决策工具,不是运维的执法权。每次叫停发布时,要附上数据(消耗了多少、什么原因、预计什么时候恢复)和行动建议(先修什么、修完能不能发)。不是"不能发"三个字,而是"现在不能发,修完 X 问题预计明天可以发”。
七、错误预算仪表盘设计
7.1 核心面板
一个能用的错误预算仪表盘至少要包含以下 5 个面板:
| 面板 | 类型 | PromQL | 用途 |
|---|---|---|---|
| 剩余预算 | Gauge | 1 - (error_rate / (1 - slo_target)) | 一眼看出预算还剩多少 |
| 燃烧速率 | Time series | error_rate_1h / (1 - slo_target) | 看消耗趋势 |
| 30 天预算消耗曲线 | Time series | 累计消耗百分比 | 看长期趋势 |
| 按服务排名 | Bar gauge | 各服务剩余预算 | 快速扫描哪些服务有问题 |
| 错误事件标记 | Annotation | 事件时间线 | 关联预算消耗和变更事件 |
7.2 Grafana JSON 面板配置
以下是一个"剩余预算"面板的简化配置,可以直接导入 Grafana:
{
"title": "Error Budget Remaining",
"type": "gauge",
"datasource": "Prometheus",
"targets": [
{
"expr": "1 - (\n sum(rate(http_requests_total{code=~\"5..\",service=\"$service\"}[$__range]))\n /\n sum(rate(http_requests_total{service=\"$service\"}[$__range]))\n) / (1 - 0.999)\n* 100",
"legendFormat": "Remaining Budget %",
"refId": "A"
}
],
"fieldConfig": {
"defaults": {
"min": 0,
"max": 100,
"thresholds": {
"steps": [
{"color": "red", "value": 0},
{"color": "yellow", "value": 25},
{"color": "green", "value": 50}
]
},
"unit": "percent"
}
},
"options": {
"reduceOptions": {"calcs": ["lastNotNull"]}
}
}
7.3 一个容易忽略的面板:变更关联
光看预算消耗不够。你得知道每次预算下降对应了什么变更。方法是在 Grafana 中加一个 Annotation 面板,数据源接 CI/CD 系统的变更日志:
# Grafana Annotations 配置(通过 API 注入)
# 每次 CI/CD 发布时自动创建 annotation
annotations:
- name: "Deploy: user-center v2.3.1"
time: "2026-09-10T14:30:00Z"
tags: ["deploy", "user-center"]
text: "Release v2.3.1 - 拼车优惠叠加功能"
有了变更标注,你看到预算突然下降就能立刻关联到是哪次发布引起的。我做过一个统计:加了变更标注后,预算异常的根因定位时间从平均 25 分钟降到 8 分钟。这个数据和告警触达体系优化后的故障定位提速数据一致——可视化关联的价值远超想象。
总结
回顾全文,错误预算从"运维的仪表盘"变成"产品决策的硬约束”,需要三层工作:
第一层:翻译。把"剩余预算 13%“翻译成"还能扛 2.6 分钟中断、约 770 万订单损失”。产品经理看不懂百分比但看得懂钱。翻译本身比计算精度重要。
第二层:规则。设计三级响应机制——绿区正常发、黄区降频加灰度、红区冻结非紧急变更。规则要提前和产品、开发三方共同签署,不能等预算归零了才临时商量。规则要硬执行——用代码嵌入 CI/CD 流水线,不能靠值班 SRE 在微信群里喊"别发了"。
第三层:预测。用燃烧速率做领先指标,不要等预算归零才行动。14.4x 燃烧速率意味着 2 天内预算见底,在预算还剩 60% 时就启动响应,而不是等到 0% 才慌。多窗口双确认避免误报,1 小时 + 5 分钟组合兼顾灵敏度和准确性。
落地时记住五个不要:不要拍脑袋定 99.99% 的 SLO;不要只监控不执行;不要靠人工执行;不要不复盘就重置预算;不要把预算当武器去卡开发。
错误预算的本质不是技术工具,是组织决策机制。它让"要不要发版"从立场之争变成数据决策——当数据说不行时,不是运维在说不,是预算在说不。这种客观约束比任何人工审批都有效。
参考资料与致谢
本文在撰写过程中参考了以下资料,感谢原作者的贡献:
- The Site Reliability Workbook: Practical Ways to Implement SRE — Google SRE 团队,提供了多窗口燃烧速率告警方法和错误预算策略的标准框架
- Burn rate is a better error rate — Datadog,详细拆解了燃烧速率的定义、计算方式和实际应用场景
- What is an error budget—and why does it matter? — Atlassian,从事件管理视角解释了错误预算的业务价值
- Tools to manage SLOs and error budgets — InfoWorld,提供了 SLO 管理工具链的选型参考
- 错误预算(Error Budget)是什么?用SRE思维平衡稳定性与迭代速度 — ManageEngine,从产品迭代视角解读错误预算的组织价值
- Service Levels and Error Budgets — Chris Jones & Nialph Murphy (Google), SREcon 2016,阐述了 SLO 如何让产品经理、开发者和 SRE 在同一框架下协作