分布式追踪实战:Jaeger、Tempo 与采样策略

系统性分布式追踪实战指南:Trace 数据模型(Span/Trace/Context/Links)、分布式追踪头传播(W3C Trace Context / B3 / Jaeger)、Jaeger Agent/Collector/Query/UI 部署架构、Tempo 低成本对象存储架构、头部采样 vs 尾部采样 vs 自适应采样、TraceQL 查询语言、Span/Event/Attribute 最佳实践、Trace 与 Metrics/Logs 关联(Exemplars/OpenTelemetry)、性能开销分析与优化、追踪驱动的延迟分析(关键路径/火焰图)、全链路压测追踪。附 Go/Java/Node.js 实战与 Kubernetes 部署 YAML。

分布式追踪是微服务架构的「X光机」。 当一次用户请求拆分成 50 个服务调用、经过 3 个消息队列、访问 5 个数据库时,没有追踪你就只能盲人摸象。追踪让你看到完整的请求路径、精确到微秒的延迟分布、以及跨服务边界的数据流。


一、Trace 数据模型

1.1 Span 详解

Span = 单一操作的最小描述单元

┌─────────────────────────────────────────┐
│  Span                                   │
│  ├── Trace ID      = abc123... (16byte) │
│  ├── Span ID       = def456... (8byte)  │
│  ├── Parent Span ID = parent789          │
│  ├── Name          = "GET /api/orders"   │
│  ├── Kind          = SERVER / CLIENT     │
│  ├── Start Time    = 1690000000123456789 │
│  ├── End Time      = 1690000000156789012 │
│  ├── Duration      = 33.33ms             │
│  ├── Status        = OK / ERROR          │
│  ├── Attributes    = {                   │
│  │   http.method   = "GET"               │
│  │   http.route    = "/api/orders"       │
│  │   http.status_code = 200              │
│  │   db.system     = "postgresql"         │
│  │   db.statement  = "SELECT * FROM..."  │
│  │   net.peer.ip   = "10.0.1.23"         │
│  │ }                                       │
│  ├── Events        = [                   │
│  │   { name="cache miss", ts=t1,         │
│  │     attrs={cache.key="user:123"} },   │
│  │   { name="db query start", ts=t2 },   │
│  │   { name="db query end", ts=t3 }      │
│  │ ]                                       │
│  └── Links         = [                   │
│       { trace_id="xxx", span_id="yyy",    │
│         attrs={relationship="parent"} }   │
│    ]                                      │
└─────────────────────────────────────────┘

1.2 Trace 树形结构

Trace: trace_abc (用户下单请求, 总耗时 1.2s)
├── [Span: api-gateway]        0ms - 1200ms  SERVER
│   ├── [Span: auth-service]   2ms - 45ms    CLIENT → SERVER
│   │   └── [Span: redis-auth] 10ms - 35ms   CLIENT
│   ├── [Span: order-service]  50ms - 800ms   CLIENT → SERVER
│   │   ├── [Span: db-query]   60ms - 120ms   CLIENT
│   │   ├── [Span: inventory]  130ms - 200ms  CLIENT → SERVER
│   │   │   └── [Span: db-inv] 135ms - 180ms  CLIENT
│   │   └── [Span: payment]    250ms - 750ms  CLIENT → SERVER
│   │       ├── [Span: bank-api] 300ms - 700ms CLIENT
│   │       └── [Span: kafka]  710ms - 720ms  PRODUCER
│   ├── [Span: kafka-consume]  750ms - 800ms  CONSUMER
│   └── [Span: notification]   810ms - 1150ms CLIENT → SERVER
│       └── [Span: send-email] 900ms - 1100ms  CLIENT
└── [Span: analytics-async]    10ms - 50ms    INTERNAL

关键路径分析:api-gateway → order-service → payment → bank-api = 700ms
                     → notification → send-email = 300ms

二、传播协议

