深刻分析 Parquet 列式存储格式

Parquet 是面向分析型业务的列式存储格式,由 Twitter 和 Cloudera 合做开发,2015 年 5 月从 Apache 的孵化器里毕业成为 Apache 顶级项目,最新的版本是 1.8.0。算法

列式存储

列式存储和行式存储相比有哪些优点呢?apache

  1. 能够跳过不符合条件的数据,只读取须要的数据,下降 IO 数据量。缓存

  2. 压缩编码能够下降磁盘存储空间。因为同一列的数据类型是同样的,可使用更高效的压缩编码(例如 Run Length Encoding 和 Delta Encoding)进一步节约存储空间。微信

  3. 只读取须要的列,支持向量运算,可以获取更好的扫描性能。app

当时 Twitter 的日增数据量达到压缩以后的 100TB+,存储在 HDFS 上,工程师会使用多种计算框架(例如 MapReduce, Hive, Pig 等)对这些数据作分析和挖掘;日志结构是复杂的嵌套数据类型,例如一个典型的日志的 schema 有 87 列,嵌套了 7 层。因此须要设计一种列式存储格式,既能支持关系型数据(简单数据类型),又能支持复杂的嵌套类型的数据,同时可以适配多种数据处理框架。框架

关系型数据的列式存储,能够将每一列的值直接排列下来,不用引入其余的概念,也不会丢失数据。关系型数据的列式存储比较好理解,而嵌套类型数据的列存储则会遇到一些麻烦。如图 1 所示,咱们把嵌套数据类型的一行叫作一个记录(record),嵌套数据类型的特色是一个 record 中的 column 除了能够是 Int, Long, String 这样的原语(primitive)类型之外,还能够是 List, Map, Set 这样的复杂类型。在行式存储中一行的多列是连续的写在一块儿的,在列式存储中数据按列分开存储,例如能够只读取 A.B.C 这一列的数据而不去读 A.E 和 A.B.D,那么如何根据读取出来的各个列的数据重构出一行记录呢?性能

图 1 行式存储和列式存储大数据

Google 的 Dremel 系统解决了这个问题,核心思想是使用“record shredding and assembly algorithm”来表示复杂的嵌套数据类型,同时辅以按列的高效压缩和编码技术,实现下降存储空间,提升 IO 效率,下降上层应用延迟。Parquet 就是基于 Dremel 的数据模型和算法实现的。ui

Parquet 适配多种计算框架

Parquet 是语言无关的,并且不与任何一种数据处理框架绑定在一块儿,适配多种语言和组件,可以与 Parquet 配合的组件有:编码

查询引擎: Hive, Impala, Pig, Presto, Drill, Tajo, HAWQ, IBM Big SQL

计算框架: MapReduce, Spark, Cascading, Crunch, Scalding, Kite

数据模型: Avro, Thrift, Protocol Buffers, POJOs

那么 Parquet 是如何与这些组件协做的呢?这个能够经过图 2 来讲明。数据从内存到 Parquet 文件或者反过来的过程主要由如下三个部分组成:

1, 存储格式 (storage format)

parquet-format 项目定义了 Parquet 内部的数据类型、存储格式等。

2, 对象模型转换器 (object model converters)

这部分功能由 parquet-mr 项目来实现,主要完成外部对象模型与 Parquet 内部数据类型的映射。

3, 对象模型 (object models)

对象模型能够简单理解为内存中的数据表示,Avro, Thrift, Protocol Buffers, Hive SerDe, Pig Tuple, Spark SQL InternalRow 等这些都是对象模型。Parquet 也提供了一个 example object model 帮助你们理解。

例如 parquet-mr 项目里的 parquet-pig 项目就是负责把内存中的 Pig Tuple 序列化并按列存储成 Parquet 格式,以及反过来把 Parquet 文件的数据反序列化成 Pig Tuple。

这里须要注意的是 Avro, Thrift, Protocol Buffers 都有他们本身的存储格式,可是 Parquet 并无使用他们,而是使用了本身在 parquet-format 项目里定义的存储格式。因此若是你的应用使用了 Avro 等对象模型,这些数据序列化到磁盘仍是使用的 parquet-mr 定义的转换器把他们转换成 Parquet 本身的存储格式。

图 2 Parquet 项目的结构

Parquet 数据模型

理解 Parquet 首先要理解这个列存储格式的数据模型。咱们以一个下面这样的 schema 和数据为例来讲明这个问题。

message AddressBook {
required string owner;
repeated string ownerPhoneNumbers;
repeated group contacts {
required string name;
optional string phoneNumber;
}
}

