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

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

- 作者：David（道雾轩）
- 专栏：指标采集与观测（https://daiw.org/manual/observability.md）
- 最后更新：2026-07-15
- 原文：https://daiw.org/manual/observability/pdata
- 转载与引用：请注明出处并附原文链接（https://daiw.org/about/copyright）

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

**所有进入 Collector 的数据，不管原本是什么格式（Prometheus 文本、Jaeger、Fluent、OTLP），都会被转成 `pdata`**——然后以这个形态[流过整条管道](https://daiw.org/manual/observability/collector-architecture)，直到 exporter 再把它转成目标格式。

**这是“[N×M → N+M](https://daiw.org/manual/observability/otel-overview)”能成立的技术前提：中间必须有一个统一的内部表示。**

## 一个包一个信号

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

<Callout type="info">
  **注意 `pprofile`——**性能剖析（profiling）正在成为 OTel 的第四大支柱**。**

  [第 0 部分讲的“三大支柱”](https://daiw.org/manual/observability/three-pillars)可能要改口了：**profiling（持续性能剖析）** 正在被纳入 OTel 标准。它回答的问题是三大支柱都答不了的：“**这个 CPU 到底花在哪一行代码上？**”

  - [指标](https://daiw.org/manual/observability/three-pillars)告诉你“CPU 80%”。
  - [追踪](https://daiw.org/manual/observability/three-pillars)告诉你“这个请求在服务 A 花了 1.8s”。
  - **profiling 告诉你“那 1.8s 里，1.2s 花在 `json.Unmarshal` 上”**。

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

## 设计自述：底层就是 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.

<Callout type="info">
  **两个决定，各有讲究：**

  - **底层直接用 OTLP protobuf 结构（`orig` 字段）** —— 因为 [OTLP 是 Collector 最主要的进出口格式](https://daiw.org/manual/observability/otel-overview)。**如果内部表示和线上格式一致，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 实现](https://daiw.org/manual/observability/data-model)：同样是“底层可换、上层不知情”的思路。**成熟的项目都会给自己留这条后路。**
</Callout>

## 刻意避免可变共享

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.**

<Callout type="warn">
  **这条约束是为了防一类极难查的 bug：**两个组件共享同一份数据，一个改了，另一个莫名其妙看到了变化**。**

  想想 [Collector 的图会扇出](https://daiw.org/manual/observability/collector-architecture)：同一批数据同时流向 `prometheus` 和 `datadog` 两条管道。如果它们**共享同一个对象**，而 datadog 管道里有个 processor 把某个属性删了——**prometheus 管道里的数据也被改了**。这种 bug **只在特定配置下出现、且极难复现**。

  pdata 用 API 设计**在类型层面**杜绝了这种共享。**代价是要复制数据**——而复制是有成本的。于是就有了[下一章那个精妙的 `MutatesData` 契约](https://daiw.org/manual/observability/consumer-backpressure)：**只在真的会改数据时才复制**。

  **这是个反复出现的主题**：[WebRTC 的 CopyOnWriteBuffer](https://daiw.org/manual/webrtc/buffers)、[TSDB 的不可变 block](https://daiw.org/manual/observability/tsdb-overview)——**“共享是危险的，除非只读”**。不同系统在不同层面，反复得出同一个结论。
</Callout>

## 会咬人的语义：delta vs cumulative

pdata 里有个东西**必须**讲，因为它是 OTel ↔ Prometheus 对接时最大的坑（`pmetric/aggregation_temporality.go:10`）：

```go
// 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
)
```

<Callout type="warn">
  **同样是“请求总数”，有两种截然不同的表达：**

  | | **Cumulative（累积）** | **Delta（增量）** |
  | --- | --- | --- |
  | **含义** | **自启动以来的总数** | **自上次上报以来的增量** |
  | **数据长相** | 1000 → 1010 → 1025 → 1040 | 10 → 15 → 15 |
  | **谁用** | **Prometheus**（[counter 就是这个](https://daiw.org/manual/observability/metric-types)） | 部分 OTel SDK、某些云厂商 |
  | **丢一个点会怎样** | **没事**（下一个点仍是累计值，[`rate()` 照样算](https://daiw.org/manual/observability/promql-rate)） | **数据永久丢失**（那段增量没了） |
  | **重启怎么办** | [归零，`rate()` 能识别重置](https://daiw.org/manual/observability/promql-rate) | 无所谓（每次都是增量） |

  **关键洞见：cumulative 对丢包有天然的鲁棒性。** 这正是 [Prometheus 选它的深层原因](https://daiw.org/manual/observability/metric-types)——[pull 模型下抓取可能失败](https://daiw.org/manual/observability/scrape-loop)，而 cumulative 让“丢一次抓取”**不损失任何信息**（下次抓到的还是累计值，中间的增量隐含在里面）。**delta 则不然：丢一个点，那段数据就永远没了。**

  **对接时的现实问题：如果你的 OTel SDK 吐 delta，而要写进 Prometheus（它期望 cumulative），Collector 必须做转换（`cumulativetodelta` / `deltatocumulative` processor）。而 delta → cumulative 的转换是**有状态**的**（要在 Collector 内存里累加），这带来：
  - **Collector 重启 = 状态丢失** → 累计值归零 → [下游 `rate()` 看到“重置”](https://daiw.org/manual/observability/promql-rate)。
  - **Collector 扩成多副本 = 状态分裂** → 同一条 series 的增量被分到不同副本，各自累加 → **结果全错**。

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

## 小结与下一章

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

数据在组件间怎么传？那个 `MutatesData` 契约到底精妙在哪？下一章。👉 [consumer 与背压](https://daiw.org/manual/observability/consumer-backpressure)。

## 参考资料

- [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] 关联[指标类型](https://daiw.org/manual/observability/metric-types)、[rate 与重置](https://daiw.org/manual/observability/promql-rate)、[pull 模型的鲁棒性](https://daiw.org/manual/observability/scrape-loop)。
