惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

S
Secure Thoughts
S
SegmentFault 最新的问题
云风的 BLOG
云风的 BLOG
T
Tailwind CSS Blog
博客园 - 聂微东
小众软件
小众软件
J
Java Code Geeks
MyScale Blog
MyScale Blog
人人都是产品经理
人人都是产品经理
量子位
GbyAI
GbyAI
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
M
MIT News - Artificial intelligence
Apple Machine Learning Research
Apple Machine Learning Research
有赞技术团队
有赞技术团队
月光博客
月光博客
B
Blog RSS Feed
D
DataBreaches.Net
酷 壳 – CoolShell
酷 壳 – CoolShell
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
D
Darknet – Hacking Tools, Hacker News & Cyber Security
Cyberwarzone
Cyberwarzone
I
Intezer
The Register - Security
The Register - Security
博客园 - 【当耐特】
博客园 - 司徒正美
L
Lohrmann on Cybersecurity
U
Unit 42
N
News and Events Feed by Topic
S
Security Affairs
V
Visual Studio Blog
Y
Y Combinator Blog
Security Latest
Security Latest
Know Your Adversary
Know Your Adversary
Google DeepMind News
Google DeepMind News
大猫的无限游戏
大猫的无限游戏
S
Schneier on Security
P
Privacy International News Feed
TaoSecurity Blog
TaoSecurity Blog
Spread Privacy
Spread Privacy
G
Google Developers Blog
NISL@THU
NISL@THU
Project Zero
Project Zero
P
Palo Alto Networks Blog
Help Net Security
Help Net Security
宝玉的分享
宝玉的分享
Stack Overflow Blog
Stack Overflow Blog
S
Security @ Cisco Blogs
G
GRAHAM CLULEY
www.infosecurity-magazine.com
www.infosecurity-magazine.com

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

