当你的微服务架构从 3 个服务扩展到 30 个,“这个请求为什么慢” 就从一个简单问题变成了噩梦。分布式系统中,一个用户请求可能跨越 8 个服务、3 个数据库和 2 个消息队列,传统的日志排查方式已经完全失效。OpenTelemetry(简称 OTel)是 CNCF 旗下增长最快的项目,2025 年已成为仅次于 Kubernetes 的第二大活跃项目,它将分布式追踪(Traces)、指标(Metrics)和日志(Logs)统一到一套标准化的采集框架中。本文将从零搭建一套完整的 OTel 可观测性方案,包含 Node.js 和 Python 的实战代码、Jaeger + Prometheus 的部署配置,以及生产环境中最常见的坑。
📊 一、OpenTelemetry 核心架构与三大信号
1.1 为什么需要 OpenTelemetry
在 OTel 出现之前,可观测性领域是碎片化的:Datadog 有自己的 SDK,New Relic 用另一套,AWS X-Ray 又是不同的 API。如果你要切换 APM 供应商,所有埋点代码都得重写。更糟糕的是,Traces、Metrics、Logs 三套数据各自为政,无法关联——你看到一个慢请求的 Trace ID,却没法直接跳转到对应的日志。
OpenTelemetry 统一了这一切。它定义了一套 vendor-neutral 的 API 和 SDK,让你的埋点代码与后端存储解耦。今天用 Jaeger 查链路,明天换成 Datadog,只需要改导出器(Exporter)配置,业务代码零改动。
📌 记住: OpenTelemetry 不是 APM 产品,它是可观测性的"标准管道"——负责数据的采集、处理和导出,不负责存储和可视化。
1.2 三大信号模型
OTel 定义了三种核心遥测信号:
| 信号 | 用途 | 数据模型 | 典型后端 |
|---|---|---|---|
| Traces | 分布式链路追踪 | Span 树(父子关系) | Jaeger, Tempo, Datadog |
| Metrics | 系统与业务指标 | 时间序列(Counter/Gauge/Histogram) | Prometheus, Mimir |
| Logs | 结构化日志 | 日志记录 + Trace ID 关联 | Loki, Elasticsearch |
⚡ 关键结论: Traces 是排查分布式问题的核心武器,Metrics 用于监控告警,Logs 提供详细上下文。三者通过 Trace ID 和 Span ID 关联,形成完整的可观测性闭环。
1.3 核心概念:Trace、Span、Context Propagation
一个 Trace 代表一个完整的请求生命周期,由多个 Span 组成。每个 Span 记录了一个操作的开始时间、结束时间、状态和属性。
Trace: abc123
├── Span: API Gateway (100ms)
│ ├── Span: Auth Service (15ms)
│ └── Span: Order Service (80ms)
│ ├── Span: MySQL Query (25ms)
│ └── Span: Redis Cache (5ms)
│ └── Span: Payment Service (45ms)
│ └── Span: Stripe API Call (40ms)
Context Propagation(上下文传播) 是跨服务传递 Trace ID 的机制。通常通过 HTTP Header(如 traceparent)或消息队列的 Metadata 传递。W3C Trace Context 是标准格式:
traceparent: 00-abc123def456-span789012-01
| | | |
version trace-id span-id flags
🔧 二、Node.js 实战:从零接入 OpenTelemetry
2.1 安装与基础配置
首先安装核心依赖。OTel 的 Node.js SDK 采用模块化设计,你需要安装 API 层、SDK 层和具体的导出器:
# 安装 OpenTelemetry Node.js SDK 核心包
npm install @opentelemetry/api \
@opentelemetry/sdk-node \
@opentelemetry/sdk-trace-node \
@opentelemetry/sdk-metrics \
@opentelemetry/sdk-logs \
@opentelemetry/exporter-trace-otlp-http \
@opentelemetry/exporter-metrics-otlp-http \
@opentelemetry/exporter-logs-otlp-http \
@opentelemetry/resources \
@opentelemetry/semantic-conventions \
@opentelemetry/instrumentation-http \
@opentelemetry/instrumentation-express \
@opentelemetry/instrumentation-pg \
@opentelemetry/instrumentation-redis
⚠️ 警告:
@opentelemetry/sdk-node和@opentelemetry/sdk-trace-node不要同时作为主初始化入口,否则会导致 TracerProvider 重复注册。通常使用@opentelemetry/sdk-node作为统一入口即可。
2.2 完整的初始化代码
创建一个独立的 tracing.ts 文件,必须在应用代码之前加载(通过 --require 或入口文件顶部 import):
// tracing.ts — OpenTelemetry 初始化,必须在所有业务代码之前加载
import { NodeSDK } from '@opentelemetry/sdk-node';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-http';
import { OTLPLogExporter } from '@opentelemetry/exporter-logs-otlp-http';
import { PeriodicExportingMetricReader } from '@opentelemetry/sdk-metrics';
import { SimpleLogRecordProcessor } from '@opentelemetry/sdk-logs';
import { Resource } from '@opentelemetry/resources';
import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from '@opentelemetry/semantic-conventions';
import { HttpInstrumentation } from '@opentelemetry/instrumentation-http';
import { ExpressInstrumentation } from '@opentelemetry/instrumentation-express';
import { PgInstrumentation } from '@opentelemetry/instrumentation-pg';
import { RedisInstrumentation } from '@opentelemetry/instrumentation-redis';
const resource = new Resource({
[ATTR_SERVICE_NAME]: 'order-service',
[ATTR_SERVICE_VERSION]: '1.2.0',
'deployment.environment': process.env.NODE_ENV || 'development',
});
const sdk = new NodeSDK({
resource,
traceExporter: new OTLPTraceExporter({
url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT + '/v1/traces',
headers: {}, // 如需认证可在此添加
}),
metricReader: new PeriodicExportingMetricReader({
exporter: new OTLPMetricExporter({
url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT + '/v1/metrics',
}),
exportIntervalMillis: 15000, // 每 15 秒导出一次指标
}),
logRecordProcessor: new SimpleLogRecordProcessor(
new OTLPLogExporter({
url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT + '/v1/logs',
})
),
instrumentations: [
new HttpInstrumentation({
// 忽略健康检查端点,避免产生大量无用 Span
ignoreIncomingRequestHook: (req) => req.url === '/health',
}),
new ExpressInstrumentation(),
new PgInstrumentation(),
new RedisInstrumentation(),
],
});
sdk.start();
console.log('✅ OpenTelemetry SDK initialized');
// 优雅关闭,确保最后一批数据被导出
process.on('SIGTERM', () => {
sdk.shutdown()
.then(() => console.log('OTel SDK shut down'))
.catch((err) => console.error('OTel shutdown error', err))
.finally(() => process.exit(0));
});
启动应用时通过 --require 预加载:
# 使用 --require 确保 tracing.ts 在业务代码之前执行
node --require ./tracing.js dist/app.js
# 或使用环境变量配置 OTLP 端点
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318 node --require ./tracing.js dist/app.js
2.3 自定义 Span:业务级链路追踪
自动埋点只能覆盖 HTTP、数据库等基础设施层。对于业务逻辑,你需要手动创建 Span 来追踪关键操作:
// order-service.ts — 手动创建自定义 Span 追踪业务逻辑
import { trace, SpanStatusCode } from '@opentelemetry/api';
const tracer = trace.getTracer('order-service', '1.2.0');
async function createOrder(userId, items) {
// 创建一个子 Span,自动成为当前活跃 Span 的子节点
return tracer.startActiveSpan('order.create', async (span) => {
try {
// 设置业务属性,便于后续筛选和分析
span.setAttribute('order.user_id', userId);
span.setAttribute('order.item_count', items.length);
span.setAttribute('order.total', calculateTotal(items));
// 子操作:验证库存
const stockResult = await tracer.startActiveSpan('order.check_stock', async (stockSpan) => {
try {
const result = await checkInventory(items);
stockSpan.setAttribute('stock.available', result.available);
return result;
} catch (err) {
stockSpan.setStatus({ code: SpanStatusCode.ERROR, message: err.message });
stockSpan.recordException(err);
throw err;
} finally {
stockSpan.end(); // 必须手动 end,否则 Span 不会被导出
}
});
// 子操作:创建支付
const payment = await tracer.startActiveSpan('order.create_payment', async (paySpan) => {
try {
const result = await processPayment(userId, calculateTotal(items));
paySpan.setAttribute('payment.method', result.method);
paySpan.setAttribute('payment.transaction_id', result.txId);
return result;
} catch (err) {
paySpan.setStatus({ code: SpanStatusCode.ERROR, message: err.message });
paySpan.recordException(err);
throw err;
} finally {
paySpan.end();
}
});
span.setStatus({ code: SpanStatusCode.OK });
return { orderId: generateId(), payment };
} catch (err) {
span.setStatus({ code: SpanStatusCode.ERROR, message: err.message });
throw err;
} finally {
span.end(); // 最外层 Span 也必须手动 end
}
});
}
💡 提示:
startActiveSpan会自动将新 Span 设为当前上下文的活跃 Span,所以http、pg等自动埋点插件会自动成为它的子 Span,无需手动传递 context。
🚀 三、Python 实战:FastAPI + OTel 完整接入
3.1 自动埋点配置
Python 的 OTel 生态同样成熟,FastAPI 项目只需几行代码即可完成全链路埋点:
# tracing.py — Python OpenTelemetry 初始化
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.resources import Resource
from opentelemetry.semconv.resource import ResourceAttributes
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
from opentelemetry.instrumentation.httpx import HTTPXClientInstrumentor
from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor
from opentelemetry.instrumentation.redis import RedisInstrumentor
def setup_telemetry(app, service_name: str):
resource = Resource.create({
ResourceAttributes.SERVICE_NAME: service_name,
ResourceAttributes.SERVICE_VERSION: "2.0.0",
"deployment.environment": "production",
})
provider = TracerProvider(resource=resource)
# 使用 BatchSpanProcessor 提高性能,异步批量导出
exporter = OTLPSpanExporter(
endpoint="http://otel-collector:4318/v1/traces",
)
provider.add_span_processor(BatchSpanProcessor(exporter))
trace.set_tracer_provider(provider)
# 自动埋点:FastAPI 路由、HTTP 客户端、SQLAlchemy、Redis
FastAPIInstrumentor.instrument_app(app)
HTTPXClientInstrumentor().instrument()
SQLAlchemyInstrumentor().instrument()
RedisInstrumentor().instrument()
return trace.get_tracer(service_name)
# main.py — FastAPI 应用入口
from fastapi import FastAPI
from tracing import setup_telemetry
app = FastAPI(title="Order Service")
tracer = setup_telemetry(app, "order-service")
@app.get("/orders/{order_id}")
async def get_order(order_id: str):
# 自动埋点已覆盖 HTTP 路由和数据库查询
# 如需业务级追踪,手动创建 Span
with tracer.start_as_current_span("order.enrich") as span:
span.set_attribute("order.id", order_id)
order = await fetch_order(order_id)
span.set_attribute("order.status", order.status)
return order
⚠️ 警告: Python SDK 的
BatchSpanProcessor是异步导出的,如果进程被SIGKILL(而不是SIGTERM),最后一批 Span 会丢失。在 Docker 中确保使用STOPSIGNAL SIGTERM并设置合理的stop_grace_period。
💡 四、部署架构与 Collector 配置
4.1 为什么需要 OTel Collector
在生产环境中,不建议让应用直接将数据发送到 Jaeger/Prometheus。原因是:
- 协议转换:应用通过 OTLP 协议发送,Collector 负责转换为各后端的原生协议
- 数据处理:采样、过滤、聚合、添加环境标签
- 缓冲削峰:应用突发大量 Span 时,Collector 做缓冲避免压垮后端
- 多路复用:同一份数据同时发往 Jaeger(查链路)和 Prometheus(做指标)
4.2 Docker Compose 一键部署
以下是一个完整的可观测性栈:OTel Collector + Jaeger + Prometheus + Grafana:
# docker-compose.yml — 可观测性栈一键部署
version: "3.9"
services:
# OTel Collector: 数据采集、处理、路由的中枢
otel-collector:
image: otel/opentelemetry-collector-contrib:0.102.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
depends_on:
- jaeger
- prometheus
# Jaeger: 分布式追踪可视化
jaeger:
image: jaegertracing/all-in-one:1.58
ports:
- "16686:16686" # Jaeger UI
environment:
- COLLECTOR_OTLP_ENABLED=true
# Prometheus: 指标存储与查询
prometheus:
image: prom/prometheus:v2.53.0
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
ports:
- "9090:9090"
# Grafana: 统一仪表盘
grafana:
image: grafana/grafana:11.1.0
ports:
- "3001:3000"
environment:
- GF_SECURITY_ADMIN_PASSWORD=admin
volumes:
- grafana-data:/var/lib/grafana
volumes:
grafana-data:
4.3 Collector 核心配置
# otel-collector-config.yaml — OTel Collector 处理管道配置
receivers:
otlp:
protocols:
http:
endpoint: "0.0.0.0:4318"
grpc:
endpoint: "0.0.0.0:4317"
processors:
# 批处理:攒够 512 个或等 5 秒再批量发送
batch:
timeout: 5s
send_batch_size: 512
# 尾部采样:对错误请求 100% 保留,正常请求只保留 10%
tail_sampling:
decision_wait: 10s
policies:
- name: errors-policy
type: status_code
status_code: { status_codes: [ERROR] }
- name: slow-requests-policy
type: latency
latency: { threshold_ms: 2000 }
- name: probabilistic-policy
type: probabilistic
probabilistic: { sampling_percentage: 10 }
# 资源属性添加:给所有数据打上环境标签
resource:
attributes:
- key: environment
value: "production"
action: upsert
exporters:
otlp/jaeger:
endpoint: "jaeger:4317"
tls:
insecure: true
prometheusremotewrite:
endpoint: "http://prometheus:9090/api/v1/write"
debug:
verbosity: basic
service:
pipelines:
traces:
receivers: [otlp]
processors: [tail_sampling, resource, batch]
exporters: [otlp/jaeger]
metrics:
receivers: [otlp]
processors: [resource, batch]
exporters: [prometheusremotewrite]
logs:
receivers: [otlp]
processors: [resource, batch]
exporters: [debug]
💡 提示: 尾部采样(Tail Sampling)是生产环境的关键配置。它会等待一个 Trace 完成后再决定是否采样,而不是在第一个 Span 到达时就做决定。这确保了错误请求和慢请求的完整 Trace 不会被丢弃。
⚠️ 五、生产环境避坑指南
5.1 性能开销评估
OTel 的性能开销是开发者最关心的问题。基于实际项目测试数据:
| 场景 | 额外延迟 | CPU 开销 | 内存开销 |
|---|---|---|---|
| 自动埋点(HTTP + DB) | +0.5-2ms/请求 | +3-5% | +20-50MB |
| 自动 + 自定义 Span(5个) | +1-3ms/请求 | +5-8% | +30-80MB |
| 开启日志关联 | +1-2ms/请求 | +2-4% | +10-30MB |
⚡ 关键结论: 对于绝大多数 Web 应用,OTel 的开销可以忽略不计。真正需要注意的是 BatchSpanProcessor 的队列大小——如果后端响应慢导致队列堆积,可能会占用大量内存。
5.2 最常见的五个坑
坑 1:Span 忘记调用 end()
这是新手最常犯的错误。startActiveSpan 返回的 Span 必须手动调用 end(),否则它会一直存在于内存中,永远不会被导出。使用 try/finally 确保 end() 被调用。
坑 2:生产环境使用 SimpleSpanProcessor
SimpleSpanProcessor 是同步导出的,每生成一个 Span 就立即发送一个 HTTP 请求。在高并发场景下,这会严重影响性能。生产环境必须使用 BatchSpanProcessor。
// ❌ 错误写法:生产环境不要用 SimpleSpanProcessor
provider.addSpanProcessor(new SimpleSpanProcessor(exporter));
// ✅ 正确写法:使用 BatchSpanProcessor 批量异步导出
provider.addSpanProcessor(new BatchSpanProcessor(exporter, {
maxQueueSize: 2048,
maxExportBatchSize: 512,
scheduledDelayMillis: 5000,
exportTimeoutMillis: 30000,
}));
坑 3:Trace 数据量爆炸
未配置采样策略的系统,每秒可能产生数万个 Span。以一个 1000 QPS 的服务为例,假设每个请求产生 10 个 Span,每天就是 8.64 亿个 Span,存储成本惊人。必须在 Collector 层配置采样策略。
坑 4:Context Propagation 丢失
跨服务调用时,如果中间经过了消息队列(如 Kafka、RabbitMQ),W3C Trace Context 不会自动传递。你需要在消息的 Header/Metadata 中手动注入和提取 Context:
// 发送消息时注入 Trace Context
import { propagation, context } from '@opentelemetry/api';
function publishMessage(topic, payload) {
const carrier = {};
// 将当前活跃 Span 的 context 注入到消息 Header
propagation.inject(context.active(), carrier);
kafka.send({
topic,
headers: carrier, // traceparent 会在这里
messages: [JSON.stringify(payload)],
});
}
// 消费消息时提取 Trace Context
function consumeMessage(message) {
const parentContext = propagation.extract(context.active(), message.headers);
// 在 parentContext 下创建新的 Span,自动关联到上游 Trace
tracer.startActiveSpan('kafka.process', {}, parentContext, (span) => {
// 处理消息...
span.end();
});
}
坑 5:Collector 单点故障
生产环境中,如果 OTel Collector 宕机,应用的遥测数据会丢失(默认行为)甚至阻塞请求(不推荐的配置)。建议:
- ✅ Collector 部署为 DaemonSet(K8s)或 sidecar,避免单点
- ✅ 应用端 SDK 配置
OTEL_BSP_MAX_QUEUE_SIZE限制内存占用 - ✅ 使用
OTEL_TRACES_SAMPLER=parentbased_traceidratio控制采样率 - ❌ 不要设置
OTEL_TRACES_EXPORTER=always_on在生产环境——会 100% 采集
🔐 六、Traces 关联 Logs:打通最后一环
可观测性的终极目标是:从一个告警出发,一键跳转到对应的 Trace,再一键跳转到相关日志。实现方式是让日志自动携带 trace_id 和 span_id。
Node.js 项目使用 winston + OTel 日志桥接:
// logger.ts — 结构化日志自动关联 Trace ID
import { logs, SeverityNumber } from '@opentelemetry/api-logs';
import { trace } from '@opentelemetry/api';
const logger = logs.getLogger('order-service');
function info(message, attributes = {}) {
const span = trace.getActiveSpan();
const spanContext = span?.spanContext();
logger.emit({
severityNumber: SeverityNumber.INFO,
severityText: 'INFO',
body: message,
attributes: {
...attributes,
// 自动注入 Trace ID 和 Span ID,实现日志与链路关联
'trace_id': spanContext?.traceId || 'no-trace',
'span_id': spanContext?.spanId || 'no-span',
},
});
}
在 Grafana 中配置 Jaeger + Loki 数据源关联后,你可以从 Jaeger 的 Trace 详情页直接跳转到该 Trace 对应的所有日志,反之亦然。
💰 七、成本对比:自建 vs 商业 APM
| 方案 | 月成本(100 万 Trace/天) | 数据主权 | 定制能力 | 运维成本 |
|---|---|---|---|---|
| 自建 OTel + Jaeger | 服务器费 ¥500-1500 | ✅ 完全自主 | ⭐⭐⭐⭐⭐ | 高 |
| Datadog APM | $23/host/月 + $0.10/1GB 日志 | ❌ 第三方 | ⭐⭐⭐ | 低 |
| New Relic | $0.30/GB(超出免费额度) | ❌ 第三方 | ⭐⭐⭐ | 低 |
| 阿里云 ARMS | ¥150-500/实例/月 | ✅ 国内合规 | ⭐⭐⭐⭐ | 中 |
⚠️ 警告: 自建方案虽然成本低,但需要投入运维精力。如果你的团队少于 5 人且没有专职 SRE,建议先用阿里云 ARMS 或 Grafana Cloud(有免费额度),等团队成长后再迁移到自建方案。
✅ 总结与最佳实践
OpenTelemetry 是可观测性领域的"HTTP 协议"——它不会让你的系统变快,但它是所有监控方案的基石。以下是我推荐的落地路径:
- 第一步: 在一个非核心服务上接入 OTel 自动埋点,验证数据链路
- 第二步: 部署 OTel Collector + Jaeger,先解决分布式追踪问题
- 第三步: 添加 Prometheus + Grafana,覆盖指标监控和告警
- 第四步: 接入日志关联,打通 Traces → Logs 的跳转
- 第五步: 配置采样策略和 Collector 集群化,为生产环境做好准备
相关工具推荐:
- 🔧 Jaeger — 开源分布式追踪后端,UI 友好,适合中小规模
- 🔧 Grafana Tempo — 高吞吐量追踪存储,适合大规模场景
- 🔧 Grafana Alloy — Grafana 出品的 OTel Collector 替代品,配置更简洁
- 🔧 SigNoz — 开源的一体化可观测性平台(Traces + Metrics + Logs in one)
- 🔧 Uptrace — 轻量级开源 APM,基于 OTel,适合快速上手