跳转至

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"

panic: 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 周,确认数据一致性后再关掉旧方案。