Golang OpenTelemetry 源码专题【左扬精讲】—— 开篇总览:从千万级遥测数据进入生产级 Collector

Golang OpenTelemetry 源码专题【左扬精讲】—— 开篇总览:从千万级遥测数据进入生产级 Collector

这是一篇 Golang OpenTelemetry 源码专题的开篇。

我们以当前本地仓库 go.opentelemetry.io/collector 为唯一源码基线,从一个真实的生产问题出发:当业务每天产生千万级乃至更高量级的 Span、Metric、LogRecord 时,遥测数据如何被接收、处理、分流和导出?Collector 在内存压力、下游故障、批量发送和多后端分流中究竟承担什么职责?

“千万级”在本文中只描述生产规模背景,不代表本仓库附带了某个固定吞吐结论。

生产容量必须由数据平均编码大小、峰值速率、处理器复杂度、队列长度、后端延迟和资源限制共同决定。

README.md                                      ← Collector 官方职责与目标
service/service.go                             ← Service 组装入口与组件 Builder
service/internal/graph/graph.go                ← Pipeline 图构建、连边与组件实例化
processor/batchprocessor/config.go             ← Batch Processor 配置与校验
processor/batchprocessor/batch_processor.go    ← 按大小或超时成批发送
processor/memorylimiterprocessor/memorylimiter.go ← 内存压力下拒绝遥测数据

GolangOpenTelemetryCollectorOTLP生产实践源码专题

学习重点

  • 理解 Collector 与业务 SDK、可观测后端之间的边界。
  • 从源码确认 Receiver、Processor、Exporter、Connector 与 Extension 的正式名称和职责。
  • 理解千万级数据量不是一个配置值,而是一组可测量的容量约束。
  • 掌握 Batch Processor 与 Memory Limiter Processor 的失败语义。
  • 建立后续阅读 Collector 源码的稳定路径。

一、为什么开篇先讲 Collector,而不是先写业务埋点

What — Collector 是什么?

仓库 README.md 第 50-64 行把 Collector 定义为一个厂商中立的遥测数据接收、处理与导出实现,并列出 Usable、Performant、Observable、Extensible、Unified 五项目标。这里的核心不是“生成 TraceID”,而是建立一条可配置的数据处理路径。

业务服务中的 OpenTelemetry SDK 负责创建和传播遥测上下文、生成遥测数据,并通过 Exporter 或协议把数据发送出去。Collector 运行在业务进程之外,它接住多个工作负载发来的数据,执行处理器链,再写向一个或多个后端。把两者混在一起,会错误地把 Collector 当成 Go SDK,也会把后端存储能力误写成 Collector 自身能力。

Why — 为什么千万级项目需要独立 Collector 层?

生产系统的遥测出口会变化:链路后端可能迁移,指标后端可能增加,导出端也可能短暂不可用。Collector 把协议接入、批处理、资源保护和后端路由从业务发布周期中拆开。业务服务只需遵循已确认的遥测协议与属性规范。

没有独立 Collector 层会发生什么?

  • 业务实例必须各自维护后端连接、重试与认证配置,运维面扩大。
  • 切换导出后端可能要求重新构建和发布全部业务服务。
  • 高峰流量与后端故障直接作用于业务进程,遥测链路和业务链路争夺资源。
How — Service 从正式组件类型开始组装

 service/service.go 第 61-82 行。Settings 明确保存 Receiver、Processor、Exporter、Connector 与 Extension 的配置及 Factory。它说明 Collector 不是写死的一条管线,而是由组件 Factory 和配置共同构建。

ReceiversConfigs   map[component.ID]component.Config
ReceiversFactories map[component.Type]receiver.Factory

ProcessorsConfigs   map[component.ID]component.Config
ProcessorsFactories map[component.Type]processor.Factory

ExportersConfigs   map[component.ID]component.Config
ExportersFactories map[component.Type]exporter.Factory

ConnectorsConfigs   map[component.ID]component.Config
ConnectorsFactories map[component.Type]connector.Factory

源码视角的结论是:先确认数据属于哪一种 Signal,再确认每条 Pipeline 中启用了哪些组件实例;不要先画一张脱离配置和 Factory 的“平台大图”。

本节结论:SDK 负责产生和传播遥测,Collector 负责接收、处理与导出,后端负责持久化、查询和展示。三者边界必须先分清。

