Parquet 和 ORC:高性能列式存储

625 阅读15分钟

青训营 Parquet 和 ORC:高性能列式存储

概述

2022の夏天,半壶水响叮当的我决定充实一下自我


一、内容介绍

    青训营

总述

本节课程主要分为四个方面:

  1. 列式存储和行式存储的区别
  2. Parquet 列存格式的原理详解
  3. ORC 列存格式的原理详解,以及和Parquet 的对比
  4. 列存格式的演进

大数据计算生态

  • 理解 OLTP 的概念和场景
  • 理解 OLAP 的概念和场景
  • 数据湖分析,参见 AWS 和 阿里云的相关产品

计算引擎

  • 熟悉 Spark 和 Spark SQL 的使用
  • 熟悉 Hive 数仓平台的概念和使用
  • 理解 Hive Metastore (HMS) 的作用
  • 熟悉 Spark on Hive 的使用

存储

  • 熟悉 HDFS 的原理和使用
  • 熟悉对象存储 (例如 AWS S3、阿里云 OSS)的原理和使用

大数据格式

  • 在 Hive 中使用 DDL 创建 Table 使用 Parquet / ORC 格式
  • Dictionary Encoding
  • Bitmap Index / Roaring Bitmap

回顾

  • 典型的大数据系统可以简单的概括为由业务层、计算层和存储层三层结构组成
  • 在计算层,有各种计算引擎解决不同场景的问题,例如

    • Spark 主要适用于批式查询场景
    • Flink 主要适用于流式处理场景
    • Presto 主要适用于交互式查询场景
    • Hive 作为 Hadoop 框架上最早的SQL数仓产品,内置了 Hive Metastore (HMS) 组件,计算引擎上支持 Map-Reduce、Tez以及 Spark。目前 HMS 依然作为大数据场景下使用最为广泛的元数据服务。
  • 在存储层,HDFS 是 Hadoop 生态里第一个分布式存储服务,大数据生态里几乎所有的计算引擎都是基于 HDFS 协议和 HDFS Client 构建的。目前距离 HDFS 的发布已经有十多年的历史了,但是 HDFS 依旧在大数据存储领域占据重要的地位。

    • 近年来,随着公有云的大力发展,对象存储最为云上的存储底座,具有低成本、高可用、超大容量等优势,越来越多的云上大数据计算生态选择基于对象存储去构建。各个云产商的对象存储会提供各自的 HDFS Client 插件,支持以 HDFS Client 和协议访问对象存储。典型的例如:

  • 一个大数据查询作业,可以简单的概括为以下几个步骤:

    • 从存储层读取文件
    • 计算层解析文件内容,运行各种计算算子
    • 计算层输出结果,或者把结果写入存储层
  • 那么请大家思考以下几个问题,正式进入今天的课程

    • 计算层是如何理解文件里的数据呢?
    • 计算引擎需要读取文件里的全部内容呢?
    • 计算引擎写入的数据在文件内部是怎么组织的?
    • 不同计算引擎创建的数据文件可以相互访问吗?

引言

所有的大数据作业简单来说都可以简化

  • 从存储服务读取数据
  • 计算引擎解析和计算数据
  • 结果呈现 “如何高效从存储读取所需的数据”是决定大数据计算作业性能的关键因素。

学习目标

  • 理解列存的应用场景
  • 理解Parquet 和ORC的原理和区别
  • 了解列存格式中常见的编码和压缩算法
  • 学会在大数据分析中选择合适的列存格式,知道具体的调优方向

二、列存 vs 行存

概述数据格式层的作用和定位,对比列式存储和行式存储的基本原理和使用场景

2.1 数据格式层概述

  • 计算层:各种计算引擎
  • 存储层:承载数据的持久化存储
  • 数据格式层:定义了存储层文件内部的组织格式,计算引擎通过格式层的支持来读写文件
  • 严格意义上,并不是一个独立的层级,而是运行在计算层的一个Library

数据查询分析服务 1.png Object Storage/对象存储: en.wikipedia.org

Aws S3/阿里云osS/火山引擎TOS(操作系统)

2.2 分层视角下的数据形态

  • 存储层:File,Blocks
  • 格式层:File 内部的数据布局 (Layout + Schema)
  • 计算引擎:Rows + Columns

image.png

