Hadoop的IO操作

Hadoop自带一套原子操作用于数据I/O操作。其中有一些技术比Hadoop本身更常用,如数据完整性和压缩,序列化框架和盘数据结构。

数据完整性
检测数据是否损坏的常见措施是,在数据第一次引入系统时计算校验和并在数据通过一个不可靠的通道进行传输时再次计算校验和,这样就能发现数据是否损坏,如果计算所得的新校验和和原来的校验和不匹配,我们就认为数据已损坏。但该技术并不能修复数据。常见的错误检测码是CRC-32(32位循环冗余检验),任何大小的数据输入均计算得到一个32位的整数校验和。

datanode负责在收到数据后存储该数据及其校验和之前对数据进行验证。它在收到客户端的数据或复制其他datanode的数据时执行这个操作。正在写数据的客户端将数据及其校验和发送到由一系列datanode组成的管线,管线中最后一个datanode负责验证校验和。如果datanode检测到错误,客户端就会收到一个IOException异常的子类。

客户端从datanode读取数据时,也会验证校验和,将它们与datanode中存储的校验和进行比较。每个datanode均持久保存有一个验证的校验和日志,所以它知道每个数据块的最后一次验证时间。客户端成功验证一个数据块后,会告诉这个datanode,datanode由此更新日志。保存这些统计信息对于检测损坏的磁盘很有价值。

不只是客户端在读取数据块时会验证校验和,每个datanode也会在一个后台线程中运行一个DataBlockScanner,从而定期验证存储在这个datanode上的所有数据块。该项措施是解决物理存储媒体上位损坏的有力措施。

由于HDFS存储着每个数据块的复本,因此它可以通过数据复本来修复损坏的数据块,进而得到一个新的,完好无损的复本。基本思路是,客户端在读取数据块时,如果检测到错误,首先向namenode报告已损坏的数据块及其正在尝试读取操作的这个datanode,再抛出ChecksumException异常。namenode将这个数据块复本标记为已损坏,这样它不再将客户端处理请求直接发送到这个节点,或尝试将这个复本复制到另一个datanode。之后,它安排这个数据块的一个复本复制到另一个datanode,这样一来,数据块的复本因子又回到期望水平。此后,已损坏的数据块复本便被删除。

LocalFileSystem
Hadoop的LocalFileSystem执行客户端的校验和验证。这意味着在你写入一个名为filename的文件时,文件系统客户端会明确在包含每个文件快校验和的同一个目录内新建一个filename.crc隐藏文件。文件块的大小作为元数据存储在.crc文件中,所以即使文件块大小的设置已经发生变化,仍然可以正确读回文件。在读取文件时需要验证校验和,并且如果检测到错误,LocalFileSystem还会抛出一个ChecksumException异常。

压缩
文件压缩有两大好处:减少存储文件所需要的磁盘空间,并加速数据在网络和磁盘上的传输。这两大好处在处理大量数据时相当重要,所以值得考虑在Hadoop中文件压缩的用法。

压缩格式选择
Hadoop应用处理的数据集非常大,因此需要借助于压缩。使用哪种压缩格式与待处理的文件的大小,格式和所使用的工具有关。下面有一些建议,大致是按照效率从高到低排列的。
1.使用容器文件格式,例如顺序文件,Avro数据文件,ORCFiles或者Parquet文件,所有这些文件格式同时支持压缩和切分,通常最好与一个快速压缩工具使用,例如LZO,LZ4或者Snappy
2.使用支持切分的压缩格式,例如bzip2或者通过索引实现切分的压缩格式,例如LZO
3.在应用中将文件切分成块,并使用任意一种压缩格式为每个数据块建立压缩文件(不论它是否支持切分)。这种情况下,需要合理选择数据块的大小,以确保压缩后数据块的大小近似于HDFS块的大小。
4.存储未经压缩的文件

对于大文件来说,不要使用不支持切分整个文件的压缩格式,因为会失去数据的本地特性,进而造成Mapreduce应用效率低下。

在Mapreduce中使用压缩

FileOutputFormat.setCompressOutput(job,true);
FileOutputFormat.setOutputCompressorClass(job,GzipCodec.class);//后一个参数设置为打算使用的压缩codec的类名