二、千万级生产项目,第一步不是调参数,而是定义数据单位

What — “千万级”究竟指什么?

它可能表示每日 Span 数、Metric DataPoint 数、LogRecord 数,也可能只是请求数。四者不能互换。一次请求可以产生多个 Span;一个 Metric 可以携带多个 DataPoint;一条业务操作也可能输出多条 LogRecord。本文只把它作为规模背景,不把它换算成未经实测的吞吐。

Signal源码中的计数方法容量评估至少要记录
Traces SpanCount() 峰值 spans/s、平均 OTLP 编码字节、属性规模
Metrics DataPointCount() 峰值 datapoints/s、时间序列基数、直方图规模
Logs LogRecordCount() 峰值 records/s、Body 大小、属性规模
Profiles SampleCount() 峰值 samples/s、采样类型、有效载荷大小

Why — 为什么必须区分计数单位?

Batch Processor 的批大小按具体 Signal 的项目数工作,Memory Limiter Processor 的观测统计也按具体 Signal 计数。用“请求量”直接替代 Span、DataPoint 或 LogRecord 数,会使批量阈值、内存预算和下游写入估算失去依据。

没有统一数据口径会发生什么?

  • 容量报告中的“每秒数据量”无法对应 Collector 内部计数。
  • 测试流量和生产流量属性规模不同,只有条数相同但内存占用完全不同。
  • 扩容后仍无法判断瓶颈在接收、处理、导出还是后端。
How — Memory Limiter 按 Signal 使用真实计数

 processor/memorylimiterprocessor/memorylimiter.go 第 52-64 行:Trace 处理路径先调用 SpanCount(),再根据 MustRefuse() 决定接受或拒绝。Metrics、Logs、Profiles 在第 67-109 行采用各自的计数方法。

func (p *memoryLimiterProcessor) processTraces(ctx context.Context, td ptrace.Traces) (ptrace.Traces, error) {
	numSpans := td.SpanCount()
	if p.memlimiter.MustRefuse() {
		p.obsrep.refused(ctx, numSpans, pipeline.SignalTraces)
		return td, memorylimiter.ErrDataRefused
	}

	p.obsrep.accepted(ctx, numSpans, pipeline.SignalTraces)
	return td, nil
}

这段源码直接给出了生产报表的最低要求:Signal、项目数和字节数必须分开记录。

本节结论:容量工程从单位开始。先测 spans/s、datapoints/s、records/s、samples/s 与字节率,再讨论节点数和配置。

三、一条 Pipeline 在源码中如何成立

What — Pipeline 是什么?

Pipeline 是某一种 Signal 从 Receiver 进入、按顺序通过 Processor、再由 Exporter 输出的处理路径。Connector 可以连接不同 Pipeline。当前源码使用有向图表达组件实例和数据流,不是靠字符串顺序临时调用。

Receiver(s)
    │
    ▼
Capabilities Node
    │
    ▼
Processor 1 → Processor 2 → ...
    │
    ▼
Fan-out Node
    ├──────────────► Exporter A
    └──────────────► Exporter B

Why — 为什么要先构图、校验,再实例化?

Pipeline 可能共享 Receiver 或 Exporter,也可能通过 Connector 跨 Signal 连接。先构图可以统一检查 Connector 的输入输出 Signal 是否受支持,并通过拓扑排序发现环。若配置图不合法,组件不应进入正常运行状态。

没有图校验会发生什么?

  • Connector 作为 Exporter 使用,却没有对应受支持的 Receiver Pipeline。
  • 处理路径形成环,数据流无法建立确定的启动与消费顺序。
  • 多 Exporter 分流逻辑散落到组件内部,扩展和生命周期管理变得不一致。
How — Build、createEdges 与 buildComponents

service/internal/graph/graph.go 第 74-95 行。Build 依次创建 Pipeline 节点容器、创建节点、连边、实例化组件。

func Build(ctx context.Context, set Settings) (*Graph, error) {
	pipelines := &Graph{
		componentGraph: simple.NewDirectedGraph(),
		pipelines:      make(map[pipeline.ID]*pipelineNodes, len(set.PipelineConfigs)),
		instanceIDs:    make(map[int64]*componentstatus.InstanceID),
		telemetry:      set.Telemetry,
	}
	// ...
	if err := pipelines.createNodes(set); err != nil {
		return nil, err
	}
	pipelines.createEdges()
	err := pipelines.buildComponents(ctx, set)
	return pipelines, err
}

