概述
在云原生和微服务时代,一个请求可能跨越数十个服务节点。传统的监控方式将 Metrics、Logs、Traces 分散在不同系统中——Prometheus 看指标、ELK 搜日志、Jaeger 追链路,三者之间没有统一的关联方式。当线上故障发生时,你需要在三个系统之间来回切换,手动拼接关联信息,效率低下。
OpenTelemetry(简称 OTel)是 CNCF 主导的可观测性统一标准,目标是用一套 SDK/API 统一采集三大信号(Metrics、Logs、Traces),通过统一的 Collector 处理后发送到任意后端。它不替代后端存储和可视化,而是解决"数据采集层碎片化"的问题。本文深入讲解 OTel 的规范、架构、实践和迁移策略。
一、为什么需要 OpenTelemetry
1.1 可观测性碎片化问题
传统可观测性架构(碎片化):
应用代码
├── Prometheus Client (Metrics)
│ └── → Prometheus → Grafana
├── Logback + Filebeat (Logs)
│ └── → Elasticsearch → Kibana
└── Jaeger Client (Traces)
└── → Jaeger → Jaeger UI
问题:
1. 三套 SDK,三套配置,三套运维
2. Metrics / Logs / Traces 之间无关联(TraceID 未关联到日志)
3. 切换后端成本高(从 Prometheus 换到 Datadog 需要改代码)
4. 每种语言需要维护不同的客户端库
1.2 OpenTelemetry 的解决思路
OpenTelemetry 统一架构:
应用代码
└── OpenTelemetry SDK (统一采集)
├── Metrics
├── Logs (带 TraceID)
└── Traces
│
▼
OpenTelemetry Collector (统一处理)
├── 过滤 / 聚合 / 采样
├── 格式转换
└── 分发到后端
│
┌───────┼───────┐
▼ ▼ ▼
Prometheus ELK Jaeger
(Grafana) (Kibana)(Jaeger UI)
核心价值:
| 价值 | 说明 |
|---|---|
| 统一采集 | 一套 SDK 采集三种信号 |
| 后端无关 | 换后端不需要改代码 |
| 上下文关联 | TraceID 自动关联 Logs 和 Metrics |
| 多语言支持 | 11 种语言的官方 SDK |
| 标准化 | CNCF 规范,行业共识 |
二、OpenTelemetry 规范
2.1 核心概念
| 概念 | 说明 |
|---|---|
| Signal(信号) | 可观测性数据类型:Metrics、Logs、Traces |
| Resource(资源) | 被监控实体的描述信息(服务名、版本、主机名) |
| InstrumentationScope | 采集范围标识(库名、包名) |
| Context(上下文) | 请求的传播信息(TraceID、SpanID) |
| Baggage(行李) | 跨服务传递的键值对(如 tenant_id) |
| Span(跨度) | 一次操作的记录(方法调用、HTTP 请求) |
| Log Record | 日志记录(带 Trace 关联) |
| Meter / Tracer / Logger | 三种信号的采集器接口 |
2.2 三大信号
┌──────────────────────────────────────────────────────────┐
│ OpenTelemetry 三大信号 │
│ │
│ Traces(追踪) │
│ ├── 记录请求在分布式系统中的完整路径 │
│ ├── 每个 Span 包含:操作名、时间、状态、属性 │
│ └── 通过 TraceID 将多个服务的 Span 串联 │
│ │
│ Metrics(指标) │
│ ├── 记录可聚合的数值数据 │
│ ├── 类型:Counter / Gauge / Histogram / Summary │
│ └── 包含 Resource 和 Attributes(标签) │
│ │
│ Logs(日志) │
│ ├── 记录离散事件 │
│ ├── 关联 TraceID / SpanID(关键特性) │
│ └── 包含 Severity、Body、Attributes │
└──────────────────────────────────────────────────────────┘
2.3 数据模型
Span 数据模型:
{
"trace_id": "7b3cf5b0123456789abcdef012345678",
"span_id": "0123456789abcdef",
"parent_span_id": "fedcba9876543210",
"name": "GET /api/orders",
"kind": "SERVER",
"start_time": "2026-07-10T10:00:00.123456789Z",
"end_time": "2026-07-10T10:00:00.456789123Z",
"status": {
"code": "ERROR",
"description": "Database connection timeout"
},
"attributes": {
"http.method": "GET",
"http.url": "/api/orders",
"http.status_code": 500,
"db.system": "mysql",
"db.statement": "SELECT * FROM orders"
},
"events": [
{
"name": "exception",
"time": "2026-07-10T10:00:00.400Z",
"attributes": {
"exception.type": "java.sql.SQLException",
"exception.message": "Connection timeout"
}
}
],
"resource": {
"service.name": "order-service",
"service.version": "1.2.3",
"host.name": "order-pod-abc123"
}
}
Log 数据模型:
{
"timestamp": "2026-07-10T10:00:00.500Z",
"trace_id": "7b3cf5b0123456789abcdef012345678",
"span_id": "0123456789abcdef",
"severity_text": "ERROR",
"severity_number": 17,
"body": "Failed to process order: Database connection timeout",
"attributes": {
"order_id": "ORD-12345",
"user_id": "USR-67890",
"retry_count": 3
},
"resource": {
"service.name": "order-service",
"service.version": "1.2.3"
}
}
关键:Log 中的
trace_id和span_id使日志可以直接关联到 Trace,这是 OTel 的核心价值之一——“点击日志中的 TraceID,直接跳转到 Trace 视图”。
三、OpenTelemetry Collector
3.1 Collector 架构
Collector 是 OTel 的数据管道核心,负责接收、处理和导出遥测数据。
┌──────────────────────────────────────────────────────┐
│ OTel Collector 架构 │
│ │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ Receiver │ │ Receiver │ │ Receiver │ ← 接收 │
│ │ (OTLP) │ │ (Jaeger) │ │ (Prom) │ │
│ └────┬────┘ └────┬────┘ └────┬────┘ │
│ │ │ │ │
│ └────────────┼────────────┘ │
│ ▼ │
│ ┌──────────┐ │
│ │ Processor│ ← 处理(过滤/采样/增强) │
│ └────┬─────┘ │
│ │ │
│ ┌───────────┼───────────┐ │
│ ▼ ▼ ▼ │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ Exporter│ │ Exporter│ │ Exporter│ ← 导出 │
│ │ (OTLP) │ │ (Prom) │ │ (ES/Loki)│ │
│ └─────────┘ └─────────┘ └─────────┘ │
└──────────────────────────────────────────────────────┘
3.2 Collector 配置
# otel-collector-config.yaml
receivers:
# 接收 OTLP 协议数据(SDK 直发)
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
# 接收 Jaeger 格式数据
jaeger:
protocols:
grpc:
endpoint: 0.0.0.0:14250
thrift_http:
endpoint: 0.0.0.0:14268
# 接收 Prometheus 拉取
prometheus:
config:
scrape_configs:
- job_name: 'otel-collector'
scrape_interval: 15s
static_configs:
- targets: ['localhost:8888']
processors:
# 批量处理
batch:
timeout: 5s
send_batch_size: 1000
send_batch_max_size: 2000
# 内存限制
memory_limiter:
check_interval: 1s
limit_mib: 512
spike_limit_mib: 128
# 过滤
filter:
traces:
span:
- 'attributes["http.route"] == "/health"' # 过滤健康检查 Span
metrics:
metric:
- 'name == "process.runtime.jvm.gc.time"'
# 属性处理
attributes:
actions:
- key: environment
value: production
action: upsert
- key: http.request_header.authorization
action: delete # 删除敏感信息
# 采样(尾部采样)
tail_sampling:
decision_wait: 10s
policies:
- name: errors
type: status_code
status_code:
status_codes: [ERROR]
- name: slow
type: latency
latency:
threshold_ms: 1000
- name: sample
type: probabilistic
probabilistic:
sampling_percentage: 10
# 资源处理
resource:
attributes:
- key: deployment.environment
value: production
action: upsert
extensions:
health_check:
endpoint: 0.0.0.0:13133
zpages:
endpoint: 0.0.0.0:55679
exporters:
# 导出到 Jaeger
otlp/jaeger:
endpoint: jaeger:4317
tls:
insecure: true
# 导出到 Prometheus(通过 remote_write)
prometheusremotewrite:
endpoint: http://prometheus:9090/api/v1/write
# 导出到 Loki
loki:
endpoint: http://loki:3100/loki/api/v1/push
# 导出到 Elasticsearch
elasticsearch:
endpoints:
- http://es:9200
index: otel-logs
service:
extensions: [health_check, zpages]
pipelines:
traces:
receivers: [otlp, jaeger]
processors: [memory_limiter, filter, tail_sampling, resource, batch]
exporters: [otlp/jaeger]
metrics:
receivers: [otlp, prometheus]
processors: [memory_limiter, filter, resource, batch]
exporters: [prometheusremotewrite]
logs:
receivers: [otlp]
processors: [memory_limiter, filter, attributes, resource, batch]
exporters: [loki, elasticsearch]
3.3 Collector 部署模式
| 模式 | 部署位置 | 用途 | 资源消耗 |
|---|---|---|---|
| Agent | 应用节点旁 | 采集本地数据 | 低 |
| Gateway | 独立集群 | 集中处理和转发 | 中-高 |
| Sidecar | Pod 内 | K8s 应用旁 | 低 |
生产推荐:Agent + Gateway 两层架构:
应用节点 → Agent Collector → Gateway Collector → 后端
(本地采集) (集中处理)
3.4 K8s 部署
# DaemonSet 模式(Agent Collector)
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: otel-collector-agent
namespace: monitoring
spec:
selector:
matchLabels:
app: otel-collector-agent
template:
metadata:
labels:
app: otel-collector-agent
spec:
containers:
- name: collector
image: otel/opentelemetry-collector-contrib:0.103.0
args: ["--config=/etc/otel/config.yaml"]
ports:
- containerPort: 4317 # OTLP gRPC
- containerPort: 4318 # OTLP HTTP
- containerPort: 13133 # Health check
volumeMounts:
- name: config
mountPath: /etc/otel
resources:
limits:
cpu: 500m
memory: 512Mi
requests:
cpu: 100m
memory: 128Mi
volumes:
- name: config
configMap:
name: otel-collector-config
四、SDK 自动埋点
4.1 Go 语言示例
package main
import (
"context"
"log"
"net/http"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
"go.opentelemetry.io/otel/propagation"
"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/otel/trace"
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
)
func initTracer() func() {
// 创建 OTLP exporter
exporter, err := otlptracehttp.New(context.Background(),
otlptracehttp.WithEndpoint("otel-collector:4318"),
otlptracehttp.WithInsecure(),
)
if err != nil {
log.Fatalf("Failed to create exporter: %v", err)
}
// 创建 Resource(标识当前服务)
res, _ := resource.New(context.Background(),
resource.WithAttributes(
semconv.ServiceName("order-service"),
semconv.ServiceVersion("1.0.0"),
semconv.DeploymentEnvironment("production"),
),
)
// 创建 TracerProvider
tp := sdktrace.NewTracerProvider(
sdktrace.WithBatcher(exporter),
sdktrace.WithResource(res),
sdktrace.WithSampler(sdktrace.TraceIDRatioBased(0.1)), // 10% 采样
)
otel.SetTracerProvider(tp)
otel.SetTextMapPropagator(propagation.TraceContext{})
return func() {
tp.Shutdown(context.Background())
}
}
func main() {
shutdown := initTracer()
defer shutdown()
// 使用 otelhttp 自动埋点 HTTP 请求
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
tracer := otel.Tracer("order-service")
ctx, span := tracer.Start(r.Context(), "processOrder",
trace.WithAttributes(
semconv.HTTPMethod(r.Method),
semconv.HTTPTarget(r.URL.Path),
),
)
defer span.End()
// 模拟业务逻辑
processOrder(ctx, r.URL.Query().Get("order_id"))
w.Write([]byte(`{"status": "ok"}`))
})
wrappedHandler := otelhttp.NewHandler(handler, "HTTP")
http.ListenAndServe(":8080", wrappedHandler)
}
func processOrder(ctx context.Context, orderID string) {
tracer := otel.Tracer("order-service")
_, span := tracer.Start(ctx, "processOrder",
trace.WithAttributes(
attribute.String("order.id", orderID),
),
)
defer span.End()
// 模拟数据库查询
queryDatabase(ctx, orderID)
}
func queryDatabase(ctx context.Context, query string) {
tracer := otel.Tracer("order-service")
_, span := tracer.Start(ctx, "db.query",
trace.WithAttributes(
attribute.String("db.system", "mysql"),
attribute.String("db.statement", query),
),
)
defer span.End()
// 数据库查询逻辑...
}
4.2 自动埋点库
OTel 为多种语言和框架提供了自动埋点库,无需修改业务代码:
| 语言 | HTTP 框架 | 数据库 | RPC |
|---|---|---|---|
| Go | net/http, Gin, Echo | database/sql, gorm | gRPC |
| Java | Spring, Servlet | JDBC, Hibernate | gRPC |
| Python | Flask, Django | SQLAlchemy, psycopg2 | gRPC |
| Node.js | Express, Fastify | mysql, pg | gRPC |
| .NET | ASP.NET Core | ADO.NET, EF Core | gRPC |
Java Spring Boot 自动埋点示例:
// 添加依赖
// build.gradle
dependencies {
implementation 'io.opentelemetry.instrumentation:opentelemetry-spring-boot-starter:1.32.0'
}
// application.yml
otel:
exporter:
otlp:
endpoint: http://otel-collector:4318
service:
name: order-service
version: 1.0.0
traces:
sampler:
type: parentbased_traceidratio
arg: 0.1
只需添加依赖和配置,Spring Boot 的 HTTP 请求、数据库查询、Kafka 消费等都会自动产生 Span,无需修改任何业务代码。
4.3 日志关联 Trace
// Java 使用 MDC 自动注入 TraceID
import org.slf4j.MDC;
// OTel SDK 自动将 TraceID 写入 MDC
// 日志格式中包含 %X{trace_id} 和 %X{span_id}
// logback.xml
<pattern>
%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level [%X{trace_id},%X{span_id}] %logger - %msg%n
</pattern>
// 输出示例:
// 2026-07-10 10:00:00 [http-nio-8080-exec-1] ERROR [7b3cf5b0123456789abcdef012345678,0123456789abcdef] OrderService - Failed to process order
在 Kibana 或 Loki 中搜索该 TraceID,可以直接跳转到 Jaeger 的 Trace 视图。
五、与后端集成
5.1 集成矩阵
| 后端 | Traces | Metrics | Logs | 协议 |
|---|---|---|---|---|
| Jaeger | ✓ | ✗ | ✗ | OTLP / Jaeger |
| Zipkin | ✓ | ✗ | ✗ | OTLP / Zipkin |
| Tempo | ✓ | ✗ | ✗ | OTLP |
| Prometheus | ✗ | ✓ | ✗ | remote_write |
| Mimir | ✗ | ✓ | ✗ | remote_write |
| VictoriaMetrics | ✗ | ✓ | ✓ | OTLP / remote_write |
| Elasticsearch | ✓ | ✓ | ✓ | OTLP / ES API |
| Loki | ✗ | ✗ | ✓ | OTLP / Loki API |
| Datadog | ✓ | ✓ | ✓ | OTLP / Datadog API |
| New Relic | ✓ | ✓ | ✓ | OTLP |
| Honeycomb | ✓ | ✓ | ✓ | OTLP |
| Grafana Cloud | ✓ | ✓ | ✓ | OTLP |
5.2 Grafana 全栈集成
OTel + Grafana 全栈是最常见的开源组合:
应用代码 (OTel SDK)
│
▼
OTel Collector
│
├── Traces → Tempo
├── Metrics → Mimir / Prometheus
└── Logs → Loki
│
▼
Grafana (统一可视化)
在 Grafana 中,三种信号通过 TraceID 关联:
- 在 Metrics 仪表盘看到错误率飙升
- 点击异常时间段的链接,跳转到 Logs 搜索
- 在 Logs 中找到错误日志,点击 TraceID
- 跳转到 Traces 视图,定位到具体服务和方法
5.3 商业后端集成
OTel 也支持发送到商业可观测性平台:
exporters:
# Datadog
datadog:
api:
key: ${DD_API_KEY}
site: datadoghq.com
# New Relic
otlp/newrelic:
endpoint: otlp.nr-data.net:4317
headers:
api-key: ${NEW_RELIC_LICENSE_KEY}
# Honeycomb
otlp/honeycomb:
endpoint: api.honeycomb.io:4317
headers:
"x-honeycomb-team": ${HONEYCOMB_API_KEY}
后端无关性的价值:使用 OTel 后,从开源后端迁移到商业后端只需修改 Collector 配置,不需要改应用代码。
六、采样策略
6.1 头部采样 vs 尾部采样
| 策略 | 采样位置 | 优势 | 劣势 |
|---|---|---|---|
| 头部采样 | 请求入口处 | 简单,资源消耗低 | 无法基于结果采样(可能漏掉错误请求) |
| 尾部采样 | 请求完成后 | 可基于结果采样(保留所有错误请求) | 需要缓存完整 Trace,资源消耗高 |
6.2 尾部采样配置
processors:
tail_sampling:
decision_wait: 10s # 等待 10s 收集完整 Trace
num_traces: 50000 # 内存中缓存的 Trace 数
expected_new_traces_per_sec: 1000
policies:
# 策略 1:保留所有错误请求
- name: errors
type: status_code
status_code:
status_codes: [ERROR]
# 策略 2:保留慢请求(> 1s)
- name: slow
type: latency
latency:
threshold_ms: 1000
# 策略 3:保留特定服务的请求
- name: critical-service
type: string_attribute
string_attribute:
key: service.name
values: ["payment-service", "order-service"]
# 策略 4:其余 10% 采样
- name: sample
type: probabilistic
probabilistic:
sampling_percentage: 10
尾部采样的效果:
1000 个请求 → 尾部采样 → 保留的 Trace:
- 5 个错误请求 → 全部保留
- 20 个慢请求 → 全部保留
- 100 个关键服务请求 → 全部保留
- 其余 875 个 × 10% → 87 个采样保留
总计: 5 + 20 + 100 + 87 = 212 个 Trace (21.2%)
七、迁移策略
7.1 从现有方案迁移到 OTel
| 现有方案 | 迁移路径 |
|---|---|
| Jaeger Client → OTel | OTel SDK 替换 Jaeger Client,Collector 转发到 Jaeger |
| Prometheus Client → OTel | OTel Metrics SDK + Collector remote_write 到 Prometheus |
| Logback/Log4j → OTel | OTel Logs SDK + Collector 导出到 ELK/Loki |
| Datadog → OTel | OTel SDK 替换 Datadog Agent,Collector 导出到 Datadog |
7.2 渐进式迁移步骤
阶段 1:部署 Collector(不改动应用)
→ Collector 接收现有格式数据,转发到现有后端
→ 验证 Collector 稳定
阶段 2:应用接入 OTel SDK(Traces 先行)
→ 新服务使用 OTel SDK
→ 老服务逐步替换
→ Collector 同时接收新旧格式
阶段 3:接入 Metrics 和 Logs
→ 应用日志关联 TraceID
→ Metrics 通过 OTel SDK 采集
阶段 4:统一后端
→ 所有数据通过 Collector 统一处理
→ 按需切换后端
7.3 兼容性配置
Collector 可以同时接收新旧格式数据,实现平滑迁移:
receivers:
# 新格式:OTel SDK 发送
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
# 旧格式:Jaeger Client 发送
jaeger:
protocols:
thrift_http:
endpoint: 0.0.0.0:14268
# 旧格式:Prometheus 拉取
prometheus:
config:
scrape_configs:
- job_name: 'legacy-apps'
static_configs:
- targets: ['app-1:9090']
八、性能与资源考量
8.1 SDK 性能影响
| 因素 | 影响 | 建议 |
|---|---|---|
| Span 数量 | CPU 和内存 | 合理采样,过滤健康检查 |
| 属性数量 | 网络和存储 | 限制每 Span 属性数 |
| 批量大小 | 网络效率 | 调整 batch 大小和超时 |
| 采样率 | 数据量和资源 | 生产环境 1-10% 采样 |
8.2 Collector 资源规划
| 规模 | CPU | 内存 | 副本数 |
|---|---|---|---|
| < 1 万 Span/s | 1 核 | 512MB | 2 |
| 1-10 万 Span/s | 2 核 | 1GB | 3 |
| 10-50 万 Span/s | 4 核 | 2GB | 3-5 |
| > 50 万 Span/s | 8 核 | 4GB | 5+ |
8.3 Collector 调优
processors:
batch:
timeout: 5s # 批量超时
send_batch_size: 1000 # 批量大小
send_batch_max_size: 2000 # 最大批量
memory_limiter:
check_interval: 1s # 检查间隔
limit_mib: 1024 # 内存上限
spike_limit_mib: 256 # 突发内存
service:
telemetry:
logs:
level: info # 日志级别
metrics:
address: 0.0.0.0:8888 # 自身指标端口
九、生产实践
9.1 资源标识规范
# Resource 属性规范
resource:
attributes:
# 必须属性
service.name: "order-service" # 服务名
service.version: "1.2.3" # 版本号
# 推荐属性
deployment.environment: "production" # 环境
host.name: "order-pod-abc" # 主机名
k8s.namespace.name: "production" # K8s 命名空间
k8s.pod.name: "order-pod-abc" # K8s Pod 名
# 可选属性
service.instance.id: "uuid-xxxx" # 实例 ID
cloud.provider: "aws" # 云提供商
cloud.region: "us-east-1" # 区域
9.2 监控 OTel 自身
# Prometheus 采集 Collector 指标
scrape_configs:
- job_name: 'otel-collector'
static_configs:
- targets: ['otel-collector:8888']
# 关键告警
groups:
- name: otel
rules:
- alert: OTelCollectorDroppingData
expr: rate(otelcol_processor_refused_spans[5m]) > 0
for: 5m
labels:
severity: critical
annotations:
summary: "OTel Collector 正在丢弃 Span 数据"
- alert: OTelCollectorQueueFull
expr: otelcol_exporter_queue_size / otelcol_exporter_queue_capacity > 0.9
for: 5m
labels:
severity: warning
annotations:
summary: "OTel Collector 导出队列接近满"
总结
OpenTelemetry 正在成为可观测性领域的统一标准:
- 统一标准:一套 SDK/API 采集三大信号(Metrics/Logs/Traces),消除数据采集层碎片化
- 后端无关:通过 Collector 解耦采集和后端,切换后端只需修改配置,不改代码
- Context 关联:TraceID 自动关联日志和指标,实现三种信号的关联分析
- 自动埋点:丰富的语言/框架库支持自动埋点,降低接入成本
- 采样策略:尾部采样保留所有错误和慢请求,同时降低正常请求的存储成本
- 渐进迁移:Collector 兼容新旧格式,可以平滑迁移而不中断现有监控
OTel 不替代后端(Prometheus/Jaeger/ELK 仍然各司其职),而是解决数据采集层的统一问题。如果你正在构建新的可观测性体系,OTel 应该是采集层的默认选择。已有系统也可以通过 Collector 渐进式迁移,享受统一标准带来的长期收益。
参考资料与致谢
本文在撰写过程中参考了以下资料,感谢原作者的贡献:
- OpenTelemetry 官方文档 — OpenTelemetry 社区,参考了OpenTelemetry 官方文档相关内容
- CNCF OpenTelemetry 规范 — GitHub 开源社区,参考了CNCF OpenTelemetry 规范相关内容