前言

说到HDFS上面存储数据的格式,一般会想到面向行存储的Avro、SequenceFile(现在较少用);面向列存储的Parquet、ORC等,那么在存储的时候如何选择呢?

面向行存储格式(以Avro和SequenceFile为例)

  • Avro基本概念
    Avro是一个独立于编程语言数据序列化系统。
    引入的原因:
    解决Writable类型缺乏语言的可移植性。
    Avro数据文件主要是面向跨语言使用而设计的,因此,我们可以用Python语言写入文件,并用C语言来读取文件。
    这样的话,Avro更易于与公众共享数据集;同时也更具有生命力,该语言将使得数据具有更长的生命周期,即使原先用于读/写该数据的语言已经不再使用。
  • Avro的数据格式
    Avro和SequenceFile的格式:(Avro与SequenceFile最大的区别就是Avro数据文件书要是面向跨语言使用而设计的)
    SequenceFile由文件头和随后的一条或多条记录组成(如下图)。SequenceFile的前三个字节为SEQ(顺序文件代码),紧随其后的一个字节表示SequenceFile的版本号。文件头还包括其他字段,例如键和值类的名称、数据压缩细节、用户定义的元数据以及同步标识(这些字段的格式细节可参考SequenceFile的文档http://bit.ly/sequence_file_docs和源码)。如前所述,同步标识用于在读取文件时能够从任意位置开始识别记录边界。每个文件都有一个随机生成的同步标识,其值存储在文件头中。同步标识位于顺序文件中的记录与记录之间。同步标识的额外存储开销要求小于1%,所以没有必要在每条记录末尾添加该标识(特别是比较短的记录)。

    记录的内部结构取决于是否启用压缩。如果已经启用压缩,则结构取决于是记录压缩还是数据块压缩。
    如果没有启用压缩(默认情况),那么每条记录则由记录长度(字节数)、键长度、键和值组成。长度字段为4字节长的整数。
    记录压缩格式与无压缩情况基本相同,只不过值是用文件头中定义的codec压缩的。注意,键没有被压缩。
    如下图所示,块压缩(block compression)是指一次性压缩多条记录,因为它可以利用记录间的相似性进行压缩,所以相较于单条记录压缩方法,该方法的压缩效率更高。可以不断向数据块中压缩记录,直到块的字节数不小于io.seqfile.compress.blocksize属性中设置的字节数:默认为1MB。每一个块的开始处都需要插入同步标识。数据块的格式如下:首先是一个指示数据块中字节数的字段;紧接着是4个压缩字段(键长度、键、值长度和值)。

面向列存储格式(以Parquet为例)

  • Parquet基本概念
    Parquet是一种能够有效存储嵌套数据列式存储格式。
    列式存储格式在文件大小查询性能上表现优秀。
    文件占用空间更小的原因:
    在列式存储格式下,同一列的数据连续保存,这种做法可以允许更高效的编码方式,从而使列式存储格式的文件常常比行式存储格式的同等文件占用更少空间。
    比如,对于存储时间戳的列,采用的编码方式可以是存储第一个时间错的值,尔后的值则只需要存储与前一个值之间的差,根据时间局部性原理(即同一个时间前后的记录彼此相邻),这种编码方式更倾向于占用较小的空间。
    提高查询性能的原因:
    另外,由于查询引擎能够跳过对本次查询无用的行,从而提高了查询性能。(可进一步参考下边面向行和面向列的区别来理解)
    Parquet最突出两个特征:
    (1)Parquet的突出贡献在于能够以真正的列式存储格式来保存具有深度嵌套结构的数据。
    (2)Parquet的另一个特点是有很多工具都可支持这种格式。
    这个是Parquet研发者,将该项目分为两部分,其一是以语言无关的方式定义文件格式的Parquet规范,另一部分是不同语言(Java和C++)的规范实现。
  • Parquet文件格式
    Parquet文件由一个文件头(header)、一个或多个紧随其后的文件块(block),以及一个用于结尾的文件尾(footer)构成。
    文件头中仅包含一个称为PAR1的4字节数字(Magic Number),它用来识别整个Parquet文件格式。
    (说白了,文件头仅有一个作用,就是告诉别人他是个Parquet格式)
    文件所有元数据都保存在文件尾中。文件尾中元数据包括文件格式的版本信息、模式信息、额外的键值对以及所有块的元数据信息。文件尾的最后两个字段分别是一个4字节字段(其中包含了文件尾中元数据长度的编码)和一个PAR1(与文件头中的相同)(共8个字节)
    由于元数据保存在文件尾中,因此在读Parquet文件时,首先要做的就是找到文件的结尾,然后(减去8个字节)读取文件尾中的元数据长度,并根据元数据长度逆向读取文件尾中的元数据。
    sequenceFile和Avro数据文件都是把元数据保存在文件头中,并且使用sync marker来分割数据块,而Parquet文件则不同,由于文件块之间的边界信息被保存在文件尾的元数据中,因此Parquet文件不需要使用sync maker。这种做法之所以可行,正是因为元数据要等到最后才能写入,此时所有文件块都已写完,只要文件没有关闭,writer就能在内存中保留这些文件块的边界位置。综上所述,由于通过读取文件尾可以定位文件块,因此Parquet文件是可分割且可并行处理的(例如通过MapReduce处理)。
    Parquet文件中的每个文件块负责存储一个行组(row group),行组由列块(column chunk)构成,且一个列块负责存储一列数据。每个列块中的数据以页(page)为单位存储。
  • hadoop 计算失败 hadoop queue_字段

  • 由于每页所包含的值都来自于同一列,因此极有可能这些值之间的差别并不大,那么使用页作为压缩单位是非常合适的。初级压缩来自编码方式,最简单的编码方式是无格式编码(plain encoding),即原封不动地存储一个值(例如使用4字节的小端字节表示法来存储int32类型),然而,这种编码方式并没有提供任何程度的压缩。
    Parquet会使用一些带有压缩效果的编码方式,包括差分编码(保存值与值之间的差)、游程长度编码(将一连串相同的值编码为一个值以及重复次数)、字典编码(创建一个字典,对字典本身进行编码,然后使用代表字典索引的一个整数来表示值)。在大多数情况下,Parquet还会使用其他一些技术,比如位紧缩法(bit packing),它将多个较小的值保存在一个字节中节省空间。
    在写文件时,Parquet会根据列的类型自动选择适当的编码方式。例如,在保存布尔类型时,Parquet会结合游程长度编码与位紧缩法。大部分数据类型的默认编码方式是字段编码,但如果字典太大,就要退回到无格式编码。触发退回的阈值称为字典页大小(dictionary page size),其默认值等于页的大小(因此,倘若使用字段编码,那么这个字典页不得超过一页的范围)。请注意,实际采用的编码方式保存在文件的元数据中,这样才能确保reader在读取数据时使用正确的编码方式。
    除编码外,还可以以页为单位,利用标准压缩算法对编码后的数据进行第二次压缩。Parquet的默认设置是不使用任何压缩算法,但它可以支持Snappy、gzip和LZO等压缩工具。
    对于嵌套数来说,每一页还需要存储该页所包含的值的列定义深度和列元素重复次数。由于这两个数都是很小的整数(最大值取决于模式指定的嵌套深度),因此使用位紧缩法与游程长度编码可以有效地进行编码。

面向行和面向列存储的区别

SequenceFile、Avro数据文件都是面向行的格式,意味着每一行的值在文件中是连续存储的。在面向列的格式中,文件中的行(或等价为Hive中的一张表)被分割成行的分片,然后每个分片以面向列的形式存储:首先存储每行第1列的值,然后是每行第2列的值,如此以往。该过程如下图所示:

hadoop 计算失败 hadoop queue_hadoop 计算失败_02


面向列的存储布局可以使一个查询跳过那些不必访问的列。让我们考虑一个只需要处理上图中表的第二列的查询。在像SequenceFile这样面向行的存储中,即使是只需要读取第二列,整个数据行(存储在SequenceFile的一条记录中)将被加载进内存。虽说“延时反序列化”(lazy deserialization)策略通过只反序列化那些被访问的列字段能节省一些处理开销,但这仍然不能避免从磁盘上读入一个数据行所有字节而付出的开销。

如果使用面向列的存储,只需要把文件中第2列所对应的的那部分读入内存。一般来说,面向列的存储格式对于那些只访问表中一小部分列的查询比较有效。相反,面向行的存储格式适合同时处理一行中很多列的情况。

由于必须在内存中缓存行的分片、而不是单独的一行,因此面向列的存储格式需要更多的内存用于读写。并且,当出现写操作时(通过flush或sync操作),这种缓存通常不太可能被控制,因此面向列的格式不适合流的写操作,这是因为,如果writer处理失败的话,当前的文件无法恢复。另一方面,对于面向行的存储格式,如SequenceFile和Avro数据文件,可以一直读取到writer失败后的最后的同步点。由于这个原因,Flume使用了面向行的存储格式。

后记

上面的详细描述之后我们就清楚的知道面向列在读取和节省空间方面有很大优势,但在写入时有可能会丢失数据