2.3 两种数据查询分析场景:OLTP vs OLAP

  • OLTP 和 OLAP 作为数据查询和分析领域两个典型的系统类型,具有不同的业务特征,适配不同的业务场景
  • 理解两者的区别可以帮助更好的理解行存和列存的设计背景
OLTPOLAP
典型场景在线业务系统,例如:订单、交易、社交、评论等数据仓库或者大数据分析系统,例如:决策分析、BI系统、推荐系统等
访问特征- 事务
- 实时性
- 低延时
- 高并发
- 高可用
- 弱事务性
- 近实时、离线分析
- 大吞吐
- 并发相对不高
- 可用性可以有一定的妥协
数据模型特征- Schema 相对简单
- 数据维度不多
- 数据规模较小
- Schema 复杂
- 数据维度很多,几百个Column 很常见
- 数据规模巨大

2.4. OLAP 列存 vs 行存

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

  • 每一行 (Row) 的数据在文件的数据空间里连续存放的
  • 读取整行的效率比较高,一次顺序 IO 即可
  • 在典型的 OLTP 型的分析和存储系统中应用广泛,例如:MySQL、Oracle、RocksDB 等
    • 关系型数据库:MySQL,Oracle ...
    • Key-Value数据库

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

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

  • 同列的数据类型一致,压缩编码的效率更好

  • 在典型的 OLAP 型分析和存储系统中广泛应用,例如:

    • 大数据分析系统:Hive、Spark,数据湖分析
    • 数据仓库:ClickHouse,Greenplum,阿里云 MaxCompute

2.5. 行存vs.列存总结

  • 格式层定义了数据的布局,连接计算引擎和存储服务
  • OLTP和OLAP场景话差异明显
  • 业务场景决定了技术实现,行存适用于OLTP,列存适用于OLAP

三、Parquet原理详解

详细介绍Parquet格式的原理、布局、以及和计算引擎的集成和优化

3.1 Parquet 详解


3.1.1 Parquet in Action - DDL

image.png

3.1.2 Parquet in Action - Spark

image.png

3.1.3 Parquet in Action - Spark

image.png

3.1.4 Parquet in Action - Parquet vs Text Format

image.png

3.1.5 Parquet in Action -Spark

image.png parquet-cli工具查看parquet文件的具体信息

github.com/apache/parq…

3.1.6 使用 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;

3.2 Dremel 数据模型

  • Protocol Buffer 定义
  • 支持可选和重复字段
  • 支持嵌套类型

image.png

3.2.1 Dremel数据模型- Continued

  • 构建出如下的语法树 image.png
  • 只有叶子节点的数据会被保存在数据文件里

image.png

3.3 数据文件布局

  • 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

3.3.1 Parquet CLI 工具

image.png

3.4 编码 Encoding

  • Plain/平原 直接存储原始数据

  • Run Length Encoding (RLE)/游程编码 :适用于列基数不大,重复值较多的场景,例如:Boolean、枚举、固定的选项等
    • Bit-Pack Encoding:配合 RLE编码使用,让整形数字存储的更加紧凑

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

image.png

  • Bit-Pack Encoding: 对于 32位或者64位的整型数而言,并不需要完整的 4B 或者 8B 去存储,高位的零在存储时可以省略掉。适用于最大值非常明确的情况下。

    • 一般配合 RLE 一起使用
  • Dictionary Encoding:适用于列基数 (Column Cardinality) 不大的字符串类型数据存储;

    • 构造字典表,用字典中的 Index 替换真实数据
    • 替换后的数据可以使用 RLE + Bit-Pack 编码存储

image.png

  • 默认场景下 parquet-mr 会自动根据数据特征选择
  • 业务自定义:org.apache.parquet.column.values.factory.ValuesWriterFactory

3.4.1 编码 Encoding

  • 默认场景下parquet-mr会自动根据数据特征选择
  • 业务自定义: org.apache.parquet.column.values.factory.ValuesWriterFactory

3.4.2 Parquet 中的数据编码

  • 在 Parquet 的 ColumnChunk 里,同一个 ColumnChunk 内部的数据都是同一个类型的,可以通过编码的方式更高效的存储

  • Parquet 支持的编码方式有如下:

