土法炼钢兴趣小组的算法知识备份

【可观测性工程】OpenTelemetry 深入:SDK、Collector、语义约定与版本演进

文章导航

分类入口
architectureobservability
标签入口
#opentelemetry#otlp#collector#semantic-conventions#tracing#metrics#logs#otel-sdk#tail-sampling

目录

可观测性(Observability)工程在过去五年里经历了一次范式迁移:从「每个后端一套 SDK」到「一套信号采集标准 + 多个后端」。这场迁移的主角是云原生计算基金会(CNCF)孵化的 OpenTelemetry(简称 OTel)。它不仅统一了链路追踪(Tracing)、指标(Metrics)、日志(Logs)三类信号的数据模型、协议和语义,还通过 Collector 将「采集层」从应用代码里彻底解耦出来。

本文面向已经落地过 Prometheus、ELK 或 Jaeger 的 SRE 与平台工程师,讲清楚 OTel 的来龙去脉、协议细节、SDK 生命周期、Collector 流水线、语义约定(Semantic Conventions)以及在国内厂商(阿里云 ARMS、观测云、夜莺)上落地时会踩的坑。文章假设读者理解本系列前面几篇关于指标、日志、链路的文章,因此在信号本身的概念上不做过多铺陈,而是聚焦在「把 OTel 塞进一个已有的、跑在生产上的可观测性体系里」这件事上。

OTel 流水线架构

一、起源:OpenTracing 与 OpenCensus 的合并(2019)

1.1 OpenTracing 时代(2016—2019)

2016 年 10 月,OpenTracing 项目正式进入 CNCF 孵化。它的目标非常克制:只定义链路追踪的 API,不定义数据协议,也不提供 SDK 实现。换句话说,OpenTracing 是一组接口(Interface),具体的 TracerSpanSpanContextPropagator 都交由各个厂商实现。

OpenTracing 的优点是「中立」。从 Jaeger、Zipkin 到 New Relic、Datadog,每家厂商都可以提供符合 OpenTracing 接口的适配器,应用代码只依赖 API。但它的问题也正出在「只做 API」上:

1.2 OpenCensus 时代(2017—2019)

几乎在 OpenTracing 成形的同时,Google 与 Microsoft 联合推出了 OpenCensus。OpenCensus 的出发点是「一套 SDK 同时处理 Traces 与 Metrics」。它不止定义 API,还给出了参考实现、导出器(Exporter)以及一个小型的代理 ocagent

OpenCensus 的设计相对完整,尤其是指标部分,直接继承了 Google 内部 Census 项目多年的打磨成果。但它也有局限:

1.3 碎片化困境与供应商锁定

到 2018 年底,企业里经常同时出现三种埋点:

  1. Prometheus 客户端库采集指标。
  2. OpenTracing 或厂商私有 SDK 采集链路。
  3. 日志框架(Log4j、Zap、Logback)直接写本地文件,再由 Filebeat 或 Fluent Bit 采集。

每个信号一套 SDK,导致应用代码里充斥着互不相通的抽象;换后端要改代码,「供应商锁定」(Vendor Lock-in)事实上并没有被解除,只是从「接 Datadog」变成了「接 Jaeger + Prometheus + ELK」。更严重的是,同一次请求的 trace_id 很难在指标和日志里关联上,因为没有跨信号的统一 Resource 概念。

1.4 合并公告与演进时间线

2019 年 5 月,OpenTracing 与 OpenCensus 两个项目的 TOC(Technical Oversight Committee)共同宣布合并为 OpenTelemetry,定位是「统一的可观测性数据采集标准」。关键时间线如下:

合并带来的直接收益是:API、SDK、协议、语义约定四者统一在同一个项目下演进,信号之间通过共享的 ResourceContext 天然关联。

二、OTel 整体架构

2.1 五层模型

OTel 的架构可以抽象为五层:

  1. API 层:定义 TracerMeterLogger 接口,不含任何实现。应用代码、第三方库只依赖 API。
  2. SDK 层:API 的默认实现,负责采样(Sampling)、批处理、资源检测(Resource Detection)、导出。
  3. Instrumentation Library 层:针对 HTTP、gRPC、数据库、消息队列等组件的埋点库,分为「手动埋点」和「自动埋点」两类。
  4. Exporter 层:把 SDK 内部的数据结构序列化为具体协议(OTLP、Zipkin、Prometheus Remote Write 等)。
  5. Collector 层:独立进程,负责接收、处理、路由遥测(Telemetry)数据到一个或多个后端。

应用只感知 API 层与 Instrumentation Library;SDK、Exporter、Collector 都可以替换或横向扩展,不影响业务代码。

2.2 OTLP 协议

OTLP 是 OpenTelemetry Protocol 的缩写,是 OTel 项目唯一「官方推荐且长期维护」的协议。它有两种传输方式:

两者共用同一套 protobuf schema,只是传输层不同。生产环境里建议默认用 gRPC,只有在以下场景退化到 HTTP:

2.3 SDK 生命周期

SDK 的内部生命周期可以概括为:Provider → Processor → Exporter

整个流水线是同步构造、异步执行的:应用线程只做 Tracer.Start() / Span.End() / Counter.Add(),这些调用只往内存缓冲区写数据;真正的网络 IO 发生在后台批处理线程里。这是 OTel 对性能的基本承诺——埋点不阻塞业务。

三、OTLP 协议详解

3.1 protobuf schema 概览

OTLP 的 protobuf 定义存放在 opentelemetry-proto 仓库中,核心是三个 Service:

