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. chrome插件: yapi 接口TypeScript代码生成器

    前言 2020-09-12 天气晴,蓝天白云,微风,甚好. 前端Jser一枚,在公司的电脑前,浏览器打开着yapi的接口文档,那密密麻麻的接口数据,要一个一个的去敲打成为TypeScript的inte ...

  2. 趣图:后端工程师做 UI 的活

      扩展阅读 趣图:苦逼的后端工程师 趣图:前端 VS 后端 [趣图]开发人员腹黑的一面

  3. 搜索引擎学习(三)Lucene查询索引

    一.查询理论 创建查询:构建一个包含了文档域和语汇单元的文档查询对象.(例:fileName:lucene) 查询过程:根据查询对象的条件,在索引中找出相应的term,然后根据term找到对应的文档i ...

  4. 使用C#创建WebService实例

    新增WebService专案 更改服务程式名称 重命名程式名称 Service1.asmx 修改为 TestService.asmx 此时下面的cs代表文件也会跟着修改,但可发现,代码中的类名并没有跟 ...

  5. 依赖注入在 dotnet core 中实现与使用:4. 集成 Autofac

    本示例使用 .net core 5 rc-1 实现. 1. 添加 Nuget 包引用 使用 Autofac 当然要添加 Autofac 的 Nuget 包,主要涉及到两个: Autofac.Exten ...

  6. 《VC++ 深入详解》 第3版 这是盗版书么~。。。

    <VC++ 深入详解> 第3版 www.broadview.com.cn 书读到一小半,发现书重复了一部分,缺失一部分.... 难受~ 比较难继续下去了 有一样的小伙伴么~ <VC+ ...

  7. 文档生成工具——Doxygen

    参考: 1.https://blog.csdn.net/liao20081228/article/details/77322584 2.https://blog.csdn.net/wang150619 ...

  8. C++中cstring.h和string.h的区别

    转载:https://blog.csdn.net/qian_chun_qiang/article/details/80648691 1.string与cstring有什么区别 <string&g ...

  9. [学习笔记] 树上倍增求LCA

    倍增这种东西,听起来挺高级,其实功能还没有线段树强大.线段树支持修改.查询,而倍增却不能支持修改,但是代码比线段树简单得多,而且当倍增这种思想被应用到树上时,它的价值就跟坐火箭一样,噌噌噌地往上涨. ...

  10. Semaphore最详细解析

    官方解释: 一个计数信号量.在概念上,信号量维持一组许可证.如果有必要,每个acquire()都会阻塞,直到许可证可用,然后才能使用它.每个release()添加许可证,潜在地释放阻塞获取方.但是,没 ...