mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
2156 字
6 分钟
分布式追踪
2025-08-05

在微服务架构中,一个用户请求可能穿越 10 个甚至 50 个服务。当 P99 延迟飙升时,指标告诉你”慢了”,日志告诉你”错了”,但只有追踪能告诉你”请求在哪个服务的哪个操作上卡住了”。

分布式追踪是可观测性三大信号中最强大的排障工具——它记录了请求在分布式系统中的完整旅程,让你看到因果链而非孤立的事件。但追踪也是成本最高的信号,需要精心设计的采样策略来平衡可见性与成本。

一、追踪的核心概念#

1.1 从单机调用栈到分布式追踪#

在单体应用中,调用栈(Call Stack)就是天然的”追踪”——你可以看到函数 A 调用了函数 B,函数 B 调用了函数 C,每个调用耗时多少。但在分布式系统中,调用栈被网络边界切断了——函数 A 在服务 A,函数 B 在服务 B,它们之间是 HTTP/gRPC 调用,没有共享的调用栈。

分布式追踪的本质就是重建跨服务的调用栈

1.2 Trace 与 Span#

graph TB subgraph Trace["Trace: abc123"] S1["Span 1: API Gateway<br/>2000ms"] S2["Span 2: Order Service<br/>1800ms"] S3["Span 3: DB Query<br/>1700ms"] S4["Span 4: Cache Lookup<br/>5ms"] S5["Span 5: Payment Service<br/>150ms"] end S1 --> S2 S2 --> S3 S2 --> S4 S1 --> S5 style S1 fill:#e3f2fd,stroke:#1565c0 style S2 fill:#e8f5e9,stroke:#2e7d32 style S3 fill:#ffcdd2,stroke:#c62828 style S4 fill:#fff3e0,stroke:#e65100 style S5 fill:#f3e5f5,stroke:#6a1b9a
概念说明类比
Trace一个请求的完整旅程,由多个 Span 组成一次快递的完整物流记录
Span一个操作单元,有开始时间和结束时间物流中的一个节点(仓库、中转站)
SpanContextSpan 的身份信息,用于跨服务传播快递单号
Parent Span当前 Span 的父 Span上一个物流节点
Root Span没有父 Span 的 Span,是追踪的入口发件人

1.3 Span 的数据结构#

一个 Span 包含以下核心字段:

{
"trace_id": "abc123def45678901234567890123456",
"span_id": "789ghi01234567890",
"parent_span_id": "0123456789abcdef0",
"operation_name": "OrderService.ProcessOrder",
"kind": "SERVER",
"start_time": "2026-06-21T03:14:22.000Z",
"end_time": "2026-06-21T03:14:23.800Z",
"status": {
"code": "ERROR",
"message": "Database connection timeout"
},
"attributes": {
"http.method": "POST",
"http.url": "/api/v1/orders",
"http.status_code": 500,
"db.system": "postgresql",
"db.operation": "INSERT",
"error.type": "timeout"
},
"events": [
{
"name": "exception",
"timestamp": "2026-06-21T03:14:23.790Z",
"attributes": {
"exception.type": "java.sql.SQLException",
"exception.message": "Connection pool exhausted"
}
}
],
"links": [],
"resource": {
"service.name": "order-service",
"service.version": "v2.3.1",
"host.name": "pod-order-7b8c9"
}
}

1.4 Span Kind#

Kind说明典型场景
INTERNAL内部操作,不跨越服务边界函数调用、内部计算
SERVER服务端接收请求HTTP Handler、gRPC Server
CLIENT客户端发起请求HTTP Client、gRPC Client、DB Client
PRODUCER消息生产者Kafka Producer、RabbitMQ Publisher
CONSUMER消息消费者Kafka Consumer、RabbitMQ Subscriber

二、W3C TraceContext 传播标准#

2.1 为什么需要标准?#

在分布式追踪中,TraceID 需要在服务间传播。如果没有统一的标准,每个追踪系统都有自己的传播格式——Jaeger 用 uber-trace-id,Zipkin 用 X-B3-TraceId,AWS X-Ray 用 X-Amzn-Trace-Id。当你的系统同时使用多种追踪系统时,传播格式不兼容会导致追踪链路断裂。

W3C TraceContext 标准解决了这个问题——它定义了一个统一的传播格式,所有追踪系统都可以使用。

2.2 traceparent 格式#

traceparent: 00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01
│ │ │ │ │
│ │ │ │ └─ trace-flags
│ │ │ └─ parent-id
│ │ └─ trace-id
│ └─ version
└─ 格式前缀
字段长度说明
version2 hex版本号,当前为 00
trace-id32 hex追踪 ID,全局唯一
parent-id16 hex父 Span ID
trace-flags2 hex标志位,当前仅定义了采样标志(01 = 采样)