service TraceService {
  rpc Export(ExportTraceServiceRequest) returns (ExportTraceServiceResponse);
}
service MetricsService {
  rpc Export(ExportMetricsServiceRequest) returns (ExportMetricsServiceResponse);
}
service LogsService {
  rpc Export(ExportLogsServiceRequest) returns (ExportLogsServiceResponse);
}

每个请求都是「资源—作用域—数据」的三级结构。以 Traces 为例:

message ResourceSpans {
  Resource resource = 1;
  repeated ScopeSpans scope_spans = 2;
  string schema_url = 3;
}
message ScopeSpans {
  InstrumentationScope scope = 1;
  repeated Span spans = 2;
  string schema_url = 3;
}
message Span {
  bytes trace_id = 1;            // 16 字节
  bytes span_id = 2;             // 8 字节
  bytes parent_span_id = 4;
  string name = 5;
  SpanKind kind = 6;
  fixed64 start_time_unix_nano = 7;
  fixed64 end_time_unix_nano = 8;
  repeated KeyValue attributes = 9;
  repeated Event events = 11;
  repeated Link links = 13;
  Status status = 15;
}

三个关键点:

Metrics 的 schema 更复杂,因为要表达 SumGaugeHistogramExponentialHistogramSummary 五种数据点,并区分 CumulativeDelta 两种时间性。Logs 的 schema 最简单,基本就是 timestamp + severity + body + attributes

3.2 gRPC 与 HTTP/protobuf 对比

维度 OTLP/gRPC OTLP/HTTP+protobuf
端口 4317 4318
HTTP/2 流式 HTTP/1.1 或 HTTP/2 单次 POST
复用连接 天然多路复用 需靠 keep-alive
压缩 gzip、zstd、snappy gzip
Web 支持 基本不支持 原生支持
流量观测性 需要 gRPC 感知的网关 标准 HTTP 可观测
常见吞吐 更高,心跳低 取决于连接复用
错误码 gRPC Status HTTP 状态码 + protobuf error

一个常见的生产选择是:节点内 Agent → Gateway 用 gRPC;业务从应用到 Collector 也用 gRPC;只在 Web 前端与第三方集成场景走 HTTP。

3.3 压缩、重试、批处理

OTLP 客户端普遍暴露以下参数,以下以 Go SDK 为例:

exp, err := otlptracegrpc.New(ctx,
    otlptracegrpc.WithEndpoint("otel-collector.observability:4317"),
    otlptracegrpc.WithInsecure(),
    otlptracegrpc.WithCompressor("gzip"),
    otlptracegrpc.WithTimeout(10*time.Second),
    otlptracegrpc.WithRetry(otlptracegrpc.RetryConfig{
        Enabled:         true,
        InitialInterval: 1 * time.Second,
        MaxInterval:     30 * time.Second,
        MaxElapsedTime:  5 * time.Minute,
    }),
    otlptracegrpc.WithHeaders(map[string]string{
        "x-tenant-id": "payments",
    }),
)

几个经验值:

3.4 错误语义

OTLP 定义了「部分成功」(Partial Success)机制:服务端在 ExportTraceServiceResponse 里返回 partial_success.rejected_spans 计数和 error_message。客户端遇到 4xxInvalidArgumentPermissionDenied)通常不应重试,5xxUnavailableDeadlineExceededResourceExhausted)应该按指数回退重试。这一点在 Collector 的 exporter/retry_on_failure 配置里也有对应开关。

四、Semantic Conventions(语义约定)

4.1 为什么要有语义约定

语义约定是 OTel 项目里最「不性感」但最关键的产出之一。它定义了「什么属性应该叫什么名字、值应该是什么类型」。没有它,http.methodhttp.verbhttpMethod 三种写法会同时存在,后端无法做统一查询和面板。

语义约定的工程意义有三层:

  1. 跨团队的数据契约(Data Contract):平台团队可以基于固定的属性名写出通用面板,无需每个业务团队另做一套。
  2. 跨厂商的可移植性:同一份遥测数据能在 Tempo、Jaeger、Datadog、阿里云 ARMS 上展现一致。
  3. 自动埋点的一致性基础:所有 opentelemetry-instrumentation-* 库都按约定发出属性,用户无需再改代码。

从 v1.21(2023 年起)开始,语义约定独立为 opentelemetry-semantic-conventions 仓库,采用自己的版本号,并通过 schema_url 字段在数据里显式声明版本,方便后端做字段迁移。

4.2 Resource 语义约定

资源属性描述「谁在产生数据」,常见字段:

属性 含义 示例
service.name 服务名,必填 order-service
service.namespace 服务所在逻辑域 payments
service.version 发布版本 2026.04.22-a1b2c3d
service.instance.id 实例唯一 ID pod-uidhostname-pid
deployment.environment 环境 prodstagingcanary
host.name 主机名 node-01
host.arch CPU 架构 amd64arm64
os.type 操作系统类型 linux
process.pid 进程 PID 12345
telemetry.sdk.name SDK 名称 opentelemetry
telemetry.sdk.language SDK 语言 gojavapython
telemetry.sdk.version SDK 版本 1.27.0

其中 service.name 是唯一一个规范标注为「必填」的字段——缺失时 Collector 会把它填为 unknown_service:<process.executable.name>,这是线上最常见的「数据到了但没人认领」的原因。

4.3 HTTP 语义约定

HTTP 属性在 v1.20(2023-03)做过一次重大重命名,从 http.method 改为 http.request.method,从 http.url 拆成 url.scheme / url.full / url.path。v1.23 起旧字段标记为 deprecated,但 SDK 为了兼容仍会双写。升级 Collector 时要留意这一点。

