Apache Arrow overview

2021/12/08 大数据理论与架构 共 7316 字,约 21 分钟

Apache Arrow 为扁平与层级嵌套数据定义了一种语言无关的列式内存格式,能够进行有效的分析型操作。 Arrow 能够进行零拷贝数据分享与 RPC 数据传输;是一种统一的数据格式;并且支持内存分析和查询处理,是向量化计算友好的

基本术语

  • Array:一组字段值组成的序列,长度已知,并且所有类型都相同
  • Slot:Array 中的一个字段值
  • Buffer:具有给定长度的连续虚拟地址空间。buffer 中的任何字节可以通过 pointer+offset 访问到
  • Physical Layout:不考虑任何语义的 Array 内存布局
  • Primitive type:基础类型,不包含任何子类型。包含固定长度、变长度以及 null 类型
  • Nested type:嵌套类型,包含一个或者多个子类型
  • Logical type:考虑语义的类型,使用某种 physical layout 实现。如 Decimal 类型使用 16 字节的 fixed-size binary 存储

物理内存格式

Buffer

所有 data 部分的数据都是分配在对齐长度的字节缓冲区,使用对齐地址和填充使得分配的内存是 8 或者 64 字节的倍数,具有以下优点

  • 保证数组元素随机访问能力
  • 对齐长度方便加载到 SIMD 寄存器,便于在没有额外条件检查的情况下使用 SIMD 指令

Validity Bitmap

所有的 Array (包括嵌套 Array,除了 Struct)使用 bitmap 去标识每个 Slot 是否为空值。这也属于 data 的一部分,使用对齐长度的字节缓冲区分配空间,所以访问为

// 判断 Array 的第 j 个 slot 是否为空
is_valid[j] -> bitmap[j / 8] & (1 << (j % 8))

Validity Bitmap 从右往左读取:

          0  1    2   3   4    5
values = [0, 1, null, 2, null, 3]

bitmap
j mod 8   7  6  5  4  3  2  1  0
          0  0  1  0  1  0  1  1

不同类型的内存物理布局

Layout TypeBuffer 0Buffer 1Buffer 2
Primitivevaliditydata 
Variable Binaryvalidityoffsetsdata
Listvalidityoffsets 
Fixed-size Listvalidity  
Structvalidity  
Sparse Uniontype ids  
Dense Uniontype idsoffsets 
Null   
Dictionary-encodedvaliditydata (indices) 

Fixed-size Layout

字段的比特宽度是固定的,即对应着基本类型,在做列式内存存储时,每个 Array 的 Slot 的字节长度都是相同的。Array 将数据存储在连续的 Value Buffer 上,长度为 Slot 宽度 * Array 长度并且加上填充。与之对应的还包括一个 Validity Bitmap,分配给 Validity Bitmap Buffer。

下面举个例子,对于 Int32 类型的一组数据 [1, null, 2, 4 ,8] 的内存物理布局:

* Length: 5, Null count: 1
* Validity bitmap buffer:

  |Byte 0 (validity bitmap) | Bytes 1-63            |
  |-------------------------|-----------------------|
  | 00011101                | 0 (padding)           |

* Value Buffer:

  |Bytes 0-3   | Bytes 4-7   | Bytes 8-11  | Bytes 12-15 | Bytes 16-19 | Bytes 20-63 |
  |------------|-------------|-------------|-------------|-------------|-------------|
  | 1          | unspecified | 2           | 4           | 8           | unspecified |

Variable-size Binary Layout

字段的比特宽度是变长的,在做列式内存存储时,每个 Array 的 Slot 的字节长度可能不同。Array 将数据分配在 Offset Buffer 和 Data Buffer 上。Offset Buffer 包含 Array 长度+1 个有符号整数,表示 Data Buffer 中每个 slot 的起始位置。所以 Offset Buffer 中第一个元素是 0 而最后一个元素是 Array 的总字节长度。通过以下方式计算第 j 个 slot 的起始位置和长度

slot_position = offsets[j]
slot_length = offsets[j + 1] - offsets[j]  // (for 0 <= j < length)

Variable-size List Layout

一种嵌套字段,每个字段有数量不同且类型相同的子字段。语义上与 Variable-size Binary Layout 相同,包含 Offset Buffer 和 Data Buffer。

下面举个例子,对于 List<List<Int8>> 类型的一组数据 [[[1, 2], [3, 4]], [[5, 6, 7], null, [8]], [[9, 10]]] 的内存物理布局:

* Length 3
* Nulls count: 0
* Validity bitmap buffer: Not required
* Offsets buffer (int32)

  | Bytes 0-3  | Bytes 4-7  | Bytes 8-11 | Bytes 12-15 | Bytes 16-63 |
  |------------|------------|------------|-------------|-------------|
  | 0          |  2         |  5         |  6          | unspecified |

