OpenTelemetry 遥测标准:Trace/Metric/Log 统一采集,从 SDK 接入到后端对接全指南
我们团队的 observability 改造是从 OpenTelemetry 开始的——之前 Prometheus + Jaeger + Loki 各自为政,SDK 满天飞,告警规则散落在三个系统里,运维成本极高。换成 OTel 之后,埋点统一、后端可替换、采样策略集中管控,整体复杂度降了一半。
这篇按实际落地顺序讲:SDK 接入 → Collector 编排 → 后端对接 → 常见问题排障。所有内容均来自我们在 Go/Python/Java 三个语言栈上的真实部署经验。
架构总览:四个关键组件
┌─────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ SDK │────▶│ Collector │────▶│ Exporter │────▶│ Backend │
│ (埋点 & 生成)│ │ (汇聚 & 处理) │ │ (协议转换) │ │ (存储 & 展示) │
└─────────────┘ └──────────────┘ └──────────────┘ └──────────────┘
Go / Python receivers processors Jaeger
Java / .NET transformers exporters Prometheus
exporters Loki
exporters Tempo
四个组件的关系: - SDK:嵌入应用代码,生成 trace/span/metric/log - Collector:进程外中转站,接收 SDK 数据、加工处理、转发给后端 - Exporter:Collector 里的处理器,负责把数据转换成后端能懂的格式 - Backend:存储和展示,可以是 Jaeger、Prometheus、Loki、Grafana Cloud 等
Step 1:SDK 接入(以 Go 为例)
Go SDK 是最简单的接入方式之一,因为大多数 Web 框架都有现成的 middleware。
// main.go
package main
import (
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
"go.opentelemetry.io/otel/sdk/metric"
"go.opentelemetry.io/otel/sdk/trace"
"context"
)
func initTracer(ctx context.Context) (*trace.TracerProvider, error) {
// OTLP HTTP 导出器 → 推给 Collector
traceExporter, err := otlptracehttp.New(ctx,
otlptracehttp.WithEndpoint("otel-collector:4318"),
otlptracehttp.WithInsecure(),
)
if err != nil { return nil, err }
tp := trace.NewTracerProvider(
trace.WithBatcher(traceExporter), // 批量发送,比 span 级别发送性能好
trace.WithSampler(trace.ParentBased(trace.TraceIDRatioBased(0.1))), // 10% 采样
)
otel.SetTracerProvider(tp)
return tp, nil
}
func initMeter(ctx context.Context) (*metric.MeterProvider, error) {
metricExporter, err := otlpmetrichttp.New(ctx,
otlpmetrichttp.WithEndpoint("otel-collector:4318"),
otlpmetrichttp.WithInsecure(),
)
if err != nil { return nil, err }
mp := metric.NewMeterProvider(
metric.WithReader(metric.NewPeriodicReader(metricExporter,
metric.WithInterval(15*time.Second))),
)
otel.SetMeterProvider(mp)
return mp, nil
}
这里最容易翻车:WithBatcher 的默认 batch size 是 512、flush timeout 是 30s。如果你的服务 QPS 很高(>1000),这个默认值会造成延迟——建议改成 WithBatchTimeout(5*time.Second),否则 Grafana 上的曲线会有明显锯齿。
Step 2:Collector 编排(最关键的环节)
Collector 的 YAML 配置是 OTel 架构的核心,写错了会导致数据丢失或重复。
# 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
send_batch_size: 1024
memory_limiter:
check_interval: 1s
limit_mib: 512 # 防 OOM,512MB 硬上限
attributes:
actions:
- key: environment
value: production
action: insert
exporters:
prometheus:
endpoint: 0.0.0.0:8889 # Prometheus 拉这个端口
otlp/jaeger:
endpoint: jaeger:4317
tls:
insecure: true
otlp/logzio: # 假设有日志后端
endpoint: logzio.example.com:8071
headers:
Authorization: Bearer ${LOGZIO_TOKEN}
service:
pipelines:
traces:
receivers: [otlp]
processors: [memory_limiter, batch, attributes]
exporters: [otlp/jaeger]
metrics:
receivers: [otlp]
processors: [memory_limiter, batch]
exporters: [prometheus]
logs:
receivers: [otlp]
processors: [memory_limiter, batch, attributes]
exporters: [otlp/logzio]
配置要点:
1. memory_limiter 必须加——没有限制的话,Collector 会在流量尖峰时 OOM 重启,导致数据断档
2. batch 的 send_batch_size 根据 QPS 调整——我们的 Go 服务 QPS 约 2000,设为 1024 比较合适
3. 环境变量引用用 ${VAR} 语法,不用 ${ENV_VAR},这是 OTel 的配置陷阱
Step 3:三种遥测数据的埋点对比
Trace:代码级调用链
tracer := otel.Tracer("github.com/example/my-service")
func HandleRequest(ctx context.Context, req Request) Response {
ctx, span := tracer.Start(ctx, "handle-request")
defer span.End()
span.SetAttributes(
attribute.String("http.method", req.Method),
attribute.Int("http.status", 200),
)
// 子 span
spanCtx, child := tracer.Start(ctx, "db-query")
result := queryDB(spanCtx, req.ID)
child.End()
return result
}
Metric:业务指标
meter := otel.Meter("my-service")
requestCount, _ := meter.Int64Counter("http.request.count")
requestDuration, _ := meter.Float64Histogram("http.request.duration",
metric.WithUnit("s"),
)
requestCount.Add(ctx, 1, metric.WithAttributes(
attribute.String("method", "GET"),
attribute.String("path", "/api/users"),
))
requestDuration.Record(ctx, duration.Seconds(), metric.WithAttributes(
attribute.String("method", "GET"),
attribute.String("path", "/api/users"),
))
注意:metric 的 attribute 数量要控制——超过 50 个 cardinality 的 attribute 会拖垮 Prometheus。
Log:结构化日志
logger := otelzap.New(octxLogger)
logger.Info("user action",
zap.String("action", "login"),
zap.String("user_id", uid),
zap.Int("duration_ms", durationMs),
)
Step 4:采样策略——别全采
采样是 OTel 最容易被忽视、但对成本影响最大的功能。
| 采样器 | 用途 | 推荐场景 |
|---|---|---|
AlwaysOn |
全量采样 | 开发环境、故障复现 |
AlwaysOff |
零采样 | 性能测试 |
TraceIDRatioBased(0.1) |
按比例 | 生产环境默认 |
ParentBased(RatioBased) |
子 span 继承父级采样 | 大多数场景 |
ProbabilitySelector |
恒定概率 | 需要确定性采样时 |
# Collector 级别的采样配置(比 SDK 级更灵活)
processors:
tail_sampling:
policies:
- name: errors
type: status_code
status_code: { status_codes: [ERROR] }
- name: slow-traces
type: latency
latency: { threshold_ms: 1000 }
- name: percentage
type: probabilistic
probabilistic: { sampling_percentage: 10 }
我的判断:生产环境用 tail_sampling 比 trace_id_ratio_based 更有效——前者只保留 error 和 slow trace,后者按比例随机丢。错误和慢请求才是你需要分析的,正常请求全采没有意义。
Step 5:对接主流后端
后端一:Prometheus(指标)
Collector 内置 prometheus exporter,Prometheus 直接拉 http://collector:8889/metrics。
# Prometheus scrape config
scrape_configs:
- job_name: 'otel-collector'
static_configs:
- targets: ['otel-collector:8889']
注意:OTel SDK 直接推给 Prometheus 的 metric name 带 otelcol_ 前缀(Collector 内部指标),业务 metric 不带。别把两个混在一起查。
后端二:Jaeger(trace)
# Collector exporters 里加
exporters:
otlp/jaeger:
endpoint: jaeger:14250 # Jaeger ingest gRPC
tls:
insecure: true
踩坑:Jaeger 对 service.name 属性敏感——不同服务必须有不同的 service.name,否则会合并到一个 span tree 里。OTel SDK 默认从环境变量 OTEL_SERVICE_NAME 读取,没设就用二进制名,容易撞。
后端三:Grafana Tempo(trace,对象存储后端)
Tempo 是 Grafana 推出的分布式 trace 后端,兼容 Jaeger 协议,但存储用的是 S3/GCS 这种对象存储,成本远低于 Elasticsearch。
# Collector → Tempo
exporters:
otlp/tempo:
endpoint: tempo:4317
tls:
insecure: true
# Tempo 配置
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
processors: [batch, memory_limiter]
exporters:
s3:
region: us-east-1
bucket: tempo-traces
endpoint: s3.amazonaws.com
Step 6:部署姿势——sidecar vs host
# 方式一:Sidecar(每个 pod 一个 Collector 容器)
# 优点:隔离好,独立配置
# 缺点:资源翻倍,调优复杂
# 方式二:Host DaemonSet(节点级别一个 Collector)
# 优点:资源节省,配置集中
# 缺点:共享资源竞争,调试困难
# 推荐:小集群用 DaemonSet,大集群用 Sidecar
# 关键原则: Collector 自身也要被监控
实操经验:我们在 50 节点集群上先试了 sidecar,每个月多花了 $2000 在 Collector 自身的资源消耗上,换成了 DaemonSet 后降到 $200/月,性能还更好了(零网络 hop)。
常见问题排障
问题一:SDK 初始化报错 "multiple TracerProviders set"
原因:同一个进程里 import 了两个版本的 OTel SDK(常见于依赖管理混乱)。检查 go.mod 里是否有两个不同版本的 go.opentelemetry.io/otel,用 go mod graph 找来源。
问题二:Collector 启动报 "port already in use"
原因:0.0.0.0:4317 或 4318 被其他进程占用。用 lsof -i :4317 查占用者,或改端口。
问题三:Jaeger 里 span 时间戳对不上
原因:不同服务的时钟偏差。OTel SDK 用 NTP 校时,但 clock skew 在 >50ms 时就会显示异常。解决方法:在 Collector 里加 clock_skew_adjuster processor(OTel 0.100+ 支持)。
问题四:Metric 丢失,Prometheus 抓不到
检查顺序:
1. SDK 是否正确调用 meterProvider.Shutdown()(否则数据在 buffer 里没 flush)
2. Collector prometheus exporter 的 endpoint 是否正确
3. Prometheus scrape_interval 是否太短导致 Collector 来不及 flush(建议 ≥15s)
FAQ
Q:OpenTelemetry 和自建埋点方案怎么选?
自建方案(如直接打 Jaeger client、直接推 Prometheus metrics)灵活但维护成本高,每个服务要自己管 SDK 升级、采样策略、错误处理。OTel 的好处是统一标准——换后端、换语言、换采样策略都是配置改动,不用改业务代码。如果你的团队有多个服务、多种语言,OTel 的 ROI 更高。
Q:OTel Collector 和 Fluentd/Fluent Bit 有什么区别?
Fluentd/Fluent Bit 是日志专用,OTel Collector 同时处理 trace/metric/log。如果你们只有日志需求,Fluent Bit 更轻量;如果需要统一 observability 栈,OTel Collector 更合适。OTel 的日志处理能力还在快速迭代中,不如 Fluent Bit 成熟,但方向是对的。
Q:OTel SDK 对已有项目的侵入性大吗?
中等。trace 和 metric 需要手动埋点(或在框架 middleware 里自动注入),log 改动最小。最大的成本不是代码改动,而是迁移期间两个系统并行跑带来的数据不一致。建议做法:先并行跑 2-4 周,确认数据一致性后再关掉旧方案。