新版字段(稳定版):

属性 示例
http.request.method GETPOST
http.response.status_code 200500
http.route /api/v1/orders/:id
url.scheme https
url.full https://api.example.com/v1/orders/42?debug=1
url.path /v1/orders/42
url.query debug=1
server.address api.example.com
server.port 443
user_agent.original Go-http-client/1.1

http.route 作为指标维度是高基数(High Cardinality)控制的经典做法——它是模板而非具体 path,/api/v1/orders/:id 永远只有一个取值。

4.4 数据库语义约定

db.system         = "postgresql" | "mysql" | "redis" | "mongodb" | "clickhouse"
db.connection_string
db.user
db.name           = 业务库名
db.statement      = 完整 SQL(注意脱敏)
db.operation      = "SELECT" | "INSERT" | "UPDATE" | "DELETE"
db.sql.table      = 主表名

db.statement 是敏感字段——上线前务必配合 Collector 的 transform processor 或 SDK 的 SpanProcessor 做参数化(例如把 WHERE id = 12345 改成 WHERE id = ?),否则慢查询报告会把密码、手机号之类的敏感数据带进 trace 后端。

4.5 消息队列语义约定

messaging.system        = "kafka" | "rabbitmq" | "rocketmq" | "pulsar"
messaging.destination.name
messaging.operation     = "publish" | "receive" | "process"
messaging.message.id
messaging.kafka.consumer.group
messaging.kafka.partition
messaging.batch.message_count

消息队列埋点里最容易漏的是 messaging.operation——没有它,就无法区分一个 span 是「发送」还是「消费」,面板上会把两类耗时搅在一起。

4.6 Kubernetes 语义约定

K8s 资源属性一般由 Collector 的 k8sattributes processor 自动注入,不需要 SDK 关心:

k8s.cluster.name
k8s.namespace.name
k8s.pod.name
k8s.pod.uid
k8s.pod.start_time
k8s.deployment.name
k8s.statefulset.name
k8s.node.name
k8s.container.name
container.image.name
container.image.tag

注入的方式是 Collector 监听 Kubernetes API,根据 IP 或 pod.uid 把 pod 元数据补上。一个实用技巧:让 Collector 以 DaemonSet 形式运行,k8sattributes 只 watch 本节点的 pod,减轻 API Server 压力。

4.7 语义约定的工程价值

假设一家公司有 300 个微服务,每个服务需要做三件事:「按环境过滤」「按版本做灰度(Canary)对比」「按 pod 定位问题」。没有语义约定时,每个业务团队各自选字段名,导致平台 Grafana 面板必须为每个服务单独写查询;有语义约定后,所有面板只需要 service.namedeployment.environmentservice.versionk8s.pod.name 这几个通用变量,面板复用率极高。这是 OTel 项目在大规模组织里能跑通的核心原因之一。

五、SDK 生命周期管理

5.1 Provider 三兄弟

SDK 里分别有 TracerProviderMeterProviderLoggerProvider 三个 Provider,它们是「工厂」而不是「数据源」。Provider 的生命周期等于进程生命周期:启动时构造,关闭前调用 Shutdown()

一个典型的 Go 初始化流程:

package otelsetup

import (
    "context"
    "fmt"
    "time"

    "go.opentelemetry.io/otel"
    "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
    "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc"
    "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc"
    "go.opentelemetry.io/otel/propagation"
    "go.opentelemetry.io/otel/sdk/metric"
    "go.opentelemetry.io/otel/sdk/resource"
    sdktrace "go.opentelemetry.io/otel/sdk/trace"
    sdklog "go.opentelemetry.io/otel/sdk/log"
    semconv "go.opentelemetry.io/otel/semconv/v1.26.0"
)

func Init(ctx context.Context, serviceName, version, env string) (func(context.Context) error, error) {
    res, err := resource.New(ctx,
        resource.WithFromEnv(),
        resource.WithTelemetrySDK(),
        resource.WithHost(),
        resource.WithProcessPID(),
        resource.WithAttributes(
            semconv.ServiceName(serviceName),
            semconv.ServiceVersion(version),
            semconv.DeploymentEnvironment(env),
        ),
    )
    if err != nil {
        return nil, fmt.Errorf("resource: %w", err)
    }

    traceExp, err := otlptracegrpc.New(ctx,
        otlptracegrpc.WithEndpoint("otel-collector:4317"),
        otlptracegrpc.WithInsecure(),
        otlptracegrpc.WithCompressor("gzip"),
    )
    if err != nil {
        return nil, err
    }
    tp := sdktrace.NewTracerProvider(
        sdktrace.WithResource(res),
        sdktrace.WithSampler(sdktrace.ParentBased(sdktrace.TraceIDRatioBased(0.1))),
        sdktrace.WithBatcher(traceExp,
            sdktrace.WithBatchTimeout(2*time.Second),
            sdktrace.WithMaxExportBatchSize(512),
            sdktrace.WithMaxQueueSize(4096),
        ),
    )
    otel.SetTracerProvider(tp)

    metricExp, err := otlpmetricgrpc.New(ctx,
        otlpmetricgrpc.WithEndpoint("otel-collector:4317"),
        otlpmetricgrpc.WithInsecure(),
    )
    if err != nil {
        return nil, err
    }
    mp := metric.NewMeterProvider(
        metric.WithResource(res),
        metric.WithReader(metric.NewPeriodicReader(metricExp,
            metric.WithInterval(15*time.Second))),
    )
    otel.SetMeterProvider(mp)

    logExp, err := otlploggrpc.New(ctx,
        otlploggrpc.WithEndpoint("otel-collector:4317"),
        otlploggrpc.WithInsecure(),
    )
    if err != nil {
        return nil, err
    }
    lp := sdklog.NewLoggerProvider(
        sdklog.WithResource(res),
        sdklog.WithProcessor(sdklog.NewBatchProcessor(logExp)),
    )

    otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(
        propagation.TraceContext{}, propagation.Baggage{},
    ))

    return func(ctx context.Context) error {
        _ = tp.Shutdown(ctx)
        _ = mp.Shutdown(ctx)
        _ = lp.Shutdown(ctx)
        return nil
    }, nil
}