* Values array (`List<Int8>`)
  * Length: 6, Null count: 1
  * Validity bitmap buffer:

    | Byte 0 (validity bitmap) | Bytes 1-63  |
    |--------------------------|-------------|
    | 00110111                 | 0 (padding) |

  * Offsets buffer (int32)

    | Bytes 0-27           | Bytes 28-63 |
    |----------------------|-------------|
    | 0, 2, 4, 7, 7, 8, 10 | unspecified |

  * Values array (Int8):
    * Length: 10, Null count: 0
    * Validity bitmap buffer: Not required

      | Bytes 0-9                     | Bytes 10-63 |
      |-------------------------------|-------------|
      | 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 | unspecified |

如果我们需要 List[1] : List<List<Int8>>,那么查询步骤将是这样的

  • Offsets buffer 表示 List<List<Int8>> 元素在 Values Array(List<Int8>) 中的偏移量。 可以查找到 List[1] 的起始偏移为 2,长度为 5-2=3
  • Values Array(List<Int8>) 的 Offset Buffer 表示 List<Int8> 元素在 Values array (Int8) 中的偏移量。由 List[1] 的起始偏移为 2,长度为 3,可以查到 List[1] 包含了三个元素,分别为 4~7、7~7、7~8
  • 从 Values Array(Int8) 的 Data Buffer 中可以提取到 [[5, 6, 7], [], [8]]

Fixed-Size List Layout

一种嵌套字段,每个字段有相同数量且类型相同的子字段。语义上与 Fixed-size Layout 相同

下面举个例子,对于 FixedSizeList<byte>[4] 类型的一组数据 [[192, 168, 0, 12], null, [192, 168, 0, 25], [192, 168, 0, 1]] 的内存物理布局:

* Length: 4, Null count: 1
* Validity bitmap buffer:

  | Byte 0 (validity bitmap) | Bytes 1-63            |
  |--------------------------|-----------------------|
  | 00001101                 | 0 (padding)           |

* Values array (byte array):
  * Length: 16,  Null count: 0
  * validity bitmap buffer: Not required

    | Bytes 0-3       | Bytes 4-7   | Bytes 8-15                      |
    |-----------------|-------------|---------------------------------|
    | 192, 168, 0, 12 | unspecified | 192, 168, 0, 25, 192, 168, 0, 1 |

Struct Layout

一种嵌套布局,每个字段由一组命名的相同数量且类型不同的子字段组成。内存布局由 Validity Bitmap 和一组子字段 Array 组成,子字段 Array 的物理布局都是相互独立的。

下面举个例子,对于 Struct<VarBinary, Int32> 类型的一组数据 [{'joe', 1}, {null, 2}, null, {'mark', 4}] 的内存物理布局:

* Length: 4, Null count: 1
* Validity bitmap buffer:

  |Byte 0 (validity bitmap) | Bytes 1-63            |
  |-------------------------|-----------------------|
  | 00001011                | 0 (padding)           |

* Children arrays:
  * field-0 array (`VarBinary`):
    * Length: 4, Null count: 2
    * Validity bitmap buffer:

      | Byte 0 (validity bitmap) | Bytes 1-63            |
      |--------------------------|-----------------------|
      | 00001001                 | 0 (padding)           |

    * Offsets buffer:

      | Bytes 0-19     |
      |----------------|
      | 0, 3, 3, 3, 7  |

     * Values array:
        * Length: 7, Null count: 0
        * Validity bitmap buffer: Not required

        * Value buffer:

          | Bytes 0-6      |
          |----------------|
          | joemark        |

  * field-1 array (int32 array):
    * Length: 4, Null count: 1
    * Validity bitmap buffer:

      | Byte 0 (validity bitmap) | Bytes 1-63            |
      |--------------------------|-----------------------|
      | 00001011                 | 0 (padding)           |

    * Value Buffer:

      |Bytes 0-3   | Bytes 4-7   | Bytes 8-11  | Bytes 12-15 | Bytes 16-63 |
      |------------|-------------|-------------|-------------|-------------|
      | 1          | 2           | unspecified | 4           | unspecified |

Union Layout

一种嵌套布局,每个字段由一个类型不同的子字段组成,每个 Slot 所包含的类型在预先定义的类型序列中选择。Union 包含 dense 和 sparse 两种类型。

Dense Union

其物理布局如下:

  • 每个类型都有一个子 Array
  • Type buffer:一组 type id 的缓冲区,每个 type id 用一个 8-bit 有符号整数表示
  • Offsets buffer:一组 int32 值的缓冲区,表示在子 Array 中的偏移量