国密算法与国密 TLS 系列索引 【系统架构设计】架构质量属性:不只是"高可用高性能" 【系统架构设计百科】告警策略:如何避免"狼来了" 【系统架构设计】CQRS:读写分离的架构哲学 【系统架构设计】空间架构:极端扩展场景的解法 【系统架构设计】微服务架构深度审视:优势、代价与适用边界 【系统架构设计】扩展性原理:水平、垂直与对角扩展 【系统架构设计】无状态设计:扩展的第一步也是最难的一步 【系统架构设计】缓存架构:从本地到分布式的多级缓存体系 【系统架构设计】管道与过滤器:Unix 哲学的架构表达 【系统架构设计】复杂性管理:架构的核心战场 【系统架构设计】消息队列架构:异步解耦的设计与陷阱 【系统架构设计】CDN 架构:全球加速的设计原理 【系统架构设计】连接池设计:被忽视的性能杀手 【系统架构设计】弹性设计模式:熔断器、舱壁与超时 【系统架构设计】高可用设计模式:冗余、故障转移与仲裁 【系统架构设计】容量规划:从拍脑袋到数据驱动 【系统架构设计】数据库扩展:分库分表的工程实践与替代方案 【系统架构设计】SLO 工程:可靠性的量化管理 【系统架构设计】性能建模:用数学思维分析系统瓶颈 【系统架构设计】混沌工程:主动验证系统的韧性 【系统架构设计】零拷贝与内存映射:数据搬运的极致优化 【系统架构设计】线程模型:从 thread-per-request 到协程 【系统架构设计】容灾架构:多活与灾备设计 【系统架构设计】数据库性能模式:索引、查询与连接管理 【系统架构设计】数据建模:从关系范式到文档模型的真实权衡 【系统架构设计】吞吐量优化:批处理、流水线与并发模型 【系统架构设计】流处理架构:从批处理到实时的范式迁移 【系统架构设计】搜索引擎架构:倒排索引之上的系统设计 【系统架构设计】时序数据架构:监控与 IoT 的存储设计 【系统架构设计】数据迁移与版本化:在线不停机的数据演进 【系统架构设计】数据湖与数据仓库:分析架构的演进路线 【系统架构设计】API 网关设计:入口层的职责边界 【系统架构设计】应用层数据一致性模式:在正确性与性能之间走钢丝 【系统架构设计】多模数据库选型:Polyglot Persistence 的工程实践 【系统架构设计】服务发现与注册:动态拓扑的基础设施 【系统架构设计】配置管理架构:从配置文件到配置中心 【系统架构设计】全链路压测:大规模系统的性能验证 【系统架构设计】幂等性设计:分布式环境下的安全重试 【系统架构设计】契约测试与 Schema 演进:服务间的信任协议 【系统架构设计】长连接与推送架构:WebSocket、SSE 与 MQTT 【系统架构设计】延迟分析:从 P50 到 P999 的全链路追踪 【系统架构设计百科】DDD 战术模式:聚合、实体与值对象 【系统架构设计百科】防腐层与开放主机服务:系统集成的 DDD 方案 【系统架构设计百科】领域事件与事件风暴:从业务到架构的桥梁 【系统架构设计百科】CQRS + Event Sourcing 完整实战:从领域建模到部署 【系统架构设计百科】DDD 与微服务:用领域模型划分服务边界 【系统架构设计】DDD 战略设计:限界上下文与上下文映射 【系统架构设计百科】认证架构:从 Session 到 JWT 到 OIDC 【系统架构设计】API 设计哲学:REST vs GraphQL vs gRPC 的真实权衡 排序算法专题:从 TimSort 到并行排序 【密码学百科】国密算法体系:SM2/SM3/SM4/SM9 全景解读 【密码学百科】承诺方案:Pedersen 承诺、向量承诺与多项式承诺 【密码学百科】不经意传输与隐私信息检索:OT、OT 扩展与 PIR 【密码学百科】门限密码学:门限签名、门限解密与分布式密钥生成 完美哈希:从理论到 gperf 实践 【密码学百科】安全多方计算:从 Yao 的混淆电路到实用 MPC 【密码学百科】同态加密:从 Paillier 到全同态加密(FHE) 【密码学百科】零知识证明系统:zk-SNARKs、zk-STARKs 与 Bulletproofs 【密码学百科】概率论与密码分析:生日攻击、差分分析与线性分析 【密码学百科】计算复杂性与归约:密码安全性证明的基石 【密码学百科】秘密共享:Shamir 方案、VSS 与安全多方计算入口 【密码学百科】椭圆曲线代数:Weierstrass 方程、点群运算与曲线选择 【密码学百科】离散对数与配对密码学:从 DLP 到 BLS 签名 【密码学百科】格密码数学基础:SVP、LWE 与格基约化 【密码学百科】抽象代数:群、环、域的密码学视角 【密码学百科】有限域算术:GF(2^n) 运算与在 AES/ECC 中的应用 【密码学百科】数论进阶:二次剩余、椭圆曲线上的 Weil 配对 【密码学百科】密码学简史:从凯撒密码到量子时代 【密码学百科】威胁模型与安全目标:CIA 三要素之外 【密码学百科】Kerckhoffs 原则与现代密码设计哲学 【密码学百科】随机性:密码学的基石 【密码学百科】信息论入门:熵、完美保密与 Shannon 定理 【密码学百科】分组密码原理:Feistel 网络与 SPN 结构 【密码学百科】AES 逐步拆解:SubBytes 到 MixColumns 的数学 【密码学百科】分组密码工作模式全览:ECB/CBC/CTR/OFB/CFB 【密码学百科】流密码:RC4 的兴衰与 ChaCha20 的崛起 【密码学百科】密码学哈希函数:MD5→SHA-2→SHA-3 的进化之路 【密码学百科】MAC 与 HMAC:消息认证的正确姿势 【密码学百科】认证加密(AEAD):GCM、ChaCha20-Poly1305 与 OCB 【密码学百科】密钥派生函数:HKDF、PBKDF2、Argon2 与密码存储 【密码学百科】公钥密码的数论基础:模运算、群、原根 【密码学百科】RSA 从原理到攻击:教科书 RSA 为什么不安全 【密码学百科】Diffie-Hellman 密钥交换与离散对数问题 【密码学百科】椭圆曲线密码学(ECC):从几何直觉到点群运算 【密码学百科】数字签名:ECDSA、EdDSA 与 Schnorr 签名 【密码学百科】现代密钥交换:X25519、ECDHE 与前向保密 【密码学百科】混合加密与 KEM/DEM 范式:ECIES 与 HPKE 【密码学百科】填充方案:PKCS#1 v1.5、OAEP 与 PSS 【密码学百科】TLS 协议全解析:从握手到 0-RTT 【密码学百科】PKI 与数字证书:信任链的构建与崩塌 【密码学百科】密码认证协议:从 SRP 到 OPAQUE 【密码学百科】零知识证明入门:如何证明你知道而不泄露 【密码学百科】安全信道构造:Noise 协议框架与 Signal 协议 【密码学百科】密钥管理工程:HSM、KMS 与密钥生命周期 【密码学百科】侧信道攻击:从时序攻击到功耗分析 【密码学百科】密码学实现陷阱:三层漏洞分类、审计工具链与系统性预防 密码敏捷性:如何设计可升级的密码系统 【密码学百科】OpenSSL/BoringSSL 架构剖析:ENGINE、Provider 与 FIPS 模块 排序基准测试:用数据说话
【可观测性工程】OpenTelemetry 深入:SDK、Collector、语义约定与版本演进
2026-04-22 · via 土法炼钢兴趣小组的算法知识备份