2.1 W3C Trace Context(推荐)

HTTP Header:
  traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-b7ad6b7169203331-01
               ↑  ↑                        ↑                      ↑              ↑
               │  version(2hex)            trace-id(32hex)       parent-id(16hex) flags(2hex)
               │                                                               └── 01=sampled
               └── 00 = version 0

  tracestate: vendor1=123,vendor2=456

2.2 B3 Propagation(Zipkin)

HTTP Headers:
  X-B3-TraceId: 4bf92f3577b34da6a3ce929d0e0e4736
  X-B3-SpanId: b7ad6b7169203331
  X-B3-ParentSpanId: 5b4185666d50d68d
  X-B3-Sampled: 1
  X-B3-Flags: 0

2.3 Jaeger Propagation

uber-trace-id: {trace-id}:{span-id}:{parent-span-id}:{flags}
uber-trace-id: 4bf92f3577b34da6a3ce929d0e0e4736:b7ad6b7169203331:5b4185666d50d68d:1

2.4 Go 传播代码

// 注入到 HTTP 请求
import "go.opentelemetry.io/otel/propagation"

propagator := propagation.TraceContext{}

// 注入
headers := make(http.Header)
propagator.Inject(ctx, propagation.HeaderCarrier(headers))
// headers 现在包含 traceparent 和 tracestate

// 提取
ctx = propagator.Extract(ctx, propagation.HeaderCarrier(req.Header))
parentSpanCtx := trace.SpanContextFromContext(ctx)

三、Jaeger 部署

3.1 架构模式

模式 1: All-in-One(开发测试)
  jaeger-all-in-one
    ├── Agent(可选)
    ├── Collector
    ├── Query
    └── UI (16686)

模式 2: 生产级(推荐)
  Agent(DaemonSet / Sidecar)
    └── 接收 UDP/gRPC → 转发给 Collector
  Collector(Deployment)
    └── 接收 → 处理 → 写入存储
  Query(Deployment)
    └── 从存储读取 → 提供 API
  UI(与 Query 同进程)

存储后端:
  - memory(测试)
  - badger(本地)
  - elasticsearch(生产推荐)
  - cassandra(高吞吐)
  - kafka(缓冲)

3.2 Kubernetes 部署

# jaeger-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: jaeger-collector
spec:
  replicas: 2
  selector:
    matchLabels:
      app: jaeger-collector
  template:
    metadata:
      labels:
        app: jaeger-collector
    spec:
      containers:
        - name: collector
          image: jaegertracing/jaeger-collector:latest
          args:
            - --es.server-urls=http://elasticsearch:9200
            - --es.index-prefix=jaeger
          ports:
            - containerPort: 14250  # gRPC
            - containerPort: 14268  # HTTP
            - containerPort: 9411   # Zipkin compatible
---
apiVersion: v1
kind: Service
metadata:
  name: jaeger-collector
spec:
  selector:
    app: jaeger-collector
  ports:
    - name: grpc
      port: 14250
      targetPort: 14250
    - name: http
      port: 14268
      targetPort: 14268
---
# DaemonSet Agent
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: jaeger-agent
spec:
  selector:
    matchLabels:
      app: jaeger-agent
  template:
    spec:
      containers:
        - name: agent
          image: jaegertracing/jaeger-agent:latest
          args:
            - --reporter.grpc.host-port=jaeger-collector:14250
          ports:
            - containerPort: 6831  # UDP compact thrift
            - containerPort: 6832  # UDP binary thrift
            - containerPort: 5778  # HTTP config

3.3 应用配置

# 应用通过 Agent 上报(DaemonSet 模式)
# 环境变量
JAEGER_AGENT_HOST: jaeger-agent
JAEGER_AGENT_PORT: "6831"
JAEGER_SERVICE_NAME: order-service
JAEGER_SAMPLER_TYPE: probabilistic
JAEGER_SAMPLER_PARAM: "0.1"

