Apache Arrow 格式如何加速查询结果传输
发布 2025 年 1 月 10 日
作者 Ian Cook, David Li, Matt Topol
翻译 日语
这是系列文章中的第一篇,旨在揭示 Arrow 作为数据库和查询引擎数据交换格式的用途。
本系列文章
“为什么需要这么长时间?”
这是数据从业者在等待查询结果时经常思考的问题。这个问题有很多可能的答案。也许您的数据源分区不佳。也许您的 SaaS 数据仓库规模不足。也许查询优化器未能将您的 SQL 语句转换为高效的执行计划。
但令人惊讶的是,通常的答案是您正在使用低效的协议将查询结果传输到客户端。在 2017 年的一篇论文中,Mark Raasveldt 和 Hannes Mühleisen 观察到,查询结果传输时间通常主导查询执行时间,尤其是对于较大的结果。然而,瓶颈可能并不在您预期的地方。
将查询结果从源传输到目的地涉及三个步骤
- 在源端,将结果从其原始格式序列化为传输格式。
- 以传输格式通过网络传输数据。1
- 在目的端,将传输格式反序列化为目标格式。
在网络速度较慢的时代,传输步骤通常是瓶颈,因此几乎没有动力加快序列化和反序列化步骤。相反,重点是减小传输数据的大小,通常通过压缩,以减少传输时间。正是在这个时代,设计了最广泛使用的数据库连接 API (ODBC 和 JDBC) 和数据库客户端协议(例如 MySQL 客户端/服务器协议和 PostgreSQL 前端/后端协议)。但随着网络速度的加快和传输时间的缩短,瓶颈已转移到序列化和反序列化步骤。2 对于产生许多数据工程和数据分析管道特征的较大结果大小的查询尤其如此。
然而,如今许多查询结果仍然流经传统 API 和协议,这些 API 和协议通过强制将数据转换为低效的传输格式而增加了大量的序列化和反序列化(“ser/de”)开销。在 2021 年的一篇论文中,Tianyu Li 等人提出了一个使用 ODBC 和 PostgreSQL 协议的示例,其中 99.996% 的总查询时间都花在了 ser/de 上。这可以说是极端情况,但我们已经在许多实际案例中观察到 90% 或更高。如今,对于数据工程和数据分析查询,有强烈的动机选择一种加快 ser/de 的传输格式。
Arrow 登场。
Apache Arrow 开源项目定义了一种 数据格式,旨在加快——在许多情况下甚至消除——查询结果传输中的 ser/de。自 2016 年创建以来,Arrow 格式及其围绕它构建的多语言工具箱已获得广泛使用,但 Arrow 如何能够大幅削减 ser/de 开销的技术细节仍然鲜为人知。为了解决这个问题,我们概述了 Arrow 格式使其成为可能的五个关键属性。
1. Arrow 格式是列式存储的。
列式(面向列)数据格式将每列的值存储在连续的内存块中。这与行式数据格式相反,行式数据格式将每行的值存储在连续的内存块中。
高性能分析数据库、数据仓库、查询引擎和存储系统已经趋向于列式架构,因为它加速了最常见的分析查询类型。现代列式查询系统包括 Amazon Redshift、Apache DataFusion、ClickHouse、Databricks Photon Engine、DuckDB、Google BigQuery、Microsoft Azure Synapse Analytics、OpenText Analytics Database (Vertica)、Snowflake 和 Voltron Data Theseus。
同样,许多分析查询结果的目的地(例如商业智能工具、数据应用平台、数据帧库和机器学习平台)都使用列式架构。列式商业智能工具的示例包括 Amazon QuickSight、Domo、GoodData、Power BI、Qlik Sense、Spotfire 和 Tableau。列式数据帧库的示例包括 cuDF、pandas 和 Polars。
因此,查询结果的源格式和目标格式都是列式格式越来越常见。在列式源和列式目标之间传输数据最有效的方法是使用列式传输格式。这消除了在序列化步骤中将数据从列转换为行的耗时转置,以及在反序列化步骤中在目标端将数据从行转换为列的另一个耗时转置。
Arrow 是一种列式数据格式。Arrow 格式中数据的列式布局与许多广泛使用的列式源系统和目标系统中数据的布局相似——在许多情况下甚至相同。
2. Arrow 格式是自描述和类型安全的。
在自描述数据格式中,描述数据结构的模式(列的名称和类型)以及其他元数据与数据一起包含在内。自描述格式为接收系统提供了安全高效地处理数据所需的所有信息。相反,当格式不是自描述时,接收系统必须扫描数据以推断其模式和结构(一个缓慢且容易出错的过程)或单独获取模式。
某些自描述数据格式的一个重要属性是能够强制执行类型安全。当一种格式强制执行类型安全时,它保证数据值符合其指定的类型,从而允许接收系统在处理数据时排除类型错误的可能性。相反,当一种格式不强制执行类型安全时,接收系统必须检查数据中每个单独值的有效性(一个计算成本高昂的过程)或在处理数据时处理类型错误。
当从非自描述、类型不安全的格式(如 CSV)读取数据时,所有这些扫描、推断和检查都会导致大量的反序列化开销。更糟糕的是,此类格式可能导致歧义、调试困难、维护挑战和安全漏洞。
Arrow 格式是自描述的并强制执行类型安全。此外,Arrow 的类型系统与许多广泛使用的数据源和目的地(包括大多数列式数据系统和许多行式系统,如 Apache Spark 和各种关系数据库)的类型系统相似——在许多情况下甚至相同或超集。使用 Arrow 格式时,这些系统可以快速安全地在它们的本机类型和相应的 Arrow 类型之间转换数据值。
3. Arrow 格式支持零拷贝。
零拷贝操作是指数据从一种介质传输到另一种介质,而无需创建任何中间副本。当数据格式支持零拷贝操作时,这意味着其在内存中的结构与在磁盘或网络上的结构相同。因此,例如,数据可以直接从网络读取到内存中可用的结构中,而无需执行任何中间副本或转换。
Arrow 格式支持零拷贝操作。为了存储数据值集合,Arrow 定义了一种面向列的表格数据结构,称为记录批处理。Arrow 记录批处理可以保存在内存中、通过网络发送或存储在磁盘上。无论记录批处理在哪种介质上以及由哪个系统生成,其二进制结构都保持不变。为了存储模式和其他元数据,Arrow 内部使用 FlatBuffers,这是一种由 Google 创建的格式,无论它在哪种介质上,也具有相同的二进制结构。
这些设计选择的结果是,Arrow 不仅可以作为传输格式,还可以作为内存格式和磁盘格式。这与 JSON 和 CSV 等基于文本的格式以及 Protocol Buffers 和 Thrift 等序列化二进制格式形成对比,后者使用专用结构语法编码数据值。要将数据从这些格式加载到可用的内存结构中,必须解析和解码数据。这还与 Parquet 和 ORC 等二进制格式形成对比,后者使用编码和压缩来减小磁盘上数据的大小。要将数据从这些格式加载到可用的内存结构中,必须解压缩和解码。3
这意味着在源系统上,如果数据以 Arrow 格式存在于内存或磁盘上,则该数据可以以 Arrow 格式通过网络传输,而无需任何序列化。在目标系统上,Arrow 格式的数据可以从网络读取到内存中或磁盘上的 Arrow 文件中,而无需任何反序列化。
Arrow 格式设计为作为分析操作的内存格式非常高效。因此,许多列式数据系统都使用 Arrow 作为其内存格式。其中包括 Apache DataFusion、cuDF、Dremio、InfluxDB、Polars、Velox 和 Voltron Data Theseus。当其中一个系统是传输的源或目的地时,可以完全消除 ser/de 开销。对于大多数其他列式数据系统,它们使用的专有内存格式与 Arrow 非常相似。对于这些系统,序列化到 Arrow 和从 Arrow 格式反序列化都快速高效。
4. Arrow 格式支持流式传输。
可流式传输数据格式是一种可以顺序处理、每次处理一个块而无需等待完整数据集的格式。当数据以可流式传输格式传输时,接收系统可以在第一个块到达后立即开始处理它。这可以通过多种方式加速数据传输:传输时间可以与处理时间重叠;接收系统可以更有效地使用内存;并且多个流可以并行传输,从而加速传输、反序列化和处理。
CSV 是可流式传输数据格式的一个例子,因为列名(如果包含)位于文件顶部的标题中,并且文件中的行可以按顺序处理。Parquet 和 ORC 是不支持流式传输的数据格式的例子,因为处理数据所需的模式和其他元数据位于文件底部的页脚中,因此必须下载整个文件(或查找文件末尾并单独下载页脚)才能开始任何处理。4
Arrow 是一种可流式传输数据格式。数据集在 Arrow 中可以表示为一系列具有相同模式的记录批次。Arrow 定义了一种流式格式,由模式后跟一个或多个记录批次组成。接收 Arrow 流的系统可以在记录批次到达时按顺序处理它们。
5. Arrow 格式是通用的。
Arrow 已成为内存中处理表格数据的实际标准格式。Arrow 格式是一种独立于语言的开放标准。有适用于 C、C++、C#、Go、Java、JavaScript、Julia、MATLAB、Python、R、Ruby、Rust 和 Swift 等语言的 Arrow 数据处理库。用几乎任何主流语言开发的应用程序都可以添加对以 Arrow 格式发送或接收数据的支持。数据无需像某些数据库连接 API(包括 JDBC)那样必须通过特定的语言运行时。
Arrow 的通用性使其能够解决加速实际数据系统中的一个根本问题:性能改进固有地受到系统瓶颈的限制。这个问题被称为 阿姆达尔定律。在实际数据管道中,查询结果通常流经多个阶段,每个阶段都会产生 ser/de 开销。例如,如果您的数据管道有五个阶段,并且您消除了其中四个阶段的 ser/de 开销,您的系统可能不会比以前更快,因为剩余一个阶段的 ser/de 将成为整个管道的瓶颈。
Arrow 在几乎任何技术栈中高效运行的能力有助于解决这个问题。您的数据是否从基于 Scala 的分布式后端(带有 NVIDIA GPU 加速的工作节点)流向基于 Jetty 的 HTTP 服务器,然后流向基于 Rails 的特征工程应用程序,用户通过基于 Node.js 的机器学习框架(带有基于 Pyodide 的浏览器前端)与该应用程序交互?没问题;Arrow 库可用于消除所有这些组件之间的 ser/de 开销。
结论
随着越来越多的商业和开源工具增加了对 Arrow 的支持,具有低或无 ser/de 开销的快速查询结果传输变得越来越普遍。如今,包括 Databricks、Dremio、Google BigQuery、InfluxDB、Snowflake 和 Voltron Data Theseus 在内的商业数据平台和查询引擎以及包括 Apache DataFusion、Apache Doris、Apache Spark、ClickHouse 和 DuckDB 在内的开源数据库和查询引擎都可以以 Arrow 格式传输查询结果。提速是巨大的
- Apache Doris:“速度提高了 20 到几百倍”
- Google BigQuery:“最高快 31 倍”
- Dremio:“快 10 倍以上”
- DuckDB:“快 38 倍”
- Snowflake:“最高快 10 倍”
在接收端,数据从业者可以通过使用基于 Arrow 的工具和 Arrow 库、接口和协议来最大化提速。到 2025 年,随着更多的项目和供应商实施对 ADBC 标准的支持,我们预计能够以 Arrow 格式接收查询结果的工具数量将加速增长。
请继续关注本系列的后续文章,它们将比较 Arrow 格式与其他数据格式,并描述客户端可用于以 Arrow 格式获取结果的协议和 API。
-
传输格式也可以称为线缆格式或序列化格式。↩
-
从 1990 年代至今,网络性能的提升超过了 CPU 性能的提升。例如,在 1990 年代末,主流桌面 CPU 大致可以达到 1 GFLOPS,典型的广域网连接速度为 56 Kb/s。如今,主流桌面 CPU 大致可以达到 100 GFLOPS,而 1 Gb/s 左右的广域网连接速度很常见。因此,虽然 CPU 性能提高了大约 100 倍,但网络速度提高了大约 10,000 倍。↩
-
这并不意味着 Arrow 在其他应用(例如归档存储)中比 Parquet 或 ORC 快。本系列的后续文章将更详细地从技术角度比较 Arrow 格式与这些和其他格式,并描述它们通常如何相互补充。↩
-
这并不意味着 CSV 传输结果会比 Parquet 或 ORC 快。在比较 CSV 与 Parquet 或 ORC 的传输性能时,此处描述的其他属性通常会超过此属性。↩