下面举个例子,对于Union<f: float, i: int32> 类型的一组数据 [{f=1.2}, null, {f=3.4}, {i=5}] 的物理布局如下:

* Length: 4, Null count: 0
* Types buffer:

  |Byte 0   | Byte 1      | Byte 2   | Byte 3   | Bytes 4-63  |
  |---------|-------------|----------|----------|-------------|
  | 0       | 0           | 0        | 1        | unspecified |

* Offset buffer:

  |Bytes 0-3 | Bytes 4-7   | Bytes 8-11 | Bytes 12-15 | Bytes 16-63 |
  |----------|-------------|------------|-------------|-------------|
  | 0        | 1           | 2          | 0           | unspecified |

* Children arrays:
  * Field-0 array (f: float):
    * Length: 2, Null count: 1
    * Validity bitmap buffer: 00000101

    * Value Buffer:

      | Bytes 0-11     | Bytes 12-63  |
      |----------------|-------------|
      | 1.2, null, 3.4 | unspecified |


  * Field-1 array (i: int32):
    * Length: 1, Null count: 0
    * Validity bitmap buffer: Not required

    * Value Buffer:

      | Bytes 0-3 | Bytes 4-63  |
      |-----------|-------------|
      | 5         | unspecified |

如上所示,每个子 Array 都是密集排列的,并且长度都不一定一致

Sparse Union

与 Dense Union 物理布局相同,但是所有的子 Array 长度都相同,所以不要使用 Offset Buffer。很明显子 Array 长度相同需要浪费更多的内存,但是相比于 Dense Union 在某些场景下更向量化计算友好

下面举个例子,对于Union<f: float, i: int32> 类型的一组数据 [{f=1.2}, null, {f=3.4}, {i=5}] 的物理布局如下:

* Length: 4, Null count: 0
* Types buffer:

  |Byte 0   | Byte 1      | Byte 2   | Byte 3   | Bytes 4-63  |
  |---------|-------------|----------|----------|-------------|
  | 0       | 0           | 0        | 1        | unspecified |

* Offset buffer:

  |Bytes 0-3 | Bytes 4-7   | Bytes 8-11 | Bytes 12-15 | Bytes 16-63 |
  |----------|-------------|------------|-------------|-------------|
  | 0        | 1           | 2          | 0           | unspecified |

* Children arrays:
  * Field-0 array (f: float):
    * Length: 2, Null count: 1
    * Validity bitmap buffer: 00000101

    * Value Buffer:

      | Bytes 0-11     | Bytes 12-63  |
      |----------------|-------------|
      | 1.2, null, 3.4 | unspecified |


  * Field-1 array (i: int32):
    * Length: 1, Null count: 0
    * Validity bitmap buffer: Not required

    * Value Buffer:

      | Bytes 0-3 | Bytes 4-63  |
      |-----------|-------------|
      | 5         | unspecified |

如上所示,每个子 Array 都是稀疏排列的,长度与行数相同

Null Layout

不会分配内存

Dictionary-encoded Layout

字典编码是一种数据表示技术,通过使用字典保存唯一的数值,来对数据进行压缩,当重复值很多时很有效。其内存布局与整型物理布局类似,字典通过列式数组存储。

下面举个例子,对于 VarBinary 类型的一组数据 ['foo', 'bar', 'foo', 'bar', null, 'baz'],使用字典编码后,效果如下

data VarBinary (dictionary-encoded)
   index_type: Int32
   values: [0, 1, 0, 1, null, 2]

dictionary
   type: VarBinary
   values: ['foo', 'bar', 'baz']

序列化与 IPC

序列化数据的基本单元为 recordBatch。从语义上说,一个 recordBatch 代表了有序 Array 的集合,一个 Array 代表一列的数据。Arrow 的序列化和反序列化可以做到不使用额外的内存拷贝。

列式 IPC 协议有以下三种类型,具体格式定义在Message.fbs

  • Schema
  • ReocrdBatch
  • DictionaryBatch

Schema

表示 RecordBatch 的 Schema,使用 Flatbuffers 进行序列化,定义在 Schema.fbs

RecordBatch

包含实际数据的 Data Buffers,定义在Message.fbs中,具体格式如下

  • length:行数
  • FieldNode:每一个字段的元信息
    • length
    • null_count
  • Buffer:每一个字段的 Buffer 信息
    • offset
    • length

DictionaryBatch

包含用于编码的字典,定义在Message.fbs中,具体格式如下

  • Id:字典编号
  • RecordBatch:记录字典值
  • isDelta:对于相同 id 的字典,使用 append 模式或者 replace 模式

REFERENCE

  1. Arrow 文档

文档信息

Search

    Table of Contents