分布式追踪与全链路可观测:OpenTelemetry、Tempo/Jaeger 与采样策略

深入讲解分布式追踪与全链路可观测的 DevOps 工程化:Trace/Span/Context 传播(W3C traceparent)、OpenTelemetry 采集架构(OTLP/Collector/采样)、Tempo/Jaeger/OTel 后端对比、RED/USE 指标、日志-追踪-指标关联、采样策略(头部/尾部/一致性)以及 CI 链路追踪与常见坑。

单体时代查问题只需看一个进程的日志;微服务时代,一次用户请求可能横跨十几个服务——“用户说慢,到底是哪个环节慢?“没有追踪根本无法回答。分布式追踪用"一束请求贯穿所有服务"的 Trace 视图,把跨服务调用链还原成一条可观测的链路。本指南从零讲清全链路可观测:Trace/Span/Context 传播(W3C traceparent)、OpenTelemetry 采集架构(OTLP/Collector/采样)、后端对比(Tempo/Jaeger/OTel)、RED/USE 指标、日志-追踪-指标关联、三类采样策略,以及 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 深度集成
JaegerElasticsearch/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 分钟内从指标进到链路、再从链路进到日志定位根因,可观测性就从"事后解释"变成了"实时作战地图"——这正是全链路可观测对现代微服务的价值。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「DevOps」更多文章

  1. 备份与容灾自动化:RPO/RTO、Velero、PITR 与恢复演练
  2. 配置漂移与安全基线:IaC漂移检测、CIS合规、供应链安全与密钥轮换
  3. 内部开发者平台(IDP)工程化:Backstage、Golden Path 与自服务能力