由于之前一段时间被安排去写一个spark项目(未来一段时间也会开始开spark的坑),因此暂时停止了读书笔记的更新,最近开始恢复读书。今天先介绍一下原书的第五章,即hadoop 的I/O知识。

数据一致性

一般来说,hadoop的用户希望数据能够保持一致性(Integrity),但是由于hadoop的高并发性,数据被破坏的风险很高。一个用来检验数据是否被破坏的经典方法是计算校验和(checksum),常见的校验码是CRC-32,不过HDFS用了它的一个高效变本CRC-32C。

在HDFS中,每当有数据被写入时都会计算校验和,并且在它们被读取时验证校验和。每dfs.bytes-perchecksum字节(默认值是512)的数据被计算一个校验和。

存入数据时:一个datanode在从client或者其他datanode收到数据时都会计算校验和,并且最后一个datanode会验证这个校验和;一旦发现校验和不正确,客户端就会收到一个异常并进一步处理它们。
而读取数据时:clietn会计算数据的校验和并拿它们和datanode上的校验和作比较。此外,在datanode里面,一直有一个后台线程DataBlockScanner周期性地检查所有block的一致性。

由于HDFS中我们会存储block的多个副本,因此当遇到block损坏的时候,HDFS可以恢复它:当一个client读取到坏的block时,在它抛出ChecksumException之前,它会把这个block和datanode报告给namenode。然后namenode会标记这个block使得其他client不会在读取这个block并且也不会让这个block被拷贝到其他datanode上。再然后这个block的一个(正确)副本会被拷贝到一个新的datanode中,最后把这个坏的block删除。

当然,在特殊情况下,我们可能需要读取这个坏的block,因此我们可以在FileSystem调用open()之前,调用setVerifyChecksum(false)来关闭验证校验和。我们也可以在shell命令中加入-ignoreCrc参数来达到同样的目的。

此外,我们还可以用hadoop fs -checksum来找到一个文件的校验和。这样可以方便地帮我们查看两个文件是否相同。

最后我们可以用RawLocalFileSystem()和ChecksumFileSystem(fs)来得到一个不做校验和的文件系统,或者使得某个文件系统fs具有校验和功能。

压缩

Hadoop中也有很多压缩技术,如下图:

hadoop 登录 认证 hadoop认证ccha_校验和


这里我们不对这些内容做过多介绍,因为已经超出了hadoop的范围,我们只简单解释下上图中Splittable的意思:你可以从压缩文件的任意地方读取,这个功能对于MapReduce是很有帮助的。唯一需要说到的是hadoop通过实现CompressionCodec接口的方式提供了这些压缩方法:

hadoop 登录 认证 hadoop认证ccha_校验和_02

此外我们简单提一下用户应该选择哪一种压缩格式(从好到坏):

  • 优先采用容器文件格式例如序列文件,Avro文件,ORC文件,和Parquet文件等,它们都支持压缩和切分。
  • 采用支持切分的压缩格式,例如bzip2
  • 先将文件切分成chunk,然后对每个chunk压缩存储
  • 最坏的就是直接存储整个文件

最后,我们还可以在MapReduce中利用压缩,不仅仅是在Reducer的Output阶段,也可以在Mapper的Output中利用压缩。例如代码:

//compress for output of reducer
FileOutputFormat.setCompressOutput(job, true);
FileOutputFormat.setOutputCompressorClass(job, GzipCodec.class);

//compress for output of mapper
conf.setBoolean(Job.MAP_OUTPUT_COMPRESS, true);
conf.setClass(Job.MAP_OUTPUT_COMPRESS_CODEC, GzipCodec.class,
CompressionCodec.class);

序列化

序列化(Serialization)是把结构对象转换为字节流的过程,后者被用作网络传输或者持久化存储。反序列化(Deserialization)是把字节流还原成原始结构。

在hadoop中,进程间的通信用RPC(remote procedure call)实现。而RPC中的序列化格式要求:

  • 可压缩
  • 速度快
  • 可扩展:为了满足需求,协议经常变化,因此原来的格式要求容易修改
  • 互用的(interoperable):在很多系统中,客户端可能用多种语言书写,格式也需要能够被解释编译成多种语言。

在hadoop中,提供了自己的序列化格式: Writable格式。这个格式是可压缩且速度快的,但是并不易于扩展,也无法被除Java以外的语言使用。当然,后来也诞生了一些其它的格式,例如Avro等。

对于hadoop的Writable接口,需要实现write(DataOutput out)和readFields(DataInput in)两个接口。在hadoop中,已经提供了很多writable类,如下图:

hadoop 登录 认证 hadoop认证ccha_hadoop_03

在详细介绍这些类之前,我们需要先提一下WritableComparable接口,这个接口是Writable和java.lang.Comparable接口的子接口。由于MapReduce中,在排序阶段中对比较要求很高。因此在Hadoop中,提供了一个RawComparator接口,即:

package org.apache.hadoop.io;
import java.util.Comparator;
public interface RawComparator<T> extends Comparator<T> {
    public int compare(byte[] b1, int s1, int l1, byte[] b2, int s2, int l2); 
}

这个比较直接从没有反序列化的字节流中比较两个对象,这样可以避免对象创建的开销。

下面我们对每一类writable类都简单介绍一下:

Java基本类型的Writable包装

hadoop中对java的全部基本类型都包装了Writable类,如下图所示:

hadoop 登录 认证 hadoop认证ccha_读书笔记_04


注意到对于int和long都有两个包装类,分别是定长编码和变长编码。用户可以根据自己的需求选择:如果数据在整个取值空间均匀分布则选择定长编码,否则用变长编码可以节省空间,而且VIntWritable和VLongWritable采用同样的编码方法,因此可以不用一开始就提交8字节长度的数字。

String的Writable包装

Text也是一个很重要的包装类,你可以把它等效为java.lang.String。它采用UTF-8编码,由于用int来记录Text的字节数,因此最长的字符串是2GB。我们需要注意到Text和String有很大的不同:

-

Hadoop: Text

Java: String

Index

字节(byte)的位置

字符(character)的位置

charAt返回值

int

char

遍历

遍历字节

遍历字符

可变性

可变(有set方法)

不可变(不同的String在内存中占用不同的空间)

Null的包装

NullWritable是一个特殊的类,它用来表示空,并且长度为0,它是一个不可变的单例,你可以用NullWritable.get()获得它的实例。

一般目的的包装

ObjectWritable和GenericWritable用来封装Java基本类型,String, emun,Writable, null或者这些类型的数组。它们两者之间的不同是,前者需要在存储数据之前,存储类名,这样会占用大量的空间。后者则考虑到在实际应用中,类型并不会很多,因此我们只需要提前定义好类型的静态数组,并在存储时只存储静态数组的索引就可以了。

Collections的包装

正如Java中提供了各种容器一样,hadoop中也提供了6中Collection Writable类: Array Writable, ArrayPrimitiveWritable, TwoDArrayWritable, MapWritable, SortedMapWritable, and EnumSetWritable. 这里不再做过多介绍。