2.3 传播流程#

sequenceDiagram participant Client participant Gateway as API Gateway participant Order as Order Service participant DB as Database Client->>Gateway: GET /orders/123<br/>(无 traceparent) Note right of Gateway: 创建新 Trace<br/>trace-id: 0af76519...<br/>span-id: b7ad6b71... Gateway->>Order: GET /orders/123<br/>traceparent: 00-0af76519...-b7ad6b71...-01 Note right of Order: 继续传播<br/>创建子 Span<br/>span-id: c8be7c82... Order->>DB: SELECT * FROM orders<br/>traceparent: 00-0af76519...-c8be7c82...-01 Note right of DB: 创建子 Span<br/>span-id: d9cf8d93... DB-->>Order: Result Order-->>Gateway: Response Gateway-->>Client: 200 OK

2.4 代码实现#

// Go: W3C TraceContext 传播
import (
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/propagation"
"go.opentelemetry.io/otel/trace"
)
func makeHTTPRequest(ctx context.Context, url string) (*http.Response, error) {
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return nil, err
}
// 将当前 Span 上下文注入 HTTP 头部
// 这会自动设置 traceparent 和 tracestate 头部
otel.GetTextMapPropagator().Inject(ctx, propagation.HeaderCarrier(req.Header))
return http.DefaultClient.Do(req)
}
func handleHTTPRequest(w http.ResponseWriter, r *http.Request) {
// 从 HTTP 头部提取 Span 上下文
ctx := otel.GetTextMapPropagator().Extract(
r.Context(),
propagation.HeaderCarrier(r.Header),
)
// 创建子 Span
ctx, span := otel.Tracer("order-service").Start(ctx, "ProcessOrder")
defer span.End()
// 处理请求...
}

2.5 tracestate#

tracestate 是 W3C TraceContext 的可选头部,用于携带厂商特定的追踪信息:

tracestate: vendor1=value1,vendor2=value2

这允许不同追踪系统在同一个请求中携带各自的额外信息,而不影响 traceparent 的标准格式。

三、采样策略#

3.1 为什么需要采样?#

追踪的成本与数据量成正比。一个中等规模的微服务集群(100 个服务,10,000 RPS),如果全量采集追踪,每秒产生 10,000 条追踪,每条追踪平均 10 个 Span,每天就是 86.4 亿个 Span。存储和查询这个量级的数据成本极高。

采样是控制追踪成本的核心手段。

3.2 采样策略对比#

graph TB subgraph 采样策略 HEAD["头部采样<br/>决策点: 请求入口<br/>简单但可能丢失错误"] TAIL["尾部采样<br/>决策点: 请求完成<br/>智能但需要缓冲"] ADAPTIVE["自适应采样<br/>决策点: 动态调整<br/>最优但最复杂"] end HEAD --> TAIL --> ADAPTIVE style HEAD fill:#e3f2fd,stroke:#1565c0 style TAIL fill:#fff3e0,stroke:#e65100 style ADAPTIVE fill:#e8f5e9,stroke:#2e7d32
策略采样点优点缺点适用场景
概率采样请求入口简单、低延迟可能丢失错误请求低错误率服务
速率限制采样请求入口控制吞吐量可能丢失突发流量成本敏感场景
尾部采样请求完成保留错误请求需要缓冲、高延迟高价值服务
自适应采样动态最优成本/价值比实现复杂大规模生产环境

3.3 头部采样#

头部采样在请求入口决定是否采样,决策基于固定概率:

# OTel Collector: 概率采样
processors:
probabilistic_sampler:
sampling_percentage: 10 # 10% 采样率

问题:如果错误率只有 1%,而采样率是 10%,那么 90% 的错误请求不会被采集。

3.4 尾部采样#

尾部采样在请求完成后决定是否采样,决策基于请求的特征(错误、延迟等):

# OTel Collector: 尾部采样
processors:
tail_sampling:
decision_wait: 10s # 等待 10s 收集所有 Span
num_traces: 100000 # 缓冲区大小
expected_new_traces_per_sec: 1000
policies:
# 策略 1: 错误请求全量采集
- name: errors
type: status_code
status_code:
status_codes:
- ERROR
# 策略 2: 慢请求全量采集(> 1s)
- name: slow-requests
type: latency
latency:
threshold_ms: 1000
# 策略 3: 正常请求 10% 采样
- name: normal-requests
type: probabilistic
probabilistic:
sampling_percentage: 10
Warning

