SLO 指标体系落地可用性别只拿 99.9% 说事一、我们 SLA 是 99.9%然后用户说昨天晚上又挂了99.9% 是很多团队的默认承诺。但这个数字掩盖了太多信息。如果一个月 99.9% 可用意味着宕机 43 分钟。这 43 分钟发生在周二下午 2 点还是周日凌晨 3 点前者是业务高峰影响巨大。后者几乎无人察觉。更关键的是不同功能的可用性要求不同。支付接口的 99.9% 和搜索建议的 99.9% 是两种要求。但很多团队没有区分。SLOService Level Objective就是解决这个问题的。SLA 是承诺给客户的违约有赔偿。SLO 是团队内部的质量目标用于指导投入。SLO 不是 SLA 的缩水版而是更精细的管理工具。99.9% 这个数字的问题在于它变成了一个安全感数字。团队说我们保证 99.9% 可用就好像给自己买了一份保险。但实际上这个数字的统计口径直接决定了它的实际含义。是在所有地域的所有时间窗口内都 99.9%还是全球平均是把 500 错误算故障还是把 5 秒以上的响应也算故障统计口径不同同样的 99.9% 意味着完全不同的用户体验。我在一次架构评审中看到过一个典型案例。一个 SaaS 产品的 SLA 写的是 99.9%但他们的统计口径只覆盖了 API 网关层面的 200 响应。后端服务返回空数据、数据库连接超时、缓存雪崩导致的降级响应——这些在网关层面都是 200不计入故障。结果就是监控大盘一片绿用户在 Twitter 上骂声一片。这就是 SLA 和 SLO 的致命区别。SLA 是静态的承诺SLO 是动态的工程指标。SLO 告诉你真实的服务质量水平而不是一个对外吹牛的数字。二、SLO 指标体系的四层金字塔flowchart TB A[业务目标] -- B[SLI: 服务等级指标] B -- C[SLO: 服务等级目标] C -- D[Error Budget: 错误预算] D -- E[决策: 继续发版 or 冻结修稳定性] subgraph SLI 指标分类 F[可用性: 成功率 ≥ 99.9%] G[延迟: P99 ≤ 200ms] H[吞吐: QPS ≥ 5000] I[质量: 错误率 ≤ 0.1%] end B -- F B -- G B -- H B -- I四类核心 SLI可用性成功响应 / 总请求数。延迟P50/P90/P99 响应时间。吞吐每秒处理请求数。质量业务层面的正确率。Error Budget 是 SLO 的核心创新。如果 SLO 是 99.9% 可用那允许 0.1% 的错误。这个 0.1% 就是 Error Budget。当 Error Budget 充足时可以快速迭代发版。当 Error Budget 耗尽时冻结发版专注稳定性。Error Budget 的巧妙之处在于它把质量和速度这对永恒矛盾变成了一个可量化的计分板。传统团队的质量讨论往往是感性的最近线上故障太多了先别发版了。但太多是多少谁的判断是准确的Error Budget 的回答是看数字不是看感觉。剩余预算 50% → 提速发版。剩余预算 20% → 刹车修稳定性。这就是 Google SRE 的核心哲学——用数据替代直觉。三、SLO 监控的 Go 实现下面的代码实现了一个轻量级的 SLI 计算器和 Error Budget 管理器。延迟分桶算法可以近似计算 P50/P90/P99适合嵌入到业务代码中做实时监控。package slo import ( fmt sync sync/atomic time ) // SLICalculator 服务等级指标计算器 type SLICalculator struct { totalRequests int64 // 总请求数 successRequests int64 // 成功请求数 errorRequests int64 // 错误请求数 // 延迟桶毫秒 latencyBuckets []int64 // [10, 50, 100, 200, 500, 1000] latencyCounts []int64 mu sync.RWMutex window time.Duration // 统计窗口 lastReset time.Time } // NewSLICalculator 创建 SLI 计算器 func NewSLICalculator(window time.Duration) *SLICalculator { return SLICalculator{ latencyBuckets: []int64{10, 50, 100, 200, 500, 1000, 5000}, latencyCounts: make([]int64, 8), // 1 为 5000ms window: window, lastReset: time.Now(), } } // RecordRequest 记录一次请求 func (s *SLICalculator) RecordRequest(success bool, latencyMs int64) { atomic.AddInt64(s.totalRequests, 1) if success { atomic.AddInt64(s.successRequests, 1) } else { atomic.AddInt64(s.errorRequests, 1) } // 延迟分桶 for i, boundary : range s.latencyBuckets { if latencyMs boundary { atomic.AddInt64(s.latencyCounts[i], 1) return } } // 超过最大边界 atomic.AddInt64(s.latencyCounts[len(s.latencyCounts)-1], 1) } // Availability 计算可用性 (0-1) func (s *SLICalculator) Availability() float64 { total : atomic.LoadInt64(s.totalRequests) success : atomic.LoadInt64(s.successRequests) if total 0 { return 1.0 } return float64(success) / float64(total) } // LatencyPercentile 计算延迟分位数 func (s *SLICalculator) LatencyPercentile(p float64) float64 { total : atomic.LoadInt64(s.totalRequests) if total 0 { return 0 } target : int64(float64(total) * p) var cumulative int64 for i, count : range s.latencyCounts { cumulative atomic.LoadInt64(count) if cumulative target { if i len(s.latencyBuckets) { return float64(s.latencyBuckets[i]) } return float64(s.latencyBuckets[len(s.latencyBuckets)-1]) * 2 } } return 0 } // P50 延迟 func (s *SLICalculator) P50() float64 { return s.LatencyPercentile(0.50) } // P90 延迟 func (s *SLICalculator) P90() float64 { return s.LatencyPercentile(0.90) } // P99 延迟 func (s *SLICalculator) P99() float64 { return s.LatencyPercentile(0.99) } // Report 生成 SLI 报告 func (s *SLICalculator) Report() string { return fmt.Sprintf( SLI Report:\n 总请求: %d\n 可用性: %.4f%%\n P50延迟: %.0fms\n P90延迟: %.0fms\n P99延迟: %.0fms\n 错误数: %d\n, atomic.LoadInt64(s.totalRequests), s.Availability()*100, s.P50(), s.P90(), s.P99(), atomic.LoadInt64(s.errorRequests), ) } // ErrorBudget 错误预算管理 type ErrorBudget struct { sli *SLICalculator target float64 // 目标可用性如 0.999 99.9% window time.Duration periodStart time.Time mu sync.Mutex } // NewErrorBudget 创建错误预算 func NewErrorBudget(sli *SLICalculator, target float64, window time.Duration) *ErrorBudget { return ErrorBudget{ sli: sli, target: target, window: window, periodStart: time.Now(), } } // RemainingBudget 剩余错误预算百分比 // 100% 预算充足, 0% 预算耗尽 func (eb *ErrorBudget) RemainingBudget() float64 { allowed : 1.0 - eb.target actual : 1.0 - eb.sli.Availability() if actual allowed { return 0 } return (allowed - actual) / allowed * 100 } // BurnRate 错误预算消耗速率 // 1 表示消耗快于预期 func (eb *ErrorBudget) BurnRate() float64 { elapsed : time.Since(eb.periodStart).Hours() if elapsed 1 { return 0 } expectedBurn : (1.0 - eb.target) * elapsed / eb.window.Hours() actualBurn : 1.0 - eb.sli.Availability() if expectedBurn 0 { return 0 } return actualBurn / expectedBurn } // Decision 根据错误预算做出决策 func (eb *ErrorBudget) Decision() string { remaining : eb.RemainingBudget() burnRate : eb.BurnRate() switch { case remaining 0: return 错误预算耗尽: 冻结所有发版全力修复稳定性 case remaining 30 burnRate 2: return 错误预算告急: 只允许紧急修复发版 case remaining 50: return 关注错误预算: 建议降低发版频率 default: return 错误预算充足: 正常迭代 } }BurnRate的设计尤其关键。只看剩余预算不够——如果预算还剩 80% 但过去 1 小时内消耗了 20%意味着按这个速度 4 小时后预算就耗尽了。BurnRate 结合剩余预算做判断才能提前发现问题而不是事后被动响应。四、SLO 实践的常见误区误区一盲目追求 99.99%。从 99.9% 到 99.99%成本可能增加 5-10 倍。先明确需要这么高可用性的理由。误区二所有功能同一套 SLO。支付接口的可用性要求 99.99%。用户头像加载的可用性 99% 就够。分层制定 SLO 节省大量资源。误区三只看没有 Error Budget。Error Budget 是 SLO 最有价值的部分。它把稳定性决策从主观感觉变成数据驱动。不适合精细 SLO 的场景日活 1000 的内部工具灰度阶段的新功能完全依赖第三方 API 的透传服务。还有一个容易被忽略的坑SLO 的滚动窗口和日历窗口之争。滚动窗口过去 30 天滚动是最推荐的方案——它平滑了时间边界避免了月末清零的作弊行为。日历窗口每月 1 号重置会让团队在月底掉以轻心反正快重置了月初又特别紧张一上来就用掉一大半。生产环境强烈建议用滚动窗口。另外SLO 的黄金指标组合是可用性 延迟。有些团队只监控可用性延迟再烂也不管。但用户在 5 秒响应和 500 错误之间的感受差别不大——都是这东西坏了。SLO 至少覆盖可用性和 P99 延迟两个维度否则就不是一个完整的服务质量视角。五、总结SLO 是对 SLA 的工程化拆解和内部管理工具。核心 SLI 包括可用性、延迟、吞吐、质量。Error Budget 是连接服务质量和发布节奏的桥梁。分层设定 SLO 避免对所有功能一刀切。SLO 的价值在于驱动决策而非仅仅监控数字。如果让我用一句话说服团队投入 SLOSLO 不是又一个监控图表它是一个决策引擎。它回答的不是现在服务好不好而是现在我能不能发版。把发版决策从老板拍板变成数据驱动这才是 SLO 最核心的价值。