单体时代查问题只需看一个进程的日志;微服务时代,一次用户请求可能横跨十几个服务——“用户说慢,到底是哪个环节慢?“没有追踪根本无法回答。分布式追踪用"一束请求贯穿所有服务"的 Trace 视图,把跨服务调用链还原成一条可观测的链路。本指南从零讲清全链路可观测:Trace/Span/Context 传播(W3C traceparent)、OpenTelemetry 采集架构(OTLP/Collector/采样)、后端对比(Tempo/Jaeger/OTel)、RED/USE 指标、日志-追踪-指标关联、三类采样策略,以及 CI 链路追踪和常见坑。
目录
- 1. 为什么需要分布式追踪
- 2. Trace / Span / Context 核心概念
- 3. Context 传播:W3C traceparent
- 4. OpenTelemetry 采集架构:OTLP / Collector / 采样
- 5. 后端对比:Tempo / Jaeger / OTel
- 6. RED / USE 指标
- 7. 关联日志 - 追踪 - 指标
- 8. 采样策略:头部 / 尾部 / 一致性
- 9. CI 链路追踪与常见坑
1. 为什么需要分布式追踪
1.1 微服务排障的痛点
一次下单请求 → 网关 → 用户服务 → 库存 → 支付 → 消息队列 → 通知服务
传统手段失效:看日志串不起来、看指标不知道哪条链路、抓包太底层
追踪的价值:完整调用链(谁调谁、顺序、耗时)+ 每段耗时分解 + 错误传播定位
1.2 追踪的适用范围
| 场景 | 追踪的价值 |
|---|---|
| 跨服务延迟排查 | 定位"慢在哪一段” |
| 微服务拓扑发现 | 自动画出服务依赖图 |
| 错误传播分析 | 根因服务定位 |
| 容量与热点分析 | 高并发路径识别 |
| 异步/消息链路 | 串联 MQ 前后环节 |
💡 核心观点:指标回答"系统出了什么问题”,追踪回答"这一次请求到底经历了什么"。两者互补,构成"先看指标发现异常,再进追踪定位根因"的标准排障流。
2. Trace / Span / Context 核心概念
2.1 三个基本概念
Trace(追踪):一次请求的全链路,由一个 128-bit TraceID 标识
Span(跨度):Trace 里的一段工作单元(一次 HTTP 调用、一次 SQL 查询)
Context(上下文):TraceID + SpanID + 采样标志等,随调用传播
层级关系:一个 Trace = 一棵 Span 树
根 Span(入口)→ 子 Span(下游调用)→ 孙 Span(再下游)
2.2 Span 的字段
{
"name": "POST /orders",
"traceId": "4bf92f3577b34da6a3ce929d0e0e4736",
"spanId": "00f067aa0ba902b7",
"parentSpanId": "a1b2c3d4e5f6a7b8",
"kind": "SERVER",
"startTimeUnixNano": "1727334000000000000",
"endTimeUnixNano": "1727334001500000000",
"attributes": {
"http.request.method": "POST",
"url.path": "/orders",
"http.response.status_code": 200
},
"status": { "code": "OK" }
}
2.3 埋点方式对比
| 方式 | 原理 | 侵入性 |
|---|---|---|
| 手动埋点 | 代码里显式创建 Span | 高 |
| 自动插桩(SDK) | 库/框架自动生成 Span | 低 |
| Zero-code(Agent) | 运行时注入(如 Java Agent) | 最低 |
| 服务网格 | Sidecar 捕获网络流量 | 无代码,但缺业务语义 |
3. Context 传播:W3C traceparent
3.1 传播的必要性
Span 在服务 A 里创建后,调用服务 B 时必须把 Trace 上下文带过去:
否则服务 B 开一个新的 Trace,链路就断了
传播载体:HTTP 头 / MQ 消息头 / gRPC metadata
标准:W3C Trace Context(traceparent + tracestate)
3.2 traceparent 头格式
traceparent = "00-" + trace-id(32 hex) + "-" + span-id(16 hex) + "-" + flags(2 hex)
示例:
traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
│ └─ TraceID(全局唯一) └─ SpanID(当前 Span)
└ 版本 flags=01 表示"采样已决定,本 Trace 应被记录"
# curl 手动带 traceparent 测试(验证链路贯穿)
curl -H 'traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01' \
https://shop.example.com/api/orders
3.3 传播的常见丢失点
- 异步任务/线程池:上下文没随任务传递 → 新线程开新 Trace
- 消息队列:只传业务体不传消息头 → MQ 断链
- 网关重写 Header:反代删了 traceparent → 下游开新 Trace
- 不标准 SDK:自定义头(X-Request-Id)没有跨服务传递语义
4. OpenTelemetry 采集架构:OTLP / Collector / 采样
4.1 OpenTelemetry 是什么
OpenTelemetry(OTel)= CNCF 可观测性事实标准
统一 API/SDK + 统一传输协议 OTLP + Collector 数据处理网关
好处:一次埋点可导出到任意后端(Tempo/Jaeger/Prometheus/Loki)
4.2 OTel Collector 流水线
# otel-collector.yaml:接收 OTLP → 过滤/采样 → 导出到 Tempo
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
processors:
batch:
timeout: 5s
memory_limiter:
check_interval: 1s
limit_mib: 512
exporters:
otlp/tempo:
endpoint: tempo:4317
tls:
insecure: true
service:
pipelines:
traces:
receivers: [otlp]
processors: [memory_limiter, batch]
exporters: [otlp/tempo]
4.3 Agent / Collector / 后端三层
应用 SDK →(OTLP)→ Collector Agent(每节点,轻量)→(OTLP)→ Collector 中心 → 后端
- Agent:就近接收、预聚合、加节点标签
- 中心 Collector:集中做过滤、采样、多后端分发
- 后端:Tempo/Jaeger/OTel 自家的观察平台
5. 后端对比:Tempo / Jaeger / OTel
5.1 三大后端对比
| 后端 | 存储 | 查询 | 定位 |
|---|---|---|---|
| Grafana Tempo | 对象存储(S3/GCS/MinIO)+ TraceID 索引 | 按 TraceID / 按标签 | 与 Grafana 深度集成 |
| Jaeger | Elasticsearch/Badger/Cassandra | 按服务/操作/TraceID | 老牌、功能全 |
| OTel Collector 自带 | 无存储,转发到其他后端 | — | 中间层,不做查询 |
5.2 选型建议
- 已经在用 Grafana 全家桶 → Tempo(指标/日志/追踪一个 UI)
- 需要独立、成熟、多语言后端 → Jaeger
- 数据量极大 → 两者都依赖对象存储,Tempo 成本更优
- 中小团队 → Tempo 起步,运维最省
5.3 Tempo 快速部署
# docker-compose 片段:Tempo + Grafana
services:
tempo:
image: grafana/tempo:2.5.0
command: ["-config.file=/etc/tempo.yaml"]
volumes:
- ./tempo.yaml:/etc/tempo.yaml
grafana:
image: grafana/grafana:11.0.0
environment:
- GF_AUTH_ANONYMOUS_ENABLED=true
# 用 Grafana 的 TraceID 关联跳转:从日志直接点进追踪
# 查询语法(Explore → Traces)
{ resource.service.name = "shop-api" } && { span.http.route = "/orders" }
6. RED / USE 指标
6.1 两组黄金指标
RED(面向服务,来自 Weaveworks):
Rate(请求速率:QPS)
Errors(错误速率:错误 QPS / 错误率)
Duration(延迟分布:P50/P95/P99)
USE(面向资源,来自 Brendan Gregg):
Utilization(利用率:CPU/内存使用率)
Saturation(饱和:队列深度、负载)
Errors(错误:丢包、重试)
6.2 RED 指标的埋点示例
# OTel 自动生成 HTTP 服务指标(通过 instrumentation + metric SDK)
# 常见指标名:
# http.server.request.duration(直方图:延迟分布)
# http.server.request.count
# http.server.request.error_count
# Prometheus 查询示例:P95 延迟
histogram_quantile(0.95, sum(rate(http_server_request_duration_bucket[5m])) by (le))
# 错误率
sum(rate(http_server_request_error_count[5m])) / sum(rate(http_server_request_count[5m]))
6.3 追踪与指标的配合
排障三步曲:
1. 指标(RED)发现异常:P99 涨了 / 错误率飙升
2. 追踪定位根因:查这条慢 Trace 的每一段耗时
3. 日志看细节:该 Trace 关联的上下文日志
7. 关联日志 - 追踪 - 指标
7.1 三支柱关联的必要性
日志没有 TraceID → 无法把"一次请求的日志"串起来
追踪没有日志上下文 → 看到慢链路却不知道具体报错
指标没有追踪链接 → 看到异常只能盲猜
解法:让三者共享同一 TraceID
日志字段 + trace_id + span_id
指标标签 + trace_id(或至少 trace 聚合维度)
7.2 日志注入 TraceID
# Python OTel SDK:把当前 span 上下文注入日志
from opentelemetry import trace
import logging, json
span = trace.get_current_span()
ctx = span.get_span_context()
logging.info(json.dumps({
"msg": "order created",
"order_id": "o-123",
"trace_id": format(ctx.trace_id, "032x"),
"span_id": format(ctx.span_id, "016x"),
}))
# 结构化日志产出(概念)
{"ts": "2026-09-27T11:00:01Z", "level": "INFO", "service": "shop-api",
"trace_id": "4bf92f3577b34da6a3ce929d0e0e4736",
"span_id": "00f067aa0ba902b7", "msg": "order created", "order_id": "o-123"}
7.3 一键跳转实践
- Loki 日志里点 trace_id → 跳 Tempo 看该 Trace
- Tempo 的 Trace 里点日志 → 跳 Loki 看关联日志
- Grafana 把 Logs/Metrics/Traces 三个数据源关联起来
- 排障从"翻日志"升级为"从异常指标/日志一键进链路"
8. 采样策略:头部 / 尾部 / 一致性
8.1 为什么必须采样
高 QPS 全量存 Trace:存储爆炸、索引查询成本高、大部分 Trace 价值低
采样目标:以最小存储覆盖最大排障价值
8.2 三种采样对比
| 策略 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| 头部采样 | 请求进入时按概率决定整条 Trace 是否记录 | 实现简单、省资源 | 低流量错误可能漏掉 |
| 尾部采样 | 结束时按规则决定(错误/慢请求必存) | 错误/慢链路不丢 | 需缓存全量 span、成本高 |
| 一致性采样 | 跨服务共享同一采样决策 | 避免"半条 Trace" | 需传播采样上下文 |
8.3 OTel Collector 采样配置
processors:
tail_sampling:
decision_wait: 10s
policies:
# 错误 Span 必存
- name: errors
type: status_code
status_code: ERROR
# 慢请求必存(延迟 > 1s)
- name: slow
type: latency
latency:
threshold_ms: 1000
# 其余按 10% 概率
- name: random
type: probabilistic
probabilistic:
sampling_percentage: 10
混合策略(推荐):
错误 + 慢请求 100% 保留,正常流量按 5%~10% 采样
既控制成本,又保证"排障要用的数据"基本都在
9. CI 链路追踪与常见坑
9.1 把追踪用在 CI 上
CI 也是"一次任务横跨多步/多机/多服务":
- 把一次构建/测试/部署当作一个 Trace
- 每步(checkout/build/test/deploy)是一个 Span
- 挂到 Tempo/Jaeger → 看 CI 时间花在哪一步
价值:CI 慢的根因(网络/缓存/单测耗时)可视化,而不是靠猜
# GitHub Actions 上报 OTLP 追踪(概念)
- name: Run tests with tracing
uses: actions/github-script@v7
with:
script: |
const span = await trace.startSpan('run-tests');
try { await runTests(); } finally { span.end(); }
9.2 常见坑
| 坑 | 现象 | 对策 |
|---|---|---|
| 上下文不传 | 链路断成几截 | 统一 W3C 标准 + 异步/MQ 显式传递 |
| 不采样 | 存储爆炸 | 尾部/混合采样 |
| 采样率太低 | 排障没有数据 | 错误/慢请求必存 |
| 埋点只加延迟 | 缺业务属性 | 加 http/业务属性 |
| 日志没带 TraceID | 无法关联 | 结构化日志注入上下文 |
| 后端选错 | 查询很痛苦 | 按 Grafana 生态/独立后端选型 |
9.3 一句话原则
分布式追踪 = "给每次请求发一张贯穿全链路的身份证,
让排障从大海捞针变成按图索骥。"
小结
分布式追踪与全链路可观测 = 统一埋点(OpenTelemetry SDK)→ 标准传播(W3C traceparent)→ 网关采集(OTLP + Collector)→ 后端存储(Tempo/Jaeger)→ 与指标/日志关联(TraceID 贯穿)→ 采样控成本(错误/慢请求必存)。落地记住五件事:统一用 OTel 避免厂商锁定、Context 传播要覆盖异步与 MQ 防断链、TraceID 注入日志做三支柱关联、混合采样保住"排障数据"、RED 指标 + 追踪 + 日志构成排障三件套。当一次"用户说慢"能在 5 分钟内从指标进到链路、再从链路进到日志定位根因,可观测性就从"事后解释"变成了"实时作战地图"——这正是全链路可观测对现代微服务的价值。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。