尾部采样需要 Collector 缓冲完整的追踪,这意味着 Collector 需要足够的内存来存储等待决策的追踪。decision_wait 越长,缓冲区越大,内存消耗越高。在生产环境中,建议从 5-10s 开始,根据实际情况调整。

3.5 自适应采样#

自适应采样根据流量模式动态调整采样率:

  • 高流量时段降低采样率
  • 低流量时段提高采样率
  • 错误/慢请求始终全量采集
  • 保证每秒采集的追踪数量大致恒定
# OTel Collector: 自适应采样(实验性)
processors:
adaptive_sampling:
sampling_rate:
min: 0.01 # 最低 1%
max: 1.0 # 最高 100%
target_throughput: 100 # 目标: 每秒 100 条追踪

四、追踪系统架构#

4.1 Jaeger 架构#

graph TB subgraph 应用[" 应用服务"] SDK1["OTel SDK"] SDK2["OTel SDK"] end subgraph 采集[" 采集层"] AGENT["Jaeger Agent<br/>UDP 14250"] COLLECTOR["Jaeger Collector<br/>:14268"] end subgraph 存储[" 存储层"] ES["Elasticsearch"] CASS["Cassandra"] MEM["内存"] end subgraph 查询[" 查询层"] QUERY["Jaeger Query<br/>:16686"] UI["Jaeger UI"] end SDK1 -->|"UDP"| AGENT --> COLLECTOR SDK2 -->|"HTTP"| COLLECTOR COLLECTOR --> ES COLLECTOR --> CASS QUERY --> ES QUERY --> CASS UI --> QUERY style 应用 fill:#e8eaf6,stroke:#283593 style 采集 fill:#e0f2f1,stroke:#00695c style 存储 fill:#fff3e0,stroke:#e65100 style 查询 fill:#e8f5e9,stroke:#2e7d32

4.2 Tempo 架构#

Tempo 是 Grafana 生态的追踪存储,设计理念是”仅索引 TraceID,其他全扫描”:

维度JaegerTempo
索引方式全索引(服务、操作、标签)仅索引 TraceID
存储成本高(索引开销大)低(仅存储原始数据)
搜索方式结构化搜索TraceID 查询 + TraceQL
对象存储不支持支持 S3/GCS
与 Grafana 集成一般深度集成

4.3 Tempo 配置#

tempo.yaml
server:
http_listen_port: 3200
distributor:
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4319
http:
endpoint: 0.0.0.0:4320
ingester:
max_block_duration: 5m
trace_idle_period: 10s
storage:
trace:
backend: local
local:
path: /var/tempo/traces
wal:
path: /var/tempo/wal
metrics_generator:
registry:
external_labels:
source: tempo
storage:
path: /var/tempo/generator/wal
remote_write:
- url: http://prometheus:9090/api/v1/write

五、追踪最佳实践#

5.1 Span 命名约定#

规则正确错误
使用动词 + 名词GET /api/v1/ordershandle_request
包含完整路由POST /api/v1/ordersPOST /api/v1/orders/12345
数据库操作带表名SELECT ordersdb_query
消息操作带主题PRODUCE order-eventskafka_send

5.2 关键属性#

每个 Span 应该包含以下属性:

属性说明示例
http.methodHTTP 方法GET, POST
http.url请求 URL(模板化)/api/v1/orders/{id}
http.status_code响应状态码200, 500
rpc.methodgRPC 方法OrderService/CreateOrder
db.system数据库类型postgresql, redis
db.operation数据库操作SELECT, INSERT
messaging.system消息系统kafka, rabbitmq
messaging.destination消息主题order-events

5.3 追踪反模式#

反模式 1:Span 太粗

// 反模式:一个 Span 覆盖整个请求处理
ctx, span := tracer.Start(ctx, "handleRequest")
defer span.End()
// ... 100 行代码 ...
// 最佳实践:关键操作各自创建 Span
ctx, span := tracer.Start(ctx, "handleRequest")
defer span.End()
ctx, dbSpan := tracer.Start(ctx, "db.QueryOrders")
// 数据库查询
dbSpan.End()
ctx, cacheSpan := tracer.Start(ctx, "cache.Lookup")
// 缓存查询
cacheSpan.End()

反模式 2:Span 太细

// 反模式:每个函数调用都创建 Span
ctx, span1 := tracer.Start(ctx, "validateInput")
// 验证输入
span1.End()
ctx, span2 := tracer.Start(ctx, "parseRequest")
// 解析请求
span2.End()
ctx, span3 := tracer.Start(ctx, "buildResponse")
// 构建响应
span3.End()

