Apache Arrow 列式内存格式

FreeGuideOnline 最新 2026-07-11

系统 A 的内存结构 → 序列化成某种中间格式(如 Protobuf、JSON) → 系统 B 反序列化成自己的内存结构


这个过程消耗大量 CPU、内存带宽,并产生不必要的延迟。

Arrow 提供的替代方案:

系统 A 生成 Arrow 格式的数据 → 通过指针传递(或共享内存) → 系统 B 直接操作同一块 Arrow 数据


由于 Arrow 格式具有明确定义的、语言无关的内存布局,C++、Python、Java、Rust 等语言都可以在不复制字节的情况下读写同一数据块。这就是**零拷贝数据传输**的核心价值。

## Arrow 列式内存格式的设计原理

### 内存布局概览

Arrow 中最基本的数据结构是 **数组(Array)**,它代表一列固定类型的数据。多个数组组合成一个 **记录批次(Record Batch)**,代表一个二维表格的片段。

一个典型的 Arrow 数组在内存中包含三个部分:

1. **有效性位图(Validity Bitmap)**:一个可选的位图,每一位表示对应位置的元素是否为 null。这是 Arrow 处理空值的统一方式。
2. **偏移量缓冲区(Offset Buffer)**:仅用于变长类型(如字符串、列表),存储每个元素的起始位置。
3. **数据缓冲区(Data Buffer)**:连续存储实际数据的平坦缓冲区。

这种设计保证了**数据访问的局部性**和**SIMD 友好性**。例如,一个`Int64`数组就是一段连续的 8 字节整数序列,前面可能有一个对应的 null 位图。

### 固定长度类型与变长类型

**固定长度类型**(如整数、浮点数、布尔值、固定大小二进制)的实现非常简单:单一数据缓冲区 + 有效性位图。计算时可以直接将数据缓冲区映射为一个原生数组。

**变长类型**(如字符串、可变长度二进制)则多了一层间接引用:实际字符串数据被依次放入一个大的数据缓冲区,同时使用一个整型偏移量数组记录每个字符串的开始位置(以及最后一个字符串的结束位置)。

例如,存储字符串`["hello", "world"]`时:

- 偏移量缓冲区:`[0, 5, 10]` (即第一个字符串从位置0开始,第二个从位置5开始,总长度10)
- 数据缓冲区:`"helloworld"` 的 UTF-8 字节

这种表示法避免了存储指针,使得序列化/反序列化极其简单——直接复制偏移量和数据缓冲区即可,无需任何指针重定位。

### 嵌套类型

Arrow 同样原生支持列表、结构体(Struct)、映射、联合体等复杂嵌套类型。这些类型通过组合基本数组来实现。

- **列表**:由`offsets`数组(表示每个列表元素的起始位置)和子元素数组构成。
- **结构体**:由一组子字段(每个字段都是一个 Arrow 数组)组成,结构体本身的 null 由父级位图控制。

这种定义方式使得即使是深层嵌套数据,依然可以维持列式存储的特性,并允许对嵌套字段进行下推筛选和投影。

## 核心数据结构:Array、ChunkedArray、RecordBatch、Table

### Array

Array 是单一数据类型的一维数组,长度固定,不可变(immutable)。它包含:

- 数据类型
- 长度
- 空计数
- 上述的缓冲区集合
- 可选的字典(用于字典编码)

### ChunkedArray

当数据量很大时,单一逻辑列可能由多个底层 Array 拼接而成,Arrow 使用 `ChunkedArray` 表示这种逻辑列。它实际上是 Array 的切片集合,对外提供统一的访问接口。这让增量构建和拼接列变得非常高效。

### RecordBatch

RecordBatch 是一组等长的 Array,每一列共享相同的行数,本质上是一个二维的数据块。它代表一批行的列式存储。通常一个查询结果或一次数据交换的单元就是一个 RecordBatch。

### Table

Table 可以看作多个 RecordBatch 的组合,这些批次具有相同的 Schema(列名和类型),但行数可以不同。Table 提供了一种便于处理超大规模数据集的抽象,支持过滤、列选择等操作,实际计算时可以按批次并行执行。

## Arrow IPC(进程间通信)格式

为了实现跨进程或跨系统的零拷贝共享,Arrow 定义了两种序列化机制:**流格式(Stream format)** 和**文件格式(File format)**。注意,这两种格式仍然是为消息传输或持久化设计的,但与磁盘列式格式(如 Parquet)有本质区别,因为其序列化后的字节可直接以极低成本重建内存中的 Arrow 数组。

