上线后排查问题时,我最头疼的就是:一个请求经过了 5 个服务,到底哪一步慢了?是数据库查询、第三方 API 还是内部 RPC?没有链路追踪,只能靠猜。这篇文章分享我们团队从零落地 Jaeger + OpenTelemetry 的完整过程,踩过的坑全在里面。
为什么需要分布式链路追踪?
在单体应用时代,一条日志 + 一个堆栈就能定位 90% 的问题。进入微服务后,一次用户请求可能经过:
用户 → API 网关 → 用户服务 → 订单服务 → 库存服务 → 支付服务
↓
消息队列 → 通知服务
这种情况下:
– 日志分散在 6 个服务的不同机器上
– 时间戳不统一(各服务时钟有偏差)
– 无法知道某个请求的完整执行路径
– 性能瓶颈埋在哪个服务完全不清楚
链路追踪就是为了解决这个问题:给每个请求一个唯一 ID,记录它在所有服务里的完整路径和耗时。
技术选型:为什么选 Jaeger + OpenTelemetry?
| 方案 | 优点 | 缺点 |
|---|---|---|
| Zipkin | 简单、轻量 | 生态较弱,功能有限 |
| SkyWalking | 功能全面、Java 友好 | 重,学习成本高 |
| Jaeger + OTel | 标准化、云原生、多语言 | 配置稍复杂 |
| 云厂商 APM | 开箱即用 | 厂商锁定,贵 |
为什么推荐 Jaeger + OpenTelemetry:
– OpenTelemetry 是 CNCF 毕业项目,已成为可观测性事实标准
– Jaeger 是 CNCF 毕业项目,Uber 开源,生产验证充分
– 一套 OTel SDK 可以同时输出到 Jaeger、Prometheus、Loki
– 开源免费,适合自建
架构设计
微服务应用(Python/Go/Java)
↓ OTel SDK(埋点)
OTel Collector(收集、过滤、转发)
↓
Jaeger(存储、查询、可视化)
↓
Elasticsearch(生产环境持久化存储)
第一步:用 Docker Compose 部署 Jaeger 全家桶
# docker-compose.yml
version: '3.8'
services:
# Elasticsearch - 生产级持久化存储
elasticsearch:
image: elasticsearch:8.11.0
environment:
- discovery.type=single-node
- xpack.security.enabled=false
- "ES_JAVA_OPTS=-Xms1g -Xmx1g"
ports:
- "9200:9200"
volumes:
- es_data:/usr/share/elasticsearch/data
# Jaeger All-in-One(生产环境拆分部署)
jaeger:
image: jaegertracing/all-in-one:1.54
environment:
- SPAN_STORAGE_TYPE=elasticsearch
- ES_SERVER_URLS=http://elasticsearch:9200
ports:
- "6831:6831/udp" # UDP thrift compact(遗留)
- "16686:16686" # Jaeger UI
- "14317:4317" # OTLP gRPC(推荐)
- "14318:4318" # OTLP HTTP
depends_on:
- elasticsearch
# OTel Collector - 收集、过滤、转发
otel-collector:
image: otel/opentelemetry-collector-contrib:0.92.0
command: ["--config=/etc/otel-collector-config.yaml"]
volumes:
- ./otel-collector-config.yaml:/etc/otel-collector-config.yaml
ports:
- "4317:4317" # OTLP gRPC 接收
- "4318:4318" # OTLP HTTP 接收
- "8888:8888" # Prometheus 指标暴露
depends_on:
- jaeger
volumes:
es_data:
# otel-collector-config.yaml
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
processors:
batch:
timeout: 1s
send_batch_size: 1024
# 采样:高流量服务只采集 10%,错误请求 100% 采集
probabilistic_sampler:
sampling_percentage: 10
# 添加资源属性
resource:
attributes:
- key: deployment.environment
value: "production"
action: upsert
exporters:
otlp/jaeger:
endpoint: jaeger:4317
tls:
insecure: true
# 同时导出 Prometheus 指标
prometheus:
endpoint: "0.0.0.0:8889"
service:
pipelines:
traces:
receivers: [otlp]
processors: [batch, resource]
exporters: [otlp/jaeger]
metrics:
receivers: [otlp]
processors: [batch]
exporters: [prometheus]
# 启动
docker-compose up -d
# 验证 Jaeger UI
curl http://localhost:16686
第二步:Python 服务接入(FastAPI 示例)
pip install opentelemetry-sdk \
opentelemetry-instrumentation-fastapi \
opentelemetry-instrumentation-sqlalchemy \
opentelemetry-instrumentation-redis \
opentelemetry-instrumentation-requests \
opentelemetry-exporter-otlp-proto-grpc
# tracing.py - 追踪初始化模块
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.sdk.resources import Resource
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor
from opentelemetry.instrumentation.redis import RedisInstrumentor
import os
def init_tracing(service_name: str, app=None, db_engine=None):
"""
初始化链路追踪
Args:
service_name: 服务名称,会显示在 Jaeger UI 的服务列表
app: FastAPI 应用实例
db_engine: SQLAlchemy engine 实例
"""
# 创建 Resource,标识服务身份
resource = Resource.create({
"service.name": service_name,
"service.version": os.getenv("APP_VERSION", "1.0.0"),
"deployment.environment": os.getenv("ENV", "development"),
})
# 创建 TracerProvider
provider = TracerProvider(resource=resource)
# 配置 OTLP 导出器(发送到 OTel Collector)
otlp_exporter = OTLPSpanExporter(
endpoint=os.getenv("OTEL_EXPORTER_OTLP_ENDPOINT", "http://localhost:4317"),
insecure=True, # 生产环境改为 False,配置 TLS
)
# 批量发送,减少网络开销
provider.add_span_processor(
BatchSpanProcessor(
otlp_exporter,
max_queue_size=2048,
max_export_batch_size=512,
export_timeout_millis=30000,
)
)
trace.set_tracer_provider(provider)
# 自动注入 FastAPI 中间件(追踪所有 HTTP 请求)
if app:
FastAPIInstrumentor.instrument_app(
app,
excluded_urls="health,metrics", # 健康检查不追踪
)
# 自动追踪数据库查询
if db_engine:
SQLAlchemyInstrumentor().instrument(engine=db_engine)
# 自动追踪 Redis 操作
RedisInstrumentor().instrument()
return trace.get_tracer(service_name)
# main.py - FastAPI 应用
from fastapi import FastAPI, Depends
from opentelemetry import trace
from tracing import init_tracing
import httpx
app = FastAPI(title="订单服务")
# 初始化追踪(应用启动时调用一次)
tracer = init_tracing(service_name="order-service", app=app)
@app.post("/orders")
async def create_order(order_data: dict):
# 获取当前 tracer
with tracer.start_as_current_span("create_order") as span:
# 添加自定义属性,方便在 Jaeger 中过滤
span.set_attribute("order.user_id", order_data.get("user_id"))
span.set_attribute("order.item_count", len(order_data.get("items", [])))
try:
# 调用库存服务(自动传递追踪 header)
inventory_result = await check_inventory(order_data["items"])
if not inventory_result["available"]:
span.set_attribute("order.result", "inventory_insufficient")
span.set_status(trace.StatusCode.ERROR, "库存不足")
return {"error": "库存不足", "details": inventory_result}
# 写入数据库(SQLAlchemy 自动追踪)
order_id = await save_order_to_db(order_data)
span.set_attribute("order.id", order_id)
span.set_attribute("order.result", "success")
return {"order_id": order_id, "status": "created"}
except Exception as e:
# 记录异常到追踪
span.record_exception(e)
span.set_status(trace.StatusCode.ERROR, str(e))
raise
async def check_inventory(items: list) -> dict:
"""调用库存服务,自动传播追踪上下文"""
# 使用 httpx,OTel 会自动注入 traceparent header
from opentelemetry.instrumentation.httpx import HTTPXClientInstrumentor
HTTPXClientInstrumentor().instrument()
async with httpx.AsyncClient() as client:
# traceparent 和 tracestate header 会自动注入
response = await client.post(
"http://inventory-service/check",
json={"items": items},
)
return response.json()
第三步:Go 服务接入
// tracing/tracing.go
package tracing
import (
"context"
"os"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
"go.opentelemetry.io/otel/sdk/resource"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
semconv "go.opentelemetry.io/otel/semconv/v1.21.0"
"google.golang.org/grpc"
)
func InitTracer(serviceName string) (*sdktrace.TracerProvider, error) {
ctx := context.Background()
endpoint := os.Getenv("OTEL_EXPORTER_OTLP_ENDPOINT")
if endpoint == "" {
endpoint = "localhost:4317"
}
// 创建 OTLP gRPC 导出器
exporter, err := otlptracegrpc.New(
ctx,
otlptracegrpc.WithEndpoint(endpoint),
otlptracegrpc.WithInsecure(),
otlptracegrpc.WithDialOption(grpc.WithBlock()),
)
if err != nil {
return nil, err
}
// 服务资源信息
res := resource.NewWithAttributes(
semconv.SchemaURL,
semconv.ServiceName(serviceName),
semconv.ServiceVersion(os.Getenv("APP_VERSION")),
)
tp := sdktrace.NewTracerProvider(
sdktrace.WithBatcher(exporter),
sdktrace.WithResource(res),
// 生产环境使用采样器
sdktrace.WithSampler(sdktrace.ParentBased(
sdktrace.TraceIDRatioBased(0.1), // 10% 采样
)),
)
otel.SetTracerProvider(tp)
return tp, nil
}
// 在 HTTP handler 中使用
func (h *OrderHandler) CreateOrder(w http.ResponseWriter, r *http.Request) {
tracer := otel.Tracer("order-service")
ctx, span := tracer.Start(r.Context(), "CreateOrder")
defer span.End()
// 设置自定义属性
span.SetAttributes(
attribute.String("order.user_id", r.Header.Get("X-User-ID")),
attribute.Int("order.priority", 1),
)
// 传递 context 给下游调用
result, err := h.inventoryClient.Check(ctx, items)
if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
http.Error(w, err.Error(), 500)
return
}
// ... 业务逻辑
}
第四步:配置采样策略(关键!)
生产环境不能全量采集,否则存储和性能开销不可接受。
# otel-collector-config.yaml - 高级采样配置
processors:
# 基于尾部的采样(Tail-based Sampling)
# 等请求完成后再决定是否保留,能捕获错误和慢请求
tail_sampling:
decision_wait: 10s # 等待 10 秒收集完整链路再决策
num_traces: 50000 # 内存中保留的链路数
policies:
# 错误请求:100% 保留
- name: errors-policy
type: status_code
status_code: {status_codes: [ERROR]}
# 慢请求(>2秒):100% 保留
- name: slow-traces-policy
type: latency
latency: {threshold_ms: 2000}
# 正常请求:只保留 5%
- name: normal-policy
type: probabilistic
probabilistic: {sampling_percentage: 5}
第五步:Jaeger UI 使用技巧
访问 http://localhost:16686,几个常用的排查场景:
场景 1:找慢请求
Service: order-service
Operation: POST /orders
Min Duration: 1s
Limit: 20
直接看哪个 span 耗时最长。
场景 2:排查某个用户的请求
Tags: order.user_id=12345
场景 3:对比两个版本的性能
在 “Compare” 功能里,选择相同操作的两条链路,一目了然。
生产踩坑记录
| 问题 | 原因 | 解决方案 |
|---|---|---|
| Span 丢失 | 网络抖动导致 gRPC 连接中断 | 启用 OTel SDK 的重试机制 + 本地 Collector |
| 存储暴涨 | 全量采集高流量服务 | 配置 tail_sampling,错误/慢请求全留,正常 5% |
| 链路断裂 | 消息队列消费时没有传播 context | 手动注入 traceparent 到消息 header |
| 时钟偏差 | 跨机器 Span 时间戳乱序 | 使用 NTP 同步,或在 collector 端修正 |
总结
落地链路追踪的关键步骤:
- 搭环境:Docker Compose 一键起 Jaeger + OTel Collector + Elasticsearch
- 接 SDK:Python/Go/Java 各有官方库,自动埋点 90% 的场景
- 配采样:tail_sampling 策略,确保错误和慢请求 100% 保留
- 用起来:排查问题时第一反应是查 Jaeger,而不是翻日志
链路追踪建立后,我们团队的平均故障定位时间从 45 分钟降到了 8 分钟。
👤 作者简介
一枚在大中原腹地(河南)卖公有云的从业者,主营腾讯云/阿里云/华为云,曾踩坑无数,现专注AI大模型应用落地。关注公众号「AI热点日报」,围观AI前沿动态~
博客:y1cloud.cn
