Apache Arrow 格式如何加速查询结果传输


发布 2025年1月10日
作者 Ian Cook, David Li, Matt Topol, Sutou Kouhei [译]
翻译 原文(英文)

本文是系列文章的第一篇,旨在揭示 Arrow 被用作数据库和查询引擎之间数据交换格式的原因。

“为什么这么久?”

这是处理数据的人在等待查询结果时经常会问的问题。可能有多种答案。也许数据源没有正确分区。也许 SaaS 数据仓库的资源不足。也许查询优化器未能将 SQL 语句转换为高效的执行计划。

然而,令人惊讶的是,在很多情况下,答案是使用了低效的协议来将查询结果传输到客户端。2017年的一篇论文中,Mark Raasveldt 和 Hannes Mühleisen 观察到,特别是在查询结果很大的情况下,查询结果的传输往往占查询执行时间的大部分。然而,瓶颈可能并不在你所期望的地方。

从源到目的地的查询结果传输包括以下三个步骤:

  1. 源端:将结果从原始格式序列化为传输格式
  2. 源端:通过网络以传输格式发送数据1
  3. 目的端:将传输格式的数据反序列化为易于处理的格式

在网络速度较慢的时代,数据传输步骤(步骤2)通常是瓶颈。因此,很少有动力去加速序列化步骤(步骤1)和反序列化步骤(步骤3)。相反,重点放在减小传输数据的大小上。在许多情况下,通过压缩数据来缩短传输时间。广泛使用的数据库连接 API(ODBC 和 JDBC)和数据库客户端协议(MySQL 客户端-服务器协议和 PostgreSQL 前端-后端协议)都是在那个时代设计的。然而,随着网络速度的加快,传输时间缩短,瓶颈已经转移到序列化和反序列化步骤2。对于生成大量结果的查询尤其如此。在许多数据工程管道和数据分析管道中,这种查询经常执行。

现在,许多查询结果仍然通过遗留的 API 和协议传输。这些 API 和协议必须使用低效的传输格式,因此存在大量的序列化和反序列化开销。2021年的一篇论文中,Tianyu Li 等人展示了一个使用 ODBC 和 PostgreSQL 协议的例子。在这个例子中,总查询执行时间的 99.996% 都花在了序列化和反序列化上。这是一个极端的例子,但我们也看到了很多超过 90% 的实际例子。现在,为了数据工程查询和数据分析查询,有充分的理由选择一种能够加速序列化和反序列化的传输格式。

这就是 Arrow 的用武之地!

Apache Arrow 开源项目定义了一种数据格式。这种格式旨在加速查询结果传输时的序列化和反序列化。在许多情况下,序列化和反序列化变得不必要。自2016年创建以来,Arrow 格式以及围绕该格式构建的多语言工具箱已得到广泛使用。然而,Arrow 如何减少序列化和反序列化开销的技术细节却鲜为人知。为了解决这个问题,我们将解释 Arrow 格式的五个特点,这些特点使其能够减少序列化和反序列化开销。

1. Arrow 格式是列式存储

列式数据格式将每列的值存储在内存中连续的区域。这与行式数据格式形成对比。行式数据格式将每行的值存储在内存中连续的区域。

図1:5行3列のテーブルの物理メモリーレイアウトは行指向と列指向でどのように違うのか。
图1:一个5行3列的表的物理内存布局在行式和列式存储下有何不同。

高性能分析数据库、数据仓库、查询引擎和存储系统通常采用列式架构。这是为了高速执行常用的分析查询。最新的列式查询系统包括 Amazon Redshift、Apache DataFusion、ClickHouse、Databricks Photon Engine、DuckDB、Google BigQuery、Microsoft Azure Synapse Analytics、OpenText Analytics Database (Vertica)、Snowflake、Voltron Data Theseus 等。

同样,许多分析查询结果的目标也采用列式架构。目标包括 BI 工具、数据应用平台、数据帧库、机器学习平台等。列式 BI 工具包括 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 定义了一个称为记录批次(record batch)的列式表格数据结构。Arrow 记录批次可以存储在内存中,也可以通过网络发送,还可以保存到磁盘。无论记录批次位于哪种介质上或由哪个系统生成,其二进制结构都不会改变。为了存储模式和其他元数据,Arrow 使用了 FlatBuffers。FlatBuffers 是 Google 创建的一种格式。FlatBuffers 同样在任何介质上都具有相同的二进制结构。

这些设计决策使得 Arrow 不仅可以作为传输格式,还可以作为内存中的格式和磁盘上的格式。这与 JSON 和 CSV 等基于文本的格式,以及 Protocol Buffers 和 Thrift 等序列化二进制格式形成对比。这些格式使用专门的语法对数据进行编码。要将这些格式的数据加载到内存中可用的结构中,需要解析和解码数据。这与 Parquet 和 ORC 等二进制格式也形成对比。这些格式为了减少磁盘上的数据大小而进行编码和压缩。要将这些格式的数据加载到内存中可用的结构中,需要解压和解码3

这意味着,如果数据源系统中有 Arrow 格式的数据,无论是内存中还是磁盘上,都可以无需序列化地通过网络传输 Arrow 格式的数据。同样,数据接收系统可以无需反序列化地从网络将数据读入内存,或将其作为 Arrow 文件写入磁盘。

