pdata · 数据在管道里长什么样
所有进入 Collector 的数据都被转成 pdata。这一章读 pdata/README 的设计自述——为什么底层直接是 OTLP protobuf 结构、为什么 API 刻意避免可变共享,以及 delta vs cumulative 这个会咬人的语义差异。
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 到底花在哪一行代码上?”
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 → 1040 | 10 → 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 模型的鲁棒性。