分布式链路追踪实战:Jaeger + OpenTelemetry 在微服务中的完整落地方案

阿里云上云采购季 2核2G云服务器99元/年 续费同价

上线后排查问题时,我最头疼的就是:一个请求经过了 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 端修正

总结

落地链路追踪的关键步骤:

  1. 搭环境:Docker Compose 一键起 Jaeger + OTel Collector + Elasticsearch
  2. 接 SDK:Python/Go/Java 各有官方库,自动埋点 90% 的场景
  3. 配采样:tail_sampling 策略,确保错误和慢请求 100% 保留
  4. 用起来:排查问题时第一反应是查 Jaeger,而不是翻日志

链路追踪建立后,我们团队的平均故障定位时间从 45 分钟降到了 8 分钟。


👤 作者简介

一枚在大中原腹地(河南)卖公有云的从业者,主营腾讯云/阿里云/华为云,曾踩坑无数,现专注AI大模型应用落地。关注公众号「AI热点日报」,围观AI前沿动态~

博客:y1cloud.cn

发表评论