OpenTelemetry 追踪一个请求经过的所有微服务(含 propagation)

起因

微服务架构里一个用户请求穿过:API gateway → auth service → product service →
inventory service → recommendation service → DB / cache 各种。
某个请求慢了或失败,看不出是哪一跳。
日志里有 request id 但要手工去多个服务的 log 里搜,痛苦。

OpenTelemetry (OTel) 是 CNCF 的分布式追踪标准,让一个请求在所有服务里的
执行被串成"瀑布图",秒级定位慢 / 错的环节。

解决方案

1. 整体架构

service A → service B → service C
   |           |           |
   +-----------+-----------+
              ↓
       OTel Collector
              ↓
       Jaeger / Tempo
              ↓
         Grafana UI

每个 service 用 OTel SDK 生成 span(一段工作)。span 通过 HTTP header
跨服务传递(context propagation)。所有 span 发到 Collector,
后端(Jaeger / Tempo / DataDog)存储 + 显示。

2. Python 服务集成(FastAPI)

uv add opentelemetry-distro opentelemetry-exporter-otlp \
       opentelemetry-instrumentation-fastapi \
       opentelemetry-instrumentation-requests \
       opentelemetry-instrumentation-psycopg

启动时打开:

OTEL_SERVICE_NAME=my-api \
OTEL_TRACES_EXPORTER=otlp \
OTEL_EXPORTER_OTLP_ENDPOINT=http://collector:4317 \
opentelemetry-instrument uvicorn app.main:app

opentelemetry-instrument 自动 patch FastAPI / requests / psycopg /
SQLAlchemy / Redis / Kafka / 几十种库。零代码改动。

每个 HTTP 请求自动开 trace span,DB query / outgoing HTTP 自动是 child span。

3. 手动加 span(业务关键路径)

from opentelemetry import trace

tracer = trace.get_tracer(__name__)

def process_order(order_id: str):
    with tracer.start_as_current_span('process_order') as span:
        span.set_attribute('order.id', order_id)

        with tracer.start_as_current_span('validate'):
            validate(order_id)

        with tracer.start_as_current_span('charge_payment'):
            charge(order_id)

        with tracer.start_as_current_span('ship'):
            ship(order_id)

UI 里看到 process_order 总耗时 350ms,其中 validate 50ms / charge 280ms
/ ship 20ms。一眼定位 charge 是瓶颈。

4. Go 服务集成

import (
    "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.24.0"
    "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
)

func setupTracing(ctx context.Context) (*sdktrace.TracerProvider, error) {
    exp, err := otlptracegrpc.New(ctx,
        otlptracegrpc.WithEndpoint("collector:4317"),
        otlptracegrpc.WithInsecure(),
    )
    if err != nil {
        return nil, err
    }
    res := resource.NewWithAttributes(
        semconv.SchemaURL,
        semconv.ServiceName("my-go-svc"),
    )
    tp := sdktrace.NewTracerProvider(
        sdktrace.WithBatcher(exp),
        sdktrace.WithResource(res),
    )
    otel.SetTracerProvider(tp)
    return tp, nil
}

// HTTP server 加 middleware
mux := http.NewServeMux()
mux.Handle("/api/", otelhttp.NewHandler(yourHandler, "api"))

// 业务里
tracer := otel.Tracer("my-go-svc")
ctx, span := tracer.Start(ctx, "fetch_user")
defer span.End()
span.SetAttributes(attribute.String("user.id", uid))

5. 跨服务 propagation

服务间 HTTP 调用时自动透传 trace context(W3C traceparent header):

GET /products HTTP/1.1
Host: products-svc
traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01

接收方解析 → 在同 trace 下创建 child span → 追踪连续。

Python / Go / Java / Node SDK 都自动处理。

6. Collector 部署

otel-collector-config.yaml

receivers:
  otlp:
    protocols:
      grpc: { endpoint: 0.0.0.0:4317 }
      http: { endpoint: 0.0.0.0:4318 }

