为什么需要分布式追踪
微服务架构下,一个用户请求往往要穿越多个服务。当某个接口耗时从 200ms 飙升到 2s 时,日志散落在 N 台机器上,你很难判断瓶颈在哪一层——是网关转发慢、下游 DB 查询慢、还是某个服务间调用排队?
分布式追踪(Distributed Tracing)解决的就是这个问题:它为每个请求生成一个全局唯一的 Trace ID,在服务间透传,最终在 UI 上画出一条完整的调用链路树,让每一段耗时一目了然。
Jaeger(发音类似"耶格")由 Uber 开源,现为 CNCF 毕业项目。本文基于 Jaeger v1.60+ 和 OpenTelemetry SDK 进行实践。官方文档
分布式追踪核心概念
Trace
一个 Trace 代表一次完整的分布式请求链路,由唯一的 128-bit Trace ID 标识。它是一棵由多个 Span 组成的树形结构:
Trace (TraceID: a1b2c3...)
├── Span: HTTP GET /api/orders [gateway]
│ ├── Span: RPC GetUser [user-service]
│ │ └── Span: SELECT * FROM users [mysql]
│ └── Span: RPC GetOrderList [order-service]
│ └── Span: Redis GET [redis]
Span
Span 是追踪的最小单元,记录一次操作的开始和结束。关键字段:
| 字段 | 说明 |
|---|---|
TraceID | 所属 Trace 的全局唯一 ID |
SpanID | 当前 Span 的唯一 ID |
ParentSpanID | 父 Span ID,用于构建调用树 |
OperationName | 操作名称,如 GET /api/orders |
StartTime / Duration | 开始时间与耗时 |
Tags | 结构化标签,如 http.status_code=200 |
Logs | 时间戳事件,如异常堆栈 |
SpanKind | CLIENT / SERVER / PRODUCER / CONSUMER / INTERNAL |
Context Propagation
上下文传播是分布式追踪的基石。当 Service A 调用 Service B 时,必须将 TraceID、SpanID、采样标志等信息通过请求头传递给 Service B,这样 Service B 创建的 Span 才能挂到同一棵 Trace 树上。
OpenTelemetry 定义了两种标准传播格式:
- W3C TraceContext(推荐):通过
traceparent和tracestate请求头传递,格式为traceparent: 00-{trace-id}-{span-id}-{flags} - Baggage:通过
baggage请求头传递业务键值对(如user.id=12345),跨服务共享业务上下文
# W3C traceparent 格式示例
traceparent: 00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01
↑ ↑______________________↑ ↑________________↑ ↑
version trace-id parent-id flags
W3C TraceContext 规范详见 W3C Recommendation
OpenTelemetry SDK 接入
OpenTelemetry(简称 OTel)是 CNCF 的可观测性标准,统一了 Trace、Metrics、Logs 三大信号的 API 和 SDK。Jaeger 原生支持 OTLP 协议接收数据。
Go 服务接入
安装依赖:
go get go.opentelemetry.io/otel \
go.opentelemetry.io/otel/sdk \
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc \
go.opentelemetry.io/otel/trace \
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp \
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc
初始化 Tracer Provider:
package tracing
import (
"context"
"time"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
"go.opentelemetry.io/otel/propagation"
"go.opentelemetry.io/otel/sdk/resource"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
semconv "go.opentelemetry.io/otel/semconv/v1.24.0"
)
func InitTracer(ctx context.Context, serviceName, otelEndpoint string) (*sdktrace.TracerProvider, error) {
// 创建 OTLP gRPC exporter,指向 OTel Collector 或 Jaeger
exporter, err := otlptracegrpc.New(ctx,
otlptracegrpc.WithEndpoint(otelEndpoint),
otlptracegrpc.WithInsecure(),
)
if err != nil {
return nil, err
}
// 定义服务级资源信息,在 Jaeger UI 中可作为筛选条件
res, _ := resource.New(ctx,
resource.WithAttributes(
semconv.ServiceName(serviceName),
semconv.ServiceVersion("1.0.0"),
semconv.DeploymentEnvironment("production"),
),
)
tp := sdktrace.NewTracerProvider(
sdktrace.WithBatcher(exporter,
sdktrace.WithBatchTimeout(5*time.Second),
sdktrace.WithMaxExportBatchSize(512),
),
sdktrace.WithResource(res),
// 设置采样器(也可用 Remote/Adaptive Sampling 替代)
sdktrace.WithSampler(sdktrace.TraceIDRatioBased(0.1)),
)
otel.SetTracerProvider(tp)
// 使用 W3C TraceContext 作为标准传播格式
otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(
propagation.TraceContext{},
propagation.Baggage{},
))
return tp, nil
}
HTTP 服务自动埋点——通过 otelhttp 中间件,无需修改业务代码:
package main
import (
"context"
"log"
"net/http"
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
)
func main() {
ctx := context.Background()
tp, err := tracing.InitTracer(ctx, "order-service", "jaeger:4317")
if err != nil {
log.Fatal(err)
}
defer tp.Shutdown(ctx)
mux := http.NewServeMux()
mux.HandleFunc("/api/orders", handleGetOrders)
// 用 otelhttp.NewHandler 包装,自动为每个请求创建 Span
// 并从入站请求头提取 trace context
wrappedHandler := otelhttp.NewHandler(mux, "order-service")
log.Println("listening on :8080")
http.ListenAndServe(":8080", wrappedHandler)
}
func handleGetOrders(w http.ResponseWriter, r *http.Request) {
// 从 context 中获取当前 span,添加业务标签
span := otelhttp.SpanFromContext(r.Context())
span.SetAttributes(attribute.String("user.id", r.URL.Query().Get("uid")))
// 调用下游服务时,otelhttp 会自动注入 traceparent 头
resp, err := otelhttp.Get(r.Context(), "http://user-service:8081/api/user")
// ...
}
Python 服务接入
# pip install opentelemetry-distro opentelemetry-exporter-otlp
# opentelemetry-bootstrap -a install
from opentelemetry import trace
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.instrumentation.flask import FlaskInstrumentor
# 初始化 Tracer
resource = Resource.create({
"service.name": "user-service",
"service.version": "1.0.0",
"deployment.environment": "production",
})
provider = TracerProvider(resource=resource)
provider.add_span_processor(
BatchSpanProcessor(
OTLPSpanExporter(endpoint="jaeger:4317", insecure=True),
max_export_batch_size=512,
)
)
trace.set_tracer_provider(provider)
# Flask 自动埋点:每个 HTTP 请求自动生成 Server Span
app = Flask(__name__)
FlaskInstrumentor().instrument_app(app)
@app.route("/api/user")
def get_user():
# 手动创建子 Span 记录 DB 查询
tracer = trace.get_tracer(__name__)
with tracer.start_as_current_span("mysql.query.user") as span:
span.set_attribute("db.system", "mysql")
span.set_attribute("db.statement", "SELECT * FROM users WHERE id=?")
user = db.query("SELECT * FROM users WHERE id=?", uid)
return jsonify(user)
gRPC 上下文传递
gRPC 调用的链路传递通过拦截器实现,确保 metadata 中自动注入和提取 traceparent:
// gRPC Server 端:注册 otelgrpc 拦截器
import "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
server := grpc.NewServer(
grpc.StatsHandler(otelgrpc.NewServerHandler()),
)
// gRPC Client 端:同样通过 StatsHandler 自动注入上下文
conn, _ := grpc.Dial("user-service:50051",
grpc.WithStatsHandler(otelgrpc.NewClientHandler()),
)
这样 Service A 调用 Service B 的 gRPC 请求会自动携带 traceparent metadata,Service B 端的 Span 会自动挂在 Service A 的 Span 下。
Jaeger 部署
Docker Compose 快速部署
最简部署使用 All-in-One 镜像(适合开发测试),内置了 Collector + Query + 内存存储:
# docker-compose.yml
version: '3.8'
services:
jaeger:
image: jaegertracing/all-in-one:1.60
ports:
- "16686:16686" # Jaeger UI
- "4317:4317" # OTLP gRPC
- "4318:4318" # OTLP HTTP
- "5778:5778" # 配置传播(sampling)
environment:
- COLLECTOR_OTLP_ENABLED=true
- LOG_LEVEL=info
restart: unless-stopped
启动后访问 http://localhost:16686 即可看到 Jaeger UI。
Kubernetes 生产部署
生产环境推荐使用 Jaeger Operator,支持自动注入 Sidecar 和灵活的存储后端配置:
# jaeger-operator.yaml
apiVersion: jaegertracing.io/v1
kind: Jaeger
metadata:
name: jaeger-prod
namespace: observability
spec:
strategy: production
storage:
type: elasticsearch
options:
es:
server-urls: https://elasticsearch:9200
index-prefix: jaeger
secretName: jaeger-es-secret
# 启用 OTLP 接收端口
collector:
options:
collector.otlp.enabled: true
query:
options:
query.base-path: /jaeger
ingress:
enabled: true
hosts:
- jaeger.sre.wang
# 采样配置
sampling:
options:
sampling.type: remote
sampling.strategies-file: /etc/jaeger/sampling.json
部署 Operator 和实例:
# 安装 Jaeger Operator
kubectl create namespace observability
kubectl apply -f https://github.com/jaegertracing/jaeger-operator/releases/download/v1.60.0/operator.yaml -n observability
# 部署 Jaeger 实例
kubectl apply -f jaeger-operator.yaml
采样策略
在高流量系统中,追踪所有请求会产生巨大的数据量和性能开销。采样策略决定了哪些 Trace 会被记录和上报。
常量采样(Const)
100% 采样或完全不采样,仅适用于开发调试:
sdktrace.WithSampler(sdktrace.AlwaysSample()) // 全量
sdktrace.WithSampler(sdktrace.NeverSample()) // 关闭
概率采样(Probabilistic)
按比例采样,例如 10% 的请求被追踪:
sdktrace.WithSampler(sdktrace.TraceIDRatioBased(0.1))
远程采样(Remote Sampling)
Remote Sampling 允许 Collector 动态下发采样策略,无需重启服务即可调整采样率。这是生产环境的推荐方案。
首先在 Jaeger Collector 侧配置策略文件:
// /etc/jaeger/sampling.json
{
"service_strategies": [
{
"service": "order-service",
"operation_strategies": [
{
"operation": "GET /api/orders",
"probabilistic": { "samplingRate": 0.5 }
},
{
"operation": "POST /api/orders",
"probabilistic": { "samplingRate": 1.0 }
}
],
"default_strategy": {
"probabilistic": { "samplingRate": 0.1 }
}
},
{
"service": "user-service",
"default_strategy": {
"probabilistic": { "samplingRate": 0.05 }
}
}
],
"default_strategy": {
"probabilistic": { "samplingRate": 0.01 }
}
}
Go 侧使用 Remote Sampling 需要额外引入 jaegerclient 或通过 OTel Collector 的采样扩展实现。OTel SDK 层面,推荐通过 OTLP 上报后由 Collector 做尾部采样。
Adaptive Sampling(自适应采样)
Jaeger 独有的 Adaptive Sampling 会根据各操作的 QPS 自动调整采样率:QPS 高的操作降低采样率,QPS 低的操作提高采样率,确保每种操作都有足够样本用于分析。
# Jaeger Collector 启用 Adaptive Sampling
SAMPLING_TYPE=adaptive
SAMPLING_TARGET_SAMPLES_PER_SECOND=1.0
Adaptive Sampling 的原理和使用可参考 Jaeger Sampling 文档
实战:微服务链路分析与瓶颈定位
场景描述
一个电商下单请求涉及 4 个服务:
Gateway → OrderService → UserService (gRPC)
→ InventoryService (HTTP)
→ PaymentService (gRPC)
用户反馈下单接口偶发超时(P99 达到 5s),但各服务自身日志无报错。
分析过程
在 Jaeger UI 中筛选 Trace:按 Service=
gateway、Operation=POST /api/orders、min duration=3s筛选出慢请求 Trace查看 Trace 树形图:展开 Trace 详情,找到耗时最长的 Span
定位瓶颈:
Gateway: POST /api/orders 5230ms
├── OrderService: CreateOrder 5200ms ← 总耗时主要在这里
│ ├── UserService: GetUser 15ms ✓ 正常
│ ├── InventoryService: Lock 20ms ✓ 正常
│ └── PaymentService: Charge 5100ms ← 瓶颈!
│ └── DB: UPDATE transactions 5080ms ← 根因:DB 行锁等待
- 确认根因:PaymentService 的 Span Tag 中
db.statement=UPDATE transactions WHERE...,db.duration=5080ms。进一步查 PaymentService 的 DB 监控发现pg_locks在该时间段大量locktype=transactionid,确认是并发下单导致同一行记录锁等待。
Span Tags 好的实践
为便于排查,建议在关键路径上添加语义化标签:
// HTTP 调用
span.SetAttributes(
semconv.HTTPMethod("POST"),
semconv.HTTPRoute("/api/orders"),
semconv.HTTPStatusCode(200),
attribute.Int("order.amount", 299),
)
// DB 查询
span.SetAttributes(
semconv.DBSystem("postgresql"),
semconv.DBStatement("UPDATE transactions SET status=? WHERE id=?"),
attribute.Int("db.rows_affected", 1),
)
// 错误记录
import "go.opentelemetry.io/otel/codes"
span.SetStatus(codes.Error, "db connection timeout")
span.RecordError(err)
span.SetAttributes(attribute.String("error.type", "ConnectionTimeout"))
基于 Trace 的告警
结合 Prometheus + Tempo/Loki,可以将 Trace 指标化为告警。OTel Collector 提供 spanmetrics connector,自动将 Span 转为 Prometheus 指标:
# otel-collector-config.yaml
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
connectors:
spanmetrics:
histogram:
explicit:
buckets: [10ms, 50ms, 100ms, 500ms, 1s, 5s, 10s]
exporters:
prometheus:
endpoint: 0.0.0.0:8889
otlp/jaeger:
endpoint: jaeger:4317
tls:
insecure: true
service:
pipelines:
traces:
receivers: [otlp]
exporters: [spanmetrics, otlp/jaeger]
metrics/spanmetrics:
receivers: [spanmetrics]
exporters: [prometheus]
这样就能在 Prometheus 中查询 traces_span_metrics_duration_seconds_bucket 并配置告警:
# P99 延迟告警
histogram_quantile(0.99, sum(rate(
traces_span_metrics_duration_seconds_bucket{
service_name="order-service",
span_name="POST /api/orders"
}[5m]
)) by (le)) > 3
总结
分布式追踪是可观测性三大支柱(Metrics、Logs、Traces)中串联前两者的关键。实践建议:
- 统一传播格式:全链路使用 W3C TraceContext,避免异构系统间上下文断裂
- 标准化语义:遵循 OpenTelemetry Semantic Conventions 命名 Span Tag,保证可查询性
- 分级采样:核心链路高采样(甚至全量),非核心低采样;用 Remote/Adaptive Sampling 动态调整
- 关联 Metrics 和 Logs:将 TraceID 注入日志(如
log.WithField("trace_id", span.SpanContext().TraceID())),实现日志-链路双向跳转 - 安全意识:Span Tag 和 Baggage 中不要放入敏感信息(密码、Token、个人隐私数据),这些数据会持久化到存储后端
OpenTelemetry 官方文档:https://opentelemetry.io/docs/
参考资料与致谢
本文在撰写过程中参考了以下资料,感谢原作者的贡献:
- 官方文档 — Jaeger 项目,参考了docs相关内容
- W3C Recommendation — W3,参考了W3C Recommendation相关内容
- Jaeger Sampling 文档 — Jaeger 项目,参考了Jaeger Sampling 文档相关内容
- opentelemetry.io — OpenTelemetry 社区,参考了docs相关内容