date: 2020-07-20 16:15:00

updated: 2020-07-27 13:40:00

Parquet 源码解析

Parquet文件是以二进制方式存储的,所以是不可以直接读取的,文件中包括该文件的数据和元数据,因此Parquet格式文件是自解析的。在HDFS文件系统和Parquet文件中存在如下几个概念。

  • HDFS块(Block):它是HDFS上的最小的副本单位,HDFS会把一个Block存储在本地的一个文件并且维护分散在不同的机器上的多个副本,通常情况下一个Block的大小为256M、512M等。
  • HDFS文件(File):一个HDFS的文件,包括数据和元数据,数据分散存储在多个Block中。
  • 行组(Row Group):按照行将数据物理上划分为多个单元,每一个行组包含一定的行数,在一个HDFS文件中至少存储一个行组,Parquet读写的时候会将整个行组缓存在内存中,所以如果每一个行组的大小是由内存大的小决定的,例如记录占用空间比较小的Schema可以在每一个行组中存储更多的行。
  • 列块(Column Chunk):在一个行组中每一列保存在一个列块中,行组中的所有列连续的存储在这个行组文件中。一个列块中的值都是相同类型的,不同的列块可能使用不同的算法进行压缩。
  • 页(Page):每一个列块划分为多个页,一个页是最小的编码的单位,在同一个列块的不同页可能使用不同的编码方式。

一般按照 Block 大小来设置行组的大小,每一个mapper处理数据的最小单位是一个 block,这样就可以把每一个行组由一个mapper处理,提高任务执行并行度

MapredParquetOutputFormat.getHiveRecordWriter()

-> DataWritableWriteSupport.setSchema(HiveSchemaConverter.convert((List)columnNames, columnTypes), jobConf) 根据列名和类型,生成schema

-> ParquetRecordWriterWrapper.ParquetRecordWriterWrapper() 生成一个 ParquetOutputFormat 对象

-> ParquetFileWriter

创建一个文件写入对象,

ParquetFileWriter(Configuration configuration, MessageType schema, Path file, ParquetFileWriter.Mode mode, long rowGroupSize, int maxPaddingSize)

参数包括:conf配置,schema表结构,file文件路径,mode文件写入的模式(新建或覆写),blockSizeHDFS块大小,也就是一个行组的大小

之后会先在这个文件的最开始的位置写入四个字节的 "PAR1" 表示该文件为 parquet 格式,"parquet.writer.max-padding", 8388608

根据这个文件写入对象,去创建一个

-> InternalParquetRecordWriter

创建一个 InternalParquetRecordWriter(fileWriter, writeSupport, schema, writeContext.getExtraMetaData(), (long)blockSize, compressor, validating, encodingProps); 对象,每读取一条数据,调用该对象的 write() 方法写入,底层实现是调用 DataWritableWriter.write(T value)

