背景

很多团队的监控体系,起点都差不多:

  • 接了 Prometheus
  • 做了几个 Grafana 大盘
  • 报警规则也写了一堆

结果线上一出问题,还是有两个老问题:

  • 真故障没及时报
  • 不重要的抖动疯狂报

本质原因通常不是“监控没接”,而是指标设计和告警语义不对

尤其在 Go 微服务里,写 metrics 很容易,写出真正有业务意义的 metrics 并不容易。

先区分:监控数据不等于告警指标

你当然可以采很多数据,但不是所有数据都适合直接拿来告警。

比如这些指标:

  • goroutine 数量
  • GC 次数
  • 进程 RSS
  • 请求总量

它们都很有价值,但更适合作为排障上下文,而不是一上来就触发 Pager。

真正适合做核心告警的,通常是离用户体验更近的信号。

这就是 SLI / SLO 的意义。

什么是 SLI / SLO

简单说:

  • SLI:你用什么指标衡量服务质量
  • SLO:这个质量要达到什么目标

以一个 HTTP API 为例,最常见的两个 SLI 是:

  1. 可用性 成功请求比例是不是足够高。
  2. 延迟 请求耗时是不是在可接受范围内。

例如:

  • 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_idrequest_id、完整 URL 这种 label,基本都是高基数灾难。

2. 只采技术指标,不采业务指标

CPU、内存、QPS 都正常,不代表下单没坏、支付没坏。

3. 告警条件太敏感

短时间流量抖动、单实例重启、本地网络抖动,不应该频繁把人叫醒。

4. 图表很多,但没有服务目标

没有 SLO,Grafana 面板再漂亮,也很难指导你判断“现在算不算故障”。

总结

Go 服务的可观测性,真正有门槛的部分不在 SDK 接入,而在于:

  • 你到底关心什么质量目标
  • 哪些指标能代表用户体验
  • 哪些告警值得把人叫醒

把这些问题想清楚之后,Prometheus、Grafana、Alertmanager 才真正开始发挥价值。

监控系统不是为了“看起来专业”,而是为了在服务出问题时,尽快把正确的人带到正确的位置上。


指标采集是起点,服务目标和告警语义才是监控体系真正的骨架。