Hadoop用户肯定都希望系统在存储和处理数据时不会丢失或损坏任何数据。尽管磁盘或网络上的每个I/O操作不太可能将错误引入自己正在读/写的数据中,但是如果系统中需要处理的数据量大到Hadoop的处理极限时,数据被损坏的概率还是很高的。检测数据是否损坏的常见措施是,在数据第一次还引入系统时计算校验和(checksum)并在数据通过一个不可靠的通道进行传输时再次计算校验和,这样就能发现数据是否损坏。如果计算所得的新校验和与原来的校验和不匹配,我们就认为数据已损坏。但该技术并不能修复数据--它只能检测出数据错误。注意,校验和也是可能损坏的,不只是数据,但由于校验和比数据小得多,所以损坏的可能性非常小。

常用的错误检测码是CRC-32(32位循环冗余校验),任何大小的数据输入均计算得到一个32位的数据校验和。Hadoop ChecksumFileSystem使用CRC-32计算校验和,HDFS用于校验和计算的则是一个更有效的变体CRC-32C。

1.HDFS的数据完整性

HDFS会对写入的所有数据计算校验和,并在读取数据时验证校验和。他针对每个由dfs.bytes-per-checksum指定字节的数据计算校验和。默认情况下为512个字节,由于CRC-32校验和是4个字节,所以存储校验和的额外开销低于1%。

datenode负责在收到数据后存储该数据及其校验和之前对数据进行验证。他在收到客户端的数据或复制其他datanode的数据时执行这个操作。正在写数据的客户端将数据及其校验和发送到由一系列datanode组成的管线,管线中最后一个datanode负责验证校验和。如果datanode检测到错误,客户端便会收到一个IOException异常的一个子类,对于该异常应以应用程序特定的方式来处理,比如重试这个操作。

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

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

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

在使用open()方法读取文件之前,将false值传递给FileSystem对象的setVerifyChecksum()方法,即可禁用校验和验证。如果在命令解释器中使用带-get选项的-ignoreCrc命令或者使用等价的-copyToLocal命令,也可以达到相同的效果。如果有一个已损坏的文件需要检查并决定如何处理,这个特性是非常有用的。例如,也许你希望在删除该文件之前尝试看看是否能够恢复部分数据。

可以用hadoop的命令fs -checksum来检查一个文件的校验和。这用于在HDFS中检查两个文件是否具有相同内容,distcp命令也具有类似功能。

2.LocalFileSystem

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

检验和的计算代价是相当低的(在java中他们是用本地代码实现的),一般只是增加少许额外的读/写文件时间。对大多数应用来说,付出这样的额外开销以保证数据完整性是可以接受的。此外我们也可以禁用校验和计算,特别是在底层文件系统本身就支持校验和的时候。在这种情况下,使用RawLocalFileSystem替代LocalFileSystem。要想在一个应用中实现全局校验和验证,需要将fs.file.impl属性设置为org.apache.hadoop.fs.RawLocalFileSystem进而实现对文件URI的重新映射。还有一个可选方案可以直接新建一个RawLocalFileSystem实例。如果想针对一些读操作禁用校验和,这个方案非常有用。示例如下:

Configuration conf=...
FileSystem fs=new RawLocalFileSystem();
fs.initialize(null,conf);

3.ChecksumFileSystem

LocalFileSystem通过ChecksumFileSystem来完成自己的任务,有了这个类项其他文件系统(无校验和系统)加入校验和就非常简单,因为ChecksumFileSystem类继承自FileSystem类。一般用法如下:

FileSystem rawFs=...
FileSystem checksummedFs=new ChecksumFileSystem(rawFs);

底层文件系统成为“源”(raw)文件系统,可以使用ChecksumFileSystem实例的getRawFileSystem()方法获取它。ChecksumFileSystem类还有其他一些与校验和有关的有用方法,比如getChecksumFile()可以获得任意一个文件的校验和文件路径。

如果ChecksumFileSystem类在读取文件时检测到错误,会调用自己的reportChecksumFailure()方法。默认实现为空方法,但LocalFileSystem类会将这个出错的文件及其校验和移到同一存储设备上一个名为bad_files的边际文件夹(side directory)中。管理员应该定期检查这些坏文件并采取相应的行动。