四、Grafana Tempo

4.1 Tempo 设计理念

Tempo 的核心设计:低成本存储,依赖对象存储(S3/GCS),通过标签索引实现查询。

Tempo 架构:
┌─────────────────────────────────────────────────┐
│                    Distributors                   │
│  接收 OTEL/gRPC / Jaeger / Zipkin               │
│  按 trace_id hash 分发到 Ingesters              │
└────────────────┬────────────────────────────────┘
                 │
        ┌────────┴────────┐
        ↓                 ↓
┌──────────────┐  ┌──────────────┐
│  Ingesters   │  │  Ingesters   │
│  (WAL + 内存) │  │  (WAL + 内存) │
└──────┬───────┘  └──────┬───────┘
       │                  │
       └────────┬─────────┘
                ↓
        ┌──────────────┐
        │   Compactor  │
        │  (压缩+索引)  │
        └──────┬───────┘
               ↓
        ┌──────────────┐
        │ Object Store │
        │ (S3/GCS/Azure)│
        └──────┬───────┘
               ↓
        ┌──────────────┐
        │   Queriers   │
        │  (查询服务)   │
        └──────────────┘

4.2 Tempo 配置

# tempo.yaml
server:
  http_listen_port: 3200
  grpc_listen_port: 9095

distributor:
  receivers:
    otlp:
      protocols:
        grpc:
          endpoint: "0.0.0.0:4317"
        http:
          endpoint: "0.0.0.0:4318"
    jaeger:
      protocols:
        grpc:
          endpoint: "0.0.0.0:14250"

ingester:
  lifecycler:
    ring:
      replication_factor: 3
      kvstore:
        store: memberlist

storage:
  trace:
    backend: s3
    s3:
      bucket: tempo-traces
      endpoint: s3.us-east-1.amazonaws.com
      region: us-east-1
    wal:
      path: /var/tempo/wal

4.3 TraceQL

TraceQL 查询示例:

# 按服务名查询
{resource.service.name = "payment-service"}

# 多条件
{resource.service.name = "api-gateway"}
  && duration > 2s
  && .http.status_code = 500

# 查询包含特定 Span 的 Trace
{span.http.route = "/api/checkout"}
  && span.http.method = "POST"

# 子查询
{resource.service.name = "order-service"}
  >> {resource.service.name = "payment-service"}
  >> {span.db.system = "postgresql"}

五、采样策略

5.1 头部采样(Head-based)

在请求入口处(第一个 Span)决定是否采样整棵树

优点:简单、低开销
缺点:无法根据结果采样(如保存所有错误 Trace)

实现:
  - 概率采样:1% / 10%
  - 限速采样:每秒最多 N 个 Trace
  - 基于属性:按用户等级采样

5.2 尾部采样(Tail-based)

先采集全部 Span,等 Trace 完成后根据整体特征决定是否保留

优点:精准保留异常/慢请求,99% 的 "垃圾" Trace 被丢弃
缺点:临时存储成本高,延迟(等 Trace 完成)

实现:OTel Collector 的 tail_sampling processor
# tail_sampling 配置
processors:
  tail_sampling:
    decision_wait: 10s          # 等 10s 看 Trace 是否完成
    num_traces: 100000          # 内存中保留的 Trace 数
    expected_new_traces_per_sec: 1000
    policies:
      # 策略 1:保存所有错误
      - name: errors
        type: status_code
        status_code: { status_codes: [ERROR] }
      # 策略 2:保存慢请求
      - name: slow
        type: latency
        latency: { threshold_ms: 2000 }
      # 策略 3:保存特定路由
      - name: important_routes
        type: string_attribute
        string_attribute:
          key: http.route
          values: ["/api/payment", "/api/checkout"]
      # 策略 4:概率采样兜底
      - name: probabilistic
        type: probabilistic
        probabilistic: { sampling_percentage: 1 }

5.3 自适应采样

