pdata · 数据在管道里长什么样

所有进入 Collector 的数据都被转成 pdata。这一章读 pdata/README 的设计自述——为什么底层直接是 OTLP protobuf 结构、为什么 API 刻意避免可变共享,以及 delta vs cumulative 这个会咬人的语义差异。

作者 David更新于 第 39 篇(共 52 篇)

pdata · 数据在管道里长什么样

所有进入 Collector 的数据,不管原本是什么格式(Prometheus 文本、Jaeger、Fluent、OTLP),都会被转成 pdata——然后以这个形态流过整条管道,直到 exporter 再把它转成目标格式。

这是“N×M → N+M”能成立的技术前提:中间必须有一个统一的内部表示。

一个包一个信号

pdata/
├── pcommon/    公共类型(属性、时间戳、资源...)
├── ptrace/     追踪
├── pmetric/    指标
├── plog/       日志
├── pprofile/   性能剖析 ← 第四个信号!
└── internal/

注意 pprofile——性能剖析(profiling)正在成为 OTel 的第四大支柱。

第 0 部分讲的“三大支柱”可能要改口了:profiling(持续性能剖析) 正在被纳入 OTel 标准。它回答的问题是三大支柱都答不了的:“这个 CPU 到底花在哪一行代码上?”

  • 指标告诉你“CPU 80%”。
  • 追踪告诉你“这个请求在服务 A 花了 1.8s”。
  • profiling 告诉你“那 1.8s 里,1.2s 花在 json.Unmarshal 上”。

pdata README 的状态表显示 traces/metrics/logs 是 stable,而 profiles 还在演进中——这是个“正在发生”的标准扩展,值得关注。

设计自述:底层就是 OTLP protobuf

README 把设计意图说得很直白(真实文档):

Current implementation primarily uses OTLP protobuf structs as the underlying data structures. We keep a pointer to OTLP protobuf in the "orig" member field. This allows efficient translation to/from OTLP wire protocol. The underlying data structure is kept private so that we are free to make changes to it in the future.

两个决定,各有讲究:

  • 底层直接用 OTLP protobuf 结构(orig 字段) —— 因为 OTLP 是 Collector 最主要的进出口格式。如果内部表示和线上格式一致,OTLP → pdata → OTLP 这条最热的路径就几乎零转换开销(不用反复序列化/反序列化)。这是个针对主流量路径的性能优化。
  • 但把它设成 private —— “kept private so that we are free to make changes to it in the future”。你只能通过 pdata 的 API 访问,碰不到底层的 protobuf 结构。 于是他们保留了日后换实现的自由(比如为了性能改成别的内存布局),而不破坏所有组件。

“用具体实现换性能,用接口封装保留自由”——这是个漂亮的平衡。 对比第 1 部分 Prometheus 的三套 Labels 实现:同样是“底层可换、上层不知情”的思路。成熟的项目都会给自己留这条后路。

刻意避免可变共享

README 里这句是本章的重点:

The pdata API is designed to avoid mutable data sharing and bugs that stem from that. Each pdata instance cannot contain a reference to an object that is used in another pdata instance.

这条约束是为了防一类极难查的 bug:两个组件共享同一份数据,一个改了,另一个莫名其妙看到了变化。

想想 Collector 的图会扇出:同一批数据同时流向 prometheus 和 datadog 两条管道。如果它们共享同一个对象,而 datadog 管道里有个 processor 把某个属性删了——prometheus 管道里的数据也被改了。这种 bug 只在特定配置下出现、且极难复现。

pdata 用 API 设计在类型层面杜绝了这种共享。代价是要复制数据——而复制是有成本的。于是就有了下一章那个精妙的 MutatesData 契约:只在真的会改数据时才复制。

这是个反复出现的主题:WebRTC 的 CopyOnWriteBuffer、TSDB 的不可变 block——“共享是危险的,除非只读”。不同系统在不同层面,反复得出同一个结论。

会咬人的语义:delta vs cumulative

pdata 里有个东西必须讲,因为它是 OTel ↔ Prometheus 对接时最大的坑(pmetric/aggregation_temporality.go:10):

// AggregationTemporality defines how a metric aggregator reports aggregated values.
// It describes how those values relate to the time interval over which they are aggregated.
const (
    // AggregationTemporalityDelta is for a metric aggregator which reports
    // changes since last report time.
    AggregationTemporalityDelta
    // AggregationTemporalityCumulative is for a metric aggregator which reports
    // changes since a fixed start time.
    AggregationTemporalityCumulative
)

同样是“请求总数”,有两种截然不同的表达:

Cumulative(累积)Delta(增量)
含义自启动以来的总数自上次上报以来的增量
数据长相1000 → 1010 → 1025 → 104010 → 15 → 15
谁用Prometheus(counter 就是这个)部分 OTel SDK、某些云厂商
丢一个点会怎样没事(下一个点仍是累计值,rate() 照样算)数据永久丢失(那段增量没了)
重启怎么办归零,rate() 能识别重置无所谓(每次都是增量)

关键洞见:cumulative 对丢包有天然的鲁棒性。 这正是 Prometheus 选它的深层原因——pull 模型下抓取可能失败,而 cumulative 让“丢一次抓取”不损失任何信息(下次抓到的还是累计值,中间的增量隐含在里面)。delta 则不然:丢一个点,那段数据就永远没了。

对接时的现实问题:如果你的 OTel SDK 吐 delta,而要写进 Prometheus(它期望 cumulative),Collector 必须做转换(cumulativetodelta / deltatocumulative processor)。而 delta → cumulative 的转换是有状态的(要在 Collector 内存里累加),这带来:

  • Collector 重启 = 状态丢失 → 累计值归零 → 下游 rate() 看到“重置”。
  • Collector 扩成多副本 = 状态分裂 → 同一条 series 的增量被分到不同副本,各自累加 → 结果全错。

所以:能让 SDK 直接吐 cumulative 就别用 delta 再转。 这个坑很多团队踩过——表面是配置问题,根子是这两种语义的信息论差异。

小结与下一章

所有进入 Collector 的数据都转成 pdata——这是 N×M → N+M 的技术前提。一个包一个信号:pcommon/ptrace/pmetric/plog,外加 pprofile——性能剖析正成为第四支柱(答“CPU 花在哪一行”,三大支柱都答不了)。设计自述两点:底层直接用 OTLP protobuf 结构(让最热的 OTLP→pdata→OTLP 路径零转换开销)、但设为 private(保留日后换实现的自由)——用具体实现换性能,用接口封装保留自由。刻意避免可变共享(扇出时两条管道共享对象,一条改了另一条遭殃——这种 bug 极难复现);代价是要复制,于是有了下一章的 MutatesData 契约——“共享是危险的,除非只读”这个结论,WebRTC 和 TSDB 也各自得出过。delta vs cumulative 是最大的对接坑:cumulative 对丢包天然鲁棒(这正是 Prometheus 选它的深层原因),delta 丢一个点数据就永久没了;delta→cumulative 转换是有状态的 → Collector 重启即归零、多副本即状态分裂结果全错——能让 SDK 直接吐 cumulative 就别转。

数据在组件间怎么传?那个 MutatesData 契约到底精妙在哪?下一章。👉 consumer 与背压。

参考资料

  • [1] OTel Collector 源码 pdata/README.md(设计自述:orig 字段、避免可变共享)、pdata/pmetric/aggregation_temporality.go:10。
  • [2] OTel Collector 源码 pdata/(pcommon/ptrace/pmetric/plog/pprofile)、pdata/documentation.md。
  • [3] 关联指标类型、rate 与重置、pull 模型的鲁棒性。

本页目录