InternalParquetRecordWriter 类
public void write(T value) throws IOException, InterruptedException {
this.writeSupport.write(value);
++this.recordCount; // 每写入一条数据,记录数+1
this.checkBlockSizeReached();
} private void checkBlockSizeReached() throws IOException {
if (this.recordCount >= this.recordCountForNextMemCheck) { // 默认值 this.recordCountForNextMemCheck = 100L,每调用一次修改为新值
// this.pageStore = new ColumnChunkPageWriteStore(this.compressor, this.schema, this.props.getAllocator());
// this.columnStore = this.props.newColumnWriteStore(this.schema, this.pageStore);
// props 就是 ParquetProperties
// 已写入到内存中的记录的总大小,除以记录数,得到平均一条记录的大小
long memSize = this.columnStore.getBufferedSize();
long recordSize = memSize / this.recordCount;
if (memSize > this.nextRowGroupSize - 2L * recordSize) {
// 如果内存中的记录总大小 > 行组大小 - 2*平均一条记录大小 ???
// 当 memSize > GroupSize(约等于blocksize),就可以刷到磁盘
LOG.debug("mem size {} > {}: flushing {} records to disk.", new Object[]{memSize, this.nextRowGroupSize, this.recordCount});
// 刷写内存的记录
this.flushRowGroupToStore();
this.initStore(); // 重置 pageStore、columnStore 等信息
this.recordCountForNextMemCheck = Math.min(Math.max(100L, this.recordCount / 2L), 10000L);
this.lastRowGroupEndPos = this.parquetFileWriter.getPos();
} else {
// 当目前内存中的记录的总大小还不够大时,修改 recordCountForNextMemCheck 的值,每次会增大一点,差不多相当于之前增量的一半,如果recordsize变化不大的话
this.recordCountForNextMemCheck = Math.min(Math.max(100L, (this.recordCount + (long)((float)this.nextRowGroupSize / (float)recordSize)) / 2L), this.recordCount + 10000L);
LOG.debug("Checked mem at {} will check again at: {}", this.recordCount, this.recordCountForNextMemCheck);
}
}
} private void flushRowGroupToStore() throws IOException {
// 先将null值刷写出去???
this.recordConsumer.flush();
LOG.debug("Flushing mem columnStore to file. allocated memory: {}", this.columnStore.getAllocatedSize());
if (this.columnStore.getAllocatedSize() > 3L * this.rowGroupSizeThreshold) {
LOG.warn("Too much memory used: {}", this.columnStore.memUsageString());
} if (this.recordCount > 0L) {
// 获取block的元数据信息
this.parquetFileWriter.startBlock(this.recordCount);
// 把每一列的值写到文件
this.columnStore.flush();
this.pageStore.flushToFileWriter(this.parquetFileWriter);
this.recordCount = 0L;
this.parquetFileWriter.endBlock();
this.nextRowGroupSize = Math.min(this.parquetFileWriter.getNextRowGroupSize(), this.rowGroupSizeThreshold);
} this.columnStore = null;
this.pageStore = null;
} ColumnWriteStoreV1 类
this.columnStore = ColumnWriteStoreV1 时,执行的方法
public void flush() {
// private final Map<ColumnDescriptor, ColumnWriterV1> columns = new TreeMap();
Collection<ColumnWriterV1> values = this.columns.values();
Iterator var2 = values.iterator(); while(var2.hasNext()) {
ColumnWriterV1 memColumn = (ColumnWriterV1)var2.next();
memColumn.flush();
}
}
ColumnWriterV1 类
public void flush() {
if (this.valueCount > 0) {
this.writePage();
// 将所有数据都转换成了Bytes
// this.pageWriter.writePage(BytesInput.concat(new BytesInput[]{this.repetitionLevelColumn.getBytes(), this.definitionLevelColumn.getBytes(), this.dataColumn.getBytes()}), this.valueCount, this.statistics, this.repetitionLevelColumn.getEncoding(), this.definitionLevelColumn.getEncoding(), this.dataColumn.getEncoding());
} DictionaryPage dictionaryPage = this.dataColumn.toDictPageAndClose();
if (dictionaryPage != null) {
try {
this.pageWriter.writeDictionaryPage(dictionaryPage);
} catch (IOException var3) {
throw new ParquetEncodingException("could not write dictionary page for " + this.path, var3);
}
this.dataColumn.resetDictionary();
}
}
ParquetProperties 默认变量信息
private Builder() {
this.pageSize = 1048576;
this.dictPageSize = 1048576;
this.enableDict = true;
this.writerVersion = ParquetProperties.DEFAULT_WRITER_VERSION => "v1";
this.minRowCountForPageSizeCheck = 100;
this.maxRowCountForPageSizeCheck = 10000;
this.estimateNextSizeCheck = true;
this.allocator = new HeapByteBufferAllocator();
this.valuesWriterFactory = ParquetProperties.DEFAULT_VALUES_WRITER_FACTORY;
}

org.apache.parquet.hadoop.api 包下

  • ReadSupport

    • GroupReadSupport
    • DataWritableReadSupport
  • WriteSupport
    • GroupWriteSupport
    • DataWritableWriteSupport

映射下推(Project PushDown)

说到列式存储的优势,映射下推是最突出的,它意味着在获取表中原始数据时只需要扫描查询中需要的列,由于每一列的所有值都是连续存储的,所以分区取出每一列的所有值就可以实现TableScan算子,而避免扫描整个表文件内容。