Jaeger 自适应采样:
  - 自动调整每个服务的采样率
  - 目标:每个服务每秒保留固定数量的 Trace
  - 高流量服务降低采样率,低流量服务提高采样率

六、Exemplars:Trace-Metric 关联

// 在 Prometheus metrics 中附加 Exemplar(TraceID)
import "github.com/prometheus/client_golang/prometheus"

requestDuration := prometheus.NewHistogramVec(prometheus.HistogramOpts{
    Name:    "http_request_duration_seconds",
    Help:    "HTTP request latency",
    Buckets: prometheus.DefBuckets,
}, []string{"method", "status"})

// 记录时附加 TraceID
func recordRequest(ctx context.Context, duration float64, method, status string) {
    span := trace.SpanFromContext(ctx)
    traceID := span.SpanContext().TraceID().String()

    requestDuration.WithLabelValues(method, status).
        (prometheus.ExemplarObserver).
        ObserveWithExemplar(duration, prometheus.Labels{
            "trace_id": traceID,
        })
}
# 在 Grafana 中:点击直方图的 Exemplar 点 → 直接跳转到对应的 Trace
http_request_duration_seconds_bucket

七、性能优化

7.1 采样优化

策略CPU 开销存储开销精度
100% 采样极高完美
1% 概率一般
尾部采样
自适应采样可控

7.2 Batch Export

// 批量导出,减少网络请求
traceExporter, _ := otlptracegrpc.New(ctx,
    otlptracegrpc.WithEndpoint("otel-collector:4317"),
)

tp := sdktrace.NewTracerProvider(
    sdktrace.WithBatcher(traceExporter,
        sdktrace.WithBatchTimeout(2*time.Second),
        sdktrace.WithMaxExportBatchSize(512),
        sdktrace.WithMaxQueueSize(2048),
    ),
)

7.3 Span 数量控制

// 不要每个函数都创建 Span —— 关注跨越边界的操作
// ✅ 关注:
//  - HTTP/GRPC 请求
//  - 数据库查询
//  - 外部 API 调用
//  - 消息队列收发
//  - 缓存读写

// ❌ 不要:
//  - 纯内存计算
//  - 工具函数
//  - 日志打印

八、追踪驱动的问题排查

场景:用户反馈支付页面「偶尔」很慢

排查流程:
1. 在 Tempo 中查询:
   {span.http.route="/api/payment"} && duration > 3s

2. 发现一个高层 Trace(12s)
   ├── api-gateway: 0ms-12000ms
   │   ├── order-service: 5ms-500ms ✅
   │   └── payment-service: 550ms-11800ms ❌
   │       ├── bank-api: 600ms-11000ms ← 瓶颈!
   │       └── notification: 11100ms-11600ms

3. 查看 bank-api Span 的 Attributes:
   - http.url = "https://bank.example.com/v2/charge"
   - http.status_code = 200
   - retry_count = 3 ← 重试了 3 次
   - error.type = "timeout"

4. 查看 Events:
   - t+600ms: "request start"
   - t+3600ms: "timeout, retry 1"
   - t+6600ms: "timeout, retry 2"
   - t+9600ms: "timeout, retry 3"
   - t+11000ms: "success"

5. 查看相同时间段 Metrics:
   - bank-api 响应时间 P99 从 200ms 飙升到 3s
   - 同时 bank-api CPU 使用率从 30% 到 95%

结论:银行 API 在 14:30-14:50 期间性能恶化,导致大量重试。
       建议:降低超时阈值 + 熔断降级 + 增容。

参考与延伸阅读

继续阅读

探索更多技术文章

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

全部文章 返回首页

「infra」更多文章

  1. 可观测性数据存储选型:TSDB、列式存储、对象存储与成本优化
  2. 云原生 APM 与性能剖析:Continuous Profiling 与火焰图
  3. Kubernetes 可观测性实战:集群、Pod、网络、存储全链路监控