第 265-289 行的 createEdges 把 Receiver 连到能力节点,按配置顺序串联 Processor,并且无论 Exporter 数量是多少都插入 fan-out 节点。第 295-372 行先拓扑排序,再逆序构建组件,使每个上游在构建时能拿到已经建立的下游 Consumer。

源码视角总结:三个固定动作

  • 创建节点:组件实例进入图。
  • 创建边:配置顺序变成数据流。
  • 逆拓扑构建:先准备下游 Consumer,再连接上游。

本节结论:Collector Pipeline 的核心事实可以从图构建源码直接读出:Receiver → Processor 链 → fan-out → Exporter。

四、Batch Processor:把高频小请求变成受控批次

What — Batch Processor 做什么?

它接收遥测数据,放入待发送批次,并在批大小达到 send_batch_size 或自上次发送后经过 timeout 时向下游发送。send_batch_max_size 可限制单次发送的最大项目数。

Why — 为什么生产管线需要批处理?

高频小请求会增加导出调用次数和协议固定开销;无限制地等待大批次又会增加端到端延迟和进程内暂存量。Batch Processor 用大小触发与时间触发同时约束聚合行为。

没有批处理会发生什么?

  • 每个小批数据都可能触发下游导出,连接与序列化开销增多。
  • 后端收到大量小请求,吞吐模式与后端批量写入能力不匹配。

只有批大小而没有超时会发生什么?

低流量 Pipeline 长时间凑不满批次,遥测数据的导出延迟没有上界。

How — 配置校验与发送触发

 processor/batchprocessor/config.go 第 15-46 行定义四类核心配置。第 53-69 行要求最大批大小不能小于触发批大小、元数据键不允许忽略大小写后重复、超时不能为负数。

func (cfg *Config) Validate() error {
	if cfg.SendBatchMaxSize > 0 && cfg.SendBatchMaxSize < cfg.SendBatchSize {
		return errors.New("send_batch_max_size must be greater or equal to send_batch_size")
	}
	// ...
	if cfg.Timeout < 0 {
		return errors.New("timeout must be greater or equal to 0")
	}
	return nil
}

processor/batchprocessor/batch_processor.go 第 38-40 行明确两种发送条件;第 113-140 行把配置转换为运行时字段,并根据 metadata_keys 选择单分片或多分片 Batcher。元数据分片不是免费的:不同键值组合会创建不同批处理实例,因此源码提供 metadata_cardinality_limit

生产注意:本文不提供通用的批大小。正确做法是用真实 OTLP 载荷和真实后端测量吞吐、导出延迟、RSS、失败率与请求大小,再确定配置。

本节结论:Batch Processor 同时控制发送频率、等待时间和最大批次;元数据分片还必须控制基数。

五、Memory Limiter:内存压力下明确拒绝,而不是假装成功

What — Memory Limiter Processor 做什么?

它查询内部 Memory Limiter 的拒绝状态。达到拒绝条件时,不把当前数据继续交给下游,而是记录拒绝计数并返回 memorylimiter.ErrDataRefused

Why — 为什么要显式返回错误?

内存压力是一种流量控制信号。显式错误让具备重试能力的上游感知拒绝,而不是让 Collector 在内存持续增长时继续接纳数据。是否重试、重试多久和最终是否丢弃,则取决于上游协议、发送方和整条链路配置,不能仅凭这一段处理器源码承诺“绝不丢数据”。

没有内存限制会发生什么?

  • 下游变慢时,进程内待处理数据可能持续累积。
  • Collector 的垃圾回收压力和常驻内存可能上升,最终由运行环境终止进程。

拒绝后仍返回成功会发生什么?

上游会把未处理数据视为已接受,失去重试机会,形成静默数据缺口。

How — 四种 Signal 使用同一种拒绝语义

processor/memorylimiterprocessor/memorylimiter.go 第 52-109 行。Traces、Metrics、Logs、Profiles 均先计算项目数,再检查 MustRefuse();拒绝时记录对应 Signal 的数量并返回同一个 ErrDataRefused