经验法则:一个 Span 应该对应一个有意义的操作——数据库查询、HTTP 调用、消息发送等。内部函数调用不需要 Span。

反模式 3:不传播上下文

// 反模式:启动 goroutine 时不传播上下文
go func() {
// 这个 goroutine 中的 Span 不会关联到原始追踪
processOrder(order)
}()
// 最佳实践:传播上下文
go func(ctx context.Context) {
ctx, span := tracer.Start(ctx, "processOrder")
defer span.End()
processOrder(ctx, order)
}(ctx)

六、TraceQL 查询语言#

Tempo 引入了 TraceQL,一种专门用于查询追踪的领域特定语言:

# 查找所有错误追踪
{ status = error }
# 查找特定服务的慢请求
{ service.name = "order-service" && duration > 1s }
# 查找包含数据库超时的追踪
{ span.db.system = "postgresql" && status = error }
# 查找跨服务的调用链
{ service.name = "api-gateway" } -> { service.name = "order-service" }
# 查找特定 HTTP 路由的请求
{ span.http.route = "/api/v1/orders" }
查询类型TraceQLPromQL(对比)
按服务过滤{ service.name = "x" }{service="x"}
按延迟过滤{ duration > 1s }histogram_quantile(0.99, ...)
按错误过滤{ status = error }rate(http_requests_total{status=~"5.."})
因果链查询{ A } -> { B }不支持

七、动手实践:搭建追踪系统#

7.1 部署 Tempo#

docker compose up -d tempo
# 验证 Tempo 运行
curl http://localhost:3200/ready

7.2 发送追踪数据#

# 通过 OTel Collector 发送追踪
curl -X POST http://localhost:4318/v1/traces \
-H "Content-Type: application/json" \
-d '{
"resourceSpans": [{
"resource": {
"attributes": [
{"key": "service.name", "value": {"stringValue": "order-service"}},
{"key": "service.version", "value": {"stringValue": "v2.3.1"}}
]
},
"scopeSpans": [{
"scope": {"name": "io.opentelemetry.sdk.trace"},
"spans": [{
"traceId": "0af7651916cd43dd8448eb211c80319c",
"spanId": "b7ad6b7169203331",
"parentSpanId": "",
"name": "GET /api/v1/orders",
"kind": 2,
"startTimeUnixNano": "1700000000000000000",
"endTimeUnixNano": "1700000002000000000",
"status": {"code": 1},
"attributes": [
{"key": "http.method", "value": {"stringValue": "GET"}},
{"key": "http.status_code", "value": {"intValue": "200"}}
]
}]
}]
}]
}'

7.3 在 Grafana 中查询#

# 打开 Grafana Explore
open http://localhost:3000/explore
# 查询方式:
# 1. 选择 Tempo 数据源
# 2. Search 模式:{ service.name = "order-service" }
# 3. TraceID 模式:0af7651916cd43dd8448eb211c80319c

7.4 验证清单#

检查项验证方式预期结果
Tempo 接收追踪发送测试追踪后查询能看到追踪详情
W3C TraceContext 传播跨服务请求追踪链路不断裂
采样策略生效检查采集量采样率符合预期
Grafana 集成Explore 页面查询能搜索和查看追踪
Warning

分布式追踪的 Span 数量与请求量成正比。在高流量系统中,务必配置采样策略(如概率采样或速率限制采样),否则追踪数据可能淹没存储后端。Tail-Based Sampling 可以在保留异常请求的完整追踪的同时,大幅减少正常请求的 Span 存储。

八、本章小结#

上一章探讨了指标体系与 Prometheus。 到这里,可观测性的第三个信号——分布式追踪的主要机制已经梳理清楚。

主题核心要点关键词
Trace 与 SpanTrace 是请求的完整旅程,Span 是操作单元。SpanContext 是跨服务传播的身份信息。Trace 与 Span
W3C TraceContext统一的传播标准,解决了多追踪系统不兼容的问题。W3C TraceContext
采样策略头部采样简单但可能丢失错误,尾部采样智能但需要缓冲,自适应采样最优但最复杂。采样策略
Jaeger vs TempoJaeger 全索引搜索灵活,Tempo 仅索引 TraceID 成本更低。Jaeger vs Tempo
最佳实践Span 命名用动词+名词,关键操作各自创建 Span,goroutine 中传播上下文。最佳实践

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

分布式追踪
https://blog.souloss.com/posts/observability/observability-distributed-tracing/
作者
Souloss
发布于
2025-08-05
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时