在Parquet中原生就支持映射下推,执行查询的时候可以通过Configuration传递需要读取的列的信息,这些列必须是Schema的子集,映射每次会扫描一个Row Group的数据,然后一次性得将该Row Group里所有需要的列的Cloumn Chunk都读取到内存中,每次读取一个Row Group的数据能够大大降低随机读的次数,除此之外,Parquet在读取的时候会考虑列是否连续,如果某些需要的列是存储位置是连续的,那么一次读操作就可以把多个列的数据读取到内存。

谓词下推(Predicate PushDown)

在数据库之类的查询系统中最常用的优化手段就是谓词下推了,通过将一些过滤条件尽可能的在最底层执行可以减少每一层交互的数据量,从而提升性能,例如”select count(1) from A Join B on A.id = B.id where A.a >数据 10 and B.b < 100”SQL查询中,在处理Join操作之前需要首先对A和B执行TableScan操作,然后再进行Join,再执行过滤,最后计算聚合函数返回,但是如果把过滤条件A.a > 10和B.b < 100分别移到A表的TableScan和B表的TableScan的时候执行,可以大大降低Join操作的输入数据。

无论是行式存储还是列式存储,都可以在将过滤条件在读取一条记录之后执行以判断该记录是否需要返回给调用者,在Parquet做了更进一步的优化,优化的方法时对每一个Row Group的每一个Column Chunk在存储的时候都计算对应的统计信息,包括该Column Chunk的最大值、最小值和空值个数。通过这些统计值和该列的过滤条件可以判断该Row Group是否需要扫描。另外Parquet未来还会增加诸如Bloom Filter和Index等优化数据,更加有效的完成谓词下推。

在使用Parquet的时候可以通过如下两种策略提升查询性能:1、类似于关系数据库的主键,对需要频繁过滤的列设置为有序的,这样在导入数据的时候会根据该列的顺序存储数据,这样可以最大化的利用最大值、最小值实现谓词下推。2、减小行组大小和页大小,这样增加跳过整个行组的可能性,但是此时需要权衡由于压缩和编码效率下降带来的I/O负载。

参考地址

4-byte magic number "PAR1"
<Column 1 Chunk 1 + Column Metadata>
<Column 2 Chunk 1 + Column Metadata>
...
<Column N Chunk 1 + Column Metadata>
<Column 1 Chunk 2 + Column Metadata>
<Column 2 Chunk 2 + Column Metadata>
...
<Column N Chunk 2 + Column Metadata>
...
<Column 1 Chunk M + Column Metadata>
<Column 2 Chunk M + Column Metadata>
...
<Column N Chunk M + Column Metadata>
File Metadata
4-byte length in bytes of file metadata
4-byte magic number "PAR1"

