Parquet 是面向分析型业务的列式存储格式,由 Twitter 和 Cloudera 合做开发,2015 年 5 月从 Apache 的孵化器里毕业成为 Apache 顶级项目,最新的版本是 1.8.0。算法
列式存储
列式存储和行式存储相比有哪些优点呢?apache
能够跳过不符合条件的数据,只读取须要的数据,下降 IO 数据量。缓存
压缩编码能够下降磁盘存储空间。因为同一列的数据类型是同样的,可使用更高效的压缩编码(例如 Run Length Encoding 和 Delta Encoding)进一步节约存储空间。微信
只读取须要的列,支持向量运算,可以获取更好的扫描性能。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 和数据为例来讲明这个问题。
这个 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:
以 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 简化成下面的样子:
这两条记录的序列化过程如图 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 就是大数据时代存储格式的事实标准。
参考文档
http://parquet.apache.org/
https://blog.twitter.com/2013/dremel-made-simple-with-parquet
http://blog.cloudera.com/blog/2015/04/using-apache-parquet-at-appnexus/
http://blog.cloudera.com/blog/2014/05/using-impala-at-scale-at-allstate/
本文分享自微信公众号 - ApacheHudi(ApacheHudi)。
若有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一块儿分享。