processors:
  batch:
    timeout: 5s
  memory_limiter:
    limit_mib: 1024

exporters:
  otlp/jaeger:
    endpoint: jaeger:4317
    tls: { insecure: true }
  logging: { loglevel: warn }

service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [memory_limiter, batch]
      exporters: [otlp/jaeger, logging]
docker run -p 4317:4317 -p 4318:4318 \
  -v $(pwd)/otel-collector-config.yaml:/etc/otelcol/config.yaml \
  otel/opentelemetry-collector-contrib

7. 后端:Jaeger / Tempo

# Jaeger(all-in-one,开发用)
docker run -p 16686:16686 -p 4317:4317 jaegertracing/all-in-one:latest

# 浏览器:http://localhost:16686
# 选 service → 查 traces → 点开看瀑布图

生产用 Tempo(与 Grafana / Loki 同生态)+ S3 后端。

8. 在 trace 里看到日志(trace + log correlation)

import logging
from opentelemetry.instrumentation.logging import LoggingInstrumentor

LoggingInstrumentor().instrument(set_logging_format=True)

logging.info('processing order %s', order_id)
# log 自动带上 trace_id / span_id
2026-05-24 10:00:01 [INFO] [trace_id=abc... span_id=def...] processing order o-123

Loki 配置识别 trace_id → 在 Grafana 里点 log 直接跳到对应 trace。
跨数据源关联无缝。

9. 采样

100% 采样在高 QPS 时数据爆炸。生产建议:

processors:
  probabilistic_sampler:
    sampling_percentage: 5    # 5% 采样

或更智能 tail sampling:保留所有 error trace + 慢 trace + 5% 普通 trace。

10. metrics + logs(统一 OTel)

OTel 不止 trace,还有 metric 和 log。一份 SDK 配置三种信号都收。
逐步替代 Prometheus client / 各种 logger,到 OTel 标准化。

效果

我们 5 微服务架构接 OTel 后:

  • "用户报反应慢" 类 issue 调查时间从 30min → 3min
  • 发现一个 N+1 query(隐藏在 lib 里)日浪费 500ms × 万次请求
  • 知道哪个下游服务最不稳定(看 trace span 错误率)
  • DBA / SRE 不再需要"装 5 个服务的 log 拼接"

与 Sentry / DataDog 等对比

OTel + Jaeger/Tempo DataDog APM Sentry Performance New Relic
开源 / 自托管 部分
学习曲线
价格 几乎免费
标准化 ✅ 行业标准 私有 私有 私有

OTel 让你的代码 vendor-neutral:今天 Jaeger,明天换 DataDog 切 exporter
就行。

踩过的坑

  1. collector 没起 → SDK 重试堆积 RAM:SDK 默认会缓存 batch 失败
    重试。collector 早死 → 应用内存涨。配 max_export_batch_size
    超时 drop。

  2. trace context 跨异步任务丢:Celery / async task 默认 trace 断开。
    要手动用 OTel context inject / extract:
    python ctx = trace.set_span_in_context(current_span) carrier = {} inject(carrier) # carrier 里有 traceparent celery_task.delay(payload, ctx=carrier)

  3. span 太多:每个 SQL query 自动一个 span,1 个请求几百个 span,
    存储成本飙。SQL instrumentation 配 enable_commenter=False
    只 trace 慢 query。

  4. PII 泄漏:默认 instrument 把 HTTP query string / body 记进 span
    attribute → trace 里包含密码 / token。配 OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT
    或自定义 sanitizer。

  5. service 间时钟漂移:trace 时间戳来自各服务本机时钟。两机器
    差 100ms → 瀑布图错位。所有服务配 chrony 同 NTP。

精确评价 共 0 人评价
可复现性
可复现 · 0 不可复现 · 0
文风
文风流畅 · 0 文风晦涩 · 0
立场
支持 · 0 反对 · 0

登录后即可对本帖作出评价。

评论区 0 条 · 所有人可在此交流

登录后参与评论。

还没有评论,来说两句。