可观测性(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」上:

  • 没有统一的数据模型。Span 上的 tagslogs(后来叫 events)字段语义由各实现自行解释。
  • 没有统一的上下文传播格式。实际生产环境里同时存在 Zipkin B3Jaeger Uber-Trace-IdW3C 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 年底,企业里经常同时出现三种埋点:

  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,定位是「统一的可观测性数据采集标准」。关键时间线如下:

  • 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、协议、语义约定四者统一在同一个项目下演进,信号之间通过共享的 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 项目唯一「官方推荐且长期维护」的协议。它有两种传输方式:

  • OTLP/gRPC:基于 HTTP/2 的 gRPC 流,默认端口 4317
  • OTLP/HTTP:基于 HTTP/1.1 的 POST,请求体可以是 application/x-protobufapplication/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.namehost.namek8s.pod.uid),在同一个 batch 里只序列化一次,大幅节省带宽。
  • InstrumentationScope 描述「谁埋的点」(比如 opentelemetry-instrumentation-http/0.45b0),用于区分不同埋点库的数据。
  • trace_idspan_id 是二进制而不是字符串,节省空间也方便后端直接按字节建索引。

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",
    }),
)

几个经验值:

  • 压缩优先选 gzip,在 Go/Java/Python SDK 上都有稳定实现;大量低基数字符串属性的场景下,压缩率可以达到 5 倍以上。
  • 重试的 MaxElapsedTime 不能太长,否则一次 Collector 抖动会导致 SDK 内存里堆积大量 span,触发 OOM。生产上常见 2~5 分钟。
  • 批处理在 SDK 侧(BatchSpanProcessor)和 Collector 侧(batch processor)都存在,通常 SDK 侧 batch 间隔小一些(1 秒),Collector 侧大一些(5~10 秒)。

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 项目在大规模组织里能跑通的核心原因之一。

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
}

几个容易被忽略的点:

  • 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_sizeotel.sdk.span.processor.dropped_spans。这两个指标务必接入告警。

5.3 Exporter 接口

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

  • otlp(首选,gRPC 或 HTTP)。
  • stdout(调试用,别上生产)。
  • prometheus(让应用直接暴露 /metrics,被 Prometheus Pull)。
  • zipkinjaeger(过渡期用)。

