Arrow+Go颠覆ETL:重新定义数据工程

几十年来,数据工程一直建立在从未为现代硬件设计的基础之上。传统的基于行的数据库、序列化数据格式和网络密集型工作流程对本应极快的数据移动和转换造成了人为的瓶颈。

即使在今天,大多数 ETL 管道仍然依赖于:

  • 基于行的数据存储(PostgreSQL、MySQL 等),即使只需要几列也会强制 CPU 加载整行。
  • 序列化繁重的格式(JSON、Avro、协议缓冲区)需要在每一步进行编码和解码,增加了不必要的 CPU 开销。
  • I/O 瓶颈在于数据在存储层之间不断复制而不是有效地传输。
大多数工程师对这些低效率习以为常,甚至不认为这是问题。但事实上,它们确实存在。它们以大多数人没有意识到的方式压垮了性能。

我主要从事Go和Apache Arrow 的工作,过去几年来,我坚信我们即将迎来数据工程的根本性转变。旧模式正在瓦解,Arrow 正在引领零拷贝、列式、高性能数据移动的发展。

本文不仅仅是一个理论讨论,它还介绍了如何在 Go 中实现 Arrow,以突破可能性的极限。让我们开始吧。

为什么选择 Apache Arrow?
Apache Arrow 不仅仅是“另一种数据格式”。它从根本上重新思考了数据在内存中的表示方式以及在系统之间传输的方式。

Arrow 解决了三个重大问题:

  1. 零拷贝共享——数据不需要在系统之间序列化/反序列化——它可以在进程、语言之间,甚至在网络之间在内存中共享。
  2. 列式存储- Arrow 不会处理行,而是将数据组织成列,这些列对 CPU 缓存友好,并且针对矢量化执行进行了 SIMD 优化。
  3. 互操作性——Arrow 是一个通用数据层,可实现系统之间的无缝移动(例如,PostgreSQL → Arrow → DuckDB → BigQuery),而无需不必要的转换。
简而言之,Arrow消除了序列化开销并最大限度地提高了 CPU 效率,这两者是数据工程中最大的性能杀手。

Go 和 Arrow:高性能数据工程的天然选择
大多数人将 Arrow 与 Python 和 C++ 联系在一起,但 Go 是一种被严重低估的高性能数据工作负载语言。与 Python 不同,Go 提供:

  • 高效并发(goroutines 和 channels)
  • 低级内存控制,无需 C++ 的复杂性
  • 与 Arrow 的柱状格式直接集成
然而,Arrow 的 Go 生态系统仍在不断发展。这意味着我构建的很多内容都涉及针对我的特定用例优化 Arrow 的 Go 实现。

让我们来谈谈我在工作中看到的实际性能提升。

案例研究:Go 中系统之间的流式 Arrow 记录
数据移动的隐性成本
在数据工程中,原始计算能力通常不是瓶颈——数据移动才是瓶颈。
每次在系统之间传输数据时,数据都会经历不必要的转换、复制和序列化开销,从而大大降低处理速度。数据移动的标准方法遵循以下模式:

  1. 从数据库中提取→通常基于行的存储,如 PostgreSQL 或 MySQL。
  2. 序列化→将数据转换为 JSON、Avro 或 Parquet 进行传输。
  3. 通过网络发送→通常通过 REST API、Kafka 或云存储。
  4. 在另一端进行反序列化→解码回结构化格式。
  5. 加载到处理引擎→BigQuery、DuckDB、Spark 等。
每个步骤都会引入可避免的延迟:
  • 序列化需要耗费 CPU 周期(JSON、Avro、Parquet 都需要解析)。
  • 处理非二进制格式时,网络传输缓慢且臃肿。
  • 反序列化在数据有用之前增加了另一个不必要的处理步骤。
  • 数据不断地在内存缓冲区和磁盘之间复制,而不是在现场有效地处理。
这就是为什么大多数 ETL 管道比它们应有的速度慢的原因——我们不断地重新排列字节,而不是直接处理它们。

不同的方法:Streaming Arrow RecordBatches
我没有遵循传统的提取-序列化-发送-反序列化-加载循环,而是使用Apache Arrow、Go、gRPC 和 Cap'n Proto构建了零拷贝流式传输管道。

这完全消除了序列化开销,实现了数据库和处理引擎之间的直接、列式、高吞吐量数据移动。

工作原理如下:

1.使用 Apache Arrow(PostgreSQL 的 ADBC 驱动程序)查询 PostgreSQL
我没有从 PostgreSQL检索行,而是直接从数据库中提取Arrow RecordBatches 。

  • 传统方法:运行 SQL 查询,返回行,将其序列化为 JSON/Parquet。
  • 我的方法:使用PostgreSQL 的 ADBC 驱动程序直接以 Arrow 格式进行查询,避免转换为基于行的格式。
由于 Arrow以列式格式本地存储数据,因此提取过程已经针对缓存局部性和矢量化执行进行了优化。

2. 使用 Cap'n Proto 通过 gRPC 流式传输 Arrow RecordBatches
一旦检索到 Arrow RecordBatches,我就会使用 gRPC 通过网络传输它们。

  • 为什么不使用协议缓冲区? → Cap'n Proto速度更快,因为它支持零拷贝反序列化。与仍然需要解析的 Protobuf 不同,Cap'n Proto 允许直接对数据进行内存映射。
  • 为什么选择 gRPC? → 它提供双向流和高效的二进制传输,使其非常适合 Arrow RecordBatches。

这在实践中意味着:
序列化性能(按格式)

  • JSON → 序列化和反序列化时间长,内存复制多。
  • Avro →中等序列化和反序列化时间,一些内存复制。
  • Parquet →中等序列化和反序列化时间,一些内存复制。
  • 协议缓冲区→低序列化时间,中等反序列化(需要解析),最少内存复制。
  • Cap'n Proto → 接近零序列化、零拷贝反序列化,无额外内存拷贝。
  • Apache Arrow →无需序列化(零拷贝内存共享,直接内存处理)。
这意味着数据传输时 CPU 使用率最低,并且没有不必要的内存复制。这就是为什么Arrow + Cap'n Proto共同成为高性能数据移动的变革者。Arrow 将数据保存在高效的内存格式中,而 Cap'n Proto允许它在零拷贝反序列化的情况下跨系统传输。

3. 直接在 DuckDB中使用 Arrow 记录
一旦 Arrow RecordBatches 到达,我不会将它们写入 Parquet 并读回,而是将它们作为内存流直接传递给 DuckDB。

  • 没有磁盘 I/O → DuckDB直接从内存读取 Arrow RecordBatches 。
  • 无需格式转换→数据保持从 PostgreSQL → gRPC → DuckDB 的列格式。
  • 查询执行是即时的→由于 DuckDB 是为内存分析而设计的,查询引擎可以在 Arrow 数据结构上进行操作,而无需将其加载到中间的基于行的格式中。
这完全消除了传统的“写入 Parquet,从 Parquet 读取”的循环,将处理延迟降低了几个数量级。

性能提升
结果不言而喻:
传统方法与箭流方法
PostgreSQL 提取

  • 传统:基于行,速度慢
  • ✅箭头流:柱状,速度快 5 倍
序列化开销
  • 传统:高级(JSON、Avro、Parquet)
  • ✅ Arrow Streaming: 零(Arrow 内存中)
网络传输
  • 传统:臃肿、缓慢
  • ✅ Arrow Streaming: 压缩,二进制
提取至 DuckDB
  • 传统:写入 Parquet → 读取 Parquet
  • ✅ Arrow Streaming: 直接内存执行
端到端延迟
  • 传统: 秒
  • ✅ Arrow Streaming: 亚秒级处理

关键要点:

  1. ✅从 PostgreSQL 中提取数据速度提高 5 倍
  2. ✅零序列化开销
  3. ✅无论数据集大小,均可在亚秒内将数据导入 DuckDB
  4. ✅流式架构消除了冗余磁盘 I/O
通过消除不必要的序列化、I/O 和内存复制,Arrow 流式传输消除了数据管道每个阶段的瓶颈。

传统 ETL 的终结?
我相信我们正处于传统 ETL 管道的末日。Arrow 的内存列式格式与 Go 的并发性和效率相结合,从根本上改变了实时数据工程的游戏规则。
接下来会发生什么?

我正在积极致力于:

  • 使用 Go 构建一个名为 ArrowArc 的高性能、列优先数据移动框架。
  • 在数据库和处理引擎之间实现直接 Arrow 流
  • 进一步优化 Go 的 Arrow 实现以适应实际数据工作负载
这仅仅是一个开始。数据工程的未来是列式、零拷贝和实时的。

如果您正在处理大数据、流媒体或高性能分析,我强烈建议您探索Go + Arrow。

大多数数据工程师没有意识到他们放弃了多少性能。我们已经习惯于接受缓慢的管道、序列化开销和低效的数据移动,这是很正常的。
但事实并非如此。

工具已存在,模式正在转变。

现在是时候重新思考什么是可能的了。