← blog 技术原理与实践 · 2026-08-12

OpenTelemetry 与 OTLP 详解:从协议规范到遥测技术原理再到 36 个实战坑点

系统拆解 OpenTelemetry 的核心定位与组成、OTLP 协议的编码传输与错误语义、从 API/SDK 分离到上下文传播和采样器的遥测技术原理,以及埋点、指标、采集器、成本等六层 36 个实战坑点。

22 min read

一、OpenTelemetry 是什么

一句话:OpenTelemetry(OTel)是 CNCF(云原生计算基金会)旗下的开源可观测性框架,用一套统一的 API、SDK 和工具来插桩、生成、采集和导出遥测数据——链路(Traces)、指标(Metrics)和日志(Logs)。官方定义是”可观测性框架和工具包”,而不是监控后端。

核心定位:是 / 不是什么

是不是
遥测数据的生成、采集、管理、导出标准❌ 不是像 Jaeger、Prometheus 那样的存储/可视化后端(存储和可视化有意识地留给其他工具)
供应商中立的开放标准❌ 不绑定任何商业产品
解决”插桩格式不统一”问题的统一方案❌ 不解决”数据存哪、怎么画图、怎么告警”

诞生背景

它由两个项目于 2019 年合并而成:

  • OpenTracing:统一追踪 API 标准
  • OpenCensus:Google 出品的指标/追踪采集库

两者都没能独立解决”缺乏标准插桩方式”的问题,合并后取长补短形成 OTel,目前已是 CNCF 的毕业项目(graduated)。

为什么用它(两个关键原则)

  1. 数据属于你自己——不被供应商锁定(vendor lock-in),今天可以发给 Jaeger,明天可以切到商业后端,代码不用大改。
  2. 只学一套 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)
  • 默认 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 多路分发。

消息结构(数据模型)

每种信号对应一对请求/响应消息:

信号请求消息响应消息
TracesExportTraceServiceRequestExportTraceServiceResponse
MetricsExportMetricsServiceRequestExportMetricsServiceResponse
LogsExportLogsServiceRequestExportLogsServiceResponse
ProfilesExportProfilesServiceRequestExportProfilesServiceResponse

数据以**信封(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——客户端必须丢弃并计数

背压与限流

  • gRPC:服务端过载时返回 UNAVAILABLE + RetryInfo(带 retry_delay),客户端按建议延迟重试,配合指数退避
  • HTTP:返回 429 Too Many Requests 或 503,可带 Retry-After 头指示等待时间;无该头则用指数退避

多目标导出

一个客户端发给多个后端时,每个目的地独立维护队列、确认和重试逻辑,队列共享不可变数据以减少内存开销——慢的后端不会阻塞快的后端。

版本兼容性(关键设计)

OTLP 没有显式协议版本号,靠三条机制保证新旧互通:

  1. 能力分”强制/可选”,新旧节点取功能交集
  2. 小改动利用 protobuf 向后兼容(新增字段,老节点忽略)
  3. 大改动作为新 OTEP 定义的可选能力,通过能力发现机制协商

已知局限

  • 重复数据:网络中断重连时,客户端无法确认上次数据是否到达,重发可能导致后端出现重复数据——这是”保证不丢”的刻意取舍,对遥测可接受。

三、OpenTelemetry Collector

Collector 是 OTel 的”数据管道”核心,采用 Receiver → Processor → Exporter 三段式流水线:

  • Receiver(接收器):从各种来源收数据(OTLP、Jaeger、Prometheus、Kafka 等)
  • Processor(处理器):批处理、过滤、采样、属性重命名、内存限制等
  • Exporter(导出器):发给目标后端(OTLP、Prometheus、Jaeger、S3 等)

两种部署形态:

  • Agent 模式:与应用同机部署(daemon),就近采集
  • Gateway 模式:独立集群部署,作为集中式数据汇聚/转发枢纽

与 Prometheus / Jaeger 的关系

OpenTelemetryPrometheusJaeger
角色数据生成/采集/传输标准指标存储 + 查询 + 告警后端链路存储 + 可视化后端
信号Traces + Metrics + LogsMetricsTraces

一句话: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 在类加载时改写字节码)
零代码(平台级)不改应用代码,由外部注入 SDKJava 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 采样是双层标志:

IsRecordingSampled含义SpanProcessorExporter
✅✅记录并导出收到收到
✅❌记录但不导出(可用于统计/尾部决策)收到不收
❌❌直接丢弃一切不收不收
❌✅规范禁止(会造成链路空洞)——
  • 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(二进制)vs application/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。


参考来源

Sources

No external sources for this entry.

Related