Parquet 和 ORC:高性能列式存储

362 阅读11分钟

这是我参与「第四届青训营 」笔记创作活动的第 10 天!

行存 vs 列存

数据格式层概述

  1. 计算层:各种计算引擎

  2. 存储层:承载数据的持久化存储

  3. 数据格式层:定义了存储层文件内部的组织格式,计算引擎通过格式层的支持来读写文件(严格意义上不是一个独立的层级,而是运行在计算层的library)

image.png

分层视角下的数据形态

  • 存储层:File,Blocks

  • 格式层:File 内部的数据布局 (Layout + Schema)

  • 计算引擎:Rows + Columns

OLTP vs OLAP

  • OLTP 和 OLAP 作为数据查询和分析领域两个典型的系统类型,具有不同的业务特征,适配不同的业务场景

  • 理解两者的区别可以帮助更好的理解行存和列存的设计背景

OLTPOLAP
典型场景在线业务系统,例如:订单、交易、社交、评论等数据仓库或者大数据分析系统,例如:决策分析、BI系统、推荐系统等
访问特征- 事务- 实时性- 低延时- 高并发- 高可用- 弱事务性- 近实时、离线分析- 大吞吐- 并发相对不高- 可用性可以有一定的妥协
数据模型特征- Schema 相对简单- 数据维度不多- 数据规模较小- Schema 复杂- 数据维度很多,几百个Column 很常见- 数据规模巨大

行式存储格式 (行存) 与 OLTP

  • 每一行 (Row) 的数据在文件的数据空间里连续存放的

  • 读取整行的效率比较高,一次顺序 IO 即可

  • 在典型的 OLTP 型的分析和存储系统中应用广泛,例如:MySQL、Oracle、RocksDB 等

image.png

列式存储格式 (列存) 与 OLAP

  • 每一列 (Column) 的数据在文件的数据空间里连续存放的
  • 同列的数据类型一致,压缩编码的效率更好
  • 在典型的 OLAP 型分析和存储系统中广泛应用,例如:

    1.大数据分析系统:Hive、Spark,数据湖分析

    2.数据仓库:ClickHouse,Greenplum,阿里云 MaxCompute

image.png

总结

1.格式层定义了数据的布局,连接计算引擎和存储服务

2.OLTP和OLAP场景话差异明显

3.业务场景决定了技术实现,行存适用于OLTP,列存适用于OLAP

Parquet 详解

使用 Parquet

# Spark
df.write.parquet("/path/to/file.parquet")
df.write
  .partitionBy(”col1")
  .format("parquet")
  .saveAsTable(”sometable")
val df = spark.read.parquet(”/path/to/file.parquet")

# Hive DDL
CREATE TABLE table_name (x INT, y STRING) STORED AS PARQUET;

Dremel数据模型

  • Protocol Buffer 定义

  • 支持可选和重复字段

  • 支持嵌套类型

image.png

Dremel数据模型一Continued

  • 构建出如下的语法树

image.png

嵌套类型只保存叶子节点数据

数据布局

  • RowGroup: 每一个行组包含一定数量或者固定大小的行的集合,在 HDFS 上,RowGroup 大小建议配置成 HDFS Block 大小
  • ColumnChunk: RowGroup 中按照列切分成多个 ColumnChunk
  • Page:ColumnChunk内部继续切分成 Page,一般建议 8KB 大小。Page 是压缩和编码的基本单元

    • 根据保存的数据类型,Page 可以分为:Data Page,Dictionary Page,Index Page
  • Footer 保存文件的元信息

    • Schema
  • Config
  • Metadata

    • RowGroup Meta

      • Column Meta

image.png

 编码Encoding

▪️ Plain直接存储原始数据

▪️ Run Length Encoding (RLE):适用于列基数不大,重复值较多的场景,例如: Boolean、 枚举、固定的选项等

1)Bit- Pack Encoding:配合RLE编码使用,让整形数字存储的更加紧凑

▪️ 字典编码Dictionary Encoding:适用于列基数不大的场景,构造字典表,写入到Dictionary Page;把数据用字典Index替换,然后用RLE编码

image.png

image.png

编码 Encoding

1)默认场景下parquet-mr会自动根据数据特征选择

2)业务自定义:

org.apache.parquet.column.values.factory.ValuesWriterFactory

2.5 压缩Compression

● Page完成Encoding以后,进行压缩

● 支持多种压缩算法

● snappy:压缩速度快,压缩比不高,适用于热数据

● gzip:压缩速度慢,压缩比高,适用于冷数据

● zstd:新引入的压缩算法,压缩比和gzip差不多,而且压缩速度比肩Snappy

● 建议选择snappy或者zstd,根据业务数据类型充分测试压缩效果,以及对查询性能的影响

image.png

2.5.1 压缩 Compression - 对比

image.png

2.6 索引Index

▪️ 和传统的数据库相比,索引支持非常简陋

▪️ Min-Max Index:记录Page内部Column的min_ value 和max_ value

▪️ Column Index:

1)Footer里的Column Metadata包含ColumnChunk的全部Page的Min-Max Value

▪️ Offset Index: 记录Page在文件中的Offset和Page的Row Range

image.png

2.6.1 索引Index - Bloom Filter

1)parquet.bloom.filter. enabled

2)对于列基数比较大的场景,或者非排序列的过滤,Min-Max Index很难发挥作用

3)引入Bloom Filter 加速过滤匹配判定

4)每个ColumnChunk的头部保存Bloom Filter 数据

5)Footer记录Bloom Filter的page offset

image.png

2.6.2 排序Ordering

1)类似于聚集索引的概念

2)排序帮助更好的过滤掉无关的RowGroup或者Page

(1)对于少量数据Seek很有帮助

3)Parquet Format支持SortingColumns

4)Parquet Library目前没有支持

5)依赖业务侧根据查询特征去保证顺序

2.7 过滤下推Predicate PushDown

1)parquet-mr库实现,实现高效的过滤机制

2)引擎侧传入 Filter Expression

3)parquet-mr转换成具体Column的条件匹配

4)查询Footer里的Column Index,定位到具体的行号

5)返回有效的数据给引擎侧

image.png

Spark 3.2.0 x1

issues.apache.org/jira/browse… 26345

https://issues apache.org/jira/browse/PARQUET-1201

2.8 Spark集成 - 向量化读

▪️ ParquetFileFormat类

▪️ 向量化读开关:spark.sql.parquet.enableVectorizedReader

▪️ 向量化读是主流大数据分析引擎的标准实践,可以极大的提升查询性能

▪️ Spark以Batch的方式从Parquet读取数据,下推的逻辑也会适配Batch的方式

2.9 深入Dremel数据模型- Repetition Level

▪️ Repetition L evel:该字段在Field Path上第几个重复字段上出现

1)0:标识新的Record

2)Name.L .anguage.Code为例,Name是第1个重复字段,Language是第2 个重复字段

image.png

2.9.1 深入Dremel数据模型一Definition Level

● Definition L evel:用来记录在field path中,有多少个字段是可以不存在 (optional/repeated)而实际出现的

● Name.Language.Code为例,Name和Language都是可以不存在的

● 第一个NULL字段,D是1说明Name是存在的,但是Language是不存在的,保留原有的信息

image.png

2.9.2 深入Dremel数据模型- Re-Assembly

● 根据全部或者部分列数据,重新构造Record

● 构造FSM状态机

● 根据同一个Column下一个记录的RepetionLevel决定继续读的列

image.png

image.png

2.10 Parquet小结

1)数据模型:基于Dremel

2)文件布局: Footer + RowGroup + ColumnChunk + Page

3)Encoding: Page粒度,Plain / RLE / Dictionary

4)Compression: Snappy / Gzip / Zstd

5)Index: Column Index ( Min-Max Index )

6)Predicate PushDown

ORC 详解

  • 产生于 Hive 项目
  • 大数据分析领域使用最广的列存格式之一

数据模型

  • ORC 会给包括根节点在内的中间节点都创建一个 Column
  • 下图中,会创建 8 个 Column
  • 嵌套类型或者集合类型支持和 Parquet 差别较大
  • optional 和 repeated 字段依赖父节点记录额外信息来重新 Assembly 数据

图片来源: orc.apache.org

数据布局

  • 类似 Parquet
  • Rooter + Stripe + Column + Page (Row Group) 结构
  • Encoding / Compression / Index 支持上和 Parquet 几乎一致

图片来源: orc.apache.org

ACID 特性

  • 支持 Hive Transactions 实现,目前只有 Hive 本身集成
  • 类似 Delta Lake / Hudi / Iceberg
  • 基于 Base + Delta + Compaction 的设计

AliORC

  • ORC 在阿里云计算平台被广泛应用,主流产品 MaxCompute + 交互式分析 Hologres 的最新版本都支持 ORC 格式
  • AliORC 是对 ORC 的深度定制版

索引增强

  • 支持 Clusterd Index,更快的主键查找
  • 支持 Bitmap Index,更快的过滤

    • Roaring Bitmap

      • 更高效的压缩保存 Bitmap Index

      • 以 16 bit 的 bitmap 空间为一个保存单元,每个单元可以是以下三种形式之一:

        • Array Container:只保存为 1 的 Index
        • Run Container:类似 RLE 编码
        • Bitset container:原始 bitmap 存储

图片来源: Roaring Bitmaps: Implementation of an Optimized Software Library

其他优化

  • 小列聚合,减少小 IO

    • 重排 ColumnChunk

图片来源:www.alibabacloud.com/blog/aliorc…

  • 异步预取优化

    • 在计算引擎处理已经读到的数据的时候,异步去预取下一批次数据

图片来源:www.alibabacloud.com/blog/aliorc…

Parquet vs ORC 对比

  • 从原理层面,最大的差别就是对于 NestedType 和复杂类型处理上
  • Parquet 的算法上要复杂很多,带来的 CPU 的开销比 ORC 要略大
  • ORC 的算法上相对加单,但是要读取更多的数据
  • 因此,这个差异的对业务效果的影响,很难做一个定性的判定,更多的时候还是要取决于实际的业务场景

性能对比 1

  • Parquet 在复杂 Schema 场景下的算法开销影响较大
  • 测试平台 Hive,Hive 上 ORC 更有优势
  • 2016年

简单 Schema 复杂 Schema

来源:www.slideshare.net/HadoopSummi…

性能对比 2

  • 在 Spark 场景下 Parquet 工作的更好;在 Hive 场景下,ORC 更好

来源:onlinelibrary.wiley.com/doi/epdf/10…

如何选择

  • 当前项目使用的??
  • 性能上很多情况下依赖于数据集和测试环境,不能迷信 Benchmark 结果
  • 根据实际业务做细粒度的调优
  • Spark 生态下 Parquet 比较普遍
  • Hive 生态下 ORC 有原生支持
  • 整体上,Spark 比 Hive 更加有优势,所以大部分情况下,Parquet 可能是个更好的选择

问题:在 Nested Type 的处理上,Parquet 和 ORC 实现机制的不同对于最终性能有什么影响?

列存演进

数仓中的列存

  • 典型的数仓,例如 ClickHouse 的 MergeTree 引擎也是基于列存构建的

    • 默认情况下列按照 Column 拆分成单独的文件,也支持单个文件形式

图片来源:www.jianshu.com/p/4a86a4f5b…

  • 支持更加丰富的索引,例如 Bitmap Index、Reverted Index、Data Skipping Index、Secondary Index 等
  • 湖仓一体的大趋势下,数仓和大数据数据湖技术和场景下趋于融合,大数据场景下的格式层会借鉴更多的数仓中的技术

存储侧下推

  • 更多的下推工作下沉到存储服务侧
  • 越接近数据,下推过滤的效率越高
  • 例如 AWS S3 Select 功能

图片来源: aws.amazon.com/blogs/aws/s…

  • 挑战:

    • 存储侧感知 Schema
    • 计算生态的兼容和集成

Column Family 支持

  • 背景:Hudi 数据湖场景下,支持部分列的快速更新
  • 在 Parquet 格式里引入 Column Family 概念,把需要更新的列拆成独立的 Column Family
  • 深度改造 Hudi 的 Update 和 Query 逻辑,根据 Column Family 选择覆盖对应的 Column Family
  • Update 操作实际效果有 10+ 倍的提升

课后

  1. 实操 TPCH 数据集,Parquet 和 ORC 两种格式的数据导入和Query
  1. 阅读 Dremel 论文,理解 Column 编码和重新装配细节

第二节:实操课-简易列式存储

前置克隆项目地址: github.com/theMYang/da… ,开课前1天完成项目运行,有问题可以记录一下,直播课跟着讲师节奏走,或者积极在弹幕区提问哈。

背景知识

行存 VS 列存

行存列存
按行写入数据,读取数据时需要读取不必要的列可以只读取请求的列
适用于OLTP系统适用于OLAP系统
适用于按记录读取数据适用于按列读取数据
不利于大数据集聚合统计操作利于大数据集聚合统计操作
不利于数据压缩利于数据压缩

Parquet 数据布局

项目目标

  1. 理解列存数据排布
  1. 代码设计 - 了解主要实现类的行为和功能
  1. 代码实现 - 以列存格式写入数据并正确读出
  1. 优化现有代码

项目设计

写入过程

示例表结构

a_field (int)b_field (long)
a1b1
a2b2
......
a100b100

写入流程

  1. 将一行行数据的字段拆分,分别存储在各字段对应的ChunkWriter中
  1. 当写入的数据达到一个块的长度,或是数据已经写完,此时会持久化数据,以及生成块的元数据

    1. 持久化数据至输出流中
    2. 生成元数据,包括每个字段的元信息以及该块已写数据量
  1. 当整个文件写完,将文件元信息写至输出流,并在文件最后记录元数据占用字节数。

image.png 最终文件组织示例

image.png

文件

image.png

读取流程

  1. 读取元数据
  1. 根据元数据,获取每个block中各个列信息
  1. 根据请求列,读取对应列数据
  1. 将读取到的数据拼成Record返回

代码流程图

写入流程

读取流程

项目仓库

代码结果

代码仓库: github.com/theMYang/da… 利用cloc工具进行代码统计:

编译

mvn clean install -DskipTests
复制代码

代码优化

  • 支持复杂类型读写

  • 增加列基本统计值,增加filter过滤不需要读取的数据

  • 支持数据压缩、编码

  • 预读取优化

  • 拆列优化

  • 对接Spark或Hive等计算引擎