如果为输出生成顺序文件,可以设置mapreduce.output.fileoutputformat.compress.type属性来控制限制使用压缩格式。默认值是RECORD,即针对每条记录进行压缩。如果将其改为BLOCK,将针对一组记录进行压缩,这是推荐的压缩策略,因为它的压缩效率更高。

对map任务输出进行压缩

尽管Mapreduce应用读写的是未经压缩的数据,但如果对map阶段的中间输入进行压缩,也可以获得不少好处。由于Map任务的输出需要写到磁盘并通过网络传输到reducer节点,所以通过LZO,LZ4或者Snappy这样的快速压缩方式,是可以得到性能提升的,因为需要传输的数据减少了。

序列化

序列化是指将结构化对象转化为字节流以便在网络上传输或写到磁盘进行永久存储的过程。反序列化是指将字节流转回结构化对象的逆过程。

序列化用于分布式数据处理的两大领域:进程间通信和永久存储

在Hadoop中,系统中多个节点进程间的通信是通过“远程过程调用”(RPC)实现的。RPC协议将消息序列化成二进制流后发送到远程节点,远程节点接着将二进制流反序列化为原始消息。通常情况下,RPC序列化格式如下:

1.紧凑
紧凑格式能充分利用网络带宽(数据中心最稀缺的资源)

2.快速
进程间通信形成了分布式系统的骨架,所以需要尽量减少序列化和反序列化的性能开销,这是基本的。

3.可扩展
为了满足新的需求,协议不断变化。所以在控制客户端和服务期的过程中,需要直接引进相应的协议。例如,需要能够在方法调用的过程中增加新的参数,并且新的服务器需要能够接受来自老客户端的老格式的消息(无新增的参数)。

4.支持互操作
对于系统来说,希望能够支持以不同语言写的客户端与服务器交互,所以需要设计一种特定的格式来满足这一需求。

表面看来,序列化框架对选择用于数据持久存储的数据格式应该会有不同的要求。毕竟,RPC的存活时间不到1秒钟,持久存储的数据却可能在写到磁盘若干年后才会被读取。但结果是,RPC序列化格式的四大理想属性对持久存储格式而言也很重要。我们希望存储格式比较紧凑(进而高效实用存储空间),快速(读、写数据的额外开销比较小),可扩展(可以透明地读取老格式的数据)且可以互操作(以可以使用不同的语言读/写永久存储的数据)。

Hadoop使用的是自己的序列化格式writable,它绝对紧凑,速度快,但不太容易用Java以外的语言进行扩展或使用。因为Writable是Hadoop的核心。

Writable接口
Writable接口定义了两个方法:一个将其状态写入DataOutput二进制流,另一个从DataInput二进制流读取状态。

BytesWritable
BytesWritable是对二进制数据数组的封装。它的序列化格式为一个指定所含数据字节数的整数域(4字节),后跟数据内容的本身。例如,长度为2的字节数组包含数值3和5,序列化形式为一个4字节的整数(00000002)和该数组中的两个字节(03和05)

NullWritable
NullWritable是writable的特殊类型,它的序列化长度为0。它并不从数据流中读取数据,也不写入数据。它充当占位符。

ObjectWritable和GenericWritable
ObjectWritable是对java基本类型(String,enum,Writable,null或这些类型组成的数组)的一个通用封装。它在Hadoop RPC中用于对方法的参数和返回类型进行封装和解封装。

Wriable集合类
io软件包共有6个Writable集合类,分别是ArrayWritable,ArrayPrimitiveWritable,TwoDArrayWritable,MapWritable,SortedMapWritable以及EnumMapWritable

ArrayWritable和TwoDArrayWritable是对Writeble的数组和两维数组(数组的数组)的实现。ArrayWritable或TwoDArrayWritable中所有元素必须是同一类的实例。ArrayWritable和TwoDArrayWritable都有get(),set()和toArray()方法,toArray()方法用于新建该数组的一个浅拷贝。

ArrayPrimitiveWritable是对Java基本数组类型的一个封装。调用set()方法时,可以识别相应组件类型,因而无需通过继承该类来设置类型。

序列化框架
尽管大多数Mapreduce程序使用的都是Writable类型的键和值,但这并不是MapReduce API强制要求使用的。事实上,可以使用任何类型,只要能有一个机制对每个类型进行类型与二进制表示的来回转换就可以。

为了支持这个机制,Hadoop有一个针对可替换序列化框架的API。序列化框架用一个Serialization实现来表示。Serialization对象定义了从类型到Serializer实例(将对象转换为字节流)和Deserializer实例(将字节流转换为对象)的映射方式。

