Parquet 源码解析
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 源码解析的更多相关文章
- [源码解析] NVIDIA HugeCTR,GPU版本参数服务器--- (2)
[源码解析] NVIDIA HugeCTR,GPU版本参数服务器--- (2) 目录 [源码解析] NVIDIA HugeCTR,GPU版本参数服务器--- (2) 0x00 摘要 0x01 总体流程 ...
- [源码解析] NVIDIA HugeCTR,GPU版本参数服务器---(3)
[源码解析] NVIDIA HugeCTR,GPU版本参数服务器---(3) 目录 [源码解析] NVIDIA HugeCTR,GPU版本参数服务器---(3) 0x00 摘要 0x01 回顾 0x0 ...
- [源码解析] NVIDIA HugeCTR,GPU版本参数服务器--- (4)
[源码解析] NVIDIA HugeCTR,GPU版本参数服务器--- (4) 目录 [源码解析] NVIDIA HugeCTR,GPU版本参数服务器--- (4) 0x00 摘要 0x01 总体流程 ...
- 【原】Android热更新开源项目Tinker源码解析系列之三:so热更新
本系列将从以下三个方面对Tinker进行源码解析: Android热更新开源项目Tinker源码解析系列之一:Dex热更新 Android热更新开源项目Tinker源码解析系列之二:资源文件热更新 A ...
- 【原】Android热更新开源项目Tinker源码解析系列之一:Dex热更新
[原]Android热更新开源项目Tinker源码解析系列之一:Dex热更新 Tinker是微信的第一个开源项目,主要用于安卓应用bug的热修复和功能的迭代. Tinker github地址:http ...
- 【原】Android热更新开源项目Tinker源码解析系列之二:资源文件热更新
上一篇文章介绍了Dex文件的热更新流程,本文将会分析Tinker中对资源文件的热更新流程. 同Dex,资源文件的热更新同样包括三个部分:资源补丁生成,资源补丁合成及资源补丁加载. 本系列将从以下三个方 ...
- 多线程爬坑之路-Thread和Runable源码解析之基本方法的运用实例
前面的文章:多线程爬坑之路-学习多线程需要来了解哪些东西?(concurrent并发包的数据结构和线程池,Locks锁,Atomic原子类) 多线程爬坑之路-Thread和Runable源码解析 前面 ...
- jQuery2.x源码解析(缓存篇)
jQuery2.x源码解析(构建篇) jQuery2.x源码解析(设计篇) jQuery2.x源码解析(回调篇) jQuery2.x源码解析(缓存篇) 缓存是jQuery中的又一核心设计,jQuery ...
- Spring IoC源码解析——Bean的创建和初始化
Spring介绍 Spring(http://spring.io/)是一个轻量级的Java 开发框架,同时也是轻量级的IoC和AOP的容器框架,主要是针对JavaBean的生命周期进行管理的轻量级容器 ...
随机推荐
- Python 之父为什么嫌弃 lambda 匿名函数?
Python 支持 lambda 匿名函数,其扩展的 BNF 表示法是lambda_expr ::= "lambda" [parameter_list] ":" ...
- 7.Semaphore-信号量
- Windows批处理(cmd/bat)常用命令学习
前言 批处理文件(batch file)包含一系列 DOS命令,通常用于自动执行重复性任务.用户只需双击批处理文件便可执行任务,而无需重复输入相同指令.编写批处理文件非常简单,但难点在于确保一切按顺序 ...
- Python爬虫练习:爬取800多所大学学校排名、星级等
前言 国内大学最新排名,北大反超,浙大仅第四,中科大跌至第八 时隔五年,"双一流"大学即将迎来首次大考,这也是继改变高校评断标准之后,第一次即将以官方对外发布,自然是引来了许多人的 ...
- 强大的 Node.js Web 框架 - Daze.js
去年年初对 Node.js 比较感兴趣,也用了很多 Node.js 的框架,但是开发体验不是特别好,我之前也是后端转前端,然后再接触 Node.js ,所以用过挺多的服务端框架,相对js而言,设计一款 ...
- 面试题总结:可能是全网最好的MySQL重要知识点
标题有点标题党的意思,但希望你在看了文章之后不会有这个想法--这篇文章是作者对之前总结的 MySQL 知识点做了完善后的产物,可以用来回顾MySQL基础知识以及备战MySQL常见面试问题. 什么是My ...
- Nginx【常见知识点速查】
文章更新时间:2020/04/10 一.简介 定义:Nginx是一个高性能的HTTP和反向代理web服务器 作用: 反向代理 正向代理 负载均衡 HTTP静态资源服务器(动静分离) 二.正向代理与反向 ...
- Android App 侧边栏菜单的简单实现
效果图 Layout 注意事项 想要实现侧边栏,需要配合使用DrawerLayout.因为会用到嵌套布局,所以根布局不能是 ConstraintLayout,最好使用 LinearLayout 布局. ...
- C++指针delete后还要置为null
非常好的一篇说明: 转载:https://blog.csdn.net/qq_36570733/article/details/80043321 众所周知,最开始我们用new来创建一个指针,那么等我们用 ...
- Example Code for a TMP102 I2c Thermometer————Arduino
参考:https://playground.arduino.cc/Code/TMP102/ Example Code for a TMP102 I2c Thermometer I've fairly ...