几个容易被忽略的点:

5.2 SpanProcessor:Simple vs Batch

SimpleSpanProcessor:  每结束一个 Span 立刻调用 Exporter
BatchSpanProcessor:   攒 Queue,定时或定量批量导出

5.3 Exporter 接口

Exporter 只有两个方法:Export(batch)Shutdown()。这个极简的设计让替换非常容易。生产里常见的 Exporter:

5.4 Propagator 与上下文传播

Propagator 负责把 SpanContext 注入和抽取到/从外部载体(Carrier)。OTel 默认注册的 Propagator 是 W3C Trace Context(traceparent / tracestate)加 W3C Baggage。国内生产环境里会遇到的历史格式:

迁移策略:在 Propagator 里同时注册 traceparentb3 两套,发送侧同时写、接收侧按优先级读,这样新旧服务可以灰度共存。

otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(
    propagation.TraceContext{},
    propagation.Baggage{},
    b3.New(b3.WithInjectEncoding(b3.B3MultipleHeader)),
))

5.5 Java 初始化示例

Java 通常用 BOM(Bill of Materials)拉依赖,推荐 opentelemetry-bomopentelemetry-instrumentation-bom-alpha 搭配:

<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>io.opentelemetry</groupId>
      <artifactId>opentelemetry-bom</artifactId>
      <version>1.42.0</version>
      <type>pom</type>
      <scope>import</scope>
    </dependency>
  </dependencies>
</dependencyManagement>

初始化:

Resource resource = Resource.getDefault().merge(
    Resource.create(Attributes.of(
        ResourceAttributes.SERVICE_NAME, "order-service",
        ResourceAttributes.SERVICE_VERSION, "2026.04.22",
        ResourceAttributes.DEPLOYMENT_ENVIRONMENT, "prod"
    )));

SdkTracerProvider tracerProvider = SdkTracerProvider.builder()
    .addSpanProcessor(BatchSpanProcessor.builder(
        OtlpGrpcSpanExporter.builder()
            .setEndpoint("http://otel-collector:4317")
            .setCompression("gzip")
            .build())
        .setMaxQueueSize(4096)
        .setScheduleDelay(Duration.ofSeconds(2))
        .build())
    .setResource(resource)
    .setSampler(Sampler.parentBased(Sampler.traceIdRatioBased(0.1)))
    .build();

OpenTelemetrySdk sdk = OpenTelemetrySdk.builder()
    .setTracerProvider(tracerProvider)
    .setMeterProvider(meterProvider)
    .setLoggerProvider(loggerProvider)
    .setPropagators(ContextPropagators.create(
        TextMapPropagator.composite(
            W3CTraceContextPropagator.getInstance(),
            W3CBaggagePropagator.getInstance())))
    .buildAndRegisterGlobal();

Runtime.getRuntime().addShutdownHook(new Thread(() -> {
    sdk.getSdkTracerProvider().shutdown().join(10, TimeUnit.SECONDS);
    sdk.getSdkMeterProvider().shutdown().join(10, TimeUnit.SECONDS);
    sdk.getSdkLoggerProvider().shutdown().join(10, TimeUnit.SECONDS);
}));

5.6 Python 初始化示例

Python 的 API/SDK 包分得比较细,需要各自安装:

pip install opentelemetry-api==1.27.0 \
            opentelemetry-sdk==1.27.0 \
            opentelemetry-exporter-otlp==1.27.0 \
            opentelemetry-instrumentation==0.48b0

初始化:

from opentelemetry import trace, metrics
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.sdk.metrics import MeterProvider
from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import OTLPMetricExporter
from opentelemetry.propagate import set_global_textmap
from opentelemetry.propagators.composite import CompositePropagator
from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator
from opentelemetry.baggage.propagation import W3CBaggagePropagator

resource = Resource.create({
    "service.name": "recommend-service",
    "service.version": "2026.04.22",
    "deployment.environment": "prod",
})

tp = TracerProvider(resource=resource)
tp.add_span_processor(BatchSpanProcessor(OTLPSpanExporter(
    endpoint="otel-collector:4317", insecure=True)))
trace.set_tracer_provider(tp)

mp = MeterProvider(resource=resource, metric_readers=[
    PeriodicExportingMetricReader(OTLPMetricExporter(
        endpoint="otel-collector:4317", insecure=True),
        export_interval_millis=15000)])
metrics.set_meter_provider(mp)

set_global_textmap(CompositePropagator([
    TraceContextTextMapPropagator(), W3CBaggagePropagator()
]))

5.7 跨进程上下文传播

OTel 的上下文传播走的是 W3C traceparent 头:

traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
             └┬┘ └──────────────┬──────────────┘ └──────┬──────┘ └┬┘
              │                 │                       │         │
            version          trace-id               span-id      flags