numRecords := ld.LogRecordCount()
if p.memlimiter.MustRefuse() {
	p.obsrep.refused(ctx, numRecords, pipeline.SignalLogs)
	return ld, memorylimiter.ErrDataRefused
}

p.obsrep.accepted(ctx, numRecords, pipeline.SignalLogs)
return ld, nil

这里还需要读懂一个细节:源码注释明确说明,即使下一个 Consumer 返回错误,这个处理器也把数据记录为自身已接受。该指标表达的是“Memory Limiter 是否拒绝”,不是整条 Pipeline 是否最终导出成功。

本节结论:Memory Limiter 的接受量不是导出成功量;生产监控必须同时观察接收、处理器拒绝、导出失败和后端写入结果。

六、千万级生产落地:用容量模型和失败模型替代拍脑袋配置

What — 生产级设计需要回答哪些问题?

至少要回答峰值字节率、Signal 项目率、单项平均与高分位大小、下游可用性、允许的数据延迟、可接受的数据损失、Collector 内存上限和横向扩展方式。缺一项,都无法严谨地把“千万级”转换成配置。

6.1 容量基线

  • 入口:分别记录 Receiver 接收的 spans/s、datapoints/s、records/s、samples/s 与 bytes/s。
  • 处理:记录每个 Processor 的拒绝、失败、处理延迟和基数变化。
  • 出口:记录 Exporter 发送量、失败量、重试状态、队列状态和请求耗时。
  • 进程:记录 RSS、Go heap、GC、CPU、goroutine 和进程重启。
  • 后端:记录写入成功率、限流、超时、查询与存储约束。

6.2 峰值而不是日均

每日一千万条数据平均到全天并不能描述生产压力。发布、故障风暴、批任务、日志级别变化和客户端重连都可能形成集中峰值。容量测试应回放真实的大小分布、属性分布和峰值波形;仅用固定大小的空 Span 做压测,不能代表生产载荷。

6.3 失败语义

故障位置必须确认的事实不能直接承诺
Receiver 前 客户端超时、重试、协议返回码 请求到达 Collector
Processor 永久错误与可重试错误、拒绝计数 所有数据继续下游
Exporter 队列、重试、超时、后端响应 发送调用等于后端持久化
进程退出 关闭顺序、尚未导出的内存数据 重启过程零损失

Why — 为什么先定义失败预算?

可观测数据管线本身也会失败。若项目只定义吞吐而不定义允许延迟和数据损失,就无法判断应采用同步背压、内存队列、持久化队列还是客户端重试,也无法判断后端故障期间要保留多久数据。

没有失败预算会发生什么?

  • 所有组件都配置更大缓存,故障时只是更晚耗尽内存或磁盘。
  • 团队无法区分“主动拒绝”“导出失败”和“最终丢失”。
  • 恢复后集中重放形成第二次流量高峰。
How — 推荐的验证顺序
  1. 采集一段脱敏后的真实 OTLP 数据分布,统计 Signal 项目数与编码字节。
  2. 以正常峰值、突发峰值和后端故障三类场景执行压测。
  3. 固定 Collector 资源上限,观察 Memory Limiter 拒绝与进程内存。
  4. 逐步调整 Batch Processor,比较请求数、批次大小、延迟和 RSS。
  5. 模拟后端限流、超时、连接中断和 Collector 重启,核对发送方重试与最终数据缺口。
  6. 依据实测结果计算单实例安全容量,并保留故障与滚动升级余量。

避坑提醒:不要把日均量除以秒数后直接作为压测速率;不要只测成功路径;不要把 Collector 接受量当作后端持久化成功量。

本节结论:千万级生产落地不是选择一个神奇参数,而是建立单位、峰值、资源、背压、重试与最终写入的一致证据链。

七、FAQ:开篇必须回答的 20 个问题

阅读说明:以下回答只使用当前仓库已验证的名称和行为;涉及具体容量的答案均要求以项目实测为准。

Q1. OpenTelemetry Go SDK 和 Collector 是同一个项目吗?

不是。SDK 在应用内生成和导出遥测;本仓库的 Collector 负责接收、处理和导出遥测数据。

Q2. Collector 自己存储 Trace 吗?

本文核验的核心职责不包含长期存储。Collector 通过 Exporter 把数据交给后端;持久化和查询能力由所选后端决定。