enum Encoding {
  /** Default encoding.
   * BOOLEAN - 1 bit per value. 0 is false; 1 is true.
   * INT32 - 4 bytes per value.  Stored as little-endian.
   * INT64 - 8 bytes per value.  Stored as little-endian.
   * FLOAT - 4 bytes per value.  IEEE. Stored as little-endian.
   * DOUBLE - 8 bytes per value.  IEEE. Stored as little-endian.
   * BYTE_ARRAY - 4 byte length stored as little endian, followed by bytes.
   * FIXED_LEN_BYTE_ARRAY - Just the bytes.
   */
  PLAIN = 0;

  /** Group VarInt encoding for INT32/INT64.
   * This encoding is deprecated. It was never used
   */
  //  GROUP_VAR_INT = 1;

  /**
   * Deprecated: Dictionary encoding. The values in the dictionary are encoded in the
   * plain type.
   * in a data page use RLE_DICTIONARY instead.
   * in a Dictionary page use PLAIN instead
   */
  PLAIN_DICTIONARY = 2;

  /** Group packed run length encoding. Usable for definition/repetition levels
   * encoding and Booleans (on one bit: 0 is false; 1 is true.)
   */
  RLE = 3;

  /** Bit packed encoding.  This can only be used if the data has a known max
   * width.  Usable for definition/repetition levels encoding.
   */
  BIT_PACKED = 4;

  /** Delta encoding for integers. This can be used for int columns and works best
   * on sorted data
   */
  DELTA_BINARY_PACKED = 5;

  /** Encoding for byte arrays to separate the length values and the data. The lengths
   * are encoded using DELTA_BINARY_PACKED
   */
  DELTA_LENGTH_BYTE_ARRAY = 6;

  /** Incremental-encoded byte array. Prefix lengths are encoded using DELTA_BINARY_PACKED.
   * Suffixes are stored as delta length byte arrays.
   */
  DELTA_BYTE_ARRAY = 7;

  /** Dictionary encoding: the ids are encoded using the RLE encoding
   */
  RLE_DICTIONARY = 8;

  /** Encoding for floating-point data.
      K byte-streams are created where K is the size in bytes of the data type.
      The individual bytes of an FP value are scattered to the corresponding stream and
      the streams are concatenated.
      This itself does not reduce the size of the data but can lead to better compression
      afterwards.
   */
  BYTE_STREAM_SPLIT = 9;
}
复制代码

3.5 压缩Compression

image.png

3.5.1 压缩Compression -对比

Parquet 中的压缩方式

  • Page 完成 Encoding 以后,进行压缩
  • 支持多种压缩算法

    • snappy: 压缩速度快,压缩比不高,适用于热数据
    • gzip:压缩速度慢,压缩比高,适用于冷数据
    • zstd:新引入的压缩算法,压缩比和 gzip 差不多,而且压缩速度略低于 Snappy

image.png

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

3.6 索引 Index and Ordering

  • 和传统的数据库相比,索引支持非常简陋
  • 主要依赖 Min-Max Index 和 排序 来加速查找
  • Page:记录 Column 的 min_value 和 max_value
  • Footer 里的 Column Metadata 包含 ColumnChunk 的全部 Page 的 Min-Max Value
  • 一般建议和排序配合使用效果最佳
  • 一个 Parquet 文件只能定义一组 Sort Column,类似聚集索引概念

典型的查找过程:

  • 读取 Footer
  • 根据 Column 过滤条件,查找 Min-Max Index 定位到 Page
  • 根据 Page 的 Offset Index 定位具体的位置
  • 读取 Page,获取行号
  • 从其他 Column 读取剩下的数据

image.png

3.6.1 索引 Index - Bloom Filter

  • parquet.bloom.filter.enabled
  • 适用场景

    • 对于列基数比较大的场景,或者非排序列的过滤,Min-Max Index 很难发挥作用
  • 引入 Bloom Filter 加速过滤匹配判定
  • 每个 ColumnChunk 的头部保存 Bloom Filter 数据
  • Footer 记录 Bloom Filter 的 page offset

image.png

3.6.2 排序Ordering

  • 类似于聚集索引的概念
  • 排序帮助更好的过滤掉无关的RowGroup 或者Page
    • 对于少量数据 Seek/寻求 很有帮助
  • Parquet Format支持SortingColumns
  • Parquet Library目前没有支持
  • 依赖业务侧根据查询特征去保证顺序

