目录
概述
hive文件存储格式包括以下几类
一、TEXTFILE
二、SEQUENCEFILE
三、RCFile文件格式
概述历史
RCFile使用
基于行存储的优点和缺点
基于列存储的优点和缺点
源码分析
1. Writer
2. append
RCFile的索引机制
flushRecords的具体逻辑
RCFile的Sync机制
RCFileclose过程
数据读取和Lazy解压
行组大小
四、ORC文件格式
ORC File格式的优点
设计思想
Stripe结构
Hive里面如何用ORCFile
五、Parquet文件格式
概述
Parquet数据模型
Parquet文件结构
Definition Level
Repetition Level
Metadata
其中TEXTFILE为默认格式,建表时不指定默认为这个格式,导入数据时会直接把数据文件拷贝到hdfs上不进行处理。
sequencefile,rcfile,orcfile格式的表不能直接从本地文件导入数据,数据要先导入到textfile格式的表中, 然后再从表中用insert导入sequencefile,rcfile,orcfile表中。
默认格式,数据不做压缩,磁盘开销大,数据解析开销大。
可结合Gzip、Bzip2使用(系统自动检查,执行查询时自动解压),但使用这种方式,hive不会对数据进行切分,从而无法对数据进行并行操作。
示例:
create table if not exists textfile_table(
site string,
url string,
pv bigint,
label string)
row format delimited fields terminated by '\t'
stored as textfile;
插入数据操作:
set hive.exec.compress.output=true;
set mapred.output.compress=true;
set mapred.output.compression.codec=org.apache.hadoop.io.compress.GzipCodec;
set io.compression.codecs=org.apache.hadoop.io.compress.GzipCodec;
insert overwrite table textfile_table select * from textfile_table;
SequenceFile是Hadoop API提供的一种二进制文件支持,其具有使用方便、可分割、可压缩的特点。
SequenceFile支持三种压缩选择:none,record,block。Record压缩率低,一般建议使用BLOCK压缩。
示例:
create table if not exists seqfile_table(
site string,
url string,
pv bigint,
label string)
row format delimited fields terminated by '\t'
stored as sequencefile;
插入数据操作:
set hive.exec.compress.output=true;
set mapred.output.compress=true;
set mapred.output.compression.codec=org.apache.hadoop.io.compress.GzipCodec;
set io.compression.codecs=org.apache.hadoop.io.compress.GzipCodec;
set mapred.output.compression.type=BLOCK;
insert overwrite table seqfile_table select * from textfile_table;
create table if not exists rcfile_table(
site string,
url string,
pv bigint,
label string)
row format delimited fields terminated by '\t'
stored as rcfile;
下图为Hadoop block中的基于行存储的示例图
优点是:具备快速数据加载和动态负载的高适应能力,因为行存储保证了相同记录的所有域都在同一个集群节点
缺点是:但是它不太满足快速的查询响应时间的要求,特别是在当查询仅仅针对所有列中的少数几列时,它就不能直接定位到所需列而跳过不需要的列,由于混合着不同数据值的列,行存储不易获得一个极高的压缩比。
下图为Hadoop block中的基于列存储的示例图
优点是:这种结构使得在查询时能够直接读取需要的列而避免不必要列的读取,并且对于相似数据也可以有一个更好的压缩比。
缺点是:它并不能提供基于Hadoop系统的快速查询处理,也不能保证同一记录的所有列都存储在同一集群节点之上,也不适应高度动态的数据负载模式。
RCFile设计思想
RCFile结合列存储和行存储的优缺点,Facebook于是提出了基于行列混合存储的RCFile,该存储结构遵循的是“先水平划分,再垂直划分”的设计理念。先将数据按行水平划分为行组,这样一行的数据就可以保证存储在同一个集群节点;然后在对行进行垂直划分。
RCFile是在Hadoop HDFS之上的存储结构,该结构强调:
通常而言,RCFile文件的整个写入过程大致可以分为三步:
我们也按照这三步来分析相应的源码。
Writer在构建函数中大体做了以下三件事情:
1)初始化一些变量值;
a. RECORD_INTERVAL:表示多少“行”数据形成一个Row Split(Record)和columnsBufferSize配合使用;
b. columnNumber:表示当前RCFile文件存储着多少“列”的数据;
c. Metadata:Metadata实例仅仅保存一个属性“hive.io.rcfile.column.number”,值为columnNumber,该实例会被序列化到RCFile文件头部;
d. columnsBufferSize:缓存数目(行数)上限阀值,超过这个数值就会将缓存的数据(行)形成一个Row Split(Record);
2)构建一些数据结构;
a. columnValuePlainLength:保存着一个Row Split(Record)内部各列原始数据的大小;
b. columnBuffers:保存着一个Row Split(Record)内部各列原始数据;
c. key:保存着一个Row Split(Record)的元数据;
d. plainTotalColumnLength:保存着一个RCFile文件内各列原始数据的大小;
e. comprTotalColumnLength:保存着一个RCFile文件内各列原始数据被压缩后的大小;
3)初始化文件输出流,并写入文件头部信息;
a. 初始化RCFile文件输出流(FSDataOutputStream);useNewMagic默认值为true,本文也以此默认值进行讨论。
b. initializeFileHeader;1. 写出MAGIC;2. 写出当前RCFile版本号(不同版本的RCFile具有不同的格式);
c. writeFileHeader;1. 写出是否使用压缩,本文按使用压缩讨论;2. 写出压缩编/解码器(CompressionCodec)类名;3. 序列化Metadata实例;
d. finalizeFileHeader;
写出一个“同步标志位”,表示RCFile文件头部信息到此结束。
我们可以得出RCFile Header的结构如下:
version |
3 bytes of magic header “RCF”, followed by 1 byte of actual version number |
compression |
A boolean which specifies if compression is turned on for keys/values in this file |
compression codec |
CompressionCodec class which is used for compression of keys and/or values |
metadata |
Metadata for this file |
sync |
A sync marker to denote end of the header |
RCFile.Writer写入数据时要求以BytesRefArrayWritable实例的形式进行“追加”,亦即一个BytesRefArrayWritable实例表示一“行”数据。
“追加”“行”数据的过程如下:
1)从一“行”数据(即BytesRefArrayWritable实例val)中解析出各“列”数据缓存到对应的ColumnBuffer(即columnBuffers[i])中;如果这“行”数据包含的“列”小于columnNumber,则缺失的列会被填充为“空值”(即BytesRefWritable.ZeroBytesRefWritable);
我们可以看出,RCFile在“追加”数据的时候还是以“行”的方式进行,“行转列”是在内部进行转换的。转换之后的列数据(列数为columnNumber)被缓存到各自的“Buffer”中,也就是说每一列都有自己独立的缓存区(ColumnBuffer),这是为后来的“列式存储”作准备的。
ColumnBuffer
这里重点介绍一下这个ColumnBuffer,它的作用就是用来缓存“列数据”的,
内部包含两个实例变量,如它们的变量名称所言,它们实际也是用来缓存数据的,columnValBuffer用来缓存“列值”的数据,valLenBuffer用来缓存“列值”各自的长度,这两个内部的缓存区都是NonSyncDataOutputBuffer实例。
从这三部分代码可以看出,NonSyncDataOutputBuffer内部的缓存区实际是使用内存中的一个字节数组(buf)构建的,而且继承自DataOutputStream,方便我们使用“流”的形式操作数据。而且valLenBuffer在缓存“列值”的长度的时候,为了有效的节约存储空间,使用了一个技巧,也就是说,如果需要保存的“列值”长度为“1,1,1,2”,需要存储四个整数,而且前面三个整数的值是一样的,那么我们将其变为“1,~2,2”,“~2”即表示我们需要将它前面的整数“1”重复两次。如果数据的重复度较高,这种方式会节省大量的存储空间。
RowSplit
2)一“行”数据转换为多“列”数据,并被缓存到各自对应的缓存区之后,需要进行两个判断:
如果上述两者条件满足其一,我们认为已经缓存足够多的数据,可以将缓存区的这些数据形成一个Row Split或Record,进行“溢写”。
这两个上限阀值(columnsBufferSize、RECORD_INTERVAL)也提示我们在实际应用中需要根据实际情况对这两个值进行调整。
“溢写”是通过flushRecords进行的,可以说是整个RCFile写入过程中最为“复杂”的操作。
前面提到过,RCFile Record(Row Split)实际是由Key、Value组成的,现在这些“列”数据已经被缓存到columnBuffers中,那么Key的数据在哪里呢?
这个Key实际上就是这个Row Split(Record)的元数据,也可以理解为Row Split(Record)的索引,它是由KeyBuffer表示的,
columnNumber:列数;
numberRows:RCFile Record(Row Split)内部存储着多少“行”数据,同一个RCFile文件,不同的Record内保存的行数可能不同;
RCFile Record Value实际就是前面提到的columnBuffers中的那些列值(可能经过压缩处理),这些columnBuffers的元数据由以下三个变量表示:
KeyBuffer被序列化之后,它的结构如下:
numberRows |
Number_of_rows_in_this_record(vint) |
columnValueLen |
Column_1_ondisk_compressed_length(vint) |
columnUncompressedValueLen |
Column_1_ondisk_uncompressed_length(vint) |
Column_1_row_1_value_plain_length |
|
Column_1_row_2_value_plain_length |
|
... |
|
columnValueLen |
Column_2_ondisk_compressed_length(vint) |
columnUncompressedValueLen |
Column_2_ondisk_uncompressed_length(vint) |
Column_2_row_1_value_plain_length |
|
Column_2_row_2_value_plain_length |
|
... |
|
注意到上面的多个columnValueLen(columnUncompressedValueLen),它保存着Record Value内多个列(簇)各自的总长度,而每个columnValueLen(columnUncompressedValueLen)后面保存着该列(簇)内多个列值各自的长度。如果我们仅仅需要读取第n列的数据,我们可以根据columnValueLen(columnUncompressedValueLen)直接跳过Record Value前面(n - 1)列的数据。
KeyBuffer的数据是在“溢写”的过程中被构建的。
key是KeyBuffer的实例,相当于在元数据中记录这个Row Split(Record)的“行数”;
这段代码在使用压缩的场景下才有意义,它构建了一个缓存区valueBuffer,并且使用“装饰器”模式构建了一个压缩输出流,用于后期将columnBuffers中的数据写入缓存区valueBuffer,valueBuffer中的数据是压缩过的
接下来就是逐个处理columnBuffers中的数据,简要来说,对于某个columnBuffers[i]而言需要做两件事情:
1)如果使用压缩,需要将columnBuffers[i]的数据通过压缩输出流deflateOut写入valueBuffer中;
2)维护相关的几个变量值;
这段代码看似较长,对于某个columnBuffers[i]而言,实际做的事情可以概括为四步:
1)如果使用压缩,将columnBuffers[i]中的全部数据写入deflateOut(实际是valueBuffer);
2)记录columnBuffers[i]经过压缩之后的长度colLen;如果没有使用使用压缩,则该值与原始数据长度相同;
3)记录columnBuffers[i]相关元数据:columnBuffers[i]压缩/未压缩数据的长度、columnBuffers[i]中各个列值的长度;
4)维护plainTotalColumnLength、comprTotalColumnLength;
代码至此,一个Record(Row Split)的所有元数据已构建完毕;如果启用压缩,columnBuffers中的数据已全部被压缩写入valueBuffer,接下来就是Record Key、Value的“持久化”。
比如我们有一个“大”的文本文件,需要使用MapReduce进行分析。Hadoop MapReduce在提交Job之前会将这个大的文本文件根据“切片”大小(假设为128M)进行“切片”,每一个MapTask处理这个文件的一个“切片”(这里不考虑处理多个切片的情况),也就是这个文件的一部分数据。文本文件是按行进行存储的,那么MapTask从某个“切片”的起始处读取文件数据时,如何定位一行记录的起始位置呢?
毕竟“切片”是按照字节大小直接切分的,很有可能正好将某行记录“切断”。这时就需要有这样的一个“sync”,相当于一个标志位的作用,让我们可以识别一行记录的起始位置,对于文本文件而言,这个“sync”就是换行符。所以,MapTask从某个“切片”的起始处读取数据时,首先会“过滤”数据,直到遇到一个换行符,然后才开始读取数据;如果读取某行数据结束之后,发现“文件游标”超过该“切片”的范围,则读取结束。
RCFile同样也需要这样的一个“sync”,对于文本文件而言,是每行文本一个“sync”;RCFile是以Record为单位进行存储的,但是并没有每个Record使用一个“sync”,而是两个“sync”之间有一个间隔限制SYNC_INTERVAL,
SYNC_INTERVAL = 100 * (4 + 16)
每次开始输出下一个Record的数据之前,都会计算当前文件的输出位置相对于上个“sync”的偏移量,如果超过SYNC_INTERVAL就输出一个“sync”。
ii. write total record length、key portion length
iii. write keyLength、keyBuffer
注意这里的keyLength与ii中的keyLength不同:ii中的keyLength相当于记录的是keyBuffer原始数据的长度;而iii中的keyLength相当于记录的是keyBuffer原始数据被压缩之后的长度,如果没有压缩,该值与ii中的keyLength相同。
代码至此,我们就完成了一个Row Split(Record)的输出。
最后就是清空相关记录,为下一个Row Split(Record)的缓存输出作准备,
RCFile文件的“关闭”操作大致可分为两步:
1)如果缓存区中仍有数据,调用flushRecords将数据“溢写”出去;
2)关闭文件输出流。
在MapReduce框架中,mapper将顺序处理HDFS块中的每个行组。当处理一个行组时,RCFile无需全部读取行组的全部内容到内存。相反,它仅仅读元数据头部和给定查询需要的列。因此,它可以跳过不必要的列以获得列存储的I/O优势。(例如,表tbl(c1, c2, c3, c4)有4个列,做一次查询“SELECT c1 FROM tbl WHERE c4 = 1”,对每个行组,RCFile仅仅读取c1和c4列的内容。).在元数据头部和需要的列数据加载到内存中后,它们需要解压。元数据头部总会解压并在内存中维护直到RCFile处理下一个行组。然而,RCFile不会解压所有加载的列,相反,它使用一种Lazy解压技术。
Lazy解压意味着列将不会在内存解压,直到RCFile决定列中数据真正对查询执行有用。由于查询使用各种WHERE条件,Lazy解压非常有用。如果一个WHERE条件不能被行组中的所有记录满足,那么RCFile将不会解压WHERE条件中不满足的列。例如,在上述查询中,所有行组中的列c4都解压了。然而,对于一个行组,如果列c4中没有值为1的域,那么就无需解压列c1。
I/O性能是RCFile关注的重点,因此RCFile需要行组够大并且大小可变。行组大小和下面几个因素相关。
ORC File,它的全名是Optimized Row Columnar (ORC) file,其实就是对RCFile做了一些优化。据官方文档介绍,这种文件格式可以提供一种高效的方法来存储Hive数据。它的设计目标是来克服Hive其他格式的缺陷。运用ORC File可以提高Hive的读、写以及处理数据的性能。
从上图我们可以看出,每个Stripe都包含index data、row data以及stripe footer。Stripe footer包含流位置的目录;Row data在表扫描的时候会用到。
Index data包含每列的最大和最小值以及每列所在的行。行索引里面提供了偏移量,它可以跳到正确的压缩块位置。具有相对频繁的行索引,使得在stripe中快速读取的过程中可以跳过很多行,尽管这个stripe的大小很大。在默认情况下,最大可以跳过10000行。拥有通过过滤谓词而跳过大量的行的能力,你可以在表的 secondary keys 进行排序,从而可以大幅减少执行时间。比如你的表的主分区是交易日期,那么你可以对次分区(state、zip code以及last name)进行排序。
在建Hive表的时候我们就应该指定文件的存储格式。所以你可以在Hive QL语句里面指定用ORCFile这种文件格式,如下:
- create table ... stored as orc
- alter table ... [partition partition_spec] set fileformat orc
- set hive.default.fileformat=orc
Parquet支持嵌套的数据模型,类似于Protocol Buffers,每一个数据模型的schema包含多个字段,每一个字段有三个属性:重复次数、数据类型和字段名,重复次数可以是以下三种:
每一个字段的数据类型可以分成两种:group(复杂类型)和primitive(基本类型)
schema示例:
可以把这个Schema转换成树状结构,根节点可以理解为repeated类型,如下图:
Parquet文件在磁盘所有数据分成多个RowGroup 和 Footer。
messageDemo {--- D = 0
optional group field1 { ----D = 1
required group fiel2 {----D = 1(required是不使用DefinitionLevel的)
optional string field3;----D = 2
}
}
}
RepetitionLevel是针对repeated字段的,对于optional和required,是没有啥关系的。意思就是指在哪一个深度上进行重复。
简单的说,就是通过数字让程序明白在路径中什么repeated字段重复了,以此来确定这个字段的位置
举个例子:
我们定一个Author的数据模型:
最后生成的数据:
分析:AuthorID:因为该字段是required,必须定义的,所以,它是没有DefinitionValue,所以都是0
Addresses:因为该字段是repeated,允许0个或多个值,所以DefinitionLevel = 1;第一个Author的第一个Addresses由于之前没有重复,是一个新的record,所以RepetitionLevel = 0; 第二个 Addresses由于在之前已经出现过一次,所以它是重复的,重复深度是1,所以RepetitionLevel = 1;
到第二Author的时候,Address是一个新的record,所以没有重复,RepetitionLevel = 0,DefinitionLevel = 1
Books.BookID:因为该字段是required,必须定义的,所以,他没有DefinitionValue,那么他的DefinitionValue和父父节点的DefinitionValue相同,DefinitionValue = 1. 因为Books是Repeated的,但是Books.BookId只出现一次,所以RepetitionLevel = 0。
到第二个Books.BookId的时候,由于之前已经有过Books,所以现在是重复的,只是Books重复,所以重复深度为1,那么此时RepetitionLevel = 1,DefinitionValue = 1. 到第三个Books.BookkId的时候,同样他也是重复的,重复深度也还是1,所以RepetitionLevel = 1,DefinitionValue = 1.
Books.Price: 由于price是optional,所以在树种有两个DefinitionLevel=2,由于第一次出现Books.Price,所以RepetitionLevel = 0;
第二个Books.Price的时候,DefinitionLevel=2,但是Books已经是重复的,所以现在RepetitionLevel = 1;第三个没有Books.Price,所以DefinitionLevel = 1(和Books的DefinitionLevel一样),RepetitionLevel = 1;
Books.Descs.Type:由于是Required,所以DefinitionLevel没有,和父节点的DefinitionLevel是一样的,故DefinitionLevel = 2;第一次出现Books.Descs.Type,所以RepetitionLevel = 0;第二次出现Books.Descs.Type,由于之前已经存在了Books.Descs,所以现在他重复了,Descs重复深度是2,所以DefinitionLevel = 2, Repetition Level = 2; 下一个Books.Descs.Type由于没有Descs,所以DefinitionLevel = 1,Repetition Level只是Books重复,所以深度为1,值为NULL;到下一个Books.Descs.Type,由于只是Books重复,所以重复深度为1,DefinitionLevel = 2
如果您看到了这,请点个赞