Q3. 一条 Pipeline 必须有哪些组件?

核心数据流由 Receiver、可选 Processor 链和 Exporter 组成。源码还插入能力节点和 fan-out 节点;Connector 用于连接 Pipeline。

Q4. Processor 的顺序重要吗?

重要。createEdges 按配置顺序串联 Processor,前一个处理结果成为后一个的输入。

Q5. 多个 Exporter 如何接收同一条 Pipeline 的数据?

通过 fan-out 节点。源码即使只有一个 Exporter 也会创建该节点;多个下游由对应的 fan-out consumer 组织。

Q6. “千万级”能直接推出需要几台 Collector 吗?

不能。必须知道 Signal 类型、峰值率、编码大小、处理器成本、后端延迟和资源上限。

Q7. 日均一千万除以 86400 就是压测目标吗?

不是。该值只是日均速率,不能覆盖集中上报、故障重试和业务峰值。

Q8. Batch Processor 只按大小发送吗?

不是。源码明确支持批大小触发和超时触发。

Q9. send_batch_max_size 可以小于 send_batch_size 吗?

不可以。配置校验会返回错误;非零最大值必须大于或等于触发值。

Q10. Batch Processor 的超时可以是负数吗?

不可以。Validate() 明确拒绝负超时。

Q11. metadata_keys 为什么需要基数上限?

不同键值组合会形成不同 Batcher。不限制组合数会增加分片及其运行时资源。

Q12. Memory Limiter 达到限制后会静默丢弃吗?

该处理器会返回明确错误。它记录拒绝量并返回 ErrDataRefused;上游能否重试需结合完整链路确认。

Q13. Memory Limiter 的 accepted 指标等于导出成功吗?

不等于。源码注释说明,即使下游 Consumer 返回错误,该处理器仍记录自身已接受。

Q14. Traces、Metrics、Logs 的容量单位相同吗?

不同。源码分别使用 SpanCount、DataPointCount 和 LogRecordCount。

Q15. Collector 支持 Profiles 吗?

当前源码路径包含 Profiles Signal。图构建与 Memory Limiter 均有 Profiles 分支;具体组件可用性仍要以所构建发行版为准。

Q16. 仓库中的 go.mod 能构建官方二进制吗?

文件注释明确说不能据此认定。go.mod 第 3-9 行指出它不用于构建任何官方二进制,并给出发行仓库位置。

Q17. 当前仓库声明哪个 Go 版本?

已验证为 Go 1.25.0。这是当前本地 go.mod 第 11 行的声明,不延伸推断其他发行版。

Q18. Collector 配置成功解析就代表 Pipeline 一定合法运行吗?

不能这样推断。图构建还会校验 Connector 支持关系、拓扑环和组件实例化错误。

Q19. 生产压测只看 CPU 可以吗?

不可以。至少要同时观察吞吐、延迟、RSS、Go heap、GC、拒绝、导出失败和后端状态。

Q20. 本专题后续阅读主线是什么?

沿数据生命周期推进。从配置加载和 Service 启动进入 Receiver,再到 pdata、Processor、Exporter、队列、重试、内部遥测和生产调优。

全篇总纲:生产级 Collector 的关键不是术语堆叠,而是四条可验证主线:组件如何组装、数据如何流动、压力如何传播、失败如何被观察。任何具体容量结论都必须建立在真实数据和故障测试上。

八、Roadmap:Golang OpenTelemetry 源码专题后续预告

下一篇:从 Collector 启动入口进入 Service.New,完整追踪配置、Builder、Graph、Extension 与 Pipeline 生命周期。

  1. Collector 配置加载与组件 Factory。
  2. Service 启动、Graph 构建和生命周期顺序。
  3. OTLP Receiver 的 gRPC/HTTP 接收链路。
  4. pdata 数据模型与零拷贝约束。
  5. Processor 消费接口、错误分类和数据修改能力。
  6. Batch Processor 的分片、定时器与发送路径。
  7. Memory Limiter 的内存检查与拒绝传播。
  8. Exporter Helper 的队列、重试、超时与持久化能力。
  9. Collector 内部遥测和生产故障定位。
  10. 基于真实 OTLP 回放的容量测试方法。
posted @ 2025-10-07 22:51  左扬  阅读(129)  评论(0)    收藏  举报