序列化IDL
还有许多其他序列化框架从不同的角度来解决问题:不通过代码来定义类型,而是使用接口定义语言以不依赖与具体语言的方式进行声明。由此,系统能够为其他语言生成模型,这种形式能有效提高互操作能力。它们一般还会定义版本控制方案。

两个比较流行的序列化框架Apache Thrift 和Google的Protocol Buffers,常常用作二进制数据的永久存储格式。Mapreduce格式对该类的支持有限,但在Hadoop内部,部分组件仍使用上述两个序列化框架来实现RPC和数据交换。

基于文件的数据结构
对于某些应用,我们需要一种特殊的数据结构来存储自己的数据。对于基于Mapreduce的数据处理,将每个二进制数据大对象单独放在各自的文件中不能实现可扩展性,所以Hadoop为此开发了很多更高层次的容器。

关于SequenceFile
考虑日志文件,其中每一行文本代表一条日志记录。纯文本不适合记录二进制类型的数据。在这种情况下,Hadoop的SequenceFile类非常合适,为二进制键值对提供了一个持久数据结构。将它作为日志文件的存储格式时,你可以自己选择键,以及值可以是Writable类型。

SequenceFile也可以作为小文件的容器。HDFS和Mapreduce是针对大文件优化的,所以通过SequenceFile类型将小文件包装起来,可以获得更高效率的存储和处理。

SequnceFile的写操作
通过createWriter()静态方法可以创建SequenceFile对象,并返回SequnceFile.Writer实例。该静态方法有多个重载版本,但都需要制定待写入的数据流,Configuration对象,以及键和值的类型。存储在SequenceFIle中的键和值并不一定是Writable类型。只要能够被Sertialization序列化和反序列化,任何类型都可以。

SequenceFile的读操作
从头到尾读取顺序文件不外乎创建SequenceFile.reader实例后反复调用next()方法迭代读取记录。读取的是哪条记录与使用的序列化框架有关。如果使用的是Writable类型,那么通过键和值作为参数的next()方法可以将数据流的下一条键值对读入变量中。

1.通过命令行接口显示SequenceFile
hadoop fs命令有一个-text选项可以以文本形式显示顺序文件。该选项可以查看文件的代码,由此检测出文件的类型并将其转换为相应的文本。该选项可以识别gzip压缩文件,顺序文件和Avro数据文件;否则,假设输入为纯文本文件。

2.SequenceFile的排序和合并
Mapreduce是对多个顺序文件进行排序(或合并)最有效的方法。Mapreduce本身是并行的,并且可由你制定使用多少个reducer。例如,通过制定一个reducer,可以得到一个输出文件。

3.SequenceFile的格式
顺序文件由文件头和随后的一条或多条记录组成。顺序文件的前三个字节为SEQ,紧随其后的一个字节表示顺序文件的版本号。文件头还包括其他字段,例如键和值的名称,数据压缩细节,用户定义的元数据以及同步标识。同步标识用于在读取文件时能够从任意位置开始识别记录边界。每个文件都有一个随机生成的同步标识,其值存储在文件头中,位于顺序文件中的记录与记录之间。同步标识的额外存储开销要求小于1%,所以没有必要在每条记录末尾添加该标识。

关于MapFile
MapFile是已经排过序的SequenceFile,它有索引,所以可以按键查找。索引本身就是一个SequenceFile,包含map中的一小部分键。由于索引能够加载进内存,因此可以提供对主数据文件的快速查找。主数据文件则是另一个SequenceFIle,包含了所有的map条目,这些条目都按照键顺序进行了排序。

其他文件格式和面向列的格式
顺序文件和map文件是Hadoop中最早的,但并不是仅有的二进制文件格式,事实上,对于新项目而言,有更好的二进制格式可供选择。

Avro数据文件在某些方面类似顺序文件,是面向大规模数据处理而设计的。但是Avro数据文件又是可移植的,它们可以跨越不同的编程语言使用。顺序文件,map文件和Avro数据文件都是面向行的格式,意味着每一行的值在文件中是连续存储的。在面向列的格式中,文件中的行被分割成行的分片,然后每个分片以面向列的形式存储:首先存储每行第一列的值,然后是每行第2列的值,如此以往。

你可能感兴趣的:(大数据)