5.4 Propagator 与上下文传播

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

  • B3 单头:X-B3-TraceIdX-B3-SpanIdX-B3-Sampled,Zipkin 使用。
  • B3 多头:b3: {traceId}-{spanId}-{sampled}-{parentId}
  • Jaeger:uber-trace-id: {traceId}:{spanId}:{parentId}:{flags}
  • 阿里云 ARMS:历史上存在私有头,现已兼容 W3C。

迁移策略:在 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 的配置是一个显式的「数据流图」,由六类组件组成:

  • Receivers:接收端,从 SDK、Prometheus、Kafka、文件等拉取或接收数据。
  • Processors:流内处理,包括批处理、限流、富化、采样、转换。
  • Exporters:发送端,把数据送到一个或多个后端。
  • Connectors:v0.80 后引入的「既是 Exporter 又是 Receiver」的组件,用于跨 pipeline 串接。例如 spanmetrics connector 把 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 之前,确保后续的 resource processor 能在 pod 信息基础上再加 overlay。
  • tail_sampling 必须放到 batch 之前,否则 batch 会把同一个 trace 的 span 拆到不同批次里,尾采样拿不到完整 trace。
  • sending_queuequeue_sizenum_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 之间加一个 loadbalancing exporter,按 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.jar

javaagent 使用 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.py

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

7.3 Go

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

  • 手动使用 go.opentelemetry.io/contrib/instrumentation/... 系列库,比如 otelhttp.NewHandler 包装 HTTP Server、otelgrpc.UnaryServerInterceptor 包装 gRPC Server。
  • 基于 eBPF 的零代码方案:odigosbeylaopentelemetry-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.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 头采样与尾采样的差异

  • 头采样(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:策略列表,按顺序判断;命中任一策略即保留。

策略类型包括: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 尾采样的落地成本

  • 内存: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.namespacedeployment.environmenttenant.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 在 otlp exporter 里注入这个头。
  • 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:attributes processor 部分动作迁移到 transform(OTTL),旧写法 Deprecated。
  • v0.82:HTTP 语义约定从 http.method 改为 http.request.method,Grafana 面板同时支持两个字段至少半年。
  • v0.95:logging exporter 更名为 debug,老配置直接报错。
  • v0.100:Metrics 聚合时 CumulativeDelta 转换规则微调,需要确认后端计算是否兼容。

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.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 议题)可以概括为几条:

  • 把 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 侧用 transform processor 在出口前把 . 替换为 _
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.methodlogging → debug 都是典型。最佳实践:

  • 升级 SDK、升级 Collector、升级 Dashboard 按「T-2 周预发,T 周生产」的节奏分开做,不要混在一次变更里。
  • 保留至少一个版本号距离的「双写双读」窗口期。

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

直接把 user.idrequest.idsession.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_samplinggroupbytracenum_traces 设置过大。
  • batch 队列与 sending_queue 叠加。
  • filelog 一次读入过大日志文件,没设 start_at: end 时尤其危险。

调优顺序:先开 memory_limiter + Linux cgroup 硬限;监控 otelcol_process_runtime_heap_alloc_bytesotelcol_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_idspan_id,文件落盘后由 Collector filelog receiver 采集。

两种都能让日志与 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 的 otlp receiver tls 字段必须同时配 cert_filekey_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

  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 常见误区

  • 认为上 OTel 就能「自动有可观测性」:语义约定不落地,数据仍不可查询。
  • 把 OTel 当 APM:OTel 只负责采集与传输,搜索、告警、根因分析仍在后端。
  • 不做采样直接生产:1 万 QPS 的服务 100% 采样,一个月光 trace 存储就能冲到 PB 级。
  • 忽视 Collector 自身的观测性:Collector 坏了整个链路暗了自己还没感觉。
  • 只埋 HTTP,不埋异步任务:真正的业务瓶颈往往在定时任务、消息消费、后台 worker。

十四、参考资料

  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 命名规范、基数治理策略与可抄的指标清单。