这个 schema 中每条记录表示一我的的 AddressBook。有且只有一个 owner,owner 能够有 0 个或者多个 ownerPhoneNumbers,owner 能够有 0 个或者多个 contacts。每一个 contact 有且只有一个 name,这个 contact 的 phoneNumber 无关紧要。这个 schema 能够用图 3 的树结构来表示。

每一个 schema 的结构是这样的:根叫作 message,message 包含多个 fields。每一个 field 包含三个属性:repetition, type, name。repetition 能够是如下三种:required(出现 1 次),optional(出现 0 次或者 1 次),repeated(出现 0 次或者屡次)。type 能够是一个 group 或者一个 primitive 类型。

Parquet 格式的数据类型没有复杂的 Map, List, Set 等,而是使用 repeated fields 和 groups 来表示。例如 List 和 Set 能够被表示成一个 repeated field,Map 能够表示成一个包含有 key-value 对的 repeated field,并且 key 是 required 的。

图 3 AddressBook 的树结构表示

Parquet 文件的存储格式

那么如何把内存中每一个 AddressBook 对象按照列式存储格式存储下来呢?

在 Parquet 格式的存储中,一个 schema 的树结构有几个叶子节点,实际的存储中就会有多少 column。例如上面这个 schema 的数据存储实际上有四个 column,如图 4 所示。

图 4 AddressBook 实际存储的列

Parquet 文件在磁盘上的分布状况如图 5 所示。全部的数据被水平切分红 Row group,一个 Row group 包含这个 Row group 对应的区间内的全部列的 column chunk。一个 column chunk 负责存储某一列的数据,这些数据是这一列的 Repetition levels, Definition levels 和 values(详见后文)。一个 column chunk 是由 Page 组成的,Page 是压缩和编码的单元,对数据模型来讲是透明的。一个 Parquet 文件最后是 Footer,存储了文件的元数据信息和统计信息。Row group 是数据读写时候的缓存单元,因此推荐设置较大的 Row group 从而带来较大的并行度,固然也须要较大的内存空间做为代价。通常状况下推荐配置一个 Row group 大小 1G,一个 HDFS 块大小 1G,一个 HDFS 文件只含有一个块。

图 5 Parquet 文件格式在磁盘的分布

拿咱们的这个 schema 为例,在任何一个 Row group 内,会顺序存储四个 column chunk。这四个 column 都是 string 类型。这个时候 Parquet 就须要把内存中的 AddressBook 对象映射到四个 string 类型的 column 中。若是读取磁盘上的 4 个 column 要可以恢复出 AddressBook 对象。这就用到了咱们前面提到的 “record shredding and assembly algorithm”。

Striping/Assembly 算法

对于嵌套数据类型,咱们除了存储数据的 value 以外还须要两个变量 Repetition Level(R), Definition Level(D) 才能存储其完整的信息用于序列化和反序列化嵌套数据类型。Repetition Level 和 Definition Level 能够说是为了支持嵌套类型而设计的,可是它一样适用于简单数据类型。在 Parquet 中咱们只需定义和存储 schema 的叶子节点所在列的 Repetition Level 和 Definition Level。

Definition Level

嵌套数据类型的特色是有些 field 能够是空的,也就是没有定义。若是一个 field 是定义的,那么它的全部的父节点都是被定义的。从根节点开始遍历,当某一个 field 的路径上的节点开始是空的时候咱们记录下当前的深度做为这个 field 的 Definition Level。若是一个 field 的 Definition Level 等于这个 field 的最大 Definition Level 就说明这个 field 是有数据的。对于 required 类型的 field 必须是有定义的,因此这个 Definition Level 是不须要的。在关系型数据中,optional 类型的 field 被编码成 0 表示空和 1 表示非空(或者反之)。

Repetition Level

记录该 field 的值是在哪个深度上重复的。只有 repeated 类型的 field 须要 Repetition Level,optional 和 required 类型的不须要。Repetition Level = 0 表示开始一个新的 record。在关系型数据中,repetion level 老是 0。

下面用 AddressBook 的例子来讲明 Striping 和 assembly 的过程。

对于每一个 column 的最大的 Repetion Level 和 Definition Level 如图 6 所示。

图 6 AddressBook 的 Max Definition Level 和 Max Repetition Level

下面这样两条 record:

AddressBook {
owner: "Julien Le Dem",
ownerPhoneNumbers: "555 123 4567",
ownerPhoneNumbers: "555 666 1337",
contacts: {
name: "Dmitriy Ryaboy",
phoneNumber: "555 987 6543",
},
contacts: {
name: "Chris Aniszczyk"
}
}
AddressBook {
owner: "A. Nonymous"
}

以 contacts.phoneNumber 这一列为例,"555 987 6543"这个 contacts.phoneNumber 的 Definition Level 是最大 Definition Level=2。而若是一个 contact 没有 phoneNumber,那么它的 Definition Level 就是 1。若是连 contact 都没有,那么它的 Definition Level 就是 0。

