OpenTelemetry 与 OTLP 详解:从协议规范到遥测技术原理再到 36 个实战坑点
系统拆解 OpenTelemetry 的核心定位与组成、OTLP 协议的编码传输与错误语义、从 API/SDK 分离到上下文传播和采样器的遥测技术原理,以及埋点、指标、采集器、成本等六层 36 个实战坑点。
一、OpenTelemetry 是什么
一句话:OpenTelemetry(OTel)是 CNCF(云原生计算基金会)旗下的开源可观测性框架,用一套统一的 API、SDK 和工具来插桩、生成、采集和导出遥测数据——链路(Traces)、指标(Metrics)和日志(Logs)。官方定义是”可观测性框架和工具包”,而不是监控后端。
核心定位:是 / 不是什么
| 是 | 不是 |
|---|---|
| 遥测数据的生成、采集、管理、导出标准 | ❌ 不是像 Jaeger、Prometheus 那样的存储/可视化后端(存储和可视化有意识地留给其他工具) |
| 供应商中立的开放标准 | ❌ 不绑定任何商业产品 |
| 解决”插桩格式不统一”问题的统一方案 | ❌ 不解决”数据存哪、怎么画图、怎么告警” |
诞生背景
它由两个项目于 2019 年合并而成:
- OpenTracing:统一追踪 API 标准
- OpenCensus:Google 出品的指标/追踪采集库
两者都没能独立解决”缺乏标准插桩方式”的问题,合并后取长补短形成 OTel,目前已是 CNCF 的毕业项目(graduated)。
为什么用它(两个关键原则)
- 数据属于你自己——不被供应商锁定(vendor lock-in),今天可以发给 Jaeger,明天可以切到商业后端,代码不用大改。
- 只学一套 API 和约定——不用为每个后端学一套 SDK。
组成部分
- 规范(Specification):定义各组件行为的标准
- 协议(OTLP):定义遥测数据形状的标准协议
- 语义约定(Semantic Conventions):为常见概念定义统一命名,例如
http.request.method、db.system - API + 语言 SDK:支持 Go、Java、Python、JS、C++ 等所有主流语言
- 插桩库生态:自动插桩常见框架/库
- 自动插桩组件:无需改代码即可生成遥测(如 Java agent、Zero-code)
- OpenTelemetry Collector:接收、处理、导出遥测数据的代理
- 其他工具:K8s Operator、Helm Charts、FaaS 支持等
三大支柱(Signals)+ 新兴信号
- Traces 链路:一次请求跨多个服务的完整调用路径,由 Span 组成,通过 traceId/spanId 关联
- Metrics 指标:可聚合的数值型数据(Counter、Gauge、Histogram 等)
- Logs 日志:结构化的日志记录
- Profiles 性能剖析(实验阶段):OTLP 1.11 中为 development 状态
二、OTLP 协议详解
OTLP 是 OTel 项目自己定义的原生遥测传输协议,描述了遥测数据在”数据源 → 中间节点(如 Collector)→ 后端”之间的编码、传输和投递机制。它是一个通用目的的遥测数据投递协议。
基本形态
- 请求/响应式协议:客户端发请求,服务端回响应,目前只定义一种操作:
Export。 - 编码:基于 Protocol Buffers(protobuf)schema(
opentelemetry-proto仓库定义)。 - 传输:两种实现——OTLP/gRPC 和 OTLP/HTTP。
- 压缩:所有服务端必须支持
none和gzip两种压缩。
两种传输方式
OTLP/gRPC
- 使用 gRPC unary 请求,一次请求对应一个
Export*ServiceRequest消息 - 默认端口:4317
- 支持顺序请求(低延迟本地场景)和并发 unary 请求(高吞吐场景)
- 客户端关闭时可选等待未确认请求,保证可靠投递
OTLP/HTTP
- HTTP POST 请求,body 为 protobuf 编码的
Export*ServiceRequest - 默认端口:4318
- 两种编码:
- 二进制 protobuf:
Content-Type: application/x-protobuf - JSON protobuf:
Content-Type: application/json(traceId/spanId 用十六进制字符串,枚举必须用整数值,字段名用 lowerCamelCase)
- 二进制 protobuf:
- 默认 URL 路径:
/v1/traces(链路)/v1/metrics(指标)/v1/logs(日志)/v1development/profiles(性能剖析,开发阶段)
- 支持 gzip(
Content-Encoding: gzip)、HTTP/1.1 与 HTTP/2(HTTP/2 失败可回退 1.1)
实践中 gRPC 和 HTTP 可以复用同一端口,服务端根据 Content-Type 多路分发。
消息结构(数据模型)
每种信号对应一对请求/响应消息:
| 信号 | 请求消息 | 响应消息 |
|---|---|---|
| Traces | ExportTraceServiceRequest | ExportTraceServiceResponse |
| Metrics | ExportMetricsServiceRequest | ExportMetricsServiceResponse |
| Logs | ExportLogsServiceRequest | ExportLogsServiceResponse |
| Profiles | ExportProfilesServiceRequest | ExportProfilesServiceResponse |
数据以**信封(envelope)**形式组织:Resource(资源)→ Scope(插桩作用域)→ 具体数据点(Span / Metric data point / LogRecord)。空信封(0 个数据点的请求)应被丢弃。
大小限制(防滥用)
- 请求体:推荐上限 64 MiB(gRPC 超限返回
RESOURCE_EXHAUSTED;HTTP 超限返回413 Content Too Large) - 响应体:推荐上限 4 MiB
响应语义:成功/部分成功/失败
- 完全成功(Full Success):服务端接受全部数据,
partial_success字段留空 - 部分成功(Partial Success):响应中
partial_success携带被拒绝的数量(rejected_spans/rejected_data_points/rejected_log_records)和英文错误说明。⚠️ 客户端收到 partial success 后不得重试。服务端也可用它传递警告(rejected=0 但 error_message 非空) - 失败(Failure):分两类——
- 可重试(retryable):如
UNAVAILABLE、DEADLINE_EXCEEDED、ABORTED、OUT_OF_RANGE、DATA_LOSS、CANCELLED;HTTP 侧为429、502、503、504 - 不可重试(non-retryable):如
INVALID_ARGUMENT、PERMISSION_DENIED、UNIMPLEMENTED、INTERNAL等;HTTP 侧如400 Bad Request——客户端必须丢弃并计数
- 可重试(retryable):如
背压与限流
- gRPC:服务端过载时返回
UNAVAILABLE+RetryInfo(带retry_delay),客户端按建议延迟重试,配合指数退避 - HTTP:返回
429 Too Many Requests或503,可带Retry-After头指示等待时间;无该头则用指数退避
多目标导出
一个客户端发给多个后端时,每个目的地独立维护队列、确认和重试逻辑,队列共享不可变数据以减少内存开销——慢的后端不会阻塞快的后端。
版本兼容性(关键设计)
OTLP 没有显式协议版本号,靠三条机制保证新旧互通:
- 能力分”强制/可选”,新旧节点取功能交集
- 小改动利用 protobuf 向后兼容(新增字段,老节点忽略)
- 大改动作为新 OTEP 定义的可选能力,通过能力发现机制协商
已知局限
- 重复数据:网络中断重连时,客户端无法确认上次数据是否到达,重发可能导致后端出现重复数据——这是”保证不丢”的刻意取舍,对遥测可接受。
三、OpenTelemetry Collector
Collector 是 OTel 的”数据管道”核心,采用 Receiver → Processor → Exporter 三段式流水线:
- Receiver(接收器):从各种来源收数据(OTLP、Jaeger、Prometheus、Kafka 等)
- Processor(处理器):批处理、过滤、采样、属性重命名、内存限制等
- Exporter(导出器):发给目标后端(OTLP、Prometheus、Jaeger、S3 等)
两种部署形态:
- Agent 模式:与应用同机部署(daemon),就近采集
- Gateway 模式:独立集群部署,作为集中式数据汇聚/转发枢纽
与 Prometheus / Jaeger 的关系
| OpenTelemetry | Prometheus | Jaeger | |
|---|---|---|---|
| 角色 | 数据生成/采集/传输标准 | 指标存储 + 查询 + 告警后端 | 链路存储 + 可视化后端 |
| 信号 | Traces + Metrics + Logs | Metrics | Traces |
一句话:OTel 负责”采”,Prometheus/Jaeger 负责”存和看”,双方是上下游协作关系。
典型落地链路
应用(带 OTel SDK/Agent)
│ OTLP/gRPC (4317) 或 OTLP/HTTP (4318)
▼
OTel Collector (Agent,同机部署)
│ OTLP (处理器: 批处理/过滤/采样)
▼
OTel Collector (Gateway,集中部署)
├──► Jaeger / Tempo (链路)
├──► Prometheus / VictoriaMetrics (指标)
└──► Loki / Elasticsearch (日志)
四、OpenTelemetry 遥测技术原理
总架构:为什么”API 与 SDK 分离”是地基
应用代码 + 插桩库 (只依赖 API 编译)
import: tracer.start_span() / counter.add()
│ 运行时绑定(全局 Provider 注册)
SDK (API 的实现,真正干活)
采样 → 聚合 → 队列 → 导出
│ OTLP (protobuf + gRPC/HTTP)
Collector / 后端
关键机制:
- API 层只有接口,默认是 No-op 实现——不装 SDK 代码也能编译运行,只是不产生数据,这让插桩代码对”是否启用可观测性”零感知。
- SDK 通过全局 Provider 注册挂载(
GlobalTracerProvider/GlobalMeterProvider),插桩代码拿到的Tracer/Meter全部来自 Provider,所以换 SDK 实现、换导出后端,业务代码一行不改。 - InstrumentationScope(插桩作用域 = 库名 + 版本) 挂在每个 Tracer/Meter 上随数据一起导出,后端据此区分自动插桩与业务代码。
插桩原理:三层注入方式
| 方式 | 原理 | 技术手段 |
|---|---|---|
| 手动插桩 | 业务代码显式调用 API 创建 span/记录指标 | 最简单,无魔法 |
| 自动插桩(库级) | 在 HTTP 客户端/服务端、DB 驱动等框架的”入口/出口”自动创建 span 并传播上下文 | Python/JS 用 monkey-patch 包装库函数;Java 用字节码增强(instrumentation agent 在类加载时改写字节码) |
| 零代码(平台级) | 不改应用代码,由外部注入 SDK | Java agent(-javaagent)、K8s 的 OpenTelemetry Operator(mutating webhook 自动注入 sidecar/环境变量) |
一个重要原则:插桩只负责”生成数据 + 传播上下文”,不负责”发送数据”。发送由 SDK 的 exporter 统一负责——这正是插桩库可以无穷多、但出口统一的原因。
上下文传播:链路能”串起来”的核心机制
1. SpanContext(身份卡)
每个 span 携带一个不变的身份:
- traceId(16 字节):全局唯一链路 ID,整条链路共享
- spanId(8 字节):本 span 唯一 ID
- traceFlags:低 1 位是 Sampled 标志(是否被采样)
- traceState:键值对,携带供应商/采样器附加信息(如概率采样的阈值
ot=th:...)
2. 进程内传播(Context 对象)
同一进程内,当前活动 span 存在语言特定的 Context 存储里——Java 用 ThreadLocal,Python/JS 用 AsyncLocal(异步上下文变量),保证跨 async/协程/线程切换仍能拿到当前 span。async 传播错一层,链路就断链。
3. 跨进程传播(W3C TraceContext 协议 + 可插拔 Propagator)
SDK 抽象出 TextMapPropagator 接口(注入/提取方法),默认实现是 W3C TraceContext:
traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
└─┘└──────────────────────────┘└────────────────┘└─┘
版本 traceId(32位hex) spanId(16位hex) flags
0x01=已采样
- 注入(Inject):出站请求时把当前 span 的 SpanContext 写进
traceparent头 - 提取(Extract):入站请求时读出头,以它为父 span 创建新 span
- Baggage:另有
baggage头传播业务键值对(不参与链路身份,只带附加值)
换协议只需换 Propagator(B3、Jaeger 头都有实现),链路身份规则不变。
链路(Tracing)内部机制
Span 生命周期
start → 写属性/事件/链接 → end(打上结束时间戳)。Span 是只写对象(end 后不可再写)。
SDK 创建 Span 的固定顺序(规范规定的 4 步)
1. 有父 traceId 则沿用,否则先生成新 traceId
(必须在采样之前,因为采样器需要 traceId 做决策)
2. 调用 Sampler.ShouldSample() 做采样决策
3. 无论如何都生成新 spanId
(即使被 DROP 也有 ID,日志/异常才能关联到它)
4. 按决策创建 span,设置两个标志
两个标志位:采样的真正运作方式
OTel 采样是双层标志:
| IsRecording | Sampled | 含义 | SpanProcessor | Exporter |
|---|---|---|---|---|
| ✅ | ✅ | 记录并导出 | 收到 | 收到 |
| ✅ | ❌ | 记录但不导出(可用于统计/尾部决策) | 收到 | 不收 |
| ❌ | ❌ | 直接丢弃一切 | 不收 | 不收 |
| ❌ | ✅ | 规范禁止(会造成链路空洞) | — | — |
Sampler.ShouldSample()返回三种决策:DROP、RECORD_ONLY、RECORD_AND_SAMPLE- 默认采样器是
ParentBased(root=AlwaysOn)——子 span 跟随父 span 的 sampled 标志(本地父/远程父 × 已采样/未采样共 4 种组合分别委托) - 根 span 用
TraceIdRatioBased:对 traceId 做确定性哈希——同一 traceId 在任何地方、任何语言算出的决策必然一致,保证”一条链路要么全采要么全弃” - 新式
ProbabilitySampler基于 W3C Trace Context L2 的 56 位随机性,用R >= T(随机数 ≥ 拒绝阈值)判定,并把阈值写进 tracestate 的ot=th:实现跨系统一致 - 头部采样(Head sampling) 在 SDK 创建 span 时决定;尾部采样(Tail sampling) 在 Collector 侧看到完整链路后决定,两者配合
SpanProcessor → Exporter 流水线
Span end → SpanProcessor.onEnd()
├── SimpleSpanProcessor (立即导出,调试用)
└── BatchSpanProcessor (队列缓冲:批量打包+定时刷新)
└── SpanExporter.export(batch) → OTLP
- 一个 TracerProvider 可注册多个 Processor/Exporter 扇出(如同时发 Jaeger 和 Prometheus)
BatchSpanProcessor内部:队列 + 定时器,按”批量大小/间隔”打包;队列满时丢弃新 span(保护应用内存)ForceFlush:立即导出所有未导出数据(进程优雅退出前必调);Shutdown:关闭一切
指标(Metrics)内部机制
一条指标的完整旅程
instrument.add(1.0) ──► SDK 内部按"instrument × 属性组合"维护聚合器
│ View(用户配置)决定: 聚合方式/是否改名/保留哪些属性
│ PeriodicExportingMetricReader 每 60s 触发"采集循环"
│ 聚合器输出数据点(带 temporality)
MetricExporter → OTLP
三个核心概念
- Instrument(仪表):Counter(只增)/UpDownCounter(可增可减)/Histogram/Gauge/ObservableX(异步,回调里取值)
- Aggregation(聚合):
Sum、LastValue、ExplicitBucketHistogram(显式分桶)、Base2ExponentialHistogram(指数分桶,自动调 scale,160 桶覆盖 1ms~100s 长尾,误差 <5%)、Drop - Temporality(时间性):
Delta(本周期增量)/Cumulative(从启动累计)。Prometheus 惯例用 Cumulative,OTLP 后端常用 Delta——这也是同一份插桩喂不同后端要换 temporality 的原因
View:指标的”整形层”
View = 选择条件(Instrument 匹配:name 通配符/type/unit/meter 名) + 流配置(改名/聚合方式/属性白名单/基数上限)。典型场景:把 HTTP 客户端默认的 Histogram 改成只报总数;属性白名单去掉含敏感信息的属性。
两个防失控机制
- 基数限制(cardinality limit):每个 instrument 每轮采集最多输出 N 个数据点,防止”user_id 当属性”导致内存爆炸(高基数攻击是指标系统头号杀手)
- Exemplar(样例):聚合时抽查保留一个原始样本及其 traceId/spanId 随数据点导出——指标与链路打通的机制:看到延迟突增的指标点,一键跳到对应 trace
异步 instrument 的采集约束
Observable 回调只在采集循环内被调用,结果只归属触发的那个 MetricReader;回调必须限时,防止卡死采集。
日志(Logs)接入原理
OTel 不要求你重写日志——核心机制是 Bridge(桥接):
log4j / zap / logrus / slog ...
│ 通过 appender/plugin 桥接到 OTel SDK
OTel SDK: 把当前 Context 里的 traceId/spanId 自动附加到每条 LogRecord
│
OTLP Logs → Collector → Loki/ES 等
效果:日志天然携带当前请求的 traceId,后端里点一条日志就能跳进对应链路——三支柱由此打通,日志与链路共用 Context 传播机制。
Resource:数据”属于谁”
每条遥测数据都带一个 Resource(进程级元数据:service.name、host.name、K8s pod 等),由 ResourceDetector 自动检测。它是数据归属的根:Resource → Scope → 数据点 三层信封,后端按 service.name 分组、跨信号关联都靠它。
Collector 内部:分布式管道与背压
- 管道是独立数据流,Receiver 与 Exporter 之间可 1:N 扇出
- 背压传递链:后端慢 → Exporter 排队 → Processor 队列积压 → Receiver 不再接收(或对客户端返回 429/503)——把压力逐级回传,而不是把 Collector 内存打爆
memory_limiter处理器在内存逼近上限时直接丢弃数据(丢弃比 OOM 好)- OTTL(OpenTelemetry Transformation Language):在管道里声明式改写数据(加属性/过滤/重命名)
五、端到端时序:一次请求的完整旅程
用户请求进来 → 服务A(带 OTel SDK + 自动插桩)
① 提取 traceparent 头 → 创建入站 span(父=上游)
② 采样器决策(RECORD_AND_SAMPLE)
③ 业务逻辑中手动插桩创建子 span
④ 出站 HTTP 请求 → 注入 traceparent 头
⑤ span end → BatchSpanProcessor 入队
Collector(Agent) → batch 合并 → memory_limiter 保护
Collector(Gateway) → tail_sampling 过滤 → 扇出
├──► Tempo/Jaeger(链路) Prometheus(指标) Loki(日志)
└── 日志里带 traceId → 点击跳转链路
一句话总结原理:API 定义接口、SDK 实现逻辑、插桩注入数据、Context 串联身份、采样控制成本、Processor/Reader 缓冲聚合、Exporter 统一出口、OTLP 统一格式、Collector 统一管道——每一层只做一件事,层与层之间全是标准接口,所以任何一层都能替换而不破坏其他层。
六、OTel / OTLP 实战常见坑点清单
埋点与插桩层的坑
1. 装了 SDK 却不初始化 Provider —— “No-op 黑洞”
OTel API 默认是 No-op 实现。忘了注册全局 Provider(或配置环境变量没生效),代码编译运行都正常,就是不产生任何数据,且没有任何报错。排查时先确认 OTEL_SDK_DISABLED 之类的环境变量是否被误设、SDK 是否真的被显式初始化。
2. 手动插桩 + 自动插桩叠加 → 重复 span
框架自动插桩已经为 HTTP 请求创建了 span,业务代码里又手动 start_span 包一层,同一个请求会出两条重复链路。原则:自动插桩只负责框架入口/出口,业务逻辑里手动插桩只加子 span,不要重复包外层。
3. Span 忘记 end() → 内存泄漏 + 链路残缺
Span 不 end,SDK 就认为它还在进行中:BatchSpanProcessor 队列里积压对象越来越多(高并发下直接内存泄漏)、后端永远显示”进行中”的悬空 span。异步操作里最容易漏 end(回调/超时分支各写一处 end 才安全,或用 with 上下文/装饰器)。
4. 语义约定不遵守
自己发明命名(http.method 写成 httpMethod、http.method 和 request.method 混用):后端无法按官方约定聚合、关联、告警;跨团队/跨信号无法关联;升级或切换后端时标准属性有现成映射,自定义属性全要重做。
5. 把请求体/响应体塞进 span 属性 隐私合规风险(明文 body、token、PII)、体积爆炸(占用 OTLP 请求额度、存储成本飙升)、高基数(URL 带 query 参数,每条请求一个值)。
上下文传播的坑(断链重灾区)
6. 异步/协程上下文丢失 → 链路神秘断掉
进程内传播靠 ThreadLocal / AsyncLocal。跨线程、协程、async/await 切换时必须显式传 Context,否则子线程里创建的 span 变成”孤儿”、日志里 traceId 附不上。Java 用 Context.makeCurrent() 在线程池任务里重新绑定;Python/JS 注意框架是否自动用 AsyncLocal 传播(连接池/线程池场景是重灾区)。
7. 网关/代理剥离 traceparent 头
K8s Ingress、API 网关、负载均衡器如果配置不当会把 traceparent 头删掉,下游服务全部变”根 span”。要验证:入站请求进来时先确认 extract 是否真的拿到了上游 traceId。
8. 多传播协议混用 默认只装 W3C TraceContext,但上游服务在用 B3 或 Jaeger 头 → 链路全断。要么统一切 W3C,要么在 SDK 里同时注册多个 Propagator(注意顺序)。
采样相关的坑
9. 头部采样与尾部采样互相打架 SDK 端头部采样已经把大部分链路 DROP 了,Collector 端尾部采样想保留慢链路/错误链路却发现数据根本不存在的**——尾部采样只能看到没被头部采样丢弃的东西。正确组合:SDK 端用高比例头部采样控制源头流量,Collector 端尾部采样做精细决策。
10. 尾部采样必须配合”同 trace 粘性路由” 多个 Collector 实例时,如果同一 trace 的 span 被负载均衡分到不同实例,每个实例独立做尾部采样决策 → 链路被劈成两半。必须用 traceId 哈希路由让同一 trace 的所有 span 落到同一实例。
11. 采样率调太低 → 故障时无据可查 错误链路、慢链路全被概率采样丢掉,出事故时后端什么都没有。至少保留:错误 100%、慢于 2×SLO 100%、关键服务高比例,其余 5~10% 基线采样。
12. 混淆 IsRecording 与 Sampled
Sampled=false 但 IsRecording=true 时,数据在本地记录但不会导出——很多人 debug 时发现 span 存在却看不到,就是这个双标志没搞清。
指标的坑(最容易出”数据对不上”的账)
13. 高基数(最经典、最致命)
把 user_id、session_id、request_uuid、完整 URL 当属性 → 一个指标几百万条时间序列 → Collector 内存涨、后端存储爆炸、查询变慢、账单惊悚。指标只允许低基数维度:环境、服务名、状态类(2xx/4xx)、端点模式(不用完整 URL)。真的要高基数标识 → 先哈希或聚合,或用 trace 而不是 metric 承载。用 processor_accepted_metric_points 按指标名监控基数增长。
14. Temporality 混用 → 数值翻倍 同一份数据 Delta 和 Cumulative 混用(比如进程内用 Delta、经 Prometheus 后端又按 Cumulative 处理),Sum 会被重复累加。OTLP 后端默认 Delta,Prometheus 惯例 Cumulative——跨后端切换时 temporality 必须显式配置一致。
15. 单位与命名习惯
不设单位或设错(Prometheus 会自动把 _total 后缀的 counter 加 rate(),命名冲突会双计);计数器语义用错(该用 Counter 却用 Gauge);用 _total 结尾的指标在 OTLP→Prometheus 转换时被重复处理。
16. 忽略时钟同步 多机时间不同步 → span 时间戳错乱、指标周期错位、链路时间线倒挂。容器/K8s 场景注意 NTP/chrony 配置。
17. View 不会用 默认聚合不一定符合需求(比如 HTTP 客户端默认 Histogram 的桶设计不合理),也不做基数上限设置、不去敏感属性——该用 View 整形的地方全默认,后面数据进后端就没法补救。
OTLP 传输层的坑
18. 端口/协议搞混
- 4317 = gRPC,4318 = HTTP——配置错端口,连接静默失败或报错难查
- HTTP 导出时 Content-Type 写错:
application/x-protobuf(二进制)vsapplication/json(JSON protobuf),服务端会拒收或解析失败
19. 请求超 64 MiB
批量太大超限:gRPC 返回 RESOURCE_EXHAUSTED,HTTP 返回 413。调小 batch size 或开 gzip。
20. 不懂”部分成功”语义 → 重复数据
收到 partial_success 后不得重试——重试会把已接受的部分再发一遍,造成重复。很多人把 partial success 当失败处理,结果后端数据翻倍。
21. 不区分可重试/不可重试错误
INVALID_ARGUMENT(400)、PERMISSION_DENIED(401/403)这类不可重试错误,重试一万次也是浪费;UNAVAILABLE(503)、DEADLINE_EXCEEDED(504)才该指数退避重试。
22. 重复投递的现实 OTLP 是”至少一次”(at-least-once)语义,网络中断重连后重发会产生重复数据,这是刻意取舍。后端要按 traceId/spanId 去重,或接受少量重复,别指望协议层保证恰好一次。
23. 压缩没开 gzip 对遥测数据通常有 5~10 倍压缩率,不开就是白烧带宽和流量费(尤其走公网/云)。
Collector 的坑(官方反模式)
24. 只用单实例 Collector,无 HA Collector crash/bug 引 OOM 被杀 → 整条遥测管线全灭,事故期间什么都记录不到。要 Agent + Gateway 两层,gateway 放 LB 后面做池化。
25. 不监控 Collector 自身
Collector 自带内部指标(processor_refused_spans、otelcol_exporter_send_failed_spans、队列长度、内存),不监控它就等于”管道堵了没人知道”。先监控监控器本身。
26. Core/Contrib 直接上生产 Core 太简陋,Contrib 组件太多(臃肿 + 攻击面大)。生产应该用 OCB(Collector Builder)自建精简发行版,只打包需要的组件。
27. 不更新 Collector Bug 修复、安全补丁、性能优化都等不到。
28. 生产环境应用直连后端(不经过 Collector) 官方明确生产应走 Collector——重试、批处理、扇出、敏感数据过滤、后端切换都集中在一处改。直连 = 每个应用各自处理,改后端要改所有应用。
29. 不配 memory_limiter
后端一慢 → exporter 队列积压 → 内存涨 → OOM。memory_limiter 是保命闸:内存越限时暂时拒收新数据,把压力回传给上游,而不是直接被打死。
30. batch 参数不调优
batch 太大:延迟高、内存压力大、可能触发 64 MiB 限制;batch 太小/超时太短:网络调用频繁、吞吐上不去。生产要按负载压测调 batch 的 send_batch_size / timeout。
成本与资源治理的坑
31. 100% 采样 + 无限 span 数 高吞吐服务每条请求串几十个 span,全量导出成本线性爆炸。先压测看 P50/P95 延迟开销,再定采样策略(5~10% 基线起步)。
32. 循环内创建 span
在 for 循环/热路径里每次迭代都 start_span,一个请求产生几千 span。热路径要么只打指标,要么循环外统计。
33. 只看单项指标延迟,不看整体开销 “加个监控”导致延迟 +12% 的现象很常见:Java agent 字节码改写、异步上下文传播、span 记录本身都有 CPU 开销。上线前必须测:无插桩基线 vs 有插桩的 P50/P95/P99。
34. 日志信号无节制 日志桥接后每条都自动带 traceId——很好,但如果日志本身量大,直接撑爆管道。日志也要采样/限流。
组织与流程的坑
35. 没有统一治理 每个团队自己发明属性命名、自己定采样率、各连各的后端 → 后端数据一团乱麻,根本无法跨服务关联。语义约定 + 属性字典 + 采样策略要组织级统一。
36. 把遥测管线当”一次性配置” 它是一等基础设施:需要容量规划、负载测试、采样策略迭代、基数治理、持续监控。“配完就忘” = 下一个 2AM incident。
一句话总结坑点:埋点层别重复插桩、别漏 end、命名守约定;传播层异步必传 Context、网关别扒头;指标层高基数禁用、Temporality 一致;传输层端口编码别混、partial success 别重试;Collector 层两层部署、必配 memory_limiter、自建发行版、监控自身;成本层先测开销再定采样,热路径别造 span。
参考来源
- OpenTelemetry 官方中文文档 - 什么是 OpenTelemetry
- OTLP Specification 1.11.0
- opentelemetry-proto 仓库(protobuf schema 定义)
- Collector 组件文档