分布式链路追踪:从日志拼查到 Trace 一键

单体时代排查:一个请求,一个日志文件,顺序看下来就行。微服务时代:一个请求经过 8 个服务,日志散在 8 个地方,“拼日志"是每个排查人的噩梦。链路追踪(Distributed Tracing)就是解决这个问题的。

一、痛点

用 痛 户 点 说 查 → → → 手 : 1 2 3 " 工 . . . 下 A 记 发 支 翻 单 P 下 现 付 跨 耗 异 失 I 它 服 6 服 时 常 败 o 调 务 务 不 链 了 网 r 了 又 个 无 知 路 " 关 d 支 调 服 法 道 无 日 e 付 了 务 串 在 法 → 志 r 服 优 联 哪 还 _ 务 惠 × ( ( 原 排 → i 券 每 网 ( 查 d → 服 各 个 关 出 开 找 务 自 服 错 始 到 → 查 . 的 务 8 的 : 请 支 . 时 只 0 完 求 查 付 . 间 知 0 整 订 服 线 道 m 调 单 务 自 s 用 服 日 = 己 , 路 务 志 的 哪 径 日 半 局 一 ) 志 小 部 段 时 ) 慢 起 ? 步 )

二、核心概念

Trace 与 Span

T S 下 ┌ │ │ │ └ r p 单 ─ ─ a a 请 ─ c n 求 a ├ │ │ ─ e : 的 p ─ ─ : 链 i ─ 一 路 T - o ─ 次 中 r g r ├ └ ─ 请 的 a a d ─ ─ ─ 求 一 c t e ─ 的 个 e e r p c ─ 完 环 : w - a o ─ 整 节 a s y u ─ 链 ( y e m p ─ 路 一 r e o ─ ( 个 ( v n n ─ 一 服 s i t - ─ 整 务 p c - s ─ 棵 的 a e s e ─ 树 一 n e r ─ ) 次 : ( r v ─ 调 s v i ─ 用 下 p i c ─ ) 单 a c e ─ 入 n e ─ 口 : ( ─ ) ( s ─ 创 s p ─ ─ 建 p a ─ ─ 订 a n ─ ─ 单 n : ─ ─ ) : ─ ─ 核 ─ ─ ─ 发 销 ─ ─ ─ 起 优 ─ ─ ─ 支 惠 ─ ─ ─ 付 券 ─ ─ ─ ) ) ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ │ ─ ┐ │ │ ─ ─ ─ ┘

三个 ID

t s p r p a a a r c n e e _ n _ i t i d _ d : s : 当 p 整 前 a 条 环 n 链 节 _ 路 唯 i 唯 一 d 一 ( : ( 每 父 请 层 环 求 生 节 入 成 ( 口 ) 构 生 成 成 树 , 结 全 构 链 ) 路 传 递 )

三、实现:上下文传递

关键:trace_id 必须在服务间传递(HTTP header / gRPC metadata / MQ 消息头)。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// 入口:生成 trace_id
func Middleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        traceID := c.GetHeader("X-Trace-Id")
        if traceID == "" {
            traceID = uuid.New()          // 入口生成
        }
        c.Set("trace_id", traceID)
        // 下发给下游
        c.Header("X-Trace-Id", traceID)
        c.Next()
    }
}

// 下游:从 header 取(没有就新生成——链路断了要告警)
traceID := c.GetHeader("X-Trace-Id")

gRPC 传递:

1
2
3
4
5
6
7
// 用 metadata 传递
md := metadata.Pairs("x-trace-id", traceID)
ctx = metadata.NewOutgoingContext(ctx, md)
// 服务端
if md, ok := metadata.FromIncomingContext(ctx); ok {
    traceID = md.Get("x-trace-id")[0]
}

MQ 传递:消息头带 trace_id(RabbitMQ headers / Kafka headers)。

四、采集与存储

采样策略(成本控制)

✅ ✅ ✅ 为 什 全 比 关 么 全 1 量 例 键 不 量 0 采 采 接 全 % 样 样 口 量 = : : 全 : 采 错 正 量 每 存 样 误 常 : 条 储 链 请 核 爆 + 路 求 心 S 炸 ( ( 链 p 错 E 1 路 a + 误 R 0 ( n 全 R % 支 写 采 O — 付 有 入 , R — / 压 能 / 统 下 2 力 覆 异 计 单 0 盖 常 够 ) + ) 了 9 ) 字 9 段 % , 排 查 场 景

存储选型

J Z S 自 a i k 建 e p y 轻 g k W 量 e i a : r n l C : : k l 开 老 i i 源 牌 n c , , g k E 轻 : H S 量 J o / a u C v s a a e s s 生 存 a 态 n 强 S d p r a a n ( 后 量 端 大 , 检 部 索 署 快 简 ) 单

五、落地效果

排 具 查 体 从 收 1 2 3 4 " 益 . . . . 半 : 小 慢 异 依 容 时 请 常 赖 量 拼 求 还 分 规 日 定 原 析 划 志 位 : : : " : 出 服 各 T 错 务 服 → r 请 调 务 a 求 用 实 " c 的 关 际 3 e 完 系 0 整 图 Q 瀑 调 ( P 秒 布 用 谁 S 看 图 链 依 / 全 看 ( 赖 耗 链 哪 哪 谁 时 路 段 个 、 统 " 耗 服 谁 计 时 务 最 ( 、 慢 一 什 ) 眼 么 看 参 到 数 支 ) 付 6 0 0 m s )

六、踩坑记录

  1. 链路断裂:下游没接 header → trace_id 丢失 → 入口生成了,下游自己建新 ID → 所有服务必须统一 SDK 传递
  2. 异步链路丢失:goroutine 里没传 context → 异步任务不在链路上 → 协程创建时带上 trace context
  3. 采样把错误丢了:只采样 10%,错误请求恰好没采到 → 错误必须全量采集
  4. Span 字段太多:采集 30 个字段 → 存储翻倍 → 只留关键字段(service/操作/耗时/状态/错误)

七、与日志/指标的配合

三 配 层 合 可 M L T 方 告 → → 观 e o r 式 警 测 t g a : 从 定 细 : r s c 指 位 节 i ( e 标 用 看 c 日 s 触 s 志 ( 发 T L ( ) 链 ( r o 指 : 路 错 a g 标 细 ) 误 c s ) 节 : 率 e ( : 排 全 ( 具 服 查 链 > 哪 体 务 ( 路 条 错 健 具 串 5 链 误 康 体 联 % 路 信 ( 报 ( ) 出 息 Q 错 请 错 ) P / 求 ) S 参 路 / 数 径 延 ) / 迟 — 耗 / — 时 错 微 分 误 观 布 率 ) ) — — — — 中 宏 观 观

总结

链路追踪的核心认知:

  1. Trace = 一次请求的完整树:trace_id 贯穿 + span 记录每个环节
  2. 上下文传递是地基:HTTP/gRPC/MQ 全链路传 trace_id,断了就是白做
  3. 采样要有策略:正常 10% + 错误全采 + 核心接口全量
  4. 三层配合:指标触发告警、链路定位、日志看细节

微服务排查的分水岭:会不会用 Trace。 有了全链路视图,“这个请求到底经历了什么"就不再是拼出来的,是看出来的。