背景
很多团队的监控体系,起点都差不多:
- 接了 Prometheus
- 做了几个 Grafana 大盘
- 报警规则也写了一堆
结果线上一出问题,还是有两个老问题:
- 真故障没及时报
- 不重要的抖动疯狂报
本质原因通常不是“监控没接”,而是指标设计和告警语义不对。
尤其在 Go 微服务里,写 metrics 很容易,写出真正有业务意义的 metrics 并不容易。
先区分:监控数据不等于告警指标
你当然可以采很多数据,但不是所有数据都适合直接拿来告警。
比如这些指标:
- goroutine 数量
- GC 次数
- 进程 RSS
- 请求总量
它们都很有价值,但更适合作为排障上下文,而不是一上来就触发 Pager。
真正适合做核心告警的,通常是离用户体验更近的信号。
这就是 SLI / SLO 的意义。
什么是 SLI / SLO
简单说:
- SLI:你用什么指标衡量服务质量
- SLO:这个质量要达到什么目标
以一个 HTTP API 为例,最常见的两个 SLI 是:
- 可用性 成功请求比例是不是足够高。
- 延迟 请求耗时是不是在可接受范围内。
例如:
- 30 天内成功率不低于 99.9%
- 95% 的请求延迟低于 200ms
这两个目标就比“CPU 超过 80% 告警”更接近真实业务体验。
Go 服务里的基础指标
假设你已经有一个标准 HTTP 中间件,可以记录请求总量和耗时。
var (
requestsTotal = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "http_requests_total",
Help: "Total number of HTTP requests",
},
[]string{"method", "route", "status"},
)
requestDuration = prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Name: "http_request_duration_seconds",
Help: "HTTP request latency in seconds",
Buckets: []float64{0.01, 0.03, 0.05, 0.1, 0.2, 0.5, 1, 2, 5},
},
[]string{"method", "route"},
)
)
func promMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
rw := &responseWriter{ResponseWriter: w, statusCode: 200}
next.ServeHTTP(rw, r)
route := normalizeRoute(r.URL.Path)
requestsTotal.WithLabelValues(r.Method, route, strconv.Itoa(rw.statusCode)).Inc()
requestDuration.WithLabelValues(r.Method, route).Observe(time.Since(start).Seconds())
})
}
这里有两个关键点:
- 路由要做归一化,别直接拿原始 URL 当 label
- 延迟用 histogram,不要只打平均值
否则卡片页里带用户 ID、订单号这种动态 path,直接就把 Prometheus 基数打爆了。
定义可用性 SLI
一个最常见的可用性 SLI,是统计非 5xx 请求的比例。
PromQL 可以这样写:
sum(rate(http_requests_total{job="user-service",status!~"5.."}[5m]))
/
sum(rate(http_requests_total{job="user-service"}[5m]))
它的含义很直接:过去 5 分钟里,请求成功率是多少。
如果服务特性更复杂,也可以按业务语义定义,比如:
- 下单成功率
- 支付确认成功率
- 消息投递成功率
很多时候,业务 SLI 比通用 HTTP SLI 更有价值。
定义延迟 SLI
如果你用 histogram 记录了延迟,就可以统计分位值。
histogram_quantile(
0.95,
sum(rate(http_request_duration_seconds_bucket{job="user-service"}[5m])) by (le)
)
这个表达式用于观察 p95 延迟。
我更关注 p95 / p99,而不是平均值。原因很简单:用户感受到的卡顿,往往藏在长尾请求里,平均值经常会把问题“抹平”。
告警要基于错误预算,而不是一有波动就响
如果 SLO 是 99.9%,那就意味着你有一个允许消耗的错误预算。
真正靠谱的告警思路,不是“错误率超过阈值立刻报警”,而是看错误预算消耗速度是否异常。
例如:
- 短窗口错误率很高,说明正在发生严重故障
- 长窗口持续偏高,说明服务在稳定退化
一个常见策略是结合多窗口、多烧毁速率。
虽然 Prometheus 规则写起来稍微复杂一点,但效果远比固定阈值强。
不要忽略业务指标
基础系统指标只能告诉你“系统忙不忙”,不一定告诉你“业务有没有坏”。
很多 Go 服务都应该主动补一层业务指标,例如:
var orderCreatedTotal = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "order_created_total",
Help: "Total number of created orders",
},
[]string{"source", "result"},
)
func (s *OrderService) CreateOrder(ctx context.Context, req *CreateOrderRequest) error {
err := s.repo.Save(ctx, req)
if err != nil {
orderCreatedTotal.WithLabelValues(req.Source, "failed").Inc()
return err
}
orderCreatedTotal.WithLabelValues(req.Source, "success").Inc()
return nil
}
有了这种指标,你就能更快回答这些问题:
- 订单失败是在所有入口都发生,还是某个来源独有
- 成功率下降是系统性问题,还是某个业务链路问题
常见坑点
1. 标签维度过多
user_id、request_id、完整 URL 这种 label,基本都是高基数灾难。
2. 只采技术指标,不采业务指标
CPU、内存、QPS 都正常,不代表下单没坏、支付没坏。
3. 告警条件太敏感
短时间流量抖动、单实例重启、本地网络抖动,不应该频繁把人叫醒。
4. 图表很多,但没有服务目标
没有 SLO,Grafana 面板再漂亮,也很难指导你判断“现在算不算故障”。
总结
Go 服务的可观测性,真正有门槛的部分不在 SDK 接入,而在于:
- 你到底关心什么质量目标
- 哪些指标能代表用户体验
- 哪些告警值得把人叫醒
把这些问题想清楚之后,Prometheus、Grafana、Alertmanager 才真正开始发挥价值。
监控系统不是为了“看起来专业”,而是为了在服务出问题时,尽快把正确的人带到正确的位置上。
指标采集是起点,服务目标和告警语义才是监控体系真正的骨架。