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

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

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

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

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

receiver → processor → exporter

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

五类组件

先把角色认全(对应源码的五个顶层目录):

组件目录职责
receiverreceiver/数据从哪进来(OTLP/Prometheus 抓取/Jaeger/文件…)
processorprocessor/中间怎么加工(批量/过滤/改属性/采样/限流)
exporterexporter/数据往哪出去(OTLP/Prometheus remote write/各云厂商)
connectorconnector/把一条管道的出口接到另一条的入口(如“从 traces 生成指标”)
extensionextension/不碰数据的辅助能力(健康检查、认证、pprof)

前三个是“数据面”,后两个值得单说:

  • connector —— 它同时是某条管道的 exporter 和另一条管道的 receiver。经典用途:spanmetrics connector 从 traces 生成 RED 指标(不用你在应用里再埋一遍!)。这正是“管道是图而不是线”的直接证据——connector 让管道之间连起来,形成了图。
  • extension —— 不参与数据流,提供“周边能力”。健康检查端点、认证器、性能分析。把它们独立成一类,是为了让数据面组件保持纯粹——receiver/processor/exporter 只管数据,不管这些横切关注点。

图的构建:三步

官方架构文档(docs/internal-architecture.md)直接给了流程,源码在 service/internal/graph/graph.go:89:

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

三步走:

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

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

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(跨管道)——这已经是一张图了,不是几条独立的线。 用图来建模,这些场景就都是自然的;用链表建模,你得为每个场景打补丁。

精妙处:反着启动,正着停止

源码里这两段注释,是本章最值得学的东西(graph.go:428 和 :471,真实注释):

// 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) { ... }
// 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 { ... }

启动:从下游往上游(反拓扑序)。停止:从上游往下游(拓扑序)。

管道:  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、流处理),启停顺序都该是这个规律。 很多人写这类系统时随手按配置顺序启停,然后在关闭时莫名其妙丢数据——根因就是这个。

启动的完整流程

官方文档给的启动图,翻译成中文:

图表加载中…

注意 Service.Start 里那个四步顺序——extension 先启动、且在管道就绪后还会被通知一次。这是为了让健康检查这类 extension 能正确反映“管道到底准备好了没”:如果健康检查在管道启动前就返回 OK,k8s 会过早把流量打进来。“先起辅助设施 → 起数据面 → 再告诉辅助设施'可以了'” ——这个三段式在很多服务框架里都能见到。

Factory 模式:怎么加一个新组件

官方文档点名:“Each component type contains a Factory interface along with its corresponding NewFactory function.”

每类组件都有工厂,新组件通过 NewFactory 注册自己:能处理哪些信号类型(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 接口,往里塞数据就完事。下游是什么、有几个,由图在构建时注入。 下一章讲 pdata,再下一章讲这个 consumer 接口。

这是依赖注入 + 面向接口的标准实践,也是为什么 Collector 能有几百个社区组件而核心保持稳定——核心只认接口,不认具体实现。这和第 1 部分 Prometheus 的 storage.Appender、Discoverer 接口是完全一样的思路。

小结与下一章

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

数据在管道里流动时,长什么样?下一章看 pdata。👉 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] 关联接口即边界、Prometheus 的对应设计。

本页目录