即使在今天,大多数 ETL 管道仍然依赖于:
- 基于行的数据存储(PostgreSQL、MySQL 等),即使只需要几列也会强制 CPU 加载整行。
- 序列化繁重的格式(JSON、Avro、协议缓冲区)需要在每一步进行编码和解码,增加了不必要的 CPU 开销。
- I/O 瓶颈在于数据在存储层之间不断复制而不是有效地传输。
我主要从事Go和Apache Arrow 的工作,过去几年来,我坚信我们即将迎来数据工程的根本性转变。旧模式正在瓦解,Arrow 正在引领零拷贝、列式、高性能数据移动的发展。
本文不仅仅是一个理论讨论,它还介绍了如何在 Go 中实现 Arrow,以突破可能性的极限。让我们开始吧。
为什么选择 Apache Arrow?
Apache Arrow 不仅仅是“另一种数据格式”。它从根本上重新思考了数据在内存中的表示方式以及在系统之间传输的方式。
Arrow 解决了三个重大问题:
- 零拷贝共享——数据不需要在系统之间序列化/反序列化——它可以在进程、语言之间,甚至在网络之间在内存中共享。
- 列式存储- Arrow 不会处理行,而是将数据组织成列,这些列对 CPU 缓存友好,并且针对矢量化执行进行了 SIMD 优化。
- 互操作性——Arrow 是一个通用数据层,可实现系统之间的无缝移动(例如,PostgreSQL → Arrow → DuckDB → BigQuery),而无需不必要的转换。
Go 和 Arrow:高性能数据工程的天然选择
大多数人将 Arrow 与 Python 和 C++ 联系在一起,但 Go 是一种被严重低估的高性能数据工作负载语言。与 Python 不同,Go 提供:
- 高效并发(goroutines 和 channels)
- 低级内存控制,无需 C++ 的复杂性
- 与 Arrow 的柱状格式直接集成
让我们来谈谈我在工作中看到的实际性能提升。
案例研究:Go 中系统之间的流式 Arrow 记录
数据移动的隐性成本
在数据工程中,原始计算能力通常不是瓶颈——数据移动才是瓶颈。
每次在系统之间传输数据时,数据都会经历不必要的转换、复制和序列化开销,从而大大降低处理速度。数据移动的标准方法遵循以下模式:
- 从数据库中提取→通常基于行的存储,如 PostgreSQL 或 MySQL。
- 序列化→将数据转换为 JSON、Avro 或 Parquet 进行传输。
- 通过网络发送→通常通过 REST API、Kafka 或云存储。
- 在另一端进行反序列化→解码回结构化格式。
- 加载到处理引擎→BigQuery、DuckDB、Spark 等。
- 序列化需要耗费 CPU 周期(JSON、Avro、Parquet 都需要解析)。
- 处理非二进制格式时,网络传输缓慢且臃肿。
- 反序列化在数据有用之前增加了另一个不必要的处理步骤。
- 数据不断地在内存缓冲区和磁盘之间复制,而不是在现场有效地处理。
不同的方法: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 格式进行查询,避免转换为基于行的格式。
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 →无需序列化(零拷贝内存共享,直接内存处理)。
3. 直接在 DuckDB中使用 Arrow 记录
一旦 Arrow RecordBatches 到达,我不会将它们写入 Parquet 并读回,而是将它们作为内存流直接传递给 DuckDB。
- 没有磁盘 I/O → DuckDB直接从内存读取 Arrow RecordBatches 。
- 无需格式转换→数据保持从 PostgreSQL → gRPC → DuckDB 的列格式。
- 查询执行是即时的→由于 DuckDB 是为内存分析而设计的,查询引擎可以在 Arrow 数据结构上进行操作,而无需将其加载到中间的基于行的格式中。
性能提升
结果不言而喻:
传统方法与箭流方法
PostgreSQL 提取
- 传统:基于行,速度慢
- ✅箭头流:柱状,速度快 5 倍
- 传统:高级(JSON、Avro、Parquet)
- ✅ Arrow Streaming: 零(Arrow 内存中)
- 传统:臃肿、缓慢
- ✅ Arrow Streaming: 压缩,二进制
- 传统:写入 Parquet → 读取 Parquet
- ✅ Arrow Streaming: 直接内存执行
- 传统: 秒
- ✅ Arrow Streaming: 亚秒级处理
关键要点:
- ✅从 PostgreSQL 中提取数据速度提高 5 倍
- ✅零序列化开销
- ✅无论数据集大小,均可在亚秒内将数据导入 DuckDB
- ✅流式架构消除了冗余磁盘 I/O
传统 ETL 的终结?
我相信我们正处于传统 ETL 管道的末日。Arrow 的内存列式格式与 Go 的并发性和效率相结合,从根本上改变了实时数据工程的游戏规则。
接下来会发生什么?
我正在积极致力于:
- 使用 Go 构建一个名为 ArrowArc 的高性能、列优先数据移动框架。
- 在数据库和处理引擎之间实现直接 Arrow 流
- 进一步优化 Go 的 Arrow 实现以适应实际数据工作负载
如果您正在处理大数据、流媒体或高性能分析,我强烈建议您探索Go + Arrow。
大多数数据工程师没有意识到他们放弃了多少性能。我们已经习惯于接受缓慢的管道、序列化开销和低效的数据移动,这是很正常的。
但事实并非如此。
工具已存在,模式正在转变。
现在是时候重新思考什么是可能的了。