# Collector 架构 · 管道其实是一张图

> receiver → processor → exporter 是教科书说法，但源码里管道是一张 DAG，靠拓扑排序构建。这一章读 service/internal/graph，讲清五类组件、图的构建流程，以及那个体现工程功力的细节——反拓扑序启动、拓扑序停止。

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

# Collector 架构 · 管道其实是一张图

几乎所有介绍 Collector 的文章都会画这张图：

```
receiver → processor → exporter
```

**这个说法不算错，但它掩盖了一个更有意思的真相**：在源码里，管道**是一张 DAG（有向无环图）**，由拓扑排序构建和启停。这一章打开 `service/internal/graph/`。

## 五类组件

先把角色认全（对应源码的五个顶层目录）：

| 组件 | 目录 | 职责 |
| --- | --- | --- |
| **receiver** | `receiver/` | **数据从哪进来**（OTLP/Prometheus 抓取/Jaeger/文件…） |
| **processor** | `processor/` | **中间怎么加工**（批量/过滤/改属性/[采样](https://daiw.org/manual/observability/sampling)/限流） |
| **exporter** | `exporter/` | **数据往哪出去**（OTLP/Prometheus remote write/各云厂商） |
| **connector** | `connector/` | **把一条管道的出口接到另一条的入口**（如“从 traces 生成指标”） |
| **extension** | `extension/` | **不碰数据**的辅助能力（健康检查、认证、pprof） |

<Callout type="info">
  **前三个是“数据面”，后两个值得单说：**

  - **connector** —— 它**同时是某条管道的 exporter 和另一条管道的 receiver**。经典用途：`spanmetrics` connector 从 [traces](https://daiw.org/manual/observability/traces-basics) **生成 [RED 指标](https://daiw.org/manual/observability/red-method)**（不用你在应用里再埋一遍！）。**这正是“管道是图而不是线”的直接证据**——connector 让管道之间连起来，形成了图。
  - **extension** —— **不参与数据流**，提供“周边能力”。健康检查端点、认证器、性能分析。**把它们独立成一类，是为了让数据面组件保持纯粹**——[receiver/processor/exporter 只管数据，不管这些横切关注点](https://daiw.org/manual/observability/otel-overview)。
</Callout>

## 图的构建：三步

[官方架构文档](https://daiw.org/manual/observability/reading-the-source)（`docs/internal-architecture.md`）直接给了流程，源码在 `service/internal/graph/graph.go:89`：

```go
if err := pipelines.createNodes(set); err != nil { ... }   // ① 建节点
pipelines.createEdges()                                     // ② 连边
err := pipelines.buildComponents(ctx, set)                  // ③ 拓扑排序 + 实例化
```

<Callout type="info">
  **三步走：**

  1. **`createNodes`** —— 把配置里的每个组件变成图的一个**节点**。同时**校验 connector**（它连接的两条管道类型对不对）。
  2. **`createEdges`** —— 按管道配置**连边**：receiver → processor → processor → exporter。**多条管道共用同一个 receiver 时，边会分叉**——图就是这么长出来的。
  3. **`buildComponents`** —— **拓扑排序**，然后**按反序实例化**每个组件。

  **为什么需要图而不是简单的链表？看几个真实场景：**

  ```yaml
  pipelines:
    metrics/1:
      receivers: [otlp]        # ← 同一个 receiver
      exporters: [prometheus]
    metrics/2:
      receivers: [otlp]        # ← 被两条管道共用 → 扇出!
      exporters: [datadog]
    traces:
      receivers: [otlp]
      exporters: [jaeger, spanmetrics]   # ← connector,输出又流进 metrics 管道
  ```

  **一个 receiver 喂多条管道（扇出）、一个 connector 把 traces 接到 metrics（跨管道）——这已经是一张图了，不是几条独立的线。** 用图来建模，这些场景就都是自然的；用链表建模，你得为每个场景打补丁。
</Callout>

## 精妙处：反着启动，正着停止

源码里这两段注释，是本章最值得学的东西（`graph.go:428` 和 `:471`，真实注释）：

```go
// Start in reverse topological order so that downstream components
// are started before upstream components. This ensures that each
// component's consumer is ready to consume.
for _, node := range slices.Backward(nodes) { ... }
```

```go
// Stop in topological order so that upstream components
// are stopped before downstream components. This ensures
// that each component has a chance to drain to its consumer
// before the consumer is stopped.
for i := range nodes { ... }
```

<Callout type="info">
  **启动：**从下游往上游**（反拓扑序）。停止：**从上游往下游**（拓扑序）。**

  ```
  管道:  receiver → processor → exporter

  启动:  exporter → processor → receiver     ← 反着来!
  停止:  receiver → processor → exporter     ← 正着来
  ```

  **为什么必须这样？两条注释各给了理由：**

  - **启动反着来** —— “**ensures that each component's consumer is ready to consume**”。如果先启动 receiver，它**立刻开始收数据**，然后往下游推——**但下游的 exporter 还没启动**！数据无处可去，要么丢、要么报错。**先把下游准备好，再打开水龙头。**
  - **停止正着来** —— “**each component has a chance to drain to its consumer before the consumer is stopped**”。先停 receiver（**不再进新数据**），然后管道里的存量数据可以**排空（drain）**到下游；如果先停 exporter，那些还在 processor 里排队的数据就**没地方去了 → 丢数据**。

  **这是“优雅启停”的教科书实现**，而且它的正确性**完全依赖拓扑排序**——只有知道了组件的依赖顺序，才能反着启、正着停。**这就是“为什么要用图”的最硬的理由**：不只是为了表达扇出，更是为了**能算出正确的启停顺序**。

  **这个模式可以直接偷到别处用：任何“数据流经多个阶段”的系统（消息管道、ETL、流处理），启停顺序都该是这个规律。** 很多人写这类系统时随手按配置顺序启停，然后在关闭时莫名其妙丢数据——**根因就是这个**。
</Callout>

## 启动的完整流程

[官方文档](https://daiw.org/manual/observability/reading-the-source)给的启动图，翻译成中文：

```mermaid
flowchart TD
    A["command.NewCommand"] --> B["NewCollector"]
    B --> C["Collector.Run<br/>(阻塞直到关闭)"]
    C --> D["setupConfigurationComponents"]
    D --> E["getConfMap<br/>(读配置)"]
    D --> F["Service.New<br/>初始化 telemetry,再初始化管道"]
    F --> G["initExtensionsAndPipeline<br/>建 extension,再建管道图"]
    G --> H["Graph.Build<br/>① createNodes ② createEdges ③ buildComponents"]
    D --> I["Service.Start<br/>1.启动所有 extension<br/>2.通知 extension 配置<br/>3.启动所有管道<br/>4.通知 extension 就绪"]
    I --> J["Graph.StartAll<br/>按反拓扑序启动每个组件"]
    style J fill:#22c55e,color:#fff
```

<Callout type="info">
  **注意 `Service.Start` 里那个四步顺序**——[extension 先启动、且在管道就绪后还会被通知一次](https://daiw.org/manual/observability/collector-architecture)。这是为了让**健康检查这类 extension** 能正确反映“管道到底准备好了没”：如果健康检查在管道启动前就返回 OK，[k8s 会过早把流量打进来](https://daiw.org/manual/observability/otel-overview)。**“先起辅助设施 → 起数据面 → 再告诉辅助设施'可以了'”** ——这个三段式在很多服务框架里都能见到。
</Callout>

## Factory 模式：怎么加一个新组件

<Callout type="info">
  **[官方文档点名](https://daiw.org/manual/observability/reading-the-source)：“Each component type contains a `Factory` interface along with its corresponding `NewFactory` function.”**

  每类组件都有工厂，新组件通过 `NewFactory` **注册**自己：能处理哪些[信号类型](https://daiw.org/manual/observability/three-pillars)（traces/metrics/logs）、默认配置是什么、怎么创建实例。

  文档里还提了个关键点：“the Collector uses this interface to give receivers a handle to a **`nextConsumer`** - which represents where the receiver will send its data next in its telemetry pipeline.”

  **`nextConsumer` 是理解整个数据流的钥匙**：每个组件**不知道下游是谁**，它只拿到一个 [`consumer` 接口](https://daiw.org/manual/observability/consumer-backpressure)，往里塞数据就完事。**下游是什么、有几个，由图在构建时注入。** [下一章讲 pdata，再下一章讲这个 consumer 接口](https://daiw.org/manual/observability/consumer-backpressure)。

  **这是[依赖注入 + 面向接口](https://daiw.org/manual/observability/reading-the-source)的标准实践**，也是为什么 Collector 能有[几百个社区组件](https://daiw.org/manual/observability/custom-components)而核心保持稳定——**核心只认接口，不认具体实现**。这和[第 1 部分 Prometheus 的 `storage.Appender`](https://daiw.org/manual/observability/prometheus-architecture)、[`Discoverer` 接口](https://daiw.org/manual/observability/service-discovery)是完全一样的思路。
</Callout>

## 小结与下一章

Collector 有**五类组件**：[receiver / processor / exporter](https://daiw.org/manual/observability/otel-overview)（数据面）、**connector**（[同时是一条管道的 exporter 和另一条的 receiver，如从 traces 生成 RED 指标——这正是“管道是图”的证据](https://daiw.org/manual/observability/red-method)）、**extension**（[不碰数据的横切能力，独立成类以保持数据面纯粹](https://daiw.org/manual/observability/otel-overview)）。**管道是一张 DAG**，`Graph.Build` 三步：[`createNodes` → `createEdges` → `buildComponents`（拓扑排序 + 反序实例化）](https://daiw.org/manual/observability/collector-architecture)；用图建模是因为[一个 receiver 可扇出到多条管道、connector 可跨管道](https://daiw.org/manual/observability/collector-architecture)。**最精妙的是启停顺序**：**反拓扑序启动**（[先备好下游的 consumer，再开水龙头，否则数据无处可去](https://daiw.org/manual/observability/collector-architecture)）、**拓扑序停止**（[先断新数据，让存量排空到下游，否则丢数据](https://daiw.org/manual/observability/collector-architecture)）——**这才是“必须用图”最硬的理由**（要算启停顺序），而且这个模式可直接偷到任何多阶段数据流系统。**Factory 模式 + `nextConsumer`**：[组件不知道下游是谁，只往注入的 consumer 接口塞数据](https://daiw.org/manual/observability/consumer-backpressure)——[和 Prometheus 的 `storage.Appender`/`Discoverer` 同一思路](https://daiw.org/manual/observability/prometheus-architecture)，这就是它能有几百个社区组件而核心稳定的原因。

数据在管道里流动时，长什么样？下一章看 pdata。👉 [pdata 数据模型](https://daiw.org/manual/observability/pdata)。

## 参考资料

- [1] OTel Collector 源码 `service/internal/graph/graph.go`（`Build:89`、`createNodes:99`、`createEdges:266`、`buildComponents:295`、`StartAll:428`、`ShutdownAll:471` 及其注释）。
- [2] OTel Collector 源码 `docs/internal-architecture.md`（启动流程图、Factory 说明）、`otelcol/collector.go`。
- [3] 关联[接口即边界](https://daiw.org/manual/observability/reading-the-source)、[Prometheus 的对应设计](https://daiw.org/manual/observability/prometheus-architecture)。
