分布式追踪是微服务架构的「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 期间性能恶化,导致大量重试。
建议:降低超时阈值 + 熔断降级 + 增容。
参考与延伸阅读
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。