Parquet 与 ORC:高性能列式存储 | 青训营笔记
这是我参与「第四届青训营」笔记创作活动的的第11天。
一、课程概述
- 介绍数据的不同存储方式 行存和列存
- 介绍parquet的数据模型和实现
- 介绍ORC的模型和实现
- 介绍列存的演进与未来
二、详细内容
1. 行存与列存
1.1 数据格式层
- 定义数据的布局
- 连接计算引擎和存储服务
1.2 分层视角下的数据形态
- 存储层:file,blocks
- 格式层:file内部数据布局(layout+schema)
- 计算引擎:rows+columns
1.3 OLTP vs OLAP
1.4 OLTP:行式存储格式
- 每行数据在文件上是连续存储的
- 读取整行数据效率高,单次IO顺序读即可
- 典型系统
- 关系型数据库:MySQL、Oracle
- Key-Value数据库
1.5 OLAP:列式存储格式
- 每列数据在文件上连续存储
- 读取整列效率较高
- 同列数据类型一致,压缩编码效率好
- 典型系统
- 大数据分析系统:SQL-on-Hadoop、数据湖分析
- 数据仓库:ClickHouse、Greenplum、阿里云MaxCompute
2. Parquet详解
2.1 Parquet简介
- 大数据分析领域使用最广的列存格式
- Spark推荐存储格式
- Github
- parquet-format:格式定义
- parquet-mr:Java实现
2.2 Parquet in action-
- DDL
CREATE TABLE XX ()
STORED AS PARQUET TBLPROPERTIES...
-
Spark
- 生成的文件会有.parquet后缀
-
vs txt
- 高效压缩,文件大小小
-
parquet-cli查看文件具体信息
2.3 Dremel数据模型
- protocol buffer定义
- 支持可选和重复字段
- 支持嵌套类型
-
嵌套类型只保存叶子节点数据
-
列可能为optional或repeated,如何对应到record?
-
2.4 数据布局
- rowgroup:每一行组包含一定数量或固定大小的行集合
- columnchunk:rowgroup按照列切分成多个columnchunk
- page:columnchunk内切分为page,一般8kb大小,压缩和编码基本单元
- footer保存文件元信息
2.5 编码 encoding
-
plain 直接存储原始数据
-
RLE:runtime length encoding 适用于列基数不大,重复值较多的场景
- bit-pack encoding:配合RLE使用,让整形数字存储更紧凑
-
字典编码 dictionary encoding:适用于列基数不大的场景,构造字典表,写入到dictionary page,把数据用字典index替换然后用RLE编码
-
默认parquet-mr会根据数据特征进行选择
-
业务自定义
2.6 压缩 copression
- page完成encoding后进行压缩
- 多种算法
- snappy压缩快、压缩比不高、适用于热数据
- gzip:压缩慢、压缩比高、适用于冷数据
- zstd综合两种
2.7 索引 index
- 索引支持非常简陋
- min-max:记录page内column的min和max
- column index:footer里column metadata包含columnchunk全部page的min-max value
- offset index:page在文件中的offset和page的row range
- bloom-filter
- 对于列基数比较大、非排序列的过滤,min-max难发挥作用
- bloom-filter加速过滤匹配判断
- columnchunk头部保存bloom filter数据
- footer记录filter的page offset
2.8 排序 ordering
- 聚集索引
- 帮助更好的过滤掉无关rowgroup或page
- 对少量数据seek有帮助
- format支持sorting columns
- 依赖业务侧根据查询特征保证顺序
2.9 过滤下推 predicate pushdown
- 依赖parquet-mr库实现,实现高效过滤机制
- 引擎侧传入Filter Expression
- parquet-mr转换成具体column的条件匹配
- 查询footer内column index,定位具体行号
- 返回有效的数据
2.10 Spark集成-向量化读
- parquetFileFormat类
- 主流大数据分析引擎的标准实践、极大提升查询性能
- 以batch形式从parquet读取,下推逻辑也适配batch形式
2.11 repetition level
- 该字段在field path上第几个重复字段上出现
- 0:标识新的record
- name.language.code为例,name是第一个重复字段,language是第二个
- definition level用来记录在field path中多少个字段可以是不存在,而实际出现的
2.12 re-assembly
- 根据全部或者部分列数据,重新构造record
- 实现方法:构造FSM状态机
- 根据同一个column下一个记录的repartition level决定继续读的列
3. ORC详解
3.1 ORC简介
- 大数据分析领域使用最广的列存格式之一
- 出自于Hive 模型
3.2 数据模型
- ORC会给包括根节点在内的中间节点都创建一个column
- 嵌套类型或集合类与parquet差别较大
- optional和repeated字段依赖父节点记录额外信息来重新assemble数据
3.3 数据布局
- 类似parquet
- rooter+stripe+column+page(rowgroup)结构
- encoding/compression/index支持上与parquet基本一致
3.4 ACID特性
- 支持Hive Transactions实现,只有Hive本身即成
- 类似数据湖三剑客
- 基于Base+Delta+compaction设计
3.5 AliORC
- 在阿里云计算广泛应用,是ORC深度定制版
- 索引增强
- 支持clustered index,更快的主键查找
- 支持bitmap index,更快过滤
- roaring index
- 小列聚合
- 聚合较小列减少小IO
- 重排chunk
- 异步预取
- 异步预取数据,计算逻辑和数据读取并行化
3.6 Parquet和ORC对比
- 原理层面最大差别就是nested type和复杂类型处理上
- parquet算法上要复杂很多,带来CPU开销比ORC略大
- ORC算法上相对简单,但是要读取更多数据
- 取决于业务场景决定优势
- 性能
- parquet在复杂schema下算法开销影响较大
- 在spark场景下parquet工作更好,Hive场景下ORC更好
- 选择
- 没有明显差距
- 依赖于数据集和测试环境
- 根据实际业务做充分测试调优
- Spark生态下parquet较普遍,Hive生态下ORC有原生支持
- 整体上Spark比Hive更有优势,所以Parquet可能比ORC更好
4. 列存演进
4.1 数仓中的列存
- ClickHouse的MergeTree引擎基于列存构建
- 默认情况下列按照column拆分
- 支持更加丰富的索引
- 湖仓一体大趋势
4.2 存储侧下推
- 更多下推工作下沉到存储服务侧
- 越接近数据下推过滤效率越高
- 例:AWS S3
- 挑战:存储侧感知schema/计算生态兼容集成
4.3 column family支持
- Hudi数据湖场景下支持部分列快速更新
- 在parquet格式内引入column family概念,把需要更新的列拆成独立的column family
- 深度改造hudi的update和query逻辑,根据column family选择覆盖
- update操作实际效果10+倍提升
三、个人总结
本节课我学习了行存和列存的不同之处,并且接触了列存的常规实现方式 如parquet和ORC等。列存是非常重要的存储格式,它在实际运用层面拥有比行存更好的空间利用率与更好的按列读取能力。parquet和ORC等是列存实际的实现,而列存也在不断演进之中。