下面咱们拿掉其余三个 column 只看 contacts.phoneNumber 这个 column,把上面的两条 record 简化成下面的样子:

AddressBook {
contacts: {
phoneNumber: "555 987 6543"
}
contacts: {
}
}
AddressBook {
}

这两条记录的序列化过程如图 7 所示:

图 7 一条记录的序列化过程

若是咱们要把这个 column 写到磁盘上,磁盘上会写入这样的数据(图 8):

图 8 一条记录的磁盘存储

注意:NULL 实际上不会被存储,若是一个 column value 的 Definition Level 小于该 column 最大 Definition Level 的话,那么就表示这是一个空值。

下面是从磁盘上读取数据并反序列化成 AddressBook 对象的过程:

1,读取第一个三元组 R=0, D=2, Value=”555 987 6543”

R=0 表示是一个新的 record,要根据 schema 建立一个新的 nested record 直到 Definition Level=2。

D=2 说明 Definition Level=Max Definition Level,那么这个 Value 就是 contacts.phoneNumber 这一列的值,赋值操做 contacts.phoneNumber=”555 987 6543”。

2,读取第二个三元组 R=1, D=1

R=1 表示不是一个新的 record,是上一个 record 中一个新的 contacts。

D=1 表示 contacts 定义了,可是 contacts 的下一个级别也就是 phoneNumber 没有被定义,因此建立一个空的 contacts。

3,读取第三个三元组 R=0, D=0

R=0 表示一个新的 record,根据 schema 建立一个新的 nested record 直到 Definition Level=0,也就是建立一个 AddressBook 根节点。

能够看出在 Parquet 列式存储中,对于一个 schema 的全部叶子节点会被当成 column 存储,并且叶子节点必定是 primitive 类型的数据。对于这样一个 primitive 类型的数据会衍生出三个 sub columns (R, D, Value),也就是从逻辑上看除了数据自己之外会存储大量的 Definition Level 和 Repetition Level。那么这些 Definition Level 和 Repetition Level 是否会带来额外的存储开销呢?实际上这部分额外的存储开销是能够忽略的。由于对于一个 schema 来讲 level 都是有上限的,并且非 repeated 类型的 field 不须要 Repetition Level,required 类型的 field 不须要 Definition Level,也能够缩短这个上限。例如对于 Twitter 的 7 层嵌套的 schema 来讲,只须要 3 个 bits 就能够表示这两个 Level 了。

对于存储关系型的 record,record 中的元素都是非空的(NOT NULL in SQL)。Repetion Level 和 Definition Level 都是 0,因此这两个 sub column 就彻底不须要存储了。因此在存储非嵌套类型的时候,Parquet 格式也是同样高效的。

上面演示了一个 column 的写入和重构,那么在不一样 column 之间是怎么跳转的呢,这里用到了有限状态机的知识,详细介绍能够参考 Dremel 。

数据压缩算法

列式存储给数据压缩也提供了更大的发挥空间,除了咱们常见的 snappy, gzip 等压缩方法之外,因为列式存储同一列的数据类型是一致的,因此可使用更多的压缩算法。

压缩算法

使用场景

Run Length Encoding

重复数据

Delta Encoding

有序数据集,例如 timestamp,自动生成的 ID,以及监控的各类 metrics

Dictionary Encoding

小规模的数据集合,例如 IP 地址

Prefix Encoding

Delta Encoding for strings

性能

Parquet 列式存储带来的性能上的提升在业内已经获得了充分的承认,特别是当大家的表很是宽(column 很是多)的时候,Parquet 不管在资源利用率仍是性能上都优点明显。具体的性能指标详见参考文档。

Spark 已经将 Parquet 设为默认的文件存储格式,Cloudera 投入了不少工程师到 Impala+Parquet 相关开发中,Hive/Pig 都原生支持 Parquet。Parquet 如今为 Twitter 至少节省了 1/3 的存储空间,同时节省了大量的表扫描和反序列化的时间。这两方面直接反应就是节约成本和提升性能。

若是说 HDFS 是大数据时代文件系统的事实标准的话,Parquet 就是大数据时代存储格式的事实标准。

参考文档

  1. http://parquet.apache.org/

  2. https://blog.twitter.com/2013/dremel-made-simple-with-parquet

  3. http://blog.cloudera.com/blog/2015/04/using-apache-parquet-at-appnexus/

  4. http://blog.cloudera.com/blog/2014/05/using-impala-at-scale-at-allstate/


本文分享自微信公众号 - ApacheHudi(ApacheHudi)。
若有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一块儿分享。

相关文章
相关标签/搜索