Arrow 格式被设计为一种内存中的格式,可以高效地进行分析操作。因此,许多列式数据系统都将 Arrow 作为内部内存中的格式。例如,Apache DataFusion、cuDF、Dremio、InfluxDB、Polars、Velox、Voltron Data Theseus 等都采用了 Arrow。当这些系统是数据源或数据接收方时,序列化和反序列化开销将完全消失。对于许多其他列式数据系统,它们使用的专有内存中格式与 Arrow 非常相似。在这些系统中,与 Arrow 格式的序列化和反序列化处理是快速且高效的。

4. Arrow 格式支持流式传输

流式数据格式可以逐个分块顺序处理,而无需等待整个数据集。当数据以流式格式传输时,接收系统可以在收到第一个分块后立即开始处理。这可以通过多种方式加速数据传输。例如,它可以在处理数据的同时接收下一个数据。例如,接收系统可以更有效地利用内存。例如,可以并行传输多个流。这可以加速数据传输、数据反序列化和数据处理。

例如,CSV 是一种流式数据格式,因为它(如果包含)文件开头的标题包含列名,并且文件中后续的行可以按顺序处理。另一方面,Parquet 和 ORC 是非流式数据格式,因为处理数据所需的模式和其他元数据位于文件的最后一个页脚中。在开始处理之前,需要下载整个文件(或移动到文件末尾并单独下载页脚)4

Arrow 是一种流式数据格式。数据集可以用 Arrow 表示为具有相同模式的记录批次列。Arrow 定义了一种流式格式。这种格式首先包含模式,然后是一个或多个记录批次。接收 Arrow 流的系统可以按接收顺序处理记录批次。

図2:3列の表を転送するArrowストリーム。最初のレコードバッチは最初の3行の値だけを含み、2つめのレコードバッチは次の3行のデータが含まれている。実際のArrowレコードバッチには数千から数百万行が含まれていることもある。
图2:传输三列表的 Arrow 流。第一个记录批次只包含前3行的值,第二个记录批次包含接下来的3行数据。实际的 Arrow 记录批次可能包含数千到数百万行。

5. Arrow 格式是通用的

Arrow 已成为内存中处理表格数据的标准格式。Arrow 是一种与语言无关的开放标准。它提供了用于处理 Arrow 数据的各种语言库。例如,有适用于 C、C++、C#、Go、Java、JavaScript、Julia、MATLAB、Python、R、Ruby、Rust 和 Swift 的官方库。用主流语言开发的应用程序可以支持以 Arrow 格式发送和接收数据。虽然一些数据库连接 API(如 JDBC)需要使用特定的语言运行时,但 Arrow 格式没有这种要求。

Arrow 的通用性有助于解决加速实际数据系统时的一个基本问题:性能提升受限于系统的瓶颈。这个问题被称为阿姆达尔定律。在实际数据管道中,查询结果经常流经多个阶段,每个阶段都存在序列化和反序列化开销。例如,如果你的数据管道有5个阶段,即使你在其中4个阶段消除了序列化和反序列化开销,你的系统也不会变快。因为剩余的1个阶段的序列化和反序列化将成为整个管道的瓶颈。

Arrow 可以在任何技术栈上高效运行,有助于解决这个问题。例如,假设有这样一个数据流:基于 Scala 的分布式后端(带有 NVIDIA GPU 的 worker)→ 基于 Jetty 的 HTTP 服务器→ 基于 Node.js 的机器学习框架(带有基于 Pyodide 的浏览器前端)与用户交互的基于 Rails 的特征工程应用程序。没问题。Arrow 库可以消除所有这些组件之间的序列化和反序列化开销。

总结

随着越来越多的商业和开源工具兼容 Arrow,无序列化或少序列化的高速查询传输变得越来越普遍。现在,许多数据库、数据平台和查询引擎都可以以 Arrow 格式传输查询结果。例如,Databricks、Dremio、Google BigQuery、InfluxDB、Snowflake、Voltron Data Theseus 等商业产品,以及 Apache DataFusion、Apache Doris、Apache Spark、ClickHouse、DuckDB 等开源产品都支持。这极大地加速了数据传输。

在数据接收端,处理数据的人可以通过使用基于 Arrow 的工具、库、接口和协议来最大限度地提高这种加速效果。2025年,随着更多项目和供应商支持ADBC标准,能够以 Arrow 格式接收查询结果的工具数量将不断增加。

敬请期待本系列的后续文章。我们将比较 Arrow 格式与其他数据格式,并解释客户端可以用来获取 Arrow 格式结果的协议和 API。


  1. 传输格式有时也称为“线格式”或“序列化格式”。

  2. 从1990年代至今,网络性能的提升超过了 CPU 性能的提升。例如,在1990年代后期,主流桌面 CPU 的处理速度约为 1 GFLOPS,常见 WAN 连接速度为 56 Kb/s。现在,主流桌面 CPU 的处理速度约为 100 GFLOPS,常见 WAN 连接速度为 1 Gb/s。这意味着 CPU 性能提升了约 100 倍,而网络速度提升了约 1 万倍。

  3. 这并不意味着 Arrow 在归档存储等应用程序中比 Parquet 或 ORC 更快。本系列的后续文章将更深入地从技术细节上比较 Arrow 格式与 Parquet、ORC 以及其他格式。我们将解释它们是相互补充的。

  4. 这并不意味着 CSV 比 Parquet/ORC 更快地传输结果。在比较 CSV 和 Parquet/ORC 的传输性能时,通常其他四个特点的影响比这个特点更大。