线上出问题的时候,最怕的不是报错,而是你不知道该看哪里。我用过的"排查方式"从最早的 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 小时。预算充足时,发布可以激进一点;预算快用完时,先停下来修稳定性,而不是继续叠新功能。
这条规则最大的价值是把"要不要发版"从吵架变成看数。
六、我实际的落地顺序(给从零搭的人)
- 结构化日志 + trace_id 中间件(半天,收益最大);
- 访问日志指标化(path/status/cost 三个标签),先能看曲线;
- 错误分类(把 5xx 按错误类型打标签),能看出"是哪一类在涨";
- 关键依赖的指标(DB 连接池、Redis 延迟、下游接口耗时);
- 告警只上三条(错误率、延迟、队列积压),跑一周再调阈值;
- SLO 和错误预算——最后做,因为它需要前三步的数据支撑。
小结
可观测性不是"装一套监控系统",而是在写代码的时候就把"出事时怎么查"设计进去:日志有统一字段、请求有贯穿的 trace_id、指标只放有限基数的维度、告警必须对应一个动作。这四件事的落地成本其实很低(我在这个站点上大概花了一天),但出事时的差别是"翻半小时日志"和"三分钟定位到某个依赖"。