Parquet 源码解析的更多相关文章

  1. [源码解析] NVIDIA HugeCTR,GPU版本参数服务器--- (2)

    [源码解析] NVIDIA HugeCTR,GPU版本参数服务器--- (2) 目录 [源码解析] NVIDIA HugeCTR,GPU版本参数服务器--- (2) 0x00 摘要 0x01 总体流程 ...

  2. [源码解析] NVIDIA HugeCTR,GPU版本参数服务器---(3)

    [源码解析] NVIDIA HugeCTR,GPU版本参数服务器---(3) 目录 [源码解析] NVIDIA HugeCTR,GPU版本参数服务器---(3) 0x00 摘要 0x01 回顾 0x0 ...

  3. [源码解析] NVIDIA HugeCTR,GPU版本参数服务器--- (4)

    [源码解析] NVIDIA HugeCTR,GPU版本参数服务器--- (4) 目录 [源码解析] NVIDIA HugeCTR,GPU版本参数服务器--- (4) 0x00 摘要 0x01 总体流程 ...

  4. 【原】Android热更新开源项目Tinker源码解析系列之三:so热更新

    本系列将从以下三个方面对Tinker进行源码解析: Android热更新开源项目Tinker源码解析系列之一:Dex热更新 Android热更新开源项目Tinker源码解析系列之二:资源文件热更新 A ...

  5. 【原】Android热更新开源项目Tinker源码解析系列之一:Dex热更新

    [原]Android热更新开源项目Tinker源码解析系列之一:Dex热更新 Tinker是微信的第一个开源项目,主要用于安卓应用bug的热修复和功能的迭代. Tinker github地址:http ...

  6. 【原】Android热更新开源项目Tinker源码解析系列之二:资源文件热更新

    上一篇文章介绍了Dex文件的热更新流程,本文将会分析Tinker中对资源文件的热更新流程. 同Dex,资源文件的热更新同样包括三个部分:资源补丁生成,资源补丁合成及资源补丁加载. 本系列将从以下三个方 ...

  7. 多线程爬坑之路-Thread和Runable源码解析之基本方法的运用实例

    前面的文章:多线程爬坑之路-学习多线程需要来了解哪些东西?(concurrent并发包的数据结构和线程池,Locks锁,Atomic原子类) 多线程爬坑之路-Thread和Runable源码解析 前面 ...

  8. jQuery2.x源码解析(缓存篇)

    jQuery2.x源码解析(构建篇) jQuery2.x源码解析(设计篇) jQuery2.x源码解析(回调篇) jQuery2.x源码解析(缓存篇) 缓存是jQuery中的又一核心设计,jQuery ...

  9. Spring IoC源码解析——Bean的创建和初始化

    Spring介绍 Spring(http://spring.io/)是一个轻量级的Java 开发框架,同时也是轻量级的IoC和AOP的容器框架,主要是针对JavaBean的生命周期进行管理的轻量级容器 ...

随机推荐

  1. Oracle学习(十四)分表分区

    一.前言 大数据量的查询,不仅查询速度非常慢,而且还会导致数据库经常宕机,在尝试添加索引及查询方式修改后,还有没有更有效的解决方案呢? 分库.分表.分区这些概念咱就应该了解一下. 二.分表 假如一个大 ...

  2. mysql load_file()

    本地mysql注入读取配置文件 遇到的问题 简单记录一下. 本地测试时,读取文件发现无论怎样都返回为NULL. >> select load_file('c:/xx/xx/xx/x.txt ...

  3. python3 进行接口测试

    最近有研究接口测试,然后查了查资料,发现有两种方法,一种是使用urllib库,一种是使用requests库.而在这里,我使用的是requests库,为什么要用这个呢? 从官方文档看出,python的标 ...

  4. Feedforward neural networks前馈神经网络

    Feedforward neural networks or deep feedforward networks or multilayer perceptrons Pass input throug ...

  5. 使用Ajax新闻系统管理需求分析

      新闻系统管理需求分析 1.1项目背景 新闻发布系统(News Release System or Content Management System),是一个基于新闻和内容管理的全站管理系统,本系 ...

  6. Java知识系统回顾整理01基础03变量03字面值

    一.字面值定义 创建一个Hero对象会用到new关键字,但是给一个基本类型变量赋值却不是用new. 因为基本类型是Java语言里的一种内置的特殊数据类型,并不是某个类的对象.  给基本类型的变量赋值的 ...

  7. c语言 static的用法

    static在c里面可以用来修饰变量,也可以用来修饰函数.先看用来修饰变量的时候.变量在c里面可分为存在全局数据区.栈和堆里.其实我们平时所说的堆栈是栈而不是堆,不要弄混.int a ;int mai ...

  8. 【题解】[JSOI2007]字符加密

    Link \(\text{Solution:}\) 后缀数组第一题祭-- 观察一下,这个是让求一个环形的原字符串的后缀,我们可以考虑一下断环为链. 对于\(aba\)我们扩展成\(abaaba,\)则 ...

  9. 使用HTML的基本结构创建网页

    1.       网页的扩展名--html或htm 2.       如何新建网页? 步骤1: 在电脑的空白处,右键选择-->新建--文本文档 步骤2: 把txt的扩展名,改成html或htm, ...

  10. Centos7系统下Docker开启认证的远程端口2376配置教程

    docker开启2375会存在安全漏洞 暴露了2375端口的Docker主机.因为没有任何加密和认证过程,知道了主机IP以后,,任何人都可以管理这台主机上的容器和镜像,以前贪图方便,只开启了没有认证的 ...