最小可观测性:一个后端工程师的三件套

不追求大而全,追求「出事五分钟内定位范围」:日志先定字段再谈采集、指标只留能触发动作的、trace_id 关键路径必采、告警必须对应一个动作,外加从零落地的六步顺序。

线上出问题的时候,最怕的不是报错,而是你不知道该看哪里。我用过的"排查方式"从最早的 tail -f 一路进化到现在的三件套,中间交了不少学费。这篇讲一个后端工程师最小可用的可观测性怎么搭——不追求大而全,追求"出事时五分钟内能定位到范围"。

一、日志:先定字段,再谈采集

最早我的日志是这样的:

2026-09-23 08:00:01 开始处理订单
2026-09-23 08:00:01 用户 id=12
2026-09-23 08:00:02 完成

出了事完全查不动:没有请求标识,没法把一次请求的日志串起来;没有结构化字段,没法按 http 状态码或耗时过滤。

现在统一成结构化日志 + 固定字段(Go 用 log/slog):

slog.Info("订单创建完成",
    "trace_id", traceID,
    "uid", uid,
    "order_id", orderID,
    "amount_cents", amount,
    "cost_ms", cost.Milliseconds(),
)

固定字段就这几个,谁都别自己发明:

字段用途
trace_id串起一次请求在所有服务/所有 goroutine 里的日志
uid / tenant定位"是不是某个用户/租户的问题"
path + method + status + cost_ms访问日志,用于统计与告警
err错误对象(含类型与消息),不要只写字符串

三条纪律:不打敏感信息(密码、token、完整手机号);错误必须带上下文(哪个订单、哪个参数);一处的日志只打一次(避免每个函数都打个"开始/结束"把日志淹掉)。

二、指标:只留能触发动作的

指标容易走两个极端:要么只有 CPU 和内存,要么埋了 200 个指标没人看。我现在按 RED + 少量资源指标 来定:

  • Rate:请求量(按 path 分组,注意控制标签基数,别把 uid 放进去);
  • Errors:错误率(4xx/5xx 分开,5xx 才值得告警);
  • Duration:延迟分位(P50/P95/P99,别只看平均——平均会掩盖长尾)。

再加上和容量相关的几个:数据库连接池使用率、Redis 命令延迟、goroutine 数、GC 暂停时间。

配置里最常见的一个坑是标签基数爆炸:给指标打上 uid 或 order_id,几千个时间序列瞬间让 Prometheus 崩掉。规则是:标签只放取值范围有限的维度(服务、路由模板、状态码、错误类型)。

三、追踪:不求全量,求"关键路径有"

全量追踪(每个请求都采)成本高,我现在的做法是采样 + 关键路径必采:

  • 默认采样率 1%~5%;
  • 出错的请求全采(用 tail-based 或者"错误时补记");
  • 跨服务的入口必须生成/透传 trace_id:HTTP 放 header(X-Request-Id),gRPC 放 metadata,消息队列放消息属性,异步任务从入参带进来。

一个很实用的中间件写法(Go + Gin):

func TraceID() gin.HandlerFunc {
    return func(c *gin.Context) {
        id := c.GetHeader("X-Request-Id")
        if id == "" {
            id = uuid.NewString()
        }
        c.Header("X-Request-Id", id)
        ctx := context.WithValue(c.Request.Context(), traceKey{}, id)
        c.Request = c.Request.WithContext(ctx)
        c.Next()
    }
}

有了它,"这个用户反馈下单失败"就能变成一次精确查询:拿 trace_id 把跨服务日志一次捞出来。

四、告警:只留"我能做点什么"的

我删掉过一大批告警,包括"CPU 超过 80%"——因为收到了也不知道该干什么。现在留下的都有明确的动作:

告警触发条件收到之后做什么
接口错误率5xx 占比 > 1%(5 分钟窗口)看 trace 定位是哪个依赖挂了
延迟劣化P99 > SLO 阈值持续 5 分钟查下游、连接池、慢查询
队列积压待处理数持续增长 10 分钟看消费者是否卡住、是否要扩容
数据一致性红线某指标"必须恒为 0"却不为 0立即回滚或修数据

第二列写完你还是不知道该做什么的告警,就删掉——信噪比比覆盖率重要得多。

五、SLO:把它当作"该不该救火"的判据

SLO 不用搞得很复杂。我的做法是给最核心的 3~5 个接口各定一个:

下单接口:99% 的请求在 300ms 内成功(按 28 天窗口)
消息投递:99.9% 的消息在 1s 内到达在线设备

然后算错误预算:99% 意味着 28 天里可以"失败"约 6.7 小时。预算充足时,发布可以激进一点;预算快用完时,先停下来修稳定性,而不是继续叠新功能。

这条规则最大的价值是把"要不要发版"从吵架变成看数。

六、我实际的落地顺序(给从零搭的人)

  1. 结构化日志 + trace_id 中间件(半天,收益最大);
  2. 访问日志指标化(path/status/cost 三个标签),先能看曲线;
  3. 错误分类(把 5xx 按错误类型打标签),能看出"是哪一类在涨";
  4. 关键依赖的指标(DB 连接池、Redis 延迟、下游接口耗时);
  5. 告警只上三条(错误率、延迟、队列积压),跑一周再调阈值;
  6. SLO 和错误预算——最后做,因为它需要前三步的数据支撑。

小结

可观测性不是"装一套监控系统",而是在写代码的时候就把"出事时怎么查"设计进去:日志有统一字段、请求有贯穿的 trace_id、指标只放有限基数的维度、告警必须对应一个动作。这四件事的落地成本其实很低(我在这个站点上大概花了一天),但出事时的差别是"翻半小时日志"和"三分钟定位到某个依赖"。

返回博客列表