flags 最低位是 sampled01 表示上游决定采样,00 表示上游决定不采样。下游通常遵循「父亲说啥就啥」的 ParentBased 采样策略,避免出现「一半 span 有数据、一半没有」的断裂链路。

对于 Kafka、RocketMQ 等消息中间件,traceparent 可以注入到消息 headers;RabbitMQ 可以放在 properties.headers;HTTP 自然走请求头;gRPC 通过 metadata 传递。跨异步调用时一定要手动调 propagator.inject(ctx, carrier),否则链路会断。

六、Collector 架构深入

6.1 六大组件

Collector 的配置是一个显式的「数据流图」,由六类组件组成:

6.2 完整 YAML 示例:OTLP 进,多后端出

receivers:
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317
        max_recv_msg_size_mib: 32
      http:
        endpoint: 0.0.0.0:4318
  prometheus:
    config:
      scrape_configs:
        - job_name: kubernetes-pods
          kubernetes_sd_configs: [{ role: pod }]
  filelog:
    include: [/var/log/pods/*/*/*.log]
    start_at: end
    include_file_path: true
    operators:
      - type: container
        id: container-parser

processors:
  memory_limiter:
    check_interval: 2s
    limit_percentage: 75
    spike_limit_percentage: 15
  batch:
    send_batch_size: 8192
    timeout: 5s
    send_batch_max_size: 16384
  k8sattributes:
    auth_type: serviceAccount
    passthrough: false
    extract:
      metadata: [k8s.pod.name, k8s.pod.uid, k8s.namespace.name, k8s.node.name,
                 k8s.deployment.name, k8s.container.name]
      labels:
        - from: pod
          key_regex: app.kubernetes.io/(.*)
          tag_name: $$1
    pod_association:
      - sources: [{ from: resource_attribute, name: k8s.pod.ip }]
      - sources: [{ from: connection }]
  resource:
    attributes:
      - key: deployment.environment
        value: prod
        action: upsert
  attributes/redact:
    actions:
      - key: db.statement
        action: hash
      - key: http.request.header.authorization
        action: delete
  transform/http_route:
    trace_statements:
      - context: span
        statements:
          - set(name, attributes["http.route"]) where attributes["http.route"] != nil
  tail_sampling:
    decision_wait: 10s
    num_traces: 200000
    expected_new_traces_per_sec: 2000
    policies:
      - name: errors
        type: status_code
        status_code: { status_codes: [ERROR] }
      - name: slow
        type: latency
        latency: { threshold_ms: 1000 }
      - name: random-1pct
        type: probabilistic
        probabilistic: { sampling_percentage: 1 }
      - name: always-for-payments
        type: string_attribute
        string_attribute:
          key: service.name
          values: [payments-service, order-service]

exporters:
  otlp/tempo:
    endpoint: tempo-distributor:4317
    tls: { insecure: true }
    sending_queue: { enabled: true, queue_size: 10000, num_consumers: 10 }
    retry_on_failure: { enabled: true, initial_interval: 5s, max_elapsed_time: 5m }
  prometheusremotewrite:
    endpoint: http://prometheus:9090/api/v1/write
    resource_to_telemetry_conversion: { enabled: true }
    external_labels: { cluster: shanghai-prod }
  loki:
    endpoint: http://loki:3100/loki/api/v1/push
  kafka:
    brokers: [kafka-0:9092, kafka-1:9092, kafka-2:9092]
    topic: otlp_spans
    encoding: otlp_proto
    producer: { compression: snappy, max_message_bytes: 1000000 }
  clickhouse:
    endpoint: tcp://clickhouse:9000?dial_timeout=10s
    database: otel
    ttl: 720h

extensions:
  health_check: { endpoint: 0.0.0.0:13133 }
  pprof: { endpoint: 0.0.0.0:1777 }
  zpages: { endpoint: 0.0.0.0:55679 }
  file_storage:
    directory: /var/lib/otelcol/queue
    timeout: 1s

service:
  extensions: [health_check, pprof, zpages, file_storage]
  telemetry:
    logs: { level: info }
    metrics: { address: 0.0.0.0:8888 }
  pipelines:
    traces:
      receivers: [otlp]
      processors: [memory_limiter, k8sattributes, resource, attributes/redact,
                   transform/http_route, tail_sampling, batch]
      exporters: [otlp/tempo, kafka, clickhouse]
    metrics:
      receivers: [otlp, prometheus]
      processors: [memory_limiter, k8sattributes, resource, batch]
      exporters: [prometheusremotewrite]
    logs:
      receivers: [otlp, filelog]
      processors: [memory_limiter, k8sattributes, resource, batch]
      exporters: [loki, clickhouse]

这个配置在实际生产里经受过 10 万 span/s 的压力测试,有几个关键细节值得单独说:

6.3 Processor 清单与职责

Processor 场景
memory_limiter 内存防护,必配
batch 批处理,几乎必配
resource 全局资源属性注入
attributes 属性的增删改、哈希、脱敏
filter 按条件过滤数据
transform OTTL 表达式做字段级别的转换,v0.80+ 新一代主力
k8sattributes K8s 元数据注入
tail_sampling 尾采样
probabilistic_sampler 头采样
spanmetrics(connector) 从 trace 派生 RED 指标
servicegraph(connector) 从 trace 派生服务依赖图
routing 按属性把数据路由到不同 exporter
groupbytrace 将同 trace span 聚合,供尾采样使用

6.4 部署模式:Agent 与 Gateway

