Parquet与 ORC :高性能列式存储 | 青训营笔记

801 阅读11分钟

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

  • 典型的大数据系统可以简单的概括为由业务层、计算层和存储层三层结构组成

  • 在计算层,有各种计算引擎解决不同场景的问题,例如

    • 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 和协议访问对象存储。典型的例如:

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

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

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

行存 VS 列存

数据格式层

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

img

分层视角下的数据形态

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

OLTP vs OLAP

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

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

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

img

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

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

  • 读取整列的效率较高

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

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

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

img

PARQUET 详解

img

  • parquet.apache.org/

  • Dremel 数据格式的社区实现

  • Apache的顶级项目

  • 查看源代码:

    • GitHub:

      • parquet-format:格式定义
      • parquet-mr:java 实现

使用 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;
 复制代码

数据模型

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

img

  • 构建出如下的语法树

img

  • 只有叶子节点的数据会被保存在数据文件里

数据文件布局

  • 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

img

Parquet CLI 工具

img

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;
 }
 复制代码
  • 更详细的解释参见:parquet.apache.org/docs/file-f…

  • 下面举例介绍常见的 Encoding:

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

    • img

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

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

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

img

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

Parquet 中的压缩方式

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

  • 支持多种压缩算法

    • snappy: 压缩速度快,压缩比不高,适用于热数据
    • gzip:压缩速度慢,压缩比高,适用于冷数据
    • zstd:新引入的压缩算法,压缩比和 gzip 差不多,而且压缩速度略低于 Snappy
  • 数据来源:quixdb.github.io/squash-benc…

img

img

img

img

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

索引和排序 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 读取剩下的数据

Bloom Filter 索引

  • parquet.bloom.filter.enabled

  • 适用场景

    • 对于列基数比较大的场景,或者非排序列的过滤,Min-Max Index 很难发挥作用
  • 引入 Bloom Filter 加速过滤匹配判定

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

  • Footer 记录 Bloom Filter 的 page offset

  • 更多参见:en.wikipedia.org/wiki/Bloom_…

img

过滤下推 Predicate PushDown

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

  • 引擎侧传入 Filter Expression

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

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

  • 返回有效的数据给引擎侧

  • 优点:

    • 在格式层过滤掉大多数不相关的数据
    • 减少真实的读取数据量

img

Parquet & Spark

  • 作为最通用的 Spark 数据格式

  • 主要实现在:ParquetFileFormat

  • 支持向量化读:spark.sql.parquet.enableVectorizedReader

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

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

img

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

ORC 详解

img

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

数据模型

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

img

图片来源: orc.apache.org

数据布局

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

img

图片来源: orc.apache.org

ACID 特性

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

img

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 存储

img

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

其他优化

  • 小列聚合,减少小 IO

    • 重排 ColumnChunk

img

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

  • 异步预取优化

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

img

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

Parquet vs ORC 对比

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

性能对比 1

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

imgimg

简单 Schema 复杂 Schema

来源:www.slideshare.net/HadoopSummi…

性能对比 2

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

imgimg

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

如何选择

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

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

列存演进

数仓中的列存

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

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

img

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

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

存储侧下推

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

img

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

  • 挑战:

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

Column Family 支持

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

img