- **流格式**:将 Schema 和后续的一系列 RecordBatch 消息依次发送。适合 gRPC、Socket 等流式场景。
- **文件格式**:在流格式基础上增加了一个页脚(footer),包含随机访问所需的元数据,允许按批次或列进行读取。

这两种格式都尽可能地保留 Arrow 内存布局,序列化仅仅是将那些缓冲区(位图、偏移量、数据)按顺序拼接,然后添加轻量的元数据头即可。反序列化几乎只是构造 Array 对象,并让指针指向收到的字节,避免了数据复制。

## 基于 Arrow 的计算与生态

### 零拷贝分析

正是因为 Arrow 内存格式与计算引擎的内部表示一致,许多现代分析引擎(如 Dremio、InfluxDB IOx、DataFusion)直接用 Arrow 作为核心计算格式。计算函数可以直接在一段 `Int64Array` 的原始缓冲区上应用 SIMD 向量化操作,无需在每次函数调用时解压数据。

### 跨语言互操作

Arrow 的规范用跨语言方式定义,目前已有十余种语言的官方实现,包括 C++、Java、Python (PyArrow)、Rust、JavaScript、Go 等。这些实现共享相同的内存布局,因此:

- Python 进程可以通过 Plasma 共享内存将 DataFrame 转换为 Arrow,然后 C++ 服务直接读取。
- Spark(Java/Scala)与 Python 库(pandas)之间的数据交换现在可以通过 Arrow 加速,速度提升可达几十倍。
- JavaScript 前端可以接收 Arrow 流,并使用轻量级库进行查询,无需复制整个数据。

### Query Engines 与内存数据库

许多项目利用 Arrow 构建高性能查询引擎:

- **Apache DataFusion**:基于 Rust + Arrow 的查询执行框架。
- **Ballista**:基于 DataFusion 的分布式计算平台。
- **InfluxDB IOx**:时序数据库,使用 Arrow 作为内部表示。
- **Velox**(Meta):C++ 向量化执行库,内部也使用类似 Arrow 的布局。

这些工具充分证明了列式内存格式作为通用计算引擎基石的能力。

## Arrow 与 Parquet / ORC 的关系

容易混淆的一点是:Arrow 不是用来替代 Parquet 或 ORC 的。它们定位完全不同:

| 特征 | Apache Arrow | Apache Parquet / ORC |
|------|--------------|----------------------|
| 设计目标 | 内存中低延迟计算与传输 | 磁盘高效存储与查询 |
| 存储格式 | 列式、面向CPU缓存友好 | 列式、面向高压缩率和高效I/O |
| 读写开销 | 极低(可直接计算) | 需要解压缩和解码 |
| 典型用例 | 网络传输、内存计算、中间数据交换 | 数据湖、持久化存储、归档 |

在实际系统中,经常将两者结合:数据以 Parquet 存储在磁盘,读入内存时转换为 Arrow 格式进行计算,计算成果又可写回 Parquet。Arrow 生态提供了高效的 Parquet 读取器/写入器,利用 Arrow 作为转换的中介。

## 亲手体验:Python 中的 Arrow

### 安装 PyArrow

```bash
pip install pyarrow

创建数组与表

import pyarrow as pa

# 创建整型数组,包含空值
int_array = pa.array([1, 2, None, 4])
print(int_array)
# 输出: <pyarrow.lib.Int64Array object at ...>
# [
#   1,
#   2,
#   null,
#   4
# ]

# 创建字符串数组
str_array = pa.array(['apple', 'banana', None, 'cherry'])
print(str_array[0].as_py())  # 'apple'

# 构建记录批次和表
batch = pa.record_batch([int_array, str_array], names=['id', 'fruit'])
table = pa.Table.from_batches([batch])
print(table)

零拷贝转换到 pandas

import pandas as pd

df = table.to_pandas()
print(df)

默认情况下,to_pandas() 会将 Arrow 缓冲区转换为 NumPy 数组(整数、浮点数)或 Python 对象(字符串),但也可以做到零拷贝(使用 pandas 扩展数组后端),从而让 DataFrame 直接管理 Arrow 内存。

读取 Parquet 并查询

import pyarrow.parquet as pq

# 读取 Parquet 文件作为 Arrow 表
table = pq.read_table('data.parquet')
# 执行选择与过滤
result = table.filter(pa.compute.greater(table.column('age'), 18))
print(result)