OTel Collector 有两种典型部署形态:

两者组合成「Agent → Gateway → Backend」的两级流水线:

App ── OTLP ── Agent(DaemonSet) ── OTLP ── Gateway(Deployment) ── {Tempo, Prom, Loki}

Agent 层关心「少丢、少漏」,Gateway 层关心「正确采样、正确路由」。

6.5 Collector 扩缩容

Metrics 和 Logs pipeline 是无状态的,可以直接基于 CPU/内存 HPA 扩容。Traces 的尾采样是「按 trace_id 聚合」的有状态操作,不能随便分片。解决方案:

exporters:
  loadbalancing:
    protocol:
      otlp:
        tls: { insecure: true }
    resolver:
      dns:
        hostname: otel-gateway.observability.svc.cluster.local
        port: 4317
    routing_key: traceID

6.6 Pipeline 有向图示例

一个 spanmetrics connector 把 trace pipeline 派生出一个 metrics pipeline 的典型配置:

connectors:
  spanmetrics:
    histogram:
      explicit:
        buckets: [5ms, 10ms, 50ms, 100ms, 500ms, 1s, 5s]
    dimensions:
      - name: service.name
      - name: http.route
      - name: http.response.status_code
service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [memory_limiter, batch]
      exporters: [otlp/tempo, spanmetrics]
    metrics/spanmetrics:
      receivers: [spanmetrics]
      exporters: [prometheusremotewrite]

这样一份 trace 流量同时产出原始 span 数据与 RED 指标,后者直接接进 Prometheus,Grafana 面板可以立刻用上。

七、自动埋点(Auto-Instrumentation)

7.1 Java Agent

Java 的零代码埋点是 OTel 生态里最成熟的一环,使用 opentelemetry-javaagent.jar

wget https://github.com/open-telemetry/opentelemetry-java-instrumentation/releases/latest/download/opentelemetry-javaagent.jar

java -javaagent:./opentelemetry-javaagent.jar \
     -Dotel.service.name=order-service \
     -Dotel.exporter.otlp.endpoint=http://otel-collector:4317 \
     -Dotel.traces.sampler=parentbased_traceidratio \
     -Dotel.traces.sampler.arg=0.1 \
     -Dotel.resource.attributes=service.version=2026.04.22,deployment.environment=prod \
     -jar app.jar

javaagent 使用 ByteBuddy 在类加载时注入字节码,覆盖 100 多种框架(Spring、Tomcat、Jetty、Netty、JDBC、Redis、Kafka、gRPC、OkHttp、HttpClient 等)。它的价值是「老系统零改动接入」,代价是:

7.2 Python

pip install opentelemetry-distro opentelemetry-exporter-otlp
opentelemetry-bootstrap -a install
OTEL_SERVICE_NAME=recommend-service \
OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317 \
OTEL_TRACES_SAMPLER=parentbased_traceidratio \
OTEL_TRACES_SAMPLER_ARG=0.1 \
opentelemetry-instrument python app.py

opentelemetry-bootstrap 会扫描当前已安装的依赖,把对应的 opentelemetry-instrumentation-* 包自动装上,省去一个个挑库的麻烦。

7.3 Go

Go 由于编译型语言的特性,没有真正的「运行时自动埋点」。现阶段有两条路:

Go 生产里主流仍是「在框架入口手动接一次」+ 「HTTP Client、DB Client、Kafka Client 都走已埋点的封装库」,代码侵入度不高。

7.4 Node.js

npm install @opentelemetry/api @opentelemetry/auto-instrumentations-node \
            @opentelemetry/exporter-trace-otlp-grpc
node --require @opentelemetry/auto-instrumentations-node/register app.js

Node.js 自动埋点基于 require hook / ESM loader,覆盖 Express、Koa、Fastify、gRPC、MongoDB、Redis 等。

7.5 零代码 vs 手动埋点权衡

维度 自动埋点 手动埋点
接入速度 分钟级 天到周
业务语义 只到框架层 可到业务事件级别
升级成本 跟 agent 走 跟代码仓库走
性能控制 不易细调 可精细裁剪
与自研框架兼容 可能缺埋点 完整覆盖

实操经验:先让自动埋点覆盖 80% 的 HTTP/DB/MQ 链路,再用手动埋点补业务关键节点(下单、风控决策、鉴权)。两种不冲突,可并存。

八、尾采样(Tail Sampling)

8.1 头采样与尾采样的差异

8.2 tail_sampling processor

上文 YAML 里已经有完整示例,这里拆解核心参数:

策略类型包括:latencystatus_codenumeric_attributestring_attributerate_limitingprobabilisticcompositeandtrace_stateottl_condition

8.3 与 loadbalancing receiver 配合

尾采样要求同 trace 的 span 落在同一个 Collector 实例,必须在上游(Agent 或第一层 Gateway)用 loadbalancing exporter 做 trace_id 哈希。第二层 Gateway 才跑 tail_sampling。下图是工程上普遍使用的三层:

App ─ OTLP ─ Agent(DaemonSet)
                  │ batch + resource
                  ▼
            Gateway-L1(LB)  ── trace_id hash ──▶ Gateway-L2(tail_sampling) ─▶ Tempo

8.4 尾采样的落地成本

一个务实的策略组合:100% 错误 + 100% > P99 延迟 + 10% 关键服务 + 1% 其它服务。能把存储量压到头采样的 1/5,同时保留绝大部分有价值的链路。

九、多租户架构

9.1 目标

平台团队常常要给多个业务方提供统一 Collector 集群,要求:

9.2 租户识别

租户信息的常见来源:

推荐统一用 Resource 属性 tenant.id,由 SDK 侧通过 OTEL_RESOURCE_ATTRIBUTES=tenant.id=payments 注入;Collector 用 routing processor/connector 分流。

9.3 路由示例

connectors:
  routing:
    default_pipelines: [traces/default]
    error_mode: ignore
    table:
      - context: resource
        condition: attributes["tenant.id"] == "payments"
        pipelines: [traces/payments]
      - context: resource
        condition: attributes["tenant.id"] == "risk"
        pipelines: [traces/risk]

service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [memory_limiter, k8sattributes]
      exporters: [routing]
    traces/payments:
      receivers: [routing]
      processors: [batch]
      exporters: [otlp/tempo-payments]
    traces/risk:
      receivers: [routing]
      processors: [batch]
      exporters: [otlp/tempo-risk]
    traces/default:
      receivers: [routing]
      processors: [batch]
      exporters: [otlp/tempo-shared]

9.4 配额与限流

9.5 后端多租户

后端也要支持多租户才有意义:

十、版本演进与稳定性

10.1 稳定性级别

OTel 规范对每个组件标注稳定性:

10.2 信号稳定化时间线

信号 Stable 时间 当前状态
Traces API/SDK 2021-02 Stable
OTLP Traces 2021-02 Stable
Metrics API/SDK 2023-02 Stable
OTLP Metrics 2023-02 Stable
Logs Bridge API 2024-04 Stable
OTLP Logs 2024-04 Stable
Profiles 进入 Development Experimental
Events API 依附 Logs Bridge Beta

10.3 Collector 版本号

Collector 有自己独立的版本体系,当前(2026 年)主线已经进入 v0.9x → v1.0 的冲刺。v1.0 标志着配置格式与内建组件 API 冻结,但大量 contrib 组件仍保持各自的稳定性级别。升级时要重点看 CHANGELOG[breaking] 标记。

10.4 常见破坏性变更

近三年升级踩过的坑:

10.5 升级节奏建议

十一、国内落地案例

十一(原文保留编号)

11.1 阿里云 ARMS

ARMS 从 2022 年起原生兼容 OTLP。接入方式有两种:

ARMS 对语义约定兼容得比较完整,service.namehttp.routedb.statement 都能正常出现在调用链界面;deployment.environment 会映射成 ARMS 的「环境」筛选项。坑点:ARMS 对 service.name 有长度限制(64 字符),团队里统一用 <business>-<module> 的命名风格时要注意。

11.2 观测云(Dataflux)

观测云(Dataflux/Guance)原生支持 OTLP 接入,既可以通过其 DataKit Agent 转发(DataKit 里集成了 OTel Collector),也可以直接走原生 OTLP。它在 Metrics 上把 OTLP 的 Sum/Gauge/Histogram 翻译成其自有模型,跨信号关联依赖 service.name + trace_id。生产里如果已经在跑 OTel Collector,建议让 DataKit 只接 logs,traces/metrics 从 Collector 直接出 OTLP 到观测云的入口,运维链路更清晰。

11.3 夜莺(Nightingale)

夜莺(Nightingale)v6 起把 OTLP 作为一等入口,内置了兼容 Jaeger 和 OTLP 的接收端。对中小团队来说,它提供的「一套前端同时看指标 + 日志 + 链路」能力吸引力很强。但其 Tracing 存储(Jaeger/Tempo)仍要自建或对接外部方案,OTLP Collector 仍然是标配。

11.4 字节跳动实践要点

字节跳动公开分享过的经验(参见 2023 年 ArchSummit、2024 年 QCon 议题)可以概括为几条:

11.5 其他国内方案

十二、工程坑点

12.1 属性命名陷阱

OTel 语义约定统一用 . 作为分隔符。但部分后端(尤其是基于 Elasticsearch 字段扁平化的方案)会把 . 当成嵌套路径,导致字段爆炸。应对:

processors:
  transform/underscore:
    log_statements:
      - context: resource
        statements:
          - replace_all_patterns(attributes, "key", "\\.", "_")

12.2 service.name 缺失或错误

前文提到:缺失时 Collector 填 unknown_service,实际上更常见的错是「填错」,例如所有 Sidecar 容器写成了 envoy、所有副本写成同一个镜像名。建议:

12.3 升级期的破坏性变更

前面列过的 http.method → http.request.methodlogging → debug 都是典型。最佳实践:

12.4 高基数属性导致指标爆炸

直接把 user.idrequest.idsession.id 打到指标维度是最经典的自杀操作。识别与治理:

processors:
  attributes/drop_highcard:
    actions:
      - key: user.id
        action: delete
      - key: request.id
        action: delete

12.5 Collector 内存管理

Collector 的内存压力主要来自:

调优顺序:先开 memory_limiter + Linux cgroup 硬限;监控 otelcol_process_runtime_heap_alloc_bytesotelcol_exporter_queue_size;按需调 batch/queue/num_traces。

12.6 跨消息队列的上下文传播

Kafka/RocketMQ 消费是异步的,极易漏传 traceparent

此外在「批量消费」场景下,一条 Consumer span 可能对应多个 Producer span,这时应该使用 Links 而不是 parent,以表达「多对一」的关系:

ctx, span := tracer.Start(ctx, "consume.batch",
    trace.WithLinks(linksFromHeaders(msgs)...))

12.7 日志与 Trace 的关联

日志接入 OTel 有两条路:

两种都能让日志与 trace 在后端相关联,但要求日志里必须带 trace_id。最常见的错误是:模板里写了 %X{traceId} 却没在 Logback 里挂上 OTel MDC Instrumentation。

12.8 时钟漂移

OTLP 的时间戳使用 unix_nano(64 位纳秒)。应用机时钟漂移超过 decision_wait 的一半时,尾采样可能认为 trace 还没结束而无限等待;最终在内存里积压至 OOM。务必上 NTP/chrony 并监控 node_timex_offset_seconds

12.9 证书与 TLS

生产里 SDK → Collector 走 mTLS 是常见要求,但不少团队会一开始用 WithInsecure() 跑起来之后再补。补的时候会发现:

建议从 Day 1 就统一用 cert-manager 签发内部证书,SDK 与 Collector 都走 mTLS。

十三、选型建议与落地清单

13.1 是否选 OTel

场景 建议
全新项目,后端未定 直接上 OTel SDK,避免绑定
后端已选 Jaeger/Tempo/ARMS/观测云 OTel SDK 全量接,后端走 OTLP
后端已选 Datadog/NewRelic/Dynatrace 且无迁移计划 可以继续用厂商 Agent,但新服务仍推荐 OTel,厂商 Agent 基本都支持 OTLP ingest
仅需指标 Prometheus Client 也可,若三信号都要再上 OTel
嵌入式/极低资源环境 评估 SDK 内存(Java ≈ 50 MB,Go ≈ 10 MB)

13.2 落地 Checklist

  1. 统一 service.nameservice.versiondeployment.environment 三个资源属性的来源(CI 里注入为宜)。
  2. 选定 SDK 版本,全司锁定在同一个 BOM;半年内升级一次。
  3. Collector 两级部署(Agent + Gateway),Agent 仅做本地收集与 resource 富化。
  4. 统一 Propagator 为 W3C Trace Context;旧的 B3/Uber-Trace-Id 仅在过渡期共存。
  5. memory_limiter 必配,sending_queue 必配 file_storage
  6. tail_samplingloadbalancing 一起上,避免错过错误链路。
  7. 高基数属性治理流程化:Prometheus 高基数巡检 + Collector 兜底 attributes/drop
  8. 跨信号关联打通:日志里强制带 trace_idspan_id;指标通过 Exemplars(v1.27+)带 trace_id
  9. 后端多租户选型:Tempo/Mimir/Loki + X-Scope-OrgID;或直接上阿里云 ARMS/观测云等商业化多租户后端。
  10. 生产 Rollback 方案:SDK 提供环境变量一键停(OTEL_SDK_DISABLED=true),Collector 配置 config hot-reload。
  11. 监控 Collector 自身:otelcol_* 指标接入 Prometheus,关键告警为 queue_sizerefused_spansexporter_send_failed_requests
  12. 性能基线:接入后跑压测,确保 P99 延迟增加 < 3%;埋点热点路径优先用 AlwaysOff + 显式打开。

13.3 常见误区

十四、参考资料

  1. OpenTelemetry Specification(v1.35+),https://github.com/open-telemetry/opentelemetry-specification
  2. OpenTelemetry Protocol Specification,https://github.com/open-telemetry/opentelemetry-proto
  3. OpenTelemetry Semantic Conventions,https://github.com/open-telemetry/semantic-conventions
  4. OpenTelemetry Collector Documentation,https://opentelemetry.io/docs/collector/
  5. OpenTelemetry Collector Contrib Repository,https://github.com/open-telemetry/opentelemetry-collector-contrib
  6. W3C Trace Context,https://www.w3.org/TR/trace-context/
  7. W3C Baggage,https://www.w3.org/TR/baggage/
  8. CNCF OpenTelemetry Announcement(2019-05),https://www.cncf.io/blog/2019/05/21/a-brief-history-of-opentelemetry-so-far/
  9. OpenCensus Project(Archived),https://opencensus.io/
  10. OpenTracing Project(Archived),https://opentracing.io/
  11. 阿里云 ARMS OpenTelemetry 接入文档,https://help.aliyun.com/zh/arms/tracing-analysis/user-guide/use-opentelemetry-to-submit-java-application-data
  12. 观测云 OpenTelemetry 接入,https://docs.guance.com/integrations/opentelemetry/
  13. 夜莺(Nightingale)官方文档,https://flashcat.cloud/docs/
  14. Ben Sigelman 等,“OpenTelemetry Is Too Complicated”,InfoQ,2023
  15. Grafana Labs,“Scaling OpenTelemetry Collector”,https://grafana.com/blog/
  16. Uber,“Distributed Tracing at Uber Scale: Cadence Tracing”,2022

上一篇Traces:Jaeger、Tempo、Zipkin、SkyWalking 与采样传播

下一篇持续性能分析(Profiling):pprof、Pyroscope、Parca、async-profiler、JFR

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。

2026-04-22 · architecture / observability

可观测性工程

从 Metrics、Logs、Traces 到 Profiling、eBPF、OpenTelemetry 与 SLO 治理,面向中国工程团队的可观测性系统化手册。

2026-04-22 · architecture / observability

【可观测性工程】指标体系设计:USE、RED、Golden Signals 与业务 KPI

USE 方法论适用于资源,RED 方法论适用于请求,Golden Signals 适用于服务——三套方法论各有其适用对象。本文从 Brendan Gregg、Tom Wilkie、Google SRE 的原始定义出发,构建覆盖资源→服务→业务的完整指标体系,并给出 Prometheus 命名规范、基数治理策略与可抄的指标清单。


By .