起因
微服务架构里一个用户请求穿过: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
就行。
踩过的坑
-
collector 没起 → SDK 重试堆积 RAM:SDK 默认会缓存 batch 失败
重试。collector 早死 → 应用内存涨。配max_export_batch_size和
超时 drop。 -
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) -
span 太多:每个 SQL query 自动一个 span,1 个请求几百个 span,
存储成本飙。SQL instrumentation 配enable_commenter=False或
只 trace 慢 query。 -
PII 泄漏:默认 instrument 把 HTTP query string / body 记进 span
attribute → trace 里包含密码 / token。配OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT
或自定义 sanitizer。 -
service 间时钟漂移:trace 时间戳来自各服务本机时钟。两机器
差 100ms → 瀑布图错位。所有服务配 chrony 同 NTP。
登录后参与评论。