3.7 过滤下推 Predicate PushDown/谓词下推

  • parquet-mr 库实现,实现高效的过滤机制
  • 引擎侧传入 Filter Expression
  • parquet-mr 转换成具体 Column 的条件匹配
  • 查询 Footer 里的 Column Index,定位到具体的行号
  • 返回有效的数据给引擎侧
  • 优点:
    • 在格式层过滤掉大多数不相关的数据
    • 减少真实的读取数据量

image.png

3.8 Spark集成-向量化读

  • ParquetFileFormat类
  • 向量化读开关:
    spark.sql.parquet.enableVectorizedReader
  • 向量化读是主流大数据分析引擎的标准实践,可以极大的提升查询性能
  • Spark 以 Batch的方式从 Parquet 读取数据,下推的逻辑也会适配 Batch/批 的方式

image.png 图片来源:databricks.com/session/vec…

  • 作为最通用的 Spark 数据格式
  • 主要实现在:ParquetFileFormat
  • 支持向量化读:spark.sql.parquet.enableVectorizedReader
  • 向量化读是主流大数据分析引擎的标准实践,可以极大的提升查询性能
  • Spark 以 Batch 的方式从 Parquet 读取数据,下推的逻辑也会适配 Batch 的方式

3.9 深入Dremel数据模型 - Repetition Level/重复级

image.png

3.9.1 深入Dremel 数据模型 - Definition Level/定义级别

  • Definition Level:用来记录在 fieldpath/字段路径 中,有多少个字段是可以不存在 (optional/repeated)(可选/重复) 而实际出现的
  • Name.Language.Code为例,Name和Language 都是可以不存在的
  • 第一个NULL字段,D是1,说明Name是存在的,但是Language是不存在的,保留原有的信息

3.9.2 深入Dremel数据模型 - Re-Assembly / 重新组装

image.png

3.10 Parquet 小结

  • 数据模型:基于Dremel
  • 文件布局:Footer + RowGroup + ColumnChunk + Page
  • Encoding: Page粒度,Plain / RLE /Dictionary
  • Compression: Snappy / Gzip / Zstd
  • Index: Column Index (Min-Max lndex)
  • Predicate PushDown/谓词下推

四、ORC 详解和对比

介绍另一个常见的列存格式ORC的原理,主要和Parquet做对比,同时介绍AliORC的重点优化

4.1. ORC简介

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

image.png

CREATE TABLE tabLe_name (xINT, y STRING)STORED AS ORC;

4.2 数据模型

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

image.png 图片来源: orc.apache.org

NestedType的不同实现对于IO模型有什么影响?

4.3 数据布局

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

image.png

4.4 ACID 特性

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

4.5 AliORC

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

4.5.1 索引增强

  • 支持 Clusterd Index,更快的主键查找
  • 支持 Bitmap Index,更快的过滤
    • Roaring Bitmap
      • 更高效的压缩保存 Bitmap Index
      • 以 16 bit 的 bitmap 空间为一个保存单元,每个单元可以是以下三种形式之一:
        • Array Container:只保存为 1 的 Index
        • Run Container:类似 RLE 编码
        • Bitset container:原始 bitmap 存储

image.png

4.5.2 AliORC - 小列聚合

  • 小列聚合,减少小 IO
    • 重排 ColumnChunk image.png

4.5.3 异步预取优化

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

image.png

4.5.4 思考

  • 小列聚合什么场景下效果比较好?
  • 异步预取什么场景下效果比较好?
  • 如何基于Parquet 实现同样的优化?

4.6 Parquet vs ORC 对比

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

4.6.1 性能对比 1

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

image.png

image.png 简单 Schema 复杂 Schema

4.6.2 性能对比 2

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

image.png

image.png

4.6.3 Parquet vs ORC 对比 - 选择

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

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

image.png


五、列存演进

介绍更广义场景下的列存,以及更多的列存演进和优化工作

5.1 数仓中的列存

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

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

image.png

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

  • ClickHouse的MergeTree引擎也是基于列存构建的
  • 默认情况下列按照 Column拆分的
  • 支持更加丰富的索引
  • 湖仓一体的大趋势

5.2 存储侧下推

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

image.png

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

5.3 Column Family 支持

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

image.png


晚安玛卡巴卡

快乐暑假