SLI/SLO/SLA 指标体系落地:从业务目标反推技术指标 SLI/SLO/SLA 指标体系落地从业务目标反推技术指标一、我们可用性是 99.9%但用户说经常打不开服务端监控面板上Prometheus 显示服务可用性 99.93%。但用户反馈群里几乎每周都有打不开很慢的投诉。矛盾在哪运维眼中的可用是 HTTP 200用户眼中的可用是页面在 2 秒内完整渲染。这两个定义的差距就是 SLI服务等级指标定义不当造成的——你测的不是用户感受到的东西。SLI/SLO/SLA 体系的问题不是要不要做而是怎么定义才能反映真实用户体验。这篇文章给出从业务目标到技术指标的完整落地路径。二、从业务目标到技术指标的推导链graph TD A[业务目标] -- B[SLAbr/服务等级协议] B -- C[SLObr/服务等级目标] C -- D[SLIbr/服务等级指标] D -- E[监控实现br/Prometheus/Grafana] A -- A1[目标: 用户留存率 85%] B -- B1[承诺: 99.9% 可用性br/(季度总计宕机 2h)] C -- C1[内部目标: 99.95% 可用性br/(比 SLA 更严格)] D -- D1[SLI 定义:] D1 -- D1a[请求成功率 99.95%] D1 -- D1b[P95 延迟 500ms] D1 -- D1c[错误预算: 季度 不超 21min] E -- E1[PromQL: 1 - (sum(rate(http_requests_total{status~5..}[5m])) / sum(rate(http_requests_total[5m])))] style A fill:#4A90D9,color:#fff style C fill:#F5A623,color:#000 style D fill:#50B86C,color:#fff核心概念定义SLIService Level Indicator可量化的指标。如请求成功率P95 延迟。SLI 必须能被监控系统直接测量不能是主观描述。SLOService Level ObjectiveSLI 的目标值。如请求成功率 ≥ 99.95%P95 延迟 ≤ 500ms。SLO 必须比 SLA 更严格给自己留缓冲空间。SLAService Level Agreement对用户的承诺。通常是 SLO 的放宽版本。未达 SLA 通常涉及赔款或补偿。错误预算Error BudgetSLO 允许的可接受失败。如 99.95% 可用性 每月允许 21.6 分钟的不可用时间。错误预算决定发布节奏——预算用完了就停止发布先提升稳定性。关键决策SLI 应该测什么错误 SLI 示例正确 SLI 示例为什么服务器 CPU 使用率请求成功率用户不关心 CPU只关心成不成功API 返回 200API 在 500ms 内返回正确数据200 可能是错误页面P50 延迟P95 / P99 延迟平均值掩盖了长尾问题每 5 分钟查一次客户端真实上报 服务端反查主动拨测 ≠ 真实用户体验三、生产级 SLI/SLO 体系实现SLI 指标采集 SLI 指标的 Prometheus 采集和 SLO 计算 from prometheus_client import Counter, Histogram, Gauge, CollectorRegistry import time from typing import Dict # 创建自定义 Registry避免和默认 registry 冲突 sli_registry CollectorRegistry() # SLI 1: 请求成功率 # Counter 不会因服务重启而丢失——Prometheus 的 rate() 函数 # 会基于两次采样之间的差值计算重启后的短暂缺失不影响 http_requests_total Counter( http_requests_total, Total HTTP requests, [method, path, status_code], registrysli_registry, ) # SLI 2: 请求延迟 # Histogram 自动计算分位数 # buckets 的选择很重要 # 桶过少 → 分位数不准 # 桶过多 → 时间序列爆炸 # 建议: [10ms, 25ms, 50ms, 100ms, 250ms, 500ms, 1s, 2.5s, 5s, 10s] http_request_duration Histogram( http_request_duration_seconds, HTTP request duration in seconds, [method, path], buckets[0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10], registrysli_registry, ) # SLI 3: 可用性不是简单的 UP/DOWN # 可用 服务能处理请求 且 核心依赖都连通 service_available Gauge( service_available, Whether the service is available (1available, 0unavailable), registrysli_registry, ) class SLIMiddleware: HTTP 中间件自动采集 SLI 指标 核心原则 - 只测最终用户的请求路径 - 过滤掉健康检查、内部探针等非用户请求 # 不计入 SLI 的路径健康检查、metrics、内部调用 NON_SLI_PATHS [ /health, /ready, /metrics, /internal/, /admin/, ] async def __call__(self, request, call_next): path request.url.path # 非用户请求不采集 if any(path.startswith(p) for p in self.NON_SLI_PATHS): response await call_next(request) return response # 记录开始时间 start time.time() try: response await call_next(request) status str(response.status_code) # 采集指标 http_requests_total.labels( methodrequest.method, pathself._normalize_path(path), status_codestatus, ).inc() # 延迟秒 duration time.time() - start http_request_duration.labels( methodrequest.method, pathself._normalize_path(path), ).observe(duration) return response except Exception as e: # 未捕获的异常也算 500 http_requests_total.labels( methodrequest.method, pathself._normalize_path(path), status_code500, ).inc() raise def _normalize_path(self, path: str) - str: 路径归一化将 ID 等变量替换为占位符 /users/123 → /users/:id /orders/abc-def → /orders/:order_id 为什么需要归一化 不归一化的话 /users/1 和 /users/2 是两个不同的时间序列 指标基数爆炸Prometheus 内存扛不住 import re # UUID path re.sub(r/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}, /:uuid, path) # 数字 ID path re.sub(r/\d, /:id, path) return pathSLO 定义与错误预算计算# slo-definitions.yaml # SLO 定义文件 slo_definitions: # SLO 1: 请求成功率 - name: http-availability description: HTTP 服务可用性5xx 视为不可用 metric: promql: | sum(rate(http_requests_total{status_code~2..|3..}[5m])) / sum(rate(http_requests_total[5m])) objective: 0.9995 # 99.95% window: 28d # 28 天滚动窗口 alerting: burn_rate: - rate: 14.4 # 1h 内消耗 2% 的错误预算 severity: critical - rate: 6 # 6h 内消耗 5% 的错误预算 severity: warning # SLO 2: 请求延迟 - name: http-latency-p95 description: HTTP 请求 P95 延迟 metric: promql: | histogram_quantile(0.95, sum(rate(http_request_duration_seconds_bucket[5m])) by (le, path) ) objective: 500 # P95 500ms objective_unit: ms # 单位毫秒 window: 7d alerting: threshold: - value: 800 # P95 800ms 告警 severity: warning - value: 1200 # P95 1200ms 告警 severity: critical # SLO 3: 错误预算 - name: error-budget description: 月度错误预算剩余 metric: # 错误预算 (1 - SLO) × 总请求数 - 实际错误数 promql: | (1 - 0.9995) * sum(rate(http_requests_total[28d])) * 28 * 86400 - sum(rate(http_requests_total{status_code~5..}[28d])) * 28 * 86400 objective: 0 # 0 表示预算未用尽 objective_unit: errors window: 28d alerting: threshold: - value: 0.5 # 预算消耗 50% 告警 severity: warning - value: 0.9 # 预算消耗 90% 告警接近用尽 severity: critical错误预算驱动的发布决策 错误预算驱动的发布决策引擎 核心理念 错误预算是指标的安全气囊。 预算充足 → 可以正常发布 预算不足 → 冻结发布先修稳定性 from dataclasses import dataclass from enum import Enum from typing import Optional import datetime class ReleaseGateDecision(Enum): 发布门禁决策 ALLOW allow # 允许发布 ALLOW_WITH_CAUTION caution # 允许但需要监控 BLOCK block # 冻结发布 dataclass class ErrorBudgetStatus: 错误预算状态 slo_name: str total_budget: float # 总预算如允许的错误次数 consumed: float # 已消耗 remaining: float # 剩余 consumption_rate: float # 消耗速率每小时 days_until_exhausted: Optional[float] # 按当前速率多少天后耗完 property def remaining_percent(self) - float: return self.remaining / self.total_budget * 100 if self.total_budget 0 else 0 class ErrorBudgetReleaseGate: 错误预算驱动的发布门禁 决策矩阵 | 预算剩余 | 消耗速率 | 决策 | |---------|---------|------| | 50% | 任意 | ALLOW | | 20-50% | 低 | ALLOW | | 20-50% | 高 | CAUTION | | 20% | 任意 | BLOCK | | 0% | 任意 | BLOCK 触发事件 | def __init__(self, prometheus_client): self.prom prometheus_client async def should_allow_release(self, service: str) - ReleaseGateDecision: 判断是否可以发布 budget_status await self._query_error_budget(service) if not budget_status: return ReleaseGateDecision.ALLOW # 查询失败不阻塞 remaining_pct budget_status.remaining_percent # 预算耗尽 → 硬阻断 if remaining_pct 0: await self._trigger_budget_exhausted_event(service) return ReleaseGateDecision.BLOCK # 预算告急 → 冻结 if remaining_pct 20: return ReleaseGateDecision.BLOCK # 预算紧张 消耗快 → 谨慎 if (remaining_pct 50 and budget_status.days_until_exhausted and budget_status.days_until_exhausted 7): return ReleaseGateDecision.ALLOW_WITH_CAUTION return ReleaseGateDecision.ALLOW async def _query_error_budget(self, service: str) - Optional[ErrorBudgetStatus]: 从 Prometheus 查询错误预算状态 # PromQL 查询剩余错误预算 query f (1 - 0.9995) * sum(rate(http_requests_total{{service{service}}}[28d])) * 28 * 86400 - sum(rate(http_requests_total{{service{service}, status_code~5..}}[28d])) * 28 * 86400 # 执行查询... pass async def _trigger_budget_exhausted_event(self, service: str): 错误预算耗尽触发事件通知 oncall、创建 Jira 工单 pass四、SLI/SLO 体系的实施边界缺点SLI 定义的辛普森悖论整体可用性 99.95% 不代表每个用户都有 99.95%。一个用户可能请求 10 次失败 5 次但在整体统计中被稀释。必须同时监控受影响用户比例。SLO 的多服务组合问题用户的一次请求可能经过 5 个微服务。如果每个服务 SLO 99.9%组合后的可用性 0.999^5 ≈ 99.5%。需要在架构层面用重试和降级来提升组合可用性。错误预算窗口的选择28 天窗口的错误预算在日常消耗缓慢但在最后一周可能突然耗尽。建议用多时间窗口7d/28d/90d同时监控。禁用场景非面向用户的后台系统如内部 CI 工具SLI/SLO 体系的维护成本超过收益。单用户的关键系统如个人数据分析报表不适用统计意义上的可用性。五、总结SLI/SLO/SLA 体系的落地路径从业务目标用户留存率反推 SLA99.9% 可用性内部设定更严格的 SLO99.95%定义可量化的 SLI请求成功率、P95 延迟用错误预算做发布门禁。三个关键工程要求SLI 必须反映真实用户体验非服务器内部指标路径归一化避免指标基数爆炸错误预算的多窗口监控防止月末耗尽效应。