在微服务架构中,一个用户请求可能穿越 10 个甚至 50 个服务。当 P99 延迟飙升时,指标告诉你”慢了”,日志告诉你”错了”,但只有追踪能告诉你”请求在哪个服务的哪个操作上卡住了”。
分布式追踪是可观测性三大信号中最强大的排障工具——它记录了请求在分布式系统中的完整旅程,让你看到因果链而非孤立的事件。但追踪也是成本最高的信号,需要精心设计的采样策略来平衡可见性与成本。
一、追踪的核心概念
1.1 从单机调用栈到分布式追踪
在单体应用中,调用栈(Call Stack)就是天然的”追踪”——你可以看到函数 A 调用了函数 B,函数 B 调用了函数 C,每个调用耗时多少。但在分布式系统中,调用栈被网络边界切断了——函数 A 在服务 A,函数 B 在服务 B,它们之间是 HTTP/gRPC 调用,没有共享的调用栈。
分布式追踪的本质就是重建跨服务的调用栈。
1.2 Trace 与 Span
| 概念 | 说明 | 类比 |
|---|---|---|
| Trace | 一个请求的完整旅程,由多个 Span 组成 | 一次快递的完整物流记录 |
| Span | 一个操作单元,有开始时间和结束时间 | 物流中的一个节点(仓库、中转站) |
| SpanContext | Span 的身份信息,用于跨服务传播 | 快递单号 |
| 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 └─ 格式前缀| 字段 | 长度 | 说明 |
|---|---|---|
| version | 2 hex | 版本号,当前为 00 |
| trace-id | 32 hex | 追踪 ID,全局唯一 |
| parent-id | 16 hex | 父 Span ID |
| trace-flags | 2 hex | 标志位,当前仅定义了采样标志(01 = 采样) |
2.3 传播流程
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 采样策略对比
| 策略 | 采样点 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 概率采样 | 请求入口 | 简单、低延迟 | 可能丢失错误请求 | 低错误率服务 |
| 速率限制采样 | 请求入口 | 控制吞吐量 | 可能丢失突发流量 | 成本敏感场景 |
| 尾部采样 | 请求完成 | 保留错误请求 | 需要缓冲、高延迟 | 高价值服务 |
| 自适应采样 | 动态 | 最优成本/价值比 | 实现复杂 | 大规模生产环境 |
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尾部采样需要 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 架构
4.2 Tempo 架构
Tempo 是 Grafana 生态的追踪存储,设计理念是”仅索引 TraceID,其他全扫描”:
| 维度 | Jaeger | Tempo |
|---|---|---|
| 索引方式 | 全索引(服务、操作、标签) | 仅索引 TraceID |
| 存储成本 | 高(索引开销大) | 低(仅存储原始数据) |
| 搜索方式 | 结构化搜索 | TraceID 查询 + TraceQL |
| 对象存储 | 不支持 | 支持 S3/GCS |
| 与 Grafana 集成 | 一般 | 深度集成 |
4.3 Tempo 配置
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/orders | handle_request |
| 包含完整路由 | POST /api/v1/orders | POST /api/v1/orders/12345 |
| 数据库操作带表名 | SELECT orders | db_query |
| 消息操作带主题 | PRODUCE order-events | kafka_send |
5.2 关键属性
每个 Span 应该包含以下属性:
| 属性 | 说明 | 示例 |
|---|---|---|
http.method | HTTP 方法 | GET, POST |
http.url | 请求 URL(模板化) | /api/v1/orders/{id} |
http.status_code | 响应状态码 | 200, 500 |
rpc.method | gRPC 方法 | 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 行代码 ...
// 最佳实践:关键操作各自创建 Spanctx, 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 太细
// 反模式:每个函数调用都创建 Spanctx, 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" }| 查询类型 | TraceQL | PromQL(对比) |
|---|---|---|
| 按服务过滤 | { 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/ready7.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 Exploreopen http://localhost:3000/explore
# 查询方式:# 1. 选择 Tempo 数据源# 2. Search 模式:{ service.name = "order-service" }# 3. TraceID 模式:0af7651916cd43dd8448eb211c80319c7.4 验证清单
| 检查项 | 验证方式 | 预期结果 |
|---|---|---|
| Tempo 接收追踪 | 发送测试追踪后查询 | 能看到追踪详情 |
| W3C TraceContext 传播 | 跨服务请求 | 追踪链路不断裂 |
| 采样策略生效 | 检查采集量 | 采样率符合预期 |
| Grafana 集成 | Explore 页面查询 | 能搜索和查看追踪 |
分布式追踪的 Span 数量与请求量成正比。在高流量系统中,务必配置采样策略(如概率采样或速率限制采样),否则追踪数据可能淹没存储后端。Tail-Based Sampling 可以在保留异常请求的完整追踪的同时,大幅减少正常请求的 Span 存储。
八、本章小结
上一章探讨了指标体系与 Prometheus。 到这里,可观测性的第三个信号——分布式追踪的主要机制已经梳理清楚。
| 主题 | 核心要点 | 关键词 |
|---|---|---|
| Trace 与 Span | Trace 是请求的完整旅程,Span 是操作单元。SpanContext 是跨服务传播的身份信息。 | Trace 与 Span |
| W3C TraceContext | 统一的传播标准,解决了多追踪系统不兼容的问题。 | W3C TraceContext |
| 采样策略 | 头部采样简单但可能丢失错误,尾部采样智能但需要缓冲,自适应采样最优但最复杂。 | 采样策略 |
| Jaeger vs Tempo | Jaeger 全索引搜索灵活,Tempo 仅索引 TraceID 成本更低。 | Jaeger vs Tempo |
| 最佳实践 | Span 命名用动词+名词,关键操作各自创建 Span,goroutine 中传播上下文。 | 最佳实践 |
支持与分享
如果这篇文章对你有帮助,欢迎支持作者或分享给更多人
部分信息可能已经过时






