可观测性(Observability)工程在过去五年里经历了一次范式迁移:从「每个后端一套 SDK」到「一套信号采集标准 + 多个后端」。这场迁移的主角是云原生计算基金会(CNCF)孵化的 OpenTelemetry(简称 OTel)。它不仅统一了链路追踪(Tracing)、指标(Metrics)、日志(Logs)三类信号的数据模型、协议和语义,还通过 Collector 将「采集层」从应用代码里彻底解耦出来。
本文面向已经落地过 Prometheus、ELK 或 Jaeger 的 SRE 与平台工程师,讲清楚 OTel 的来龙去脉、协议细节、SDK 生命周期、Collector 流水线、语义约定(Semantic Conventions)以及在国内厂商(阿里云 ARMS、观测云、夜莺)上落地时会踩的坑。文章假设读者理解本系列前面几篇关于指标、日志、链路的文章,因此在信号本身的概念上不做过多铺陈,而是聚焦在「把 OTel 塞进一个已有的、跑在生产上的可观测性体系里」这件事上。
一、起源:OpenTracing 与 OpenCensus 的合并(2019)
1.1 OpenTracing 时代(2016—2019)
2016 年 10 月,OpenTracing 项目正式进入 CNCF
孵化。它的目标非常克制:只定义链路追踪的
API,不定义数据协议,也不提供 SDK
实现。换句话说,OpenTracing 是一组接口(Interface),具体的
Tracer、Span、SpanContext、Propagator
都交由各个厂商实现。
OpenTracing 的优点是「中立」。从 Jaeger、Zipkin 到 New Relic、Datadog,每家厂商都可以提供符合 OpenTracing 接口的适配器,应用代码只依赖 API。但它的问题也正出在「只做 API」上:
- 没有统一的数据模型。
Span上的tags、logs(后来叫events)字段语义由各实现自行解释。 - 没有统一的上下文传播格式。实际生产环境里同时存在
Zipkin B3、Jaeger Uber-Trace-Id、W3C traceparent等多套头字段。 - 没有指标和日志。OpenTracing 只覆盖 Tracing 一个信号。
1.2 OpenCensus 时代(2017—2019)
几乎在 OpenTracing 成形的同时,Google 与 Microsoft
联合推出了 OpenCensus。OpenCensus 的出发点是「一套 SDK
同时处理 Traces 与 Metrics」。它不止定义
API,还给出了参考实现、导出器(Exporter)以及一个小型的代理
ocagent。
OpenCensus 的设计相对完整,尤其是指标部分,直接继承了 Google 内部 Census 项目多年的打磨成果。但它也有局限:
- 不覆盖日志信号。
- 生态比 OpenTracing 小,很多厂商仍然以 OpenTracing 为主做适配。
- 厂商中立性不如 OpenTracing,毕竟 Google 是主导者。
1.3 碎片化困境与供应商锁定
到 2018 年底,企业里经常同时出现三种埋点:
- Prometheus 客户端库采集指标。
- OpenTracing 或厂商私有 SDK 采集链路。
- 日志框架(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,定位是「统一的可观测性数据采集标准」。关键时间线如下:
- 2019-05:合并公告,OTel 进入 CNCF 沙箱(Sandbox)。
- 2019-11:发布 v0.1,规范草案初具雏形。
- 2021-02:Traces 规范与 SDK v1.0 稳定发布,这是 OTel 第一个 Stable 信号。
- 2021-08:OTel 进入 CNCF 孵化(Incubating)阶段。
- 2023-02:Metrics 规范与 SDK v1.0 稳定发布,OTLP 指标格式正式冻结。
- 2024-04:Logs 规范与 SDK v1.0 稳定发布,日志信号成为一等公民。
- 2024 年起:Profiles 信号(持续性能分析)进入开发,OTLP Profiles 的 protobuf schema 逐步成型。
合并带来的直接收益是:API、SDK、协议、语义约定四者统一在同一个项目下演进,信号之间通过共享的
Resource 与 Context 天然关联。
二、OTel 整体架构
2.1 五层模型
OTel 的架构可以抽象为五层:
- API 层:定义
Tracer、Meter、Logger接口,不含任何实现。应用代码、第三方库只依赖 API。 - SDK 层:API 的默认实现,负责采样(Sampling)、批处理、资源检测(Resource Detection)、导出。
- Instrumentation Library 层:针对 HTTP、gRPC、数据库、消息队列等组件的埋点库,分为「手动埋点」和「自动埋点」两类。
- Exporter 层:把 SDK 内部的数据结构序列化为具体协议(OTLP、Zipkin、Prometheus Remote Write 等)。
- Collector 层:独立进程,负责接收、处理、路由遥测(Telemetry)数据到一个或多个后端。
应用只感知 API 层与 Instrumentation Library;SDK、Exporter、Collector 都可以替换或横向扩展,不影响业务代码。
2.2 OTLP 协议
OTLP 是 OpenTelemetry Protocol 的缩写,是 OTel 项目唯一「官方推荐且长期维护」的协议。它有两种传输方式:
OTLP/gRPC:基于 HTTP/2 的 gRPC 流,默认端口4317。OTLP/HTTP:基于 HTTP/1.1 的POST,请求体可以是application/x-protobuf或application/json,默认端口4318。
两者共用同一套 protobuf schema,只是传输层不同。生产环境里建议默认用 gRPC,只有在以下场景退化到 HTTP:
- 浏览器端(Web Instrumentation)无法发起 gRPC 请求。
- 经过某些 HTTP 层网关(如只支持 HTTP/1.1 的 WAF)。
- 客户端所在环境对 HTTP/2 支持不佳(如旧版 Nginx、某些 API 网关)。
2.3 SDK 生命周期
SDK
的内部生命周期可以概括为:Provider → Processor → Exporter。
- Provider(
TracerProvider/MeterProvider/LoggerProvider):全局单例,管理 Resource、Sampler、Processor 列表。 - Processor(
SpanProcessor/MetricReader/LogRecordProcessor):决定数据在什么时机、以什么方式交给 Exporter。 - Exporter:把数据序列化并发送到外部系统,通常是 Collector,也可以直连后端。
整个流水线是同步构造、异步执行的:应用线程只做
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;
}三个关键点:
Resource是进程级别的共享属性(例如service.name、host.name、k8s.pod.uid),在同一个 batch 里只序列化一次,大幅节省带宽。InstrumentationScope描述「谁埋的点」(比如opentelemetry-instrumentation-http/0.45b0),用于区分不同埋点库的数据。trace_id与span_id是二进制而不是字符串,节省空间也方便后端直接按字节建索引。
Metrics 的 schema 更复杂,因为要表达
Sum、Gauge、Histogram、ExponentialHistogram、Summary
五种数据点,并区分 Cumulative 与
Delta 两种时间性。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",
}),
)几个经验值:
- 压缩优先选
gzip,在 Go/Java/Python SDK 上都有稳定实现;大量低基数字符串属性的场景下,压缩率可以达到 5 倍以上。 - 重试的
MaxElapsedTime不能太长,否则一次 Collector 抖动会导致 SDK 内存里堆积大量 span,触发 OOM。生产上常见 2~5 分钟。 - 批处理在 SDK 侧(
BatchSpanProcessor)和 Collector 侧(batchprocessor)都存在,通常 SDK 侧 batch 间隔小一些(1 秒),Collector 侧大一些(5~10 秒)。
3.4 错误语义
OTLP 定义了「部分成功」(Partial Success)机制:服务端在
ExportTraceServiceResponse 里返回
partial_success.rejected_spans 计数和
error_message。客户端遇到
4xx(InvalidArgument、PermissionDenied)通常不应重试,5xx(Unavailable、DeadlineExceeded、ResourceExhausted)应该按指数回退重试。这一点在
Collector 的 exporter/retry_on_failure
配置里也有对应开关。
四、Semantic Conventions(语义约定)
4.1 为什么要有语义约定
语义约定是 OTel
项目里最「不性感」但最关键的产出之一。它定义了「什么属性应该叫什么名字、值应该是什么类型」。没有它,http.method、http.verb、httpMethod
三种写法会同时存在,后端无法做统一查询和面板。
语义约定的工程意义有三层:
- 跨团队的数据契约(Data Contract):平台团队可以基于固定的属性名写出通用面板,无需每个业务团队另做一套。
- 跨厂商的可移植性:同一份遥测数据能在 Tempo、Jaeger、Datadog、阿里云 ARMS 上展现一致。
- 自动埋点的一致性基础:所有
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-uid 或 hostname-pid |
deployment.environment |
环境 | prod、staging、canary |
host.name |
主机名 | node-01 |
host.arch |
CPU 架构 | amd64、arm64 |
os.type |
操作系统类型 | linux |
process.pid |
进程 PID | 12345 |
telemetry.sdk.name |
SDK 名称 | opentelemetry |
telemetry.sdk.language |
SDK 语言 | go、java、python |
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 |
GET、POST |
http.response.status_code |
200、500 |
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.name、deployment.environment、service.version、k8s.pod.name
这几个通用变量,面板复用率极高。这是 OTel
项目在大规模组织里能跑通的核心原因之一。
五、SDK 生命周期管理
5.1 Provider 三兄弟
SDK 里分别有
TracerProvider、MeterProvider、LoggerProvider
三个 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
}几个容易被忽略的点:
resource.WithFromEnv()会读取OTEL_RESOURCE_ATTRIBUTES环境变量,把k=v,k2=v2格式的键值对合并到资源里。这是 K8s 下由 Downward API 注入 pod 信息最常用的通道。- Provider 顺序关闭很关键:先关 Tracer,再关 Metric,再关 Logger;如果倒过来,Tracer 关闭过程中产生的日志会丢。
Shutdown要给足超时(5~10 秒)。生产里别直接ctx.Done()就走,否则批量里的数据全部扔掉。
5.2 SpanProcessor:Simple vs Batch
SimpleSpanProcessor: 每结束一个 Span 立刻调用 Exporter
BatchSpanProcessor: 攒 Queue,定时或定量批量导出
SimpleSpanProcessor只适合测试和开发;生产永远用 Batch,否则每条 span 一次 gRPC,延迟敏感服务会被埋点压垮。- Batch 队列满时的行为是「丢新数据」(drop on full),并在
SDK 内部记录自监控指标
otel.sdk.span.processor.queue_size、otel.sdk.span.processor.dropped_spans。这两个指标务必接入告警。
5.3 Exporter 接口
Exporter 只有两个方法:Export(batch) 与
Shutdown()。这个极简的设计让替换非常容易。生产里常见的
Exporter:
otlp(首选,gRPC 或 HTTP)。stdout(调试用,别上生产)。prometheus(让应用直接暴露/metrics,被 Prometheus Pull)。zipkin、jaeger(过渡期用)。
5.4 Propagator 与上下文传播
Propagator 负责把 SpanContext
注入和抽取到/从外部载体(Carrier)。OTel 默认注册的
Propagator 是 W3C Trace Context(traceparent /
tracestate)加 W3C
Baggage。国内生产环境里会遇到的历史格式:
- B3
单头:
X-B3-TraceId、X-B3-SpanId、X-B3-Sampled,Zipkin 使用。 - B3
多头:
b3: {traceId}-{spanId}-{sampled}-{parentId}。 - Jaeger:
uber-trace-id: {traceId}:{spanId}:{parentId}:{flags}。 - 阿里云
ARMS:历史上存在私有头,现已兼容 W3C。
迁移策略:在 Propagator 里同时注册
traceparent 和 b3
两套,发送侧同时写、接收侧按优先级读,这样新旧服务可以灰度共存。
otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(
propagation.TraceContext{},
propagation.Baggage{},
b3.New(b3.WithInjectEncoding(b3.B3MultipleHeader)),
))5.5 Java 初始化示例
Java 通常用 BOM(Bill of Materials)拉依赖,推荐
opentelemetry-bom 与
opentelemetry-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 最低位是
sampled:01
表示上游决定采样,00
表示上游决定不采样。下游通常遵循「父亲说啥就啥」的
ParentBased 采样策略,避免出现「一半 span
有数据、一半没有」的断裂链路。
对于 Kafka、RocketMQ
等消息中间件,traceparent 可以注入到消息
headers;RabbitMQ 可以放在
properties.headers;HTTP 自然走请求头;gRPC
通过 metadata 传递。跨异步调用时一定要手动调
propagator.inject(ctx, carrier),否则链路会断。
六、Collector 架构深入
6.1 六大组件
Collector 的配置是一个显式的「数据流图」,由六类组件组成:
- Receivers:接收端,从 SDK、Prometheus、Kafka、文件等拉取或接收数据。
- Processors:流内处理,包括批处理、限流、富化、采样、转换。
- Exporters:发送端,把数据送到一个或多个后端。
- Connectors:v0.80 后引入的「既是 Exporter 又是
Receiver」的组件,用于跨 pipeline 串接。例如
spanmetricsconnector 把 trace 转成 RED 指标。 - Extensions:与数据流平行的辅助组件,如健康检查、pprof、zpages、
file_storage(持久化队列)。 - Pipelines:把上述组件编排成
traces/metrics/logs三类有向图。
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 的压力测试,有几个关键细节值得单独说:
memory_limiter一定放在batch之前。顺序反了,Collector OOM 前来不及回压,SDK 侧也不会减速。k8sattributes放在resource之前,确保后续的resourceprocessor 能在 pod 信息基础上再加 overlay。tail_sampling必须放到batch之前,否则 batch 会把同一个 trace 的 span 拆到不同批次里,尾采样拿不到完整 trace。sending_queue的queue_size与num_consumers是调吞吐的主要旋钮,别忘了还要配合file_storage做持久化,否则重启丢数据。
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 模式:每个节点(Node/Host)或每个 pod Sidecar 跑一份 Collector,紧贴应用。职责是「本地预处理、resource 富化、压缩后转发」。常用 DaemonSet 或 Sidecar 部署。
- Gateway 模式:独立的 Collector 集群,接收来自 Agent 的 OTLP 流量,做尾采样、路由、跨租户分流,最终对接后端。通常以 Deployment + HPA 部署,前面挂 TCP/gRPC 负载均衡。
两者组合成「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 聚合」的有状态操作,不能随便分片。解决方案:
- 在 Agent → Gateway 之间加一个
loadbalancingexporter,按trace_id哈希路由,保证同一 trace 的所有 span 落到同一个 Gateway 实例:
exporters:
loadbalancing:
protocol:
otlp:
tls: { insecure: true }
resolver:
dns:
hostname: otel-gateway.observability.svc.cluster.local
port: 4317
routing_key: traceID- Gateway 实例本身是无状态可扩缩的,但
groupbytrace+tail_sampling的内存占用与num_traces× 平均 span 数成正比,扩容要提前预估。
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.jarjavaagent 使用 ByteBuddy 在类加载时注入字节码,覆盖 100 多种框架(Spring、Tomcat、Jetty、Netty、JDBC、Redis、Kafka、gRPC、OkHttp、HttpClient 等)。它的价值是「老系统零改动接入」,代价是:
- 启动时间增加 1~3 秒。
- 内存开销增加 50~150 MB。
- 字节码注入偶尔与 AspectJ、Skywalking Agent 冲突,生产环境上线前一定要做兼容测试。
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.pyopentelemetry-bootstrap
会扫描当前已安装的依赖,把对应的
opentelemetry-instrumentation-*
包自动装上,省去一个个挑库的麻烦。
7.3 Go
Go 由于编译型语言的特性,没有真正的「运行时自动埋点」。现阶段有两条路:
- 手动使用
go.opentelemetry.io/contrib/instrumentation/...系列库,比如otelhttp.NewHandler包装 HTTP Server、otelgrpc.UnaryServerInterceptor包装 gRPC Server。 - 基于 eBPF
的零代码方案:
odigos、beyla、opentelemetry-go-instrumentation(基于 uprobes,仍在 Alpha)。
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.jsNode.js 自动埋点基于 require hook / ESM loader,覆盖 Express、Koa、Fastify、gRPC、MongoDB、Redis 等。
7.5 零代码 vs 手动埋点权衡
| 维度 | 自动埋点 | 手动埋点 |
|---|---|---|
| 接入速度 | 分钟级 | 天到周 |
| 业务语义 | 只到框架层 | 可到业务事件级别 |
| 升级成本 | 跟 agent 走 | 跟代码仓库走 |
| 性能控制 | 不易细调 | 可精细裁剪 |
| 与自研框架兼容 | 可能缺埋点 | 完整覆盖 |
实操经验:先让自动埋点覆盖 80% 的 HTTP/DB/MQ 链路,再用手动埋点补业务关键节点(下单、风控决策、鉴权)。两种不冲突,可并存。
八、尾采样(Tail Sampling)
8.1 头采样与尾采样的差异
- 头采样(Head Sampling):在请求进入时决定是否保留,基于
trace_id概率。优点是开销低、决策本地;缺点是无法感知「这条链路有没有错」。 - 尾采样(Tail Sampling):在 trace 结束后,根据链路整体属性(是否包含 5xx、是否慢、是否命中特定用户)决定保留。优点是能按质量采样;缺点是需要在 Collector 侧把同 trace 的 span 聚合起来,内存占用大。
8.2 tail_sampling processor
上文 YAML 里已经有完整示例,这里拆解核心参数:
decision_wait:Collector 等多久才对一条 trace 作出决策。通常设为「链路最长耗时 + 安全余量」,生产常用 10~30 秒。num_traces:同时在内存里保留的 trace 数上限,内存吃紧时首先调低它。expected_new_traces_per_sec:辅助 Collector 做内存预估。policies:策略列表,按顺序判断;命中任一策略即保留。
策略类型包括:latency、status_code、numeric_attribute、string_attribute、rate_limiting、probabilistic、composite、and、trace_state、ottl_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 尾采样的落地成本
- 内存:
num_traces × 平均 span 数 × 平均 span 大小。10 万 trace × 30 span × 2 KB ≈ 6 GB,Gateway 要预留足够内存。 - 延迟:trace 在 Gateway 要多停
decision_wait秒,实时排障体验变差。 - 决策复杂度:策略多时 CPU 开销显著增加,建议用
composite组合少量高价值规则。
一个务实的策略组合:100% 错误 + 100% > P99 延迟 + 10% 关键服务 + 1% 其它服务。能把存储量压到头采样的 1/5,同时保留绝大部分有价值的链路。
九、多租户架构
9.1 目标
平台团队常常要给多个业务方提供统一 Collector 集群,要求:
- 租户间流量互不影响,某个租户打爆不能影响其他租户。
- 不同租户数据写入不同后端租户或命名空间。
- 能按租户做计费或配额。
9.2 租户识别
租户信息的常见来源:
- OTLP
请求头:
X-Scope-OrgID(Loki/Tempo/Mimir 原生约定)或自定义x-tenant-id。 - Resource
属性:
service.namespace、deployment.environment、tenant.id。 - mTLS 客户端证书 CN。
推荐统一用 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 配额与限流
memory_limiter是进程级的,不能按租户限。groupbyattrs+ratelimit(社区 processor)可以按tenant.id做速率限制,但目前还处于 Beta。- 更健壮的做法是每个大租户独享一组 Gateway pod,通过 K8s ResourceQuota 做物理隔离。
9.5 后端多租户
后端也要支持多租户才有意义:
- Tempo/Loki/Mimir:用
X-Scope-OrgID划分租户,Collector 在otlpexporter 里注入这个头。 - Prometheus:本身不支持多租户,用
external_labels+ 多实例方案,或者换成 Mimir/Thanos。 - ClickHouse:用独立数据库或表,配合 RBAC。
十、版本演进与稳定性
10.1 稳定性级别
OTel 规范对每个组件标注稳定性:
- Stable:API 与 SDK 遵循语义化版本,除非 2.x 否则不做不兼容变更。
- Beta:可能有不兼容变更,但生产可用。
- Alpha:早期版本,变更频繁。
- Development / Experimental:只供社区验证。
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 常见破坏性变更
近三年升级踩过的坑:
- v0.60:
resourcedetection重命名配置字段;Dockerfile 里固化旧名会启动失败。 - v0.80:
attributesprocessor 部分动作迁移到transform(OTTL),旧写法 Deprecated。 - v0.82:HTTP 语义约定从
http.method改为http.request.method,Grafana 面板同时支持两个字段至少半年。 - v0.95:
loggingexporter 更名为debug,老配置直接报错。 - v0.100:Metrics 聚合时
Cumulative→Delta转换规则微调,需要确认后端计算是否兼容。
10.5 升级节奏建议
- SDK:每季度跟一次小版本,主线追到最新 Stable。
- Collector:每月跟 contrib 最新 release,先在预发灰度 72 小时。
- 语义约定:
schema_url在数据里显式带上,后端通过OpenTelemetry Schema Transformations做跨版本翻译,不强依赖 SDK 版本对齐。
十一、国内落地案例
十一(原文保留编号)
11.1 阿里云 ARMS
ARMS 从 2022 年起原生兼容 OTLP。接入方式有两种:
- 直接把
OTEL_EXPORTER_OTLP_ENDPOINT指到https://tracing-analysis-dc-hz.aliyuncs.com/...:4317,并通过 Header 带Authentication: {LicenseKey}。 - 更常见的做法是自建 Collector 转发:企业内 Gateway
统一出口,Gateway 再
otlp到 ARMS。这样可以在出口做脱敏、采样,并避免每个业务服务持有 LicenseKey。
ARMS
对语义约定兼容得比较完整,service.name、http.route、db.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 议题)可以概括为几条:
- 把 OTel SDK 包进内部统一的「金融级 SDK」里,业务方依赖一个大包即可,语义约定内部再做一层加固。
- Collector 双层部署:每机房一层 Agent(DaemonSet),每大区一层 Gateway;Gateway 层做尾采样、路由和跨机房合流。
- 自研基于 ClickHouse 的 Trace 后端,OTLP 数据通过 Kafka
落盘,ClickHouse 消费后按
trace_id做 ORDER BY 存储。 - 采样策略按业务域划分,交易类默认 100% 错误 + 10% 成功;内容分发类默认 1% 成功 + 100% 错误 + 100% P99 慢请求。
11.5 其他国内方案
- 腾讯云 APM:支持 OTLP,Gateway 出口模式。
- 华为云 AOM:2024 年起支持 OTLP 接入。
- 信通院「可观测性成熟度模型」在 2024 版里把「OTel 原生兼容」列为 L3 能力指标,国内头部金融、电信客户开始普遍验收。
十二、工程坑点
12.1 属性命名陷阱
OTel 语义约定统一用 .
作为分隔符。但部分后端(尤其是基于 Elasticsearch
字段扁平化的方案)会把 .
当成嵌套路径,导致字段爆炸。应对:
- Collector 侧用
transformprocessor 在出口前把.替换为_:
processors:
transform/underscore:
log_statements:
- context: resource
statements:
- replace_all_patterns(attributes, "key", "\\.", "_")- 或者用支持 dotted key 的后端(Loki、Tempo、ClickHouse 都可以原生处理)。
12.2
service.name 缺失或错误
前文提到:缺失时 Collector 填
unknown_service,实际上更常见的错是「填错」,例如所有
Sidecar 容器写成了
envoy、所有副本写成同一个镜像名。建议:
- Deployment 模板里用
OTEL_SERVICE_NAME强制覆盖。 - 加一个 Collector 侧
transform规则,检测service.name == "unknown_service"时直接告警。
12.3 升级期的破坏性变更
前面列过的
http.method → http.request.method、logging → debug
都是典型。最佳实践:
- 升级 SDK、升级 Collector、升级 Dashboard 按「T-2 周预发,T 周生产」的节奏分开做,不要混在一次变更里。
- 保留至少一个版本号距离的「双写双读」窗口期。
12.4 高基数属性导致指标爆炸
直接把
user.id、request.id、session.id
打到指标维度是最经典的自杀操作。识别与治理:
- Prometheus
count by (__name__) (count by (__name__, label) (metric))找高基数维度。 - Collector 用
attributes/drop:
processors:
attributes/drop_highcard:
actions:
- key: user.id
action: delete
- key: request.id
action: delete- 从源头治理,培训研发不要往指标里打 ID 类字段;Trace 和 Log 才是放 ID 的地方。
12.5 Collector 内存管理
Collector 的内存压力主要来自:
- 未启用
memory_limiter时,OOM 无感知。 tail_sampling与groupbytrace的num_traces设置过大。batch队列与sending_queue叠加。filelog一次读入过大日志文件,没设start_at: end时尤其危险。
调优顺序:先开 memory_limiter + Linux cgroup
硬限;监控
otelcol_process_runtime_heap_alloc_bytes 与
otelcol_exporter_queue_size;按需调
batch/queue/num_traces。
12.6 跨消息队列的上下文传播
Kafka/RocketMQ 消费是异步的,极易漏传
traceparent:
- Producer:一定要
propagator.inject(ctx, kafkaHeaders)。 - Consumer:一定要
ctx = propagator.extract(kafkaHeaders),随后用tracer.Start(ctx, ...)开新的子 span。
此外在「批量消费」场景下,一条 Consumer span 可能对应多个
Producer span,这时应该使用 Links 而不是
parent,以表达「多对一」的关系:
ctx, span := tracer.Start(ctx, "consume.batch",
trace.WithLinks(linksFromHeaders(msgs)...))12.7 日志与 Trace 的关联
日志接入 OTel 有两条路:
- Logs Bridge
API:让日志库(Logback、Log4j2、Zap、Structlog
等)把日志通过 Bridge 转成
LogRecord,进入LoggerProvider流水线。 - 结构化日志 + 手动字段:在 MDC/Context 里写入
trace_id与span_id,文件落盘后由 Collectorfilelogreceiver 采集。
两种都能让日志与 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()
跑起来之后再补。补的时候会发现:
opentelemetry-java-instrumentation的 TLS 配置来自OTEL_EXPORTER_OTLP_CERTIFICATE等环境变量,容易被忽视。- Go SDK 要自己构造
credentials.TransportCredentials。 - Collector 的
otlpreceivertls字段必须同时配cert_file与key_file,否则加载失败。
建议从 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
- 统一
service.name、service.version、deployment.environment三个资源属性的来源(CI 里注入为宜)。 - 选定 SDK 版本,全司锁定在同一个 BOM;半年内升级一次。
- Collector 两级部署(Agent + Gateway),Agent 仅做本地收集与 resource 富化。
- 统一 Propagator 为 W3C Trace Context;旧的 B3/Uber-Trace-Id 仅在过渡期共存。
memory_limiter必配,sending_queue必配file_storage。tail_sampling与loadbalancing一起上,避免错过错误链路。- 高基数属性治理流程化:Prometheus 高基数巡检 + Collector
兜底
attributes/drop。 - 跨信号关联打通:日志里强制带
trace_id、span_id;指标通过Exemplars(v1.27+)带trace_id。 - 后端多租户选型:Tempo/Mimir/Loki +
X-Scope-OrgID;或直接上阿里云 ARMS/观测云等商业化多租户后端。 - 生产 Rollback 方案:SDK
提供环境变量一键停(
OTEL_SDK_DISABLED=true),Collector 配置 config hot-reload。 - 监控 Collector 自身:
otelcol_*指标接入 Prometheus,关键告警为queue_size、refused_spans、exporter_send_failed_requests。 - 性能基线:接入后跑压测,确保 P99 延迟增加 <
3%;埋点热点路径优先用
AlwaysOff+ 显式打开。
13.3 常见误区
- 认为上 OTel 就能「自动有可观测性」:语义约定不落地,数据仍不可查询。
- 把 OTel 当 APM:OTel 只负责采集与传输,搜索、告警、根因分析仍在后端。
- 不做采样直接生产:1 万 QPS 的服务 100% 采样,一个月光 trace 存储就能冲到 PB 级。
- 忽视 Collector 自身的观测性:Collector 坏了整个链路暗了自己还没感觉。
- 只埋 HTTP,不埋异步任务:真正的业务瓶颈往往在定时任务、消息消费、后台 worker。
十四、参考资料
- OpenTelemetry Specification(v1.35+),https://github.com/open-telemetry/opentelemetry-specification
- OpenTelemetry Protocol Specification,https://github.com/open-telemetry/opentelemetry-proto
- OpenTelemetry Semantic Conventions,https://github.com/open-telemetry/semantic-conventions
- OpenTelemetry Collector Documentation,https://opentelemetry.io/docs/collector/
- OpenTelemetry Collector Contrib Repository,https://github.com/open-telemetry/opentelemetry-collector-contrib
- W3C Trace Context,https://www.w3.org/TR/trace-context/
- W3C Baggage,https://www.w3.org/TR/baggage/
- CNCF OpenTelemetry Announcement(2019-05),https://www.cncf.io/blog/2019/05/21/a-brief-history-of-opentelemetry-so-far/
- OpenCensus Project(Archived),https://opencensus.io/
- OpenTracing Project(Archived),https://opentracing.io/
- 阿里云 ARMS OpenTelemetry 接入文档,https://help.aliyun.com/zh/arms/tracing-analysis/user-guide/use-opentelemetry-to-submit-java-application-data
- 观测云 OpenTelemetry 接入,https://docs.guance.com/integrations/opentelemetry/
- 夜莺(Nightingale)官方文档,https://flashcat.cloud/docs/
- Ben Sigelman 等,“OpenTelemetry Is Too Complicated”,InfoQ,2023
- Grafana Labs,“Scaling OpenTelemetry Collector”,https://grafana.com/blog/
- Uber,“Distributed Tracing at Uber Scale: Cadence Tracing”,2022
上一篇:Traces:Jaeger、Tempo、Zipkin、SkyWalking 与采样传播
下一篇:持续性能分析(Profiling):pprof、Pyroscope、Parca、async-profiler、JFR
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
可观测性工程
从 Metrics、Logs、Traces 到 Profiling、eBPF、OpenTelemetry 与 SLO 治理,面向中国工程团队的可观测性系统化手册。
【可观测性工程】可观测性全景:Metrics、Logs、Traces、Profiles、Events 五大支柱
从控制论到云原生:拆解可观测性的五大信号支柱,对比监控与可观测性的本质区别,梳理开源/商业/SaaS 分类,以及国内互联网公司三大支柱落地现状与典型工程坑点。
【可观测性工程】可观测性 vs 监控:从 Zabbix/Nagios 到 OpenTelemetry 的二十年
监控与可观测性不是新旧迭代,而是认知模型的根本转换。本文梳理从 1999 年 Nagios 到 2019 年 OpenTelemetry 的二十年演进时间线,对比 push/pull 模型、数据模型差异,以及国内从 Zabbix 到 Prometheus 再到 OTel 的典型迁移路径与工程坑点。
【可观测性工程】指标体系设计:USE、RED、Golden Signals 与业务 KPI
USE 方法论适用于资源,RED 方法论适用于请求,Golden Signals 适用于服务——三套方法论各有其适用对象。本文从 Brendan Gregg、Tom Wilkie、Google SRE 的原始定义出发,构建覆盖资源→服务→业务的完整指标体系,并给出 Prometheus 命名规范、基数治理策略与可抄的指标清单。