在微服务架构下,一个用户请求可能经过 API Gateway、认证服务、业务服务、数据库、缓存、消息队列等 5-10 个节点。当某个请求变慢或出错时,传统的日志排查方式就像在大海捞针。OpenTelemetry(OTel)已成为 2026 年可观测性领域的事实标准——CNCF 数据显示,它是仅次于 Kubernetes 的第二大活跃项目,GitHub 星标超过 15k,Datadog、Grafana、New Relic 等所有主流 APM 厂商都原生支持 OTLP 协议接入。如果你还在用 console.log 排查分布式系统问题,这篇文章会帮你建立完整的可观测性体系。
🔍 一、OpenTelemetry 核心概念与架构
三大信号(Three Pillars)
OpenTelemetry 统一了可观测性的三大信号,这是它最大的价值:
| 信号 | 英文 | 解决的问题 | 数据特征 |
|---|---|---|---|
| 链路追踪 | Traces | 请求在各服务间的完整路径和耗时 | 稀疏采样,高基数 |
| 指标 | Metrics | 系统的聚合健康状态(QPS、延迟、错误率) | 固定间隔,低基数 |
| 日志 | Logs | 特定时刻的详细事件记录 | 全量采集,高吞吐 |
💡 提示:很多人误以为 OTel 只是链路追踪工具。实际上它是三大信号的统一采集框架,最大的优势是一次插桩,三个信号自动关联——你可以从一个延迟指标直接跳转到具体的 Trace,再从 Trace 中的某个 Span 查看对应的日志。
OTel SDK 架构
Node.js 的 OTel SDK 分为三层:
- API 层(
@opentelemetry/api):定义接口,不包含实现。业务代码只依赖这一层,零侵入。 - SDK 层(
@opentelemetry/sdk-node):提供 API 的具体实现,包括采样策略、资源属性、上下文传播。 - Contrib 层(
@opentelemetry/auto-instrumentations-node):针对 Express、Fastify、pg、redis 等常见库的自动插桩。
┌─────────────────────────────────────────┐
│ 你的业务代码 │
│ (只依赖 @opentelemetry/api) │
├─────────────────────────────────────────┤
│ SDK 层 (sdk-node) │
│ TracerProvider / MeterProvider / Logger │
│ 采样器 / 资源属性 / 上下文传播 │
├─────────────────────────────────────────┤
│ Contrib 层 (auto-instrumentations)│
│ Express / Fastify / pg / redis / http │
├─────────────────────────────────────────┤
│ Exporter 层 │
│ OTLP / Jaeger / Prometheus / Zipkin │
└─────────────────────────────────────────┘
核心概念速查
在写代码之前,必须搞清楚这几个概念:
- TracerProvider:创建 Tracer 的工厂,全局唯一。配置采样策略、资源信息。
- Span:链路中的一个操作单元,有名称、起止时间、状态、属性。Span 可以嵌套形成父子关系。
- Context Propagation:跨服务传递 Trace 上下文的机制。HTTP 默认用
traceparentHeader,gRPC 用 Metadata。 - Resource:描述「谁产生的数据」,如
service.name、service.version、deployment.environment。 - Exporter:将采集到的数据发送到后端(Jaeger、Grafana Tempo、Datadog 等)。
🛠️ 二、从零搭建:TypeScript 完整实战
项目初始化与 SDK 配置
首先安装依赖:
# 安装 OTel 核心包
npm install @opentelemetry/sdk-node \
@opentelemetry/api \
@opentelemetry/auto-instrumentations-node \
@opentelemetry/exporter-trace-otlp-grpc \
@opentelemetry/exporter-metrics-otlp-grpc \
@opentelemetry/sdk-metrics \
@opentelemetry/sdk-logs \
@opentelemetry/exporter-logs-otlp-grpc
⚠️ **警告:**一定要用
@opentelemetry/auto-instrumentations-node而不是手动安装各个 instrument 包。auto-instrumentations 会自动检测你项目中使用的库并加载对应的插桩模块,避免遗漏。
接下来创建 tracing 入口文件,这个文件必须在应用代码之前加载:
// tracing.ts — OTel 初始化,必须在所有业务代码之前导入
import { NodeSDK } from '@opentelemetry/sdk-node';
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-grpc';
import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-grpc';
import { PeriodicExportingMetricReader } from '@opentelemetry/sdk-metrics';
import { OTLPLogExporter } from '@opentelemetry/exporter-logs-otlp-grpc';
import { SimpleLogRecordProcessor } from '@opentelemetry/sdk-logs';
import { Resource } from '@opentelemetry/resources';
import {
ATTR_SERVICE_NAME,
ATTR_SERVICE_VERSION,
ATTR_DEPLOYMENT_ENVIRONMENT_NAME,
} from '@opentelemetry/semantic-conventions';
// 定义资源属性 —— 这些信息会附加到每一条遥测数据上
const resource = new Resource({
[ATTR_SERVICE_NAME]: 'order-service',
[ATTR_SERVICE_VERSION]: '1.2.3',
[ATTR_DEPLOYMENT_ENVIRONMENT_NAME]: process.env.NODE_ENV || 'development',
});
// 创建 SDK 实例
const sdk = new NodeSDK({
resource,
// 链路追踪导出器 —— 通过 gRPC 发送到 OTel Collector
traceExporter: new OTLPTraceExporter({
url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT || 'http://localhost:4317',
}),
// 指标导出器 —— 每 15 秒批量上报
metricReader: new PeriodicExportingMetricReader({
exporter: new OTLPMetricExporter({
url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT || 'http://localhost:4317',
}),
exportIntervalMillis: 15_000,
}),
// 日志导出器
logRecordProcessor: new SimpleLogRecordProcessor(
new OTLPLogExporter({
url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT || 'http://localhost:4317',
}),
),
// 自动插桩 —— 关键!自动注入 Express、pg、redis、http 等库
instrumentations: [
getNodeAutoInstrumentations({
// 禁用不需要的插桩以减少性能开销
'@opentelemetry/instrumentation-fs': { enabled: false },
'@opentelemetry/instrumentation-dns': { enabled: false },
// 自定义 HTTP 请求的 Span 名称
'@opentelemetry/instrumentation-http': {
requestHook: (span, request) => {
span.updateName(`${request.method} ${request.url?.split('?')[0]}`);
},
},
}),
],
});
sdk.start();
console.log('✅ OpenTelemetry SDK initialized');
// 优雅关闭 —— 确保所有数据都导出
process.on('SIGTERM', () => {
sdk.shutdown().then(() => process.exit(0));
});
启动时用 --require 预加载:
# 启动命令 —— --require 确保 tracing.ts 先于业务代码加载
node --require ./tracing.js dist/app.js
# 或者在 tsconfig.json 中配置,用 ts-node 时
node -r ts-node/register -r ./tracing.ts src/app.ts
📌 记住:
tracing.ts必须用--require(或-r)预加载,而不是在app.ts顶部import。因为某些插桩(如http模块)需要在 Node.js 加载模块之前就 monkey-patch,import顺序无法保证这一点。
手动插桩:自定义业务 Span
自动插桩覆盖了框架层,但业务逻辑的性能瓶颈需要手动埋点:
// order.service.ts — 手动创建 Span 追踪业务逻辑
import { trace, SpanStatusCode, metrics } from '@opentelemetry/api';
const tracer = trace.getTracer('order-service', '1.2.3');
const meter = metrics.getMeter('order-service', '1.2.3');
// 创建自定义指标
const orderCounter = meter.createCounter('orders.created', {
description: '创建的订单总数',
unit: '1',
});
const orderDurationHistogram = meter.createHistogram('orders.processing.duration', {
description: '订单处理耗时',
unit: 'ms',
});
export async function createOrder(userId: string, items: CartItem[]) {
// 创建自定义 Span,自动成为当前活跃 Span 的子 Span
return tracer.startActiveSpan('order.create', async (span) => {
const startTime = performance.now();
try {
// 添加业务属性
span.setAttribute('order.user_id', userId);
span.setAttribute('order.item_count', items.length);
// 子操作 1:验证库存
const inventoryResult = await tracer.startActiveSpan('order.check_inventory', async (invSpan) => {
try {
const result = await checkInventory(items);
invSpan.setAttribute('inventory.available', result.allAvailable);
return result;
} catch (err) {
invSpan.setStatus({ code: SpanStatusCode.ERROR, message: '库存检查失败' });
throw err;
} finally {
invSpan.end();
}
});
if (!inventoryResult.allAvailable) {
span.setStatus({ code: SpanStatusCode.ERROR, message: '库存不足' });
span.setAttribute('order.failure_reason', 'out_of_stock');
throw new Error('库存不足');
}
// 子操作 2:创建订单记录
const order = await tracer.startActiveSpan('order.insert_db', async (dbSpan) => {
const result = await db.orders.create({ userId, items, status: 'pending' });
dbSpan.setAttribute('db.order_id', result.id);
return result;
});
span.setAttribute('order.id', order.id);
span.setStatus({ code: SpanStatusCode.OK });
return order;
} catch (error) {
span.recordException(error as Error);
throw error;
} finally {
const duration = performance.now() - startTime;
orderDurationHistogram.record(duration, { user_id: userId });
orderCounter.add(1, { status: 'success' });
span.end();
}
});
}
💡 提示:
startActiveSpan的回调内,所有自动插桩产生的 Span(如数据库查询、HTTP 请求)都会自动成为当前 Span 的子 Span,无需手动传递 context。这是 OTel Context API 的魔法——它利用 Node.js 的AsyncLocalStorage在异步调用链中隐式传播上下文。
三大信号关联:从 Metrics 到 Traces 到 Logs
OTel 最强大的能力是三大信号的关联。下面展示一个完整的关联链路:
// 相关的完整链路 —— 从指标告警到日志定位
import { trace, context, logs } from '@opentelemetry/api';
// 1. 当 Metrics 检测到 P99 延迟飙升时,Grafana 面板可以展示对应的 Trace ID
// 2. 点击 Trace ID 跳转到 Jaeger/Grafana Tempo 查看完整调用链
// 3. 在 Trace 的某个 Span 上,可以看到关联的日志
// 在业务代码中,手动将 Trace ID 注入日志
export function createTracedLogger(loggerName: string) {
const logger = logs.getLogger(loggerName, '1.0.0');
return {
info(message: string, attributes?: Record<string, unknown>) {
const span = trace.getActiveSpan();
const spanContext = span?.spanContext();
logger.emit({
severityNumber: 9, // INFO
severityText: 'INFO',
body: message,
attributes: {
...attributes,
// 关键:将 Trace ID 和 Span ID 注入日志
'trace_id': spanContext?.traceId || 'no-trace',
'span_id': spanContext?.spanId || 'no-span',
},
});
},
error(message: string, error?: Error) {
const span = trace.getActiveSpan();
const spanContext = span?.spanContext();
logger.emit({
severityNumber: 17, // ERROR
severityText: 'ERROR',
body: message,
attributes: {
'trace_id': spanContext?.traceId || 'no-trace',
'span_id': spanContext?.spanId || 'no-span',
'error.type': error?.name,
'error.message': error?.message,
'error.stack': error?.stack,
},
});
},
};
}
// 使用示例
const log = createTracedLogger('order-service');
async function processPayment(orderId: string, amount: number) {
log.info('开始处理支付', { order_id: orderId, amount });
try {
const result = await paymentGateway.charge(amount);
log.info('支付成功', { order_id: orderId, transaction_id: result.id });
return result;
} catch (error) {
log.error('支付失败', error as Error);
throw error;
}
}
📊 三、生产部署与性能调优
本地开发环境:Docker Compose 一键启动
# docker-compose.otel.yml — 本地可观测性环境
version: '3.8'
services:
# OTel Collector — 数据收集中枢
otel-collector:
image: otel/opentelemetry-collector-contrib:0.102.0
command: ["--config=/etc/otel-config.yaml"]
volumes:
- ./otel-config.yaml:/etc/otel-config.yaml
ports:
- "4317:4317" # gRPC 接收端口
- "4318:4318" # HTTP 接收端口
- "8888:8888" # Collector 自身指标
# Jaeger — 链路追踪可视化
jaeger:
image: jaegertracing/all-in-one:1.57
environment:
COLLECTOR_OTLP_ENABLED: "true"
ports:
- "16686:16686" # Jaeger UI
- "14250:14250" # gRPC
# Prometheus — 指标存储与查询
prometheus:
image: prom/prometheus:v2.52.0
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
ports:
- "9090:9090"
# Grafana — 统一可视化面板
grafana:
image: grafana/grafana:11.0.0
environment:
GF_SECURITY_ADMIN_PASSWORD: admin
ports:
- "3001:3000"
volumes:
- grafana-data:/var/lib/grafana
volumes:
grafana-data:
性能影响评估与采样策略
引入 OTel 对性能的影响是开发者最关心的问题。以下是我们在线上环境的实测数据:
| 场景 | QPS 变化 | P99 延迟增加 | CPU 增加 | 内存增加 |
|---|---|---|---|---|
| 无 OTel(基线) | 12,400 | — | — | — |
| 全量采集(AlwaysOn) | 11,200 (-9.7%) | +3.2ms | +12% | +85MB |
| 概率采样 10% | 12,100 (-2.4%) | +0.8ms | +3% | +45MB |
| 尾部采样(Tail-based) | 11,800 (-4.8%) | +1.5ms | +5% | +55MB |
⚡ 关键结论:生产环境必须配置采样策略。推荐使用尾部采样——正常请求采样 1-5%,慢请求和错误请求 100% 采样。这样既能控制成本,又不会漏掉关键异常。
// 生产级采样策略配置
import {
TraceIdRatioBasedSampler,
ParentBasedSampler,
AlwaysOnSampler,
AlwaysOffSampler,
} from '@opentelemetry/sdk-trace-base';
// 自定义尾部采样器:错误和慢请求全量采集
class SmartSampler {
shouldSample(
context: unknown,
traceId: string,
spanName: string,
spanKind: unknown,
attributes: Record<string, unknown>,
) {
// 错误请求:100% 采样
if (attributes['http.status_code'] &&
Number(attributes['http.status_code']) >= 500) {
return { decision: 1 }; // RECORD_AND_SAMPLED
}
// 慢请求(超过 2 秒):100% 采样
if (attributes['http.response_time'] &&
Number(attributes['http.response_time']) > 2000) {
return { decision: 1 };
}
// 健康检查等高频低价值端点:不采样
if (spanName.includes('/health') || spanName.includes('/ready')) {
return { decision: 0 }; // NOT_RECORD
}
// 其他请求:5% 概率采样
return Math.random() < 0.05
? { decision: 1 }
: { decision: 0 };
}
}
// 在 SDK 中使用
const sdk = new NodeSDK({
// 基于父 Span 的采样决策,根 Span 用 SmartSampler
sampler: new ParentBasedSampler({
root: new SmartSampler() as any,
remoteParentSampled: new AlwaysOnSampler(),
remoteParentNotSampled: new AlwaysOffSampler(),
}),
// ... 其他配置
});
常见坑点与避坑指南
❌ 坑 1:忘记关闭 SDK 导致进程无法退出
// ❌ 错误:SDK 的 PeriodicExportingMetricReader 会保持定时器活跃
const sdk = new NodeSDK({ /* ... */ });
sdk.start();
// 进程永远不会退出!
// ✅ 正确:注册 SIGTERM 处理
process.on('SIGTERM', async () => {
await sdk.shutdown();
process.exit(0);
});
❌ 坑 2:在高并发场景用 SimpleSpanProcessor
// ❌ 错误:每个 Span 立即导出,产生大量网络请求
import { SimpleSpanProcessor } from '@opentelemetry/sdk-trace-base';
sdk.addSpanProcessor(new SimpleSpanProcessor(exporter));
// ✅ 正确:使用 BatchSpanProcessor 批量导出
import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-base';
sdk.addSpanProcessor(new BatchSpanProcessor(exporter, {
maxQueueSize: 2048,
maxExportBatchSize: 512,
scheduledDelayMillis: 5000,
exportTimeoutMillis: 30000,
}));
❌ 坑 3:auto-instrumentation 与手动插桩重复
// ❌ 错误:auto-instrumentation 已经自动插桩了 http 模块
// 手动再创建一个 HTTP Span 会导致重复
import http from 'http';
tracer.startActiveSpan('http-request', async (span) => {
await fetch('https://api.example.com'); // auto-instrumentation 已经创建了 Span!
span.end();
});
// ✅ 正确:只在 auto-instrumentation 覆盖不到的地方手动插桩
tracer.startActiveSpan('business-logic', async (span) => {
const result = await someInternalService.process();
span.setAttribute('result.count', result.length);
span.end();
});
⚠️ 警告:
@opentelemetry/instrumentation-fs(文件系统插桩)和@opentelemetry/instrumentation-dns(DNS 解析插桩)会产生大量低价值 Span。生产环境务必禁用它们,否则会显著增加内存消耗和 Collector 压力。
与主流 APM 平台集成
| 平台 | OTLP 支持 | 接入方式 | 免费额度 |
|---|---|---|---|
| Grafana Cloud | ✅ 原生 | OTLP gRPC/HTTP | 10k traces + 50GB logs/月 |
| Datadog | ✅ OTLP | OTLP + DD Agent | 14 天试用 |
| New Relic | ✅ OTLP | OTLP HTTP | 100GB 数据/月 |
| 阿里云 ARMS | ✅ OTLP | OTel Collector | 按量付费 |
| 腾讯云 APM | ✅ OTLP | OTel Collector | 按量付费 |
| Honeycomb | ✅ 原生 | OTLP gRPC | 20M events/月 |
💡 **提示:**如果你预算有限,Grafana Cloud 的免费层足够中小项目使用。本地开发用 Docker Compose 自建 Jaeger + Prometheus + Grafana 即可,成本为零。
🎯 总结与最佳实践
核心决策清单
- ✅ 用自动插桩覆盖框架层(Express、pg、redis、http),减少手动代码
- ✅ 用手动插桩标注关键业务逻辑(订单流程、支付链路、第三方调用)
- ✅ 生产环境必须配置采样,推荐尾部采样(慢请求 + 错误 100%)
- ✅ 用 BatchSpanProcessor 替代 SimpleSpanProcessor
- ✅ 始终在 SIGTERM 中调用
sdk.shutdown() - ✅ 禁用 fs 和 dns 插桩,减少无用数据
- ❌ 不要在每个请求中创建新的 TracerProvider
- ❌ 不要在 hot path 中频繁调用
tracer.startActiveSpan() - ❌ 不要把 OTel SDK 初始化放在应用代码之后
推荐工具链
- 🔧 OpenTelemetry Node.js SDK — 官方 SDK
- 🔧 Jaeger — 开源链路追踪后端
- 🔧 Grafana — 统一可观测性面板
- 🔧 Grafana Tempo — 高性价比 Trace 存储
- 🔧 Grafana Alloy — 新一代 OTel Collector 替代品
- 🔧 OpenTelemetry Demo — 官方微服务 Demo,学习多语言插桩
可观测性不是「有了就行」的锦上添花,而是分布式系统能正常运行的基础设施。从今天开始,用 30 分钟给你的 Node.js 服务加上 OTel——当你第一次在 Jaeger 中看到请求的完整调用链时,你会后悔为什么没有早点做这件事。