Go服务应暴露两个Counter指标:http_requests_total和http_requests_slo_ok_total,由Prometheus用rate()计算滑动窗口SLO比率;禁用Gauge记录瞬时成功率,确保可回溯、可聚合、抗重启。

Go 服务怎么暴露 SLO 指标给 Prometheus
直接暴露 SLO 指标,不是靠“写个监控系统”,而是把 SLO 拆成可量化的 counter 和 histogram,让 Prometheus 自己算。关键在指标设计:SLO 是“成功请求数 / 总请求数”,所以必须同时暴露分子(成功)和分母(总数),不能只暴露成功率——否则无法做滑动窗口计算。
常见错误是用 Gauge 记录“当前成功率”,这会导致 Prometheus 无法回溯、无法聚合、无法应对重启丢数。正确做法是用两个 Counter:http_requests_total 和 http_requests_slo_ok_total,再在 Prometheus 中用 rate() 做比率。
- 分母用
http_requests_total{handler="api",status_code=~"2..|3.."},覆盖所有应计入 SLO 的请求 - 分子用
http_requests_slo_ok_total{handler="api"},只打点符合 SLO 定义的成功(比如 P95 - 不要在 Go 里算比率后上报——浮点精度丢失、重启归零、无法下钻标签维度
如何用 Go 判断单次请求是否满足 SLO 条件
判断依据不是“响应快”,而是“是否落入 SLO 承诺的延迟 + 状态组合”。例如 SLO 定义为“99% 请求 P99 ≤ 300ms 且状态码为 2xx”,那每次请求结束时,你要检查两个条件:实际耗时 ≤ 300ms 且 状态码匹配 2xx。缺一不可。
容易踩的坑是只看延迟、忽略状态码;或者把 4xx 当失败——但很多 4xx(如 400、404)是客户端问题,按 SLO 定义可能不计入分母(取决于你的 SLO 协议)。
立即学习“go语言免费学习笔记(深入)”;
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
- 用
http.ResponseWriter包装器(如responseWriter结构体)捕获状态码和耗时 - 延迟测量必须从
http.Handler入口开始,用time.Now(),别用中间件外的时间 - 记录分子指标前,先判断:
dur - 如果 SLO 分多级(如核心接口 99.9%,非核心 99%),需用不同指标名或标签隔离,避免混算
Go 中 histogram 指标怎么设 bucket 才对齐 SLO 目标
bucket 不是为了“画图好看”,而是为了支撑 Prometheus 的 histogram_quantile() 函数准确算 P90/P99。如果你的 SLO 要求 P99 ≤ 300ms,那么 300 必须是 bucket 边界之一,且前面要有足够细粒度(如 100ms、200ms),后面要有冗余(如 500ms、1s)——否则插值误差大,P99 可能虚高或虚低。
默认的 prometheus.DefaultHistogramBuckets(从 10ms 到 10s)对大多数 Web 服务太粗,P99 误差常超 ±50ms,无法用于 SLO 校验。
- 推荐 bucket:
[]float64{50, 100, 200, 300, 400, 500, 750, 1000, 2000}(单位 ms) - 务必包含 SLO 阈值本身(如
300),且至少比它小一级、大一级各一个 bucket - 避免用指数增长 bucket(如
10, 100, 1000),P99 在中间段抖动剧烈 - 如果服务有明显双峰延迟(如缓存命中 vs 未命中),考虑用多个 histogram 按标签拆分,而不是塞进一个
为什么 rate(http_requests_slo_ok_total[7d]) / rate(http_requests_total[7d]) 不等于 SLO 达成率
因为 SLO 是滑动窗口成功率,不是固定周期平均值。Prometheus 的 rate() 基于最近采样点做线性外推,7 天窗口下,只要中间有 1 分钟断采,整个 rate() 就会跳变甚至归零,导致分母突降、比率失真。真实 SLO 计算需要连续、对齐、无插值的原始计数。
真正靠谱的做法是用 increase() + 归一化时间范围,再配合 recording rule 预计算每日达标率,最后用 avg_over_time() 滑动聚合。但这要求数据保留充足(至少比最长 SLO 窗口长 2 倍),且采集间隔稳定(scrape_interval ≤ SLO 窗口的 1/10)。
- 错误写法:
rate(http_requests_slo_ok_total[7d]) / rate(http_requests_total[7d]) - 较准写法:
sum(increase(http_requests_slo_ok_total{job="api"}[7d])) / sum(increase(http_requests_total{job="api"}[7d])) - 但注意:
increase()在跨 scrape 间隔断点时仍可能低估,生产环境建议用 VictoriaMetrics 或 Thanos 的rollup功能补点 - 更稳方案:在 Go 里每分钟打一次
slo_window_success_count{window="28d",date="2024-06-01"},绕过 Prometheus 插值缺陷
最麻烦的不是埋点,是定义清楚“什么算一次有效请求”“哪些错误该排除在分母外”“时区与窗口对齐方式”。这些没共识,再准的指标也没意义。

















