Hadoop 的 I/O 操作
Hadoop 自带了一套基本数据类型的数据 I/O . 其中的一些技术比 Hadoop 本身更加通用,例如数据完整性和压缩机制,但当处理多字节 (multiterabyte datasets)数据集时应给予专门的考虑。其他则是 Hadoop 工具或 API ,它们形成开发分布式系统的构件,例如序列化框架(serialization frameworks)和 磁盘数据结构(on-disk data structures)。
1. 数据完整性 (Data Integrity)
Hadoop 用户肯定期望在数据存储或处理过程中没有数据丢失或损坏。但读写数据时,磁盘或网络上执行的每个 I/O 操作都有很小的可能将错误引入数据,但如果
系统中处理的数据量大到 Hadoop 的处理能力极限时,数据损坏发生的几率还是很高的。
检测损坏数据有效的方法是在数据第一进入系统时为其计算一个校验和(a checksum ),并且一旦数据通过一个不可靠的通道进行传输因而可能损坏数据时
再次计算校验和。如果新计算的校验和与原先的校验和不能精确匹配,数据被视为损坏的。这种方法不能用于修复数据 —— 它仅仅是错误检测(务必使用 ECC 内存)
常用的错误检测码是 CRC-32 (error-detecting code is CRC-32 (32-bit cyclic redundancy check)) ,为任何大小的输入数据计算出 32-bit 整数校验和 。
CRC-32 用于 Hadoop 的 ChecksumFileSystem 中计算校验和 ,HDFS 使用一个更高效的变体称为 CRC-32C
1. HDFS 中的数据完整性 (Data Integrity in HDFS)
-------------------------------------------------------------------------------------------------------------------------------------------
HDFS 透明地对所有写入它的数据计算校验和,并且默认情况下在读取数据时验证校验和。对每 dfs.bytes-per-checksum 字节数的数据创建一个独立的校验和。
默认为 512 字节,因为一个 CRC-32C 校验和是 4 字节长度,因此存储开销低于 1% 。
在存储数据和它的校验和之前,datanode 负责验证它们接收的数据。应用于它们从客户端接收数据和复制期间从其他 datanode 接收过来的数据。客户端写数据时
将数据发送到一系列 datanode 管线中,管线中最后一个 datanode 验证校验和。如果这个 datanode 检测出错误,客户端会会收到一个 IOException 子类异常,
客户端应处理此应用特定的异常(application-specific),例如重试操作。
客户端从 datanode 读取数据时,它们也验证校验和,比较存储在 datanode 上的校验和。每个 datanode 保留一个校验和验证的持久化日志(a persistent log
of checksum verifications) ,因此它知道它的每个块验证的最后时间。当客户端成功验证一个数据块时,它通知 datanode , datanode 更新它的日志。保留
这样的统计信息对于检测坏掉的磁盘是很由价值的。
除了客户端读取时进行数据块验证,每个 datanode 在后台线程上运行一个 DataBlockScanner 来定期验证存储在 datanode 上所有的数据块。这是为了防护
物理存储媒体上位损坏( bit rot )。
因为 HDFS 存储多个数据块复本,它通过复制一个完好的复本来产生一个新的复本,把损坏的块恢复健康。工作过程是,如果一个客户端在读取一个数据块时
检测到一个错误,它会在抛出一个 ChecksumException 异常之前,将这个坏掉的数据块和它从中读取的 datanode 报告给 namenode . namenode 标记这个块复本
为损坏的,这样它就不会再指定给任何的客户端,或者尝试复制这个复本到其他的 datanode. 然后它调度一个数据块拷贝在另外的 datanode 上复制,这样它的
复制因子(replication factor)恢复到期望的水平上。一旦完成,那个损坏的复本就会被删除。
禁用校验和验证也是可以的,使用 open() method 读取文件之前,在 FileSystem 上调用 setVerifyChecksum() method 传递 false 值。
在 shell 上使用 -get 或等价的 -copyToLocal command 加上 -ignoreCrc 选项获得相同的效果。如果有个损坏的文件想要探查一下并决定如何处理它,这个
特性是很有用的。例如,删除某个损坏的文件之前,可以看看它是否还能抢救。
可以查看文件的校验和,命令:
hadoop fs -checksum
这在 HDFS 中检查两个文件是否具有相同的内容是很有用的 —— something that distcp does
2. LocalFileSystem
----------------------------------------------------------------------------------------------------------------------------------------------
Hadoop 的 LocalFileSystem 执行客户端校验和校验。这意味着当写入一个名为 filename 的文件时,文件系统客户端透明地在该文件的同一目录内创建一个隐藏
类型的校验和文件,文件名为 .filename.crc ,该.crc 文件内包含对应文件的内容的每个块的校验和(for each chunk of the file). 块的大小(The chunk size)
由 file.bytes-per-checksum 属性控制,默认值为 512 字节。块大小作为元数据存储在这个 .crc 文件中,这样即使块大小的属性值被改变了,文件也能正确读回。
文件被读取的时候进行校验和验证,如果检测到错误, LocalFileSystem 抛出 ChecksumException 异常。
校验和的计算开销是非常低的(in Java, they are implemented in native code),通常只会增加稍许的读/写文件数据。对大多数应用来说,为了保证数据完整性,
这是个可接受的代价。
也可以禁用校验和功能,这通常用在底层文件系统本身就支持校验和的时候。适用于 RawLocalFileSystem 替代 LocalFileSystem 的情况。要在应用程序中全局做
到这一点,重新映射 file URI 就可以了,设置属性 fs.file.impl 的值为 org.apache.hadoop.fs.RawLocalFileSystem.
另一种方法,可以直接创建 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() method 获取。ChecksumFileSystem 有几个用于校验和相关很有用
的放方法,如 getChecksumFile() 用于获取任何文件的校验和文件路径。
如果 ChecksumFileSystem 读取文件时检测到错误,它会调用它的 reportChecksumFailure() method 。默认实现是什么也不做(does nothing),但 LocalFileSystem
会将这个由问题的文件连同它的校验和文件移动到同一设备上旁边的(a side directory)名为 bad_files 的目录中。管理员应定期检查这些坏掉的文件。
*
*
*
2 压缩 (Compression)
--------------------------------------------------------------------------------------------------------------------------------------------------
文件压缩有量大好处:降低文件所需的存储空间,增加数据网络或磁盘上的传输速度。在处理打规模数据时,这两方面的优势更为明显,因此有必要仔细考虑 Hadoop
如何使用压缩。有很多的压缩格式,工具,以及算法,每一种都有不同的特点。
A summary of compression formats
+===========+=======+===========+===============+===============+
| 压缩格式 | 工具 | 算法 | 文件扩展名 | Splittable |
+-----------+-------+-----------+---------------+---------------+
| DEFLATE | N/A | DEFLATE | .deflate | No |
+-----------+-------+-----------+---------------+---------------+
| gzip | gzip | DEFLATE | .gz | No |
+-----------+-------+-----------+---------------+---------------+
| bzip2 | bzip2 | bzip2 | .bz2 | Yes |
+-----------+-------+-----------+---------------+---------------+
| LZO | lzop | LZO | .lzo | No |
+-----------+-------+-----------+---------------+---------------+
| LZ4 | N/A | LZ4 | .lz4 | No |
+-----------+-------+-----------+---------------+---------------+
| Snappy | N/A | Snappy | .snappy | No |
+-----------+-------+-----------+---------------+---------------+
注解:
-------------------------------------------------------------------------------------------------------------------------------------------
DEFLATE is a compression algorithm whose standard implementation is zlib.There is no commonly available command-line tool for producing files
in DEFLATE format, as gzip is normally used. (Note that the gzip file format is DEFLATE with extra headers and a footer.)
The .deflate filename extension is a Hadoop convention.
However, LZO files are splittable if they have been indexed in a preprocessing step.
所有的压缩算法都表现为空间/时间上的权衡:更快的压缩和解压缩速度通常导致节省更少的空间。表中列出的压缩工具通常都提供了 9 个不同的选项来控制压缩时间上的
权衡:-1 表示最优的压缩速度, -9 表示最优的压缩空间。例如,下面的命令使用最快的压缩方法创建压缩文件 file.gz
% gzip -1 file
不同的压缩工具具有非常不同的压缩特点。gzip 是一个通用的压缩工具,在时间和空间的权衡上处于中间位置。bzip2 比 gzip 压缩效果更好,但速度比它慢。 bzip2 的
解压速度比它自己的压缩速度更快,但还是比其他格式的速度慢。LZO, LZ4, 以及 Snappy 都优化了速度大约比 gzip 快了一个数量级,但压缩效果比较低。Snappy 和 LZ4
解压缩都比 LZO 速度快得多。
可切分(Splittable) 指明压缩格式是否支持切分(that is, whether you can seek to any point in the stream and start reading from some point further on)。
可切分压缩格式尤其适合于 MapReduce 使用。
1. 编解码器 (Codecs)
----------------------------------------------------------------------------------------------------------------------------------------------
一个 codec 是一个压缩-解压缩算法的实现(A codec is the implementation of a compression-decompression algorithm)。在 Hadoop 中,一个 codec 由一个
CompressionCodec 接口的实现表现。例如, GzipCodec 封装了 gzip 的压缩和解压缩算法。下表列出 Hadoop 可用的 codecs
Hadoop compression codecs
+===========+===============================================+
| 压缩格式 | Hadoop CompressionCodec |
+-----------+-----------------------------------------------+
| DEFLATE | org.apache.hadoop.io.compress.DefaultCodec |
+-----------+-----------------------------------------------+
| gzip | org.apache.hadoop.io.compress.GzipCodec |
+-----------+-----------------------------------------------+
| bzip2 | org.apache.hadoop.io.compress.BZip2Codec |
+-----------+-----------------------------------------------+
| LZO | com.hadoop.compression.lzo.LzopCodec |
+-----------+-----------------------------------------------+
| LZ4 | org.apache.hadoop.io.compress.Lz4Codec |
+-----------+-----------------------------------------------+
| Snappy | org.apache.hadoop.io.compress.SnappyCodec |
+-----------------------------------------------------------+
The LZO libraries are GPL licensed and may not be included in Apache distributions, so for this reason the Hadoop codecs must be downloaded
separately from Google (or GitHub, which includes bug fixes and more tools).
使用 CompressionCodec 对数据流进行压缩和解压缩 (Compressing and decompressing streams with CompressionCodec)
------------------------------------------------------------------------------------------------------------------------------------------
CompressionCodec 有两个方法可以轻松压缩和解压缩数据。
要对写出到一个输出流的数据进行压缩,使用 createOutputStream(OutputStream out) method 创建一个CompressionOutputStream ,通过它把未压缩的数据
以压缩的形式写出到底层系统。
要对从一个输入流中读取的数据解压缩,调用 createInputStream(InputStream in) 获得一个 CompressionInputStream ,通过它从底层系统读取解压缩的数据
CompressionOutputStream 和 CompressionInputStream 与 java.util.zip.DeflaterOutputStream 和 java.util.zip.DeflaterInputStream 类似,除了前者
提供了重置底层压缩器和解压器的能力。这对将数据流的各个部分压缩为单独的数据块的应用是非常重要的(compress sections of the data stream as
separate blocks),例如 SequenceFile 。
演示如何使用 API 压缩从标准输入读取的数据并把它写到标准输出
public class StreamCompressor {
public static void main(String[] args) throws Exception {
String codecClassname = args[0];
Class<?> codecClass = Class.forName(codecClassname);
Configuration conf = new Configuration();
CompressionCodec codec = (CompressionCodec)
ReflectionUtils.newInstance(codecClass, conf);
CompressionOutputStream out = codec.createOutputStream(System.out);
IOUtils.copyBytes(System.in, out, 4096, false);
out.finish();
}
}
这个应用使用一个 CompressionCodec 实现的全限定名作为第一个命令行参数 (fully qualified name)。执行:
% echo "Text" | hadoop StreamCompressor org.apache.hadoop.io.compress.GzipCodec | gunzip -
Text
通过 CompressionCodecFactory 推断 CompressionCodecs (Inferring CompressionCodecs using CompressionCodecFactory)
------------------------------------------------------------------------------------------------------------------------------------------
如果读取一个压缩文件,通常可以通过查看它的文件扩展名推断出(infer)使用的 codec 。一个以 .gz 结尾文件可使用 GzipCodec 编解码器,等等。
CompressionCodecFactory 通过它的 getCodec() method 提供了一个文件扩展名到 CompressionCodec 的映射,为其提供一个文件的 Path 对象参数,获取到
文件的 codec 。下面的范例使用这一特性解压文件:
// A program to decompress a compressed file using a codec inferred from the file’s extension
public class FileDecompressor {
public static void main(String[] args) throws Exception {
String uri = args[0];
Configuration conf = new Configuration();
FileSystem fs = FileSystem.get(URI.create(uri), conf);
Path inputPath = new Path(uri);
CompressionCodecFactory factory = new CompressionCodecFactory(conf);
CompressionCodec codec = factory.getCodec(inputPath);
if (codec == null) {
System.err.println("No codec found for " + uri);
System.exit(1);
}
String outputUri =
CompressionCodecFactory.removeSuffix(uri, codec.getDefaultExtension());
InputStream in = null;
OutputStream out = null;
try {
in = codec.createInputStream(fs.open(inputPath));
out = fs.create(new Path(outputUri));
IOUtils.copyBytes(in, out, conf);
} finally {
IOUtils.closeStream(in);
IOUtils.closeStream(out);
}
}
}
执行:
% hadoop FileDecompressor file.gz
CompressionCodecFactory 载入 "Hadoop compression codecs" 表中列出的除 LZO 之外的所有 codec,也会载入 io.compression.codecs 配置属性中列出的codec
默认情况下,这个属性值为空,仅当希望注册一个自定义的 codec 时需要改变这个值。每个 codec 知道它自己默认的文件扩展名,这就允许
CompressionCodecFactory 查找所有已注册的 codec,为一个给定扩展名找出一个匹配的 codec
Compression codec properties
+-----------------------+-------------------------------+-------------------------------------------------------------------------------+
| 属性名 | 类型 | 描述 |
+-----------------------+-------------------------------+-------------------------------------------------------------------------------+
| io.compression.codecs | Comma-separated Class names | A list of additional CompressionCodec classes for compression/decompression |
+-----------------------+-------------------------------+-------------------------------------------------------------------------------+
原生类库 (Native libraries)
----------------------------------------------------------------------------------------------------------------------------------------------
出于性能上的考虑,最好使用原生类库 (native library) 压缩和解压缩。例如,在一个测试中,使用原生的 gzip 类库解压降低了 50% 的时间,压缩大约 10%
的时间(与内置的 Java 实现比较)。
下面表格列出每种压缩格式的 Java 和原生实现的可用性。所有格式都有原生实现,但不是所有格式都有 Java 实现:
Compression library implementations
+-----------+---------------+---------------+
| 压缩格式 | Java 实现? | 原生实现 ? |
+-----------+---------------+---------------+
| DEFLATE | Yes | Yes |
+-----------+---------------+---------------+
| gzip | Yes | Yes |
+-----------+---------------+---------------+
| bzip2 | Yes | Yes |
+-----------+---------------+---------------+
| LZO | No | Yes |
+-----------+---------------+---------------+
| LZ4 | No | Yes |
+-----------+---------------+---------------+
| Snappy | No | Yes |
+-----------+---------------+---------------+
Apache Hadoop binary tarball 自带了预编译的原生压缩类库 for 64-bit Linux, libhadoop.so, 其他平台,需要用户自己编译各个类库。
原生类库通过 Java 系统属性 java.library.path 选择, 由 etc/hadoop 目录中 Hadoop 脚本设置这个属性,但如果不使用脚本,需要在应用程序中设置。
默认情况下, Hadoop 为其运行的平台查找原生类库,如果找到会自动加载。这意味着不必为使用原生类库改变任何配置设置。然而,有些场景下,可能
希望禁用原生类库,例如调试一个压缩相关的问题时,这时可以通过设置属性 io.native.lib.available 值为 false ,来使内置的 Java 实现获得使用。
CodecPool
------------------------------------------------------------------------------------------------------------------------------------------
如果在应用程序中使用原生类库做大量的压缩或解压缩,可以考虑使用 CodecPool, 它支持重用压缩器和解压器,从而降低创建这些对象的开销。如下范例:
// A program to compress data read from standard input and write it to standard output using a pooled compressor
public class PooledStreamCompressor {
public static void main(String[] args) throws Exception {
String codecClassname = args[0];
Class<?> codecClass = Class.forName(codecClassname);
Configuration conf = new Configuration();
CompressionCodec codec = (CompressionCodec)
ReflectionUtils.newInstance(codecClass, conf);
Compressor compressor = null;
try {
compressor = CodecPool.getCompressor(codec);
CompressionOutputStream out =
codec.createOutputStream(System.out, compressor);
IOUtils.copyBytes(System.in, out, 4096, false);
out.finish();
} finally {
CodecPool.returnCompressor(compressor);
}
}
}
从池中(pool) 获取一个给定 CompressionCodec 的 Compressor 实例,使用它的 createOutputStream() method 创建 CompressionOutputStream 。在 finally
块中,确保 compressor 返还给池,即便在流之间复制字节时有 IOException 发生。
2. 压缩和输入切片 (Compression and Input Splits)
----------------------------------------------------------------------------------------------------------------------------------------------
在考虑要由 MapReduce 处理的数如何压缩时,理解压缩格式是否支持切分(supports splitting)是非常重要的。考虑一个存储在 HDFS上 1GB 大小的未压缩文件,
HDFS 块大小为 128MB, 文件会被存储为 8 个数据块,一个 MapReduce 作业使用这个文件作为输入将会创建 8 个输入分片(splits),每个分片作为一个单独的
map 任务输入独立地处理。
现在想象一下,这是个 gzip 压缩文件,压缩后大小为 1GB. 跟前面那个一样, HDFS 存储这个文件为 8 个数据块。然而,为每个数据块创建一个分片不能工作,
因为在 gzip 流中从任意的点开始读取是不可能的,因此一个 map 任务独立于其他分片读取它的分片也是不可能的。 gzip 格式使用 DEFLATE 来存储压缩数据,
而 DEFLATE 以一系列的压缩块存储数据。问题是无法分辨每个块的开始位置来允许一个 reader 定位到数据流上的任意一个点来向前推进下一个块的起始位置,
以使它同步数据流。由于这个原因, gzip 不支持切分(splitting)。
这种情况下, MapReduce 会做正确的事,它不会尝试切分 gzipped 文件,因为它知道这个输入是 gzip 压缩的(通过查看它的文件扩展名),而 gzip 不支持切分。
它能够工作,但牺牲了数据的本地性:一个单一的 map 任务处理这 8 个 HDFS 数据块,大多数不是 map 本地的。而且,使用较少的 map , 作业是少粒度的,因而
会花费较长时间运行。
如果在我们这个假想的例子中,文件是一个 LZO 文件,会遇到相同的问题,因为底层的压缩格式也没有提供一个 reader 和数据流同步的方法。然而,使用一个由
Hadoop LZO 库自带的 indexer 工具进行预处理 LZO 文件是可以的。这个工具为分片点构建一个索引,当使用合适的 MapReduce 输入格式时,有效地使它变成可
切分的。
一个 bzip2 文件,在块之间提供了一个同步标记(marker),因此它支持切分。
应该使用哪种压缩格式 (WHICH COMPRESSION FORMAT SHOULD I USE?)
--------------------------------------------------------------------------------------------------------------------------------------------
Hadoop 应用程序处理大规模数据集,因此应该力图压缩的优势。使用哪种压缩格式依赖于这样的考虑,如文件大小,格式,处理数据使用的工具等。下面是一些
建议,大致按效率从高到低排列:
◇ 使用容器文件格式,例如顺序文件(sequence files), Avro datafiles, ORCFiles, or Parquet files ,所有这些文件都支持压缩和切分。一个快速的
压缩器如 LZO, LZ4, or Snappy 通常是一个好的选择。
◇ 使用一个支持切分的压缩格式,如 bzip2 (尽管 bzip2 相当慢), 或者可以通过索引来支持切分,例如 LZO 。
◇ 在应用程序中把文件切分成大块(chunk),然后使用任何支持的压缩格式压缩每个大块(不管它是否支持切分)。这种情况下,应该选择合适的 chunk 大小
以使压缩后的 chunk 大致和 HDFS block 大小相同。
◇ 不压缩存储文件。
对于大文件,不应为整个文件使用一个不支持切分的压缩格式,因为会丢失本地性,并使 MapReduce 应用程序非常低效。
3. 在 MapReduce 中使用压缩 (Using Compression in MapReduce)
----------------------------------------------------------------------------------------------------------------------------------------------
正如 Inferring CompressionCodecs using CompressionCodecFactory 中描述的,如果输入文件是压缩的,在由 MapReduce 读取时它们会自动解压,使用文件
扩展名确定使用哪个 codec 。
为了压缩 MapReduce 作业的输出,在作业的配置中设置 mapreduce.output.fileoutputformat.compress 属性值为 true , 并设置
mapreduce.output.fileoutputformat.compress.codec 属性为要使用的压缩 codec . 另一种方法,可以使用 FileOutputFormat 的静态方法来这些设置属性。
范例:
// Application to run the maximum temperature job producing compressed output
public class MaxTemperatureWithCompression {
public static void main(String[] args) throws Exception {
if (args.length != 2) {
System.err.println("Usage: MaxTemperatureWithCompression <input path> " +
"<output path>");
System.exit(-1);
www.it-ebooks.info
}
Job job = new Job();
job.setJarByClass(MaxTemperature.class);
FileInputFormat.addInputPath(job, new Path(args[0]));
FileOutputFormat.setOutputPath(job, new Path(args[1]));
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(IntWritable.class);
FileOutputFormat.setCompressOutput(job, true);
FileOutputFormat.setOutputCompressorClass(job, GzipCodec.class);
job.setMapperClass(MaxTemperatureMapper.class);
job.setCombinerClass(MaxTemperatureReducer.class);
job.setReducerClass(MaxTemperatureReducer.class);
System.exit(job.waitForCompletion(true) ? 0 : 1);
}
}
在压缩的输入上运行程序(不必和输出使用一样的压缩格式,即便本例中使用了一样的压缩格式),如下所示:
% hadoop MaxTemperatureWithCompression input/ncdc/sample.txt.gz output
如果要为输出生成顺序文件(sequence file), 可以设置 mapreduce.output.fileoutputformat.compress.type 属性来控制所用压缩的类型。默认值是 RECORD ,
即按每条单独的记录进行压缩。将其改为 BLOCK ,即按记录组进行压缩,这是推荐的设置,因为它的压缩效果更好。
SequenceFileOutputFormat 也有个静态工具方法,setOutputCompressionType() 可以设置这个属性。
为 MapReduce 作业输出设置压缩的配置属性总结到下面表格中。如果 MapReduce driver 使用 Tool 接口,可以在命令行上给程序传递这些属性,这比修改程序
硬编码压缩属性更加方便。
MapReduce compression properties
+---------------------------------------------------+---------------+-----------------------------------------------+
| 属性名 | 类型 | 默认值 |
+---------------------------------------------------+---------------+-----------------------------------------------+
| mapreduce.output.fileoutputformat.compress | boolean | false |
+---------------------------------------------------+---------------+-----------------------------------------------+
| mapreduce.output.fileoutputformat.compress.codec | Class name | org.apache.hadoop.io.compress.DefaultCodec |
+---------------------------------------------------+---------------+-----------------------------------------------+
| mapreduce.output.fileoutputformat.compress.type | String | RECORD |
+---------------------------------------------------+---------------+-----------------------------------------------+
压缩 map 任务输出 (Compressing map output)
-------------------------------------------------------------------------------------------------------------------------------------
即便你的 MapReduce 应用程序读和写都是未压缩的数据,压缩 map 阶段输出的中间文件也是有益的。 map 的输出数据写到磁盘并通过网络传输给
reducer 节点,因此通过使用一个快速的压缩器如 LZO, LZ4, or Snappy 可以很简单地获得性能的提升,因为传输的数据量降低了。
为 map 输出启用压缩的配置属性和设置压缩格式如下表所示:
Map output compression properties
+---------------------------------------+---------------+-----------------------------------------------+-----------------------------------+
| 属性名 | 类型 | 默认值 | 描述 |
+---------------------------------------+---------------+-----------------------------------------------+-----------------------------------+
| mapreduce.map.output.compress | boolean | false | Whether to compress map outputs |
+---------------------------------------+---------------+-----------------------------------------------+-----------------------------------+
| mapreduce.map.output.compress.codec | Class name | org.apache.hadoop.io.compress.DefaultCodec | The compression codec to use for |
| | | | map outputs |
+---------------------------------------+---------------+-----------------------------------------------+-----------------------------------+
添加下面几行为作业中的 map 输出启用 gzip 压缩:
Configuration conf = new Configuration();
conf.setBoolean(Job.MAP_OUTPUT_COMPRESS, true);
conf.setClass(Job.MAP_OUTPUT_COMPRESS_CODEC, GzipCodec.class, CompressionCodec.class);
Job job = new Job(conf);
*
*
*
3 序列化 (Serialization)
--------------------------------------------------------------------------------------------------------------------------------------------------
序列化 (Serialization) 是将结构化的对象转换为字节流以便在网络上传输或者写入到持久性存储介质的过程。 反序列化(Deserialization) 是将一个字节流转换回
结构化对象的逆过程。
序列化用在分布式数据处理的两个截然不同的领域:进程间通信和持久化存储。
在 Hadoop 中,系统中节点间进程通信是使用远程过程调用(remote procedure calls —— RPCs)实现的。 RPC 协议将消息序列化为二进制流发送到远程节点,远程节点
将二进制流反序列化为原始消息。总体来说, RPC 序列化格式应具有如下理想的特点:
紧凑 (Compact)
--------------------------------------------------------------------------------------------------------------------------------------------------
紧凑格式能有效利用网络带宽 ———— 数据中心最稀缺的资源。
快速 (Fast)
--------------------------------------------------------------------------------------------------------------------------------------------------
进程间通信构成了分布式系统的骨干,因此,序列化和反序列化使用尽可能小的性能开销是最基本的要素。
可扩展性 (Extensible)
--------------------------------------------------------------------------------------------------------------------------------------------------
协议会不断变化以满足新的需求,因此以可控的方式来发展和演化客户端和服务器端协议是很明确的。例如,可以向一个方法调用添加一个新的参数,并且有新
服务器可以接受来自老版本客户端的旧格式消息(没有新参数的)。
互操作性 (Interoperable)
---------------------------------------------------------------------------------------------------------------------------------------------------
有些系统,能支持由不同语言写的客户端与服务器通信更理想,因此需要设计能实现这种需求的格式。
表面上来看,对持久化存储数据格式的选择和一个序列化框架(from a serialization framework)有不同的需求。毕竟,一个 RPC 的存活期不到一秒,而持久化存储的
数据可能写入之后几年之后才会读取。但结果是 RPC 序列化格式的四个理想属性对一个持久化存储格式同样至关重要。我们希望存储格式是紧凑的(更有效地利用存储
空间),快速(读取和写入几 TB 数据开销最小),可扩展性(能够透明地读取以旧格式写入的数据),以及互操作性(能够使用不同的语言读写持久化数据)。
Hadoop 使用它自己的序列化格式, Writables ,当然是紧凑和快速的,但不容易扩展或使用 Java 以外的语言进行操作。
Writables 是 Hadoop 的核心(很多 MapReduce 程序使用它们作为 key 和 value 的数据类型)。
Avro , 一个序列化系统(a serialization system), 设计用于克服某些 Writables 的限制 (that was designed to overcome some of the limitations of Writables)。
1. Writable 接口 (The Writable Interface)
-----------------------------------------------------------------------------------------------------------------------------------------------
Writable 接口定义了两个方法 ———— 一个用于把它的状态写入到一个 DataOutput 二进制流,一个用于从一个 DataInput 二进制流中读取它的状态。
package org.apache.hadoop.io;
import java.io.DataOutput;
import java.io.DataInput;
import java.io.IOException;
public interface Writable {
void write(DataOutput out) throws IOException;
void readFields(DataInput in) throws IOException;
}
查看一个具体的 Writable 看看能用它做什么。 IntWritable, Java int 的一个封装。
IntWritable writable = new IntWritable();
writable.set(163);
可以使用带整数值参数的构造函数获得相同的效果:
IntWritable writable = new IntWritable(163);
写一个小的 helper method 序列化 IntWritable object:
public static byte[] serialize(Writable writable) throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
DataOutputStream dataOut = new DataOutputStream(out);
writable.write(dataOut);
dataOut.close();
return out.toByteArray();
}
The bytes are written in big-endian order (so the most significant byte is written to the stream first, which is dictated by the java.io.DataOutput
interface), and we can see their hexadecimal representation by using a method on Hadoop’s StringUtils:
assertThat(StringUtils.byteToHexString(bytes), is("000000a3"));
写一个反序列化 helper method 从一个字节数组读取一个 Writable object :
public static byte[] deserialize(Writable writable, byte[] bytes)
throws IOException {
ByteArrayInputStream in = new ByteArrayInputStream(bytes);
DataInputStream dataIn = new DataInputStream(in);
writable.readFields(dataIn);
dataIn.close();
return bytes;
}
call:
IntWritable newWritable = new IntWritable();
deserialize(newWritable, bytes);
assertThat(newWritable.get(), is(163));
WritableComparable and comparators
-----------------------------------------------------------------------------------------------------------------------------------------------
IntWritable 实现 WritableComparable interface , 是 Writable 和 java.lang.Comparable 接口的子接口:
package org.apache.hadoop.io;
public interface WritableComparable<T> extends Writable, Comparable<T> {
}
类型比较(Comparison of types) 对 MapReduce 是至关重要的,在排序阶段会使用 key 进行比较。 Hadoop 提供的一个优化是扩展自 Java Comparator 的
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);
}
这个接口允许实现者比较从流中读取的记录而不必把它们反序列化成对象,因而避免了创建对象的任何开销。例如 IntWritables 的比较器(comparator)实现了
原始的 compare() method , 分别从每个字节数组(byte arrays) b1 和 b2 读取一个整数,并且直接以给定的起始点(s1 和 s2) 及长度 (l1 and l2) 比较它们。
WritableComparator 是一个 WritableComparable 实现类的 RawComparator 的通用实现。它提供了两个主要功能,首先,它为 raw compare() method 提供了
一个默认实现,从流中反序列化对象,然后调用对象的 compare() method 。第二,它作为 RawComparator 实例的工厂(Writable 实现已经注册了),例如,为
IntWritable 获得一个 comparator :
RawComparator<IntWritable> comparator = WritableComparator.get(IntWritable.class);
这个 comparator 可以用于比较两个 IntWritable object:
IntWritable w1 = new IntWritable(163);
IntWritable w2 = new IntWritable(67);
assertThat(comparator.compare(w1, w2), greaterThan(0));
或者它们的序列化形式:
byte[] b1 = serialize(w1);
byte[] b2 = serialize(w2);
assertThat(comparator.compare(b1, 0, b1.length, b2, 0, b2.length),
greaterThan(0));
2. Writable 类 (Writable Classes)
-----------------------------------------------------------------------------------------------------------------------------------------------
Hadoop 自带了很多可供选择的 Writable 类,它们在 org.apache.hadoop.io 包。
原始类型(Primtives) 其他(Others)
+-----------------------+ +-----------------------+
| BooleanWritable | | NullWritable |
| | | |
+-----------------------+ +-----------------------+
+-----------------------+ +-----------------------+
| ByteWritable | | Text |
| | | |
+-----------------------+ +-----------------------+
+-----------------------+ +-----------------------+
| ShortWritable | | BytesWritable |
| | | |
+-----------------------+ +-----------------------+
+-----------------------+ +-----------------------+ +-----------------------+ +-----------------------+
| <<interface>> | | <<interface>> | | IntWritable | | MD5Hash |
| Writable |<——————| WritableComparable |<------| | | |
| org.apache.hadoop.io | +-----------------------+ +-----------------------+ +-----------------------+
+-----------------------+
+-----------------------+ +-----------------------+
| ArrayWritable | | VIntWritable |
| | | |
+-----------------------+ +-----------------------+
+-----------------------+ +-----------------------+
| ArrayPrimitiveWritable| | FloatWritable |
| | | |
+-----------------------+ +-----------------------+
+-----------------------+ +-----------------------+
| TwoDArrayWritable | | LongWritable |
| | | |
+-----------------------+ +-----------------------+
+-----------------------+ +-----------------------+
| VersionedWritable | | VLongWritable |
| | | |
+-----------------------+ +-----------------------+
+-----------------------+ +-----------------------+
| EnumSetWritable | | DoubleWritable |
| | | |
+-----------------------+ +-----------------------+
+-----------------------+
| CompressedWritable |
| |
+-----------------------+
+-----------------------+ +-----------------------+
| AbstractMapWritable |<——————| MapWritable |
| | | | |
+-----------------------+ | +-----------------------+
+-----------------------+ | +-----------------------+
| ObjectWritable | |——| SortedMapWritable |
| | | |
+-----------------------+ +-----------------------+
+-----------------------+
| GenericWritable |
| |
+-----------------------+
Java 基本类型的 Writable 封装器 (Writable wrappers for Java primitives)
-------------------------------------------------------------------------------------------------------------------------------------------
除了 char (可以存储在 IntWritable 中) 之外,所有的 Java 基本类型都有 Writable 封装(wrappers) ,都有一个 get() and set() method 用于获取和
存储封装的值。
Writable wrapper classes for Java primitives
+---------------+-------------------+-------------------+
| Java 基本类型 | Writable 实现 | 序列化大小(bytes) |
+---------------+-------------------+-------------------+
| boolean | BooleanWritable | 1 |
+---------------+-------------------+-------------------+
| byte | ByteWritable | 1 |
+---------------+-------------------+-------------------+
| short | ShortWritable | 2 |
+---------------+-------------------+-------------------+
| int | IntWritable | 4 |
+---------------+-------------------+-------------------+
| | VIntWritable | 1-5 |
+---------------+-------------------+-------------------+
| float | FloatWritable | 4 |
+---------------+-------------------+-------------------+
| long | LongWritable | 8 |
+---------------+-------------------+-------------------+
| | VLongWritable | 8-9 |
+---------------+-------------------+-------------------+
| double | DoubleWritable | 8 |
+---------------+-------------------+-------------------+
当对整数进行编码时,可以选择固定长度格式(IntWritable and LongWritable) 和可变长度格式(VIntWritable and VLongWritable)。可变长度格式如果值
足够小(between –112 and 127, inclusive)仅使用一个字节编码,否则,它们用第一个字节指明值是正数或者负数,以及后面跟的字节数。
例如,163 需要两字节:
byte[] data = serialize(new VIntWritable(163));
assertThat(StringUtils.byteToHexString(data), is("8fa3"));
如何在固定长度(fixed-length) 和可变长度(variable-length)编码之间做出选择?固定长度编码适合于值的分布在整个值空间(value space)非常均匀的情况。
例如,使用一个精心设计 hash 函数。打多数数值变量趋向于非均匀的分布,因此,平均起来看,可变长度编码更能节省空间。另一个使用可变长度编码的有点
是可以从 VIntWritable 切换到 VLongWritable ,因为它们的编码方式实际上是相同的,因此,通过选择一个可变长度的表现形式,不从一开始就提交 8 字节
的 long 形式数据,会有足够的增长空间。
Text
-------------------------------------------------------------------------------------------------------------------------------------------
Text 是一个针对 UTF-8 序列的 Writable 类。一般可以把它当作 ava.lang.String 的 Writable 类的等价物。
Text 使用一个 int (with a variable-length encoding) 存储字符串编码的字节数,因此它的最大值为 2GB 。此外, Text 使用标准的 UTF-8 ,因此可以
很容易地与其他能理解 UTF-8 的工具进行互操作。
索引 (Indexing)
--------------------------------------------------------------------------------------------------------------------------------------
因其强调的是标准 UTF-8 (standard UTF-8), 因此在 Text 和 Java 的 String 类之间有一些不同。 Text 类的索引是指已被编码的字节序列的位置,
不是 Unicode 字符或 Java 中 char 代码点(char code unit, as it is for String) 在字符串中位置。对于 ASCII 字符串,这三个索引位置的概念
是一致的。下面这个范例演示了 charAt() method 的使用:
Text t = new Text("hadoop");
assertThat(t.getLength(), is(6));
assertThat(t.getBytes().length, is(6));
assertThat(t.charAt(2), is((int) 'd'));
assertThat("Out of bounds", t.charAt(100), is(-1));
注意, charAt() 返回一个 int 值表示为一个 Unicode code point, 不像 String 变体,返回一个 char. Text 也有一个 find() method, 类似于
String 类的 indexOf():
Text t = new Text("hadoop");
assertThat("Find a substring", t.find("do"), is(2));
assertThat("Finds first 'o'", t.find("o"), is(3));
assertThat("Finds 'o' from position 4 or later", t.find("o", 4), is(4));
assertThat("No match", t.find("pig"), is(-1));
Unicode
--------------------------------------------------------------------------------------------------------------------------------------
一旦使用了编码超过一个字节的字符, Text 和 String 之间的区别就变得明显了。
迭代 (Iteration)
--------------------------------------------------------------------------------------------------------------------------------------
通过使用字节偏移进行位置索引,来迭代 Text 中的 Unicode 字符是非常复杂的,因为不能只是简单地增加索引值来实现。
可变性 (Mutability)
--------------------------------------------------------------------------------------------------------------------------------------
另一个与 String 类不同的是 Text 是可变的(mutable) ———— (like all Writable implementations in Hadoop, except NullWritable,
which is a singleton)。通过调用其中一个 set() method 可以重用一个 Text 实例。例如:
Text t = new Text("hadoop");
t.set("pig");
assertThat(t.getLength(), is(3));
assertThat(t.getBytes().length, is(3));
WARNING:
-----------------------------------------------------------------------------------------------------------------------------------
某些情况下,由 getBytes() method 返回的字节数组可能比 getLength() 返回的更长。
Text t = new Text("hadoop");
t.set(new Text("pig"));
assertThat(t.getLength(), is(3));
assertThat("Byte length not shortened", t.getBytes().length,
is(6));
这说明了为什么在调用 getBytes() 方法之后总应该调用 getLength() method 是有必要的,这样才能知道得到的字节数组有多少有效数据。
转储为字符串 (Resorting to String)
--------------------------------------------------------------------------------------------------------------------------------------
Text 不像 java.lang.String 那样有丰富的 API 来操作字符串,因此,在有些情况下,需要将 Text 对象转换为一个 String 类型的字符串。这是通常
的做法,使用它的 toString() method:
assertThat(new Text("hadoop").toString(), is("hadoop"));
BytesWritable
-------------------------------------------------------------------------------------------------------------------------------------------
BytesWritable 是对二进制数据数组的封装。它的序列化格式为,一个 4 字节整数字段指定其后跟随的字节数,后面紧跟字节序列本身。例如,一个长度为2的
字节数组,其元素值为 3 和 5,序列化为一个 4-byte 的整数 (00000002),后面跟两个字节的数组(其值为 03 和 05):
BytesWritable b = new BytesWritable(new byte[] { 3, 5 });
byte[] bytes = serialize(b);
assertThat(StringUtils.byteToHexString(bytes), is("000000020305"));
BytesWritable 是可变的,它的值可以通道调用其 set() method 改变。与 Text 一样,由 BytesWritable 的 getBytes() method 返回的字节数组大小 —— 是
它的容量(the capacity) —— 不能反映存储在 BytesWritable 中数据的实际大小(the actual size of the data)。应该通过调用 getLength() 方法确定它
BytesWritable 的大小(size):
b.setCapacity(11);
assertThat(b.getLength(), is(2));
assertThat(b.getBytes().length, is(11));
NullWritable
-------------------------------------------------------------------------------------------------------------------------------------------
NullWritable 是一种特殊类型的 Writable, 有 0 长度的序列化(zero-length serialization),没有字节写入流或从流中读取。它被用作一个占位符(used as
a placeholder),例如,在 MapReduce 中,如果不需要使用某个位置上的 key 或 value 时,可以把这个 key 或 value 声明为 NullWritable 类型,以高效
存储一个常量空值(effectively storing a constant empty value)。 在一个顺序文件中(in a SequenceFile),如果只想存储一些列的 value, 而不是key-value
对,可以把 NullWritable 用作顺序文件的 key。
NullWritable 是一个不可变的单例类型,可以通过调用 NullWritable.get() 来获取它的实例。
ObjectWritable and GenericWritable
-------------------------------------------------------------------------------------------------------------------------------------------
ObjectWritable 是对如下类型的一个通用封装(a general-purpose wrapper):Java primitives, String, enum, Writable, null, 或这些类型的数组。
它在 Hadoop 的 RPC 中用于对方法参数和返回值类型进行封装和解封装(marshal and unmarshal)。
当一个字段可能有多种类型时,ObjectWritable 非常有用。例如,如果一个顺序文件中的 value 有多种类型时,就可以声明 value 类型为 ObjectWritable,
并且把每种类型都封装成 ObjectWritable 。作为一种通用机制,它会合理地占用一些空间,因为每次序列化时会写入其封装类型的 classname 。如果类型数量
很小,并且提前知道这些类型,通过使用一个静态的类型数组,序列化时使用数组中的索引来引用类型可以提升其空间占用的效率。这正是 GenericWritable 使
用的方法,用户必须在继承它的子类中指明它支持的类型。
Writable 集合类 (Writable collections)
-------------------------------------------------------------------------------------------------------------------------------------------
org.apache.hadoop.io 包中包含了 6 个 Writable 的集合类型:
ArrayWritable,
ArrayPrimitiveWritable,
TwoDArrayWritable,
MapWritable,
SortedMapWritable,
EnumSetWritable.
ArrayWritable 和 TwoDArrayWritable 是 Writable 实例的一维数组和二维数组的 Writable 实现。ArrayWritable 和 TwoDArrayWritable 中的所有元素必须
是同一个类实例,在构造函数中指定:
ArrayWritable writable = new ArrayWritable(Text.class);
在 Writable 由类型来定义的环境下,例如顺序文件的 key 或 value, 或者更常见的情况为 MapReduce 的输入,则需要子类化 ArrayWritable(or TwoDArrayWritable,
as appropriate) 来静态地设置类型,例如:
public class TextArrayWritable extends ArrayWritable {
public TextArrayWritable() {
super(Text.class);
}
}
ArrayWritable 和 TwoDArrayWritable 都有 get() and set() 方法,也都有 toArray() 方法,用于创建一个数组(或 2D 数组)的浅拷贝。
ArrayPrimitiveWritable 是对 Java 基本类型数组的封装,元素类型可以在调用 set() 方法时识别出来,因此,不需要通过子类化来设置类型。
MapWritable 是一个 java.util.Map<Writable, Writable> 的实现, SortedMapWritable 是 java.util.SortedMap<WritableComparable, Writable> 的实现。
每个 key 和 value 字段的类型是那个字段序列化的一部分。其类型作为一个类型数组(an array of types)的索引存储在一个单独的字节中。类型数组存储的是
org.apache.hadoop.io 包中的标准类型,但也可以容纳自定义的 Writable 类型,通过写入一个头来为非标准类型编码到类型数组中(by writing a header that
encodes the type array for nonstandard types)。根据它们的实现,MapWritable 和 SortedMapWritable 把字节的正数部分用于自定义类型,因此,最多可以有
127 个 非标准的 Writable 类可用于任何特定的 MapWritable 或 SortedMapWritable 实例中。
下面是对不同类型的 key 和 value 使用 MapWritable 演示:
MapWritable src = new MapWritable();
src.put(new IntWritable(1), new Text("cat"));
src.put(new VIntWritable(2), new LongWritable(163));
MapWritable dest = new MapWritable();
WritableUtils.cloneInto(dest, src);
assertThat((Text) dest.get(new IntWritable(1)), is(new Text("cat")));
assertThat((LongWritable) dest.get(new VIntWritable(2)),
is(new LongWritable(163)));
显然,没有 set 和 list 的 Writable 集合实现。一般的 set 可以使用 NullWritable 类型 value 的 MapWritable (or a SortedMapWritable for a sorted set)
来模拟。也有一个 EnumSetWritable 对应于 enum 类型的 set.
对于单一 Writable 类型的 list, ArrayWritable 类就足够用了。但对于在一个 list 中存储多个 Writable 类型的元素,需要使用 GenericWritable 来封装
ArrayWritable 中的元素。另一种办法,可以利用 MapWritable 中的思想写一个通用的 ListWritable。
3. 实现一个自定义的 Writable (Implementing a Custom Writable)
-----------------------------------------------------------------------------------------------------------------------------------------------
Hadoop 自带了一套非常有用的 Writable 实现用以服务于大部分需要。然而,有时也需要写一个自定义的实现。通过一个自定义的 Writable, 可以完全控制二进制
数据的表现形式和数据的排序次序。由于 Writable 是 MapReduce 数据途径的核心,调节二进制表现形式能对性能产生显著的效果。 Hadoop 自带的 Writable 实现
已经过很好的调优了,但对于更复杂的结构,创建一个新的 Writable 类型比组合那些固有的类型通常会更好。
提示:
------------------------------------------------------------------------------------------------------------------------------------------
如果正在考虑写一个自定义的 Writable ,尝试另一个序列化框架(serialization framework)可能更好。 比如 Avro, 它支持通过声明来定义自定义类型。
演示创建一个自定义的 Writable, 写一个表示一对字符串的实现, TextPair :
// A Writable implementation that stores a pair of Text objects
import java.io.*;
import org.apache.hadoop.io.*;
public class TextPair implements WritableComparable<TextPair> {
private Text first;
private Text second;
public TextPair() {
set(new Text(), new Text());
}
public TextPair(String first, String second) {
set(new Text(first), new Text(second));
}
public TextPair(Text first, Text second) {
set(first, second);
}
public void set(Text first, Text second) {
this.first = first;
this.second = second;
}
public Text getFirst() {
return first;
}
public Text getSecond() {
return second;
}
@Override
public void write(DataOutput out) throws IOException {
first.write(out);
second.write(out);
}
@Override
public void readFields(DataInput in) throws IOException {
first.readFields(in);
second.readFields(in);
}
@Override
public int hashCode() {
return first.hashCode() * 163 + second.hashCode();
}
@Override
public boolean equals(Object o) {
if (o instanceof TextPair) {
TextPair tp = (TextPair) o;
return first.equals(tp.first) && second.equals(tp.second);
}
return false;
}
@Override
public String toString() {
return first + "\t" + second;
}
@Override
public int compareTo(TextPair tp) {
int cmp = first.compareTo(tp.first);
if (cmp != 0) {
return cmp;
}
return second.compareTo(tp.second);
}
}
所有的 Writable 实现都必须有一个默认的构造器,这样 MapReduce framework 才能实例化它们,然后通过调用 readFields() 操作它们的字段。
Writable 实例是可变的并且会经常重用,因此应该小心避免在 write() 或 readFields() 方法中分配对象。
TextPair 的 write() method 通过委托给 Text 对象本身,依次将每个 Text 对象序列化到输出流中。类似地,readFields() 通过委托给每个 Text 对象,
反序列化输入流中的字节。
就像在 Java 中写任何值对象做的那样,应该重写 java.lang.Object 继承来 hashCode(), equals(), and toString() 方法。 hashCode() method 被
HashPartitioner (the default partitioner in MapReduce)使用来选择一个 reduce 分区,因此,要写一个好用的 hash 函数,通过足够的混合以确保
每个 reduce 分区大小相似。
WARNING:
------------------------------------------------------------------------------------------------------------------------------------------
如果计划将自定义的 Writable 与 TextOutputFormat 关联使用,就必须实现它的 toString() method . TextOutputFormat 在 key 和 value 上调用其
toString() 方法用于它们的输出表示。对于 TextPair, 我们把两个底层的 Text object 作为字符串写出,由一个 tab 字符分隔。
TextPair 是 WritableComparable 的一个实现,因此它提供了 compareTo() method 的实现按我们期望的那样排列次序:先按第一个字符串排序,然后按第二个
字符串排序。
为提升速度实现一个 RawComparator (Implementing a RawComparator for speed)
-------------------------------------------------------------------------------------------------------------------------------------------
TextPair 的代码可以按其固有的方式运行,然而,我们可以对它进一步优化。
如前所述,当把 TextPair 用于 MapReduce 的 key时,它必须反序列化为一个对象来调用 compareTo() method 。那么是否有可能仅仅查看它们的序列化形式就
能比较两个 TextPair 对象呢?
事实表明,我们可以这样做,因为 TextPair 是两个相互关联的 Text 对象,而一个 Text 对象的二进制表现形式为,一个长度可变的(variable-length)整数
包含其字符串的 UTF-8 形式的字节数,后面紧跟 UTF-8 字节序列本身。诀窍在于读取对象开始部分的长度,这样就能知道第一个 Text 对象的字节表现形式有
多长,然后就能委托给 Text 的 RawComparator 并为其第一个和第二个字符串提供合适的偏移量来调用它的 compare() method 。
下面给出相信daim(注意代码是嵌入到 TextPair 类之中的内部类)
// A RawComparator for comparing TextPair byte representations
public static class Comparator extends WritableComparator {
private static final Text.Comparator TEXT_COMPARATOR = new Text.Comparator();
public Comparator() {
super(TextPair.class);
}
@Override
public int compare(byte[] b1, int s1, int l1, byte[] b2, int s2, int l2) {
try {
int firstL1 = WritableUtils.decodeVIntSize(b1[s1]) + readVInt(b1, s1);
int firstL2 = WritableUtils.decodeVIntSize(b2[s2]) + readVInt(b2, s2);
int cmp = TEXT_COMPARATOR.compare(b1, s1, firstL1, b2, s2, firstL2);
if (cmp != 0) {
return cmp;
}
return TEXT_COMPARATOR.compare(b1, s1 + firstL1, l1 - firstL1,
b2, s2 + firstL2, l2 - firstL2);
} catch (IOException e) {
throw new IllegalArgumentException(e);
}
}
}
static {
WritableComparator.define(TextPair.class, new Comparator());
}
代码从 WritableComparator 继承实现子类化而非直接实现 RawComparator, 因为 WritableComparator 提供了一些便利的方法及默认实现。
代码的微妙之处在于计算 firstL1 和 firstL2, 每个字节流中第一个 Text 字段的长度。每个都由变长整数的长度(由 WritableUtils 的 decodeVIntSize()
方法返回)和编码的值(由 readVInt() 方法返回)组成。
静态块部分用于注册这个 raw comparator, 这样当 MapReduce 遇到 TextPair 类时,它就知道使用这个 raw comparator 作为其默认的 comparator .
自定义 comparator (Custom comparators)
-------------------------------------------------------------------------------------------------------------------------------------------
从 TextPair 中可以看出,写一个 raw comparator 要谨慎小心,因为必须要处理字节级别的细节。如果真的需要写自己的 raw comparator 实现,参考一些
org.apache.hadoop.io 包中 Writable 的实现获得更深入的思路很有价值。WritableUtils 的工具方法也非常好用。
如果可能,自定义 comparator 也可以写为 RawComparators. 这些 comparator 实现一个与默认定义的自然排序不同的排序次序。
下面展示一个 TextPair 的 comparator, 名为 FirstComparator, 只考虑 TextPair 对象中第一个字符串。
// A custom RawComparator for comparing the first field of TextPair byte representations
public static class FirstComparator extends WritableComparator {
private static final Text.Comparator TEXT_COMPARATOR = new Text.Comparator();
public FirstComparator() {
super(TextPair.class);
}
@Override
public int compare(byte[] b1, int s1, int l1, byte[] b2, int s2, int l2) {
try {
int firstL1 = WritableUtils.decodeVIntSize(b1[s1]) + readVInt(b1, s1);
int firstL2 = WritableUtils.decodeVIntSize(b2[s2]) + readVInt(b2, s2);
return TEXT_COMPARATOR.compare(b1, s1, firstL1, b2, s2, firstL2);
} catch (IOException e) {
throw new IllegalArgumentException(e);
}
}
@Override
public int compare(WritableComparable a, WritableComparable b) {
if (a instanceof TextPair && b instanceof TextPair) {
return ((TextPair) a).first.compareTo(((TextPair) b).first);
}
return super.compare(a, b);
}
}
4. 序列化框架 (Serialization Frameworks)
-----------------------------------------------------------------------------------------------------------------------------------------------
尽管 MapReduce 程序使用 Writable 的 key 和 value 类型,但这并不是 MapReduce API 强制的。实际上,任何类型都可以使用,唯一的要求是每种类型都有将
对象转换为二进制表示和将二进制表示转换为对象的机制。
为了支持这种能力, Hadoop 为可插拔的序列化框架(pluggable serialization frameworks)提供了一个 API 。序列化框架通过一个 Serialization (在
org.apache.hadoop.io.serializer 包中)的实现来表现。例如, WritableSerialization ,是 为 Writable 类型提供的 Serialization 实现。
一个 Serialization 定义了一个从类型到 Serializer 实例(for turning an object into a byte stream)和 Deserializer 实例(for turning a byte stream
into an object)的映射。
设置 io.serializations 属性为一个逗号分隔的(comma-separated)类名列表来注册 Serialization 的实现。默认值包括
org.apache.hadoop.io.serializer.WritableSerialization, Avro Specific 以及 Reflect serializations, 表明只有 Writable 或 Avro objects 能被序列化
或反序列化。
Hadoop 包含一个 JavaSerialization 类,用于 Java Object 序列化。虽然这使得在 MapReduce 程序中使用标准 Java 类型,如 Integer 或 String 非常方便,
但 Java Object Serialization 不如 Writable 效率高,因此没有使用价值。
序列化 IDL (Serialization IDL)
-------------------------------------------------------------------------------------------------------------------------------------------
有很多其他的序列化框架以不同的方法来解决问题:不是通过具体代码,而是通过使用一种中立语言(language-neutral),声明式的接口定义语言
(interface description language (IDL))来定义类型。系统可以使用不同类型的语言来产生类型,这很好地解决了互操作性。它们通常还会定义版本体系(
versioning schemes) 来使类型演化清晰直接。
Apache Thrift 和 Google Protocol Buffers 是两个比较流行的序列化框架,它们通常用于二进制数据持久化存储格式。然而,对于 MapReduce 格式,它们的
支持很有限,只是在 Hadoop 内部的某些部分用于 RPC 和数据交换。
*
*
*
4 基于文件的数据结构 (File-Based Data Structures)
--------------------------------------------------------------------------------------------------------------------------------------------------
有些应用中,需要一种特定的数据结构来存储数据(to hold your data)。对基于 MapReduce 的处理,把每个二进制数据的大对象(blob) 放到它自己的文件中不适于
系统扩展(scale), 因此, Hadoop 为此开发了很多高级别的容器。
1. 顺序文件 (SequenceFile)
-----------------------------------------------------------------------------------------------------------------------------------------------
考虑一个日志文件,每条日志记录都是一个新行的文本。如果想要记录二进制类型,纯文本就不是一个合适的格式了。 Hadoop 的 SequenceFile 类符合这种场景的
要求,它为二进制的 key-value 对提供一个持久化数据结构。使用它作为一个日志文件的格式,为其选择一个 key ,例如由 LongWritable 表示的时间戳,value
是一个表示记入日志内容的 Writable 类型。
SequenceFiles 作为小文件的容器也工作得很好。HDFS 和 MapReduce 都是对单文件优化的,因此把很多的小文件打包到一个 SequenceFiles来存储和处理更加高效
写入 SequenceFiles (Writing a SequenceFile)
--------------------------------------------------------------------------------------------------------------------------------------------
要创建 SequenceFiles, 使用一个 createWriter() static method , 返回一个 SequenceFile.Writer 实例。
存储在 SequenceFile 中的 keys 和 values 不必一定是 Writable 的类型,任何能够通过 Serialization 序列化和反序列化的类型都可以使用。
有了一个 SequenceFile.Writer ,就可以使用 append() method 将 key-value 对写入文件。写操作结束时,调用 close() method (SequenceFile.Writer
implements java.io.Closeable).
示例:
----------------------------------------------------------------------------------------------------------------------------------------
public class SequenceFileWriteDemo {
private static final String[] DATA = {
"One, two, buckle my shoe",
"Three, four, shut the door",
"Five, six, pick up sticks",
"Seven, eight, lay them straight",
"Nine, ten, a big fat hen"
};
public static void main(String[] args) throws IOException {
String uri = args[0];
Configuration conf = new Configuration();
FileSystem fs = FileSystem.get(URI.create(uri), conf);
Path path = new Path(uri);
IntWritable key = new IntWritable();
Text value = new Text();
SequenceFile.Writer writer = null;
try {
writer = SequenceFile.createWriter(fs, conf, path, key.getClass(), value.getClass());
for (int i = 0; i < 100; i++) {
key.set(100 - i);
value.set(DATA[i % DATA.length]);
System.out.printf("[%s]\t%s\t%s\n", writer.getLength(), key, value);
writer.append(key, value);
}
} finally {
IOUtils.closeStream(writer);
}
}
}
运行:
% hadoop SequenceFileWriteDemo numbers.seq
读取 SequenceFiles (Reading a SequenceFile)
--------------------------------------------------------------------------------------------------------------------------------------------
从头到尾读取 SequenceFile 不外乎创建一个 SequenceFile.Reader 的实例然后反复调用 next() 方法来迭代文件内的记录 (record)。使用哪个方法与使用的
序列化框架相关。如果使用的是 Writable 类型,可以使用提供一个 key 和 一个 value 参数的 next() method, 将流中的下一条记录的 key 和 value 读取
到这些变量中:
public boolean next(Writable key, Writable val)
如果读取到一个 key-value 对返回值为 true, 如果已到达文件末尾,返回值为 false.
对于其他非 Writable (non-Writable) 的序列化框架(such as Apache Thrift), 应该使用如下两个方法:
public Object next(Object key) throws IOException
public Object getCurrentValue(Object val) throws IOException
这种情况下,确定要使用的序列化已在 io.serializations 属性中设置。
如果 next() 方法返回一个非空对象(a non-null object),表明一个 key-value 对已从流中读取,并且可以使用 getCurrentValue() 方法获取到 value 对象。
否则,如果 next() 方法返回 null, 表明已到达文件末尾。
下面程序演示了如何读取一个含有 Writable 类型的 key 和 value 的顺序文件。注意如何在 SequenceFile.Reader 对象上通过调用 getKeyClass() 和
getValueClass() 方法获取 key 和 value 的类型,然后使用 ReflectionUtils 创建一个 key 的实例和一个 value 的实例。这种技术的使用使得该程序可以用于
任何具有 Writable 类型 key 和 value 的顺序文件。
// Reading a SequenceFile
public class SequenceFileReadDemo {
public static void main(String[] args) throws IOException {
String uri = args[0];
Configuration conf = new Configuration();
FileSystem fs = FileSystem.get(URI.create(uri), conf);
Path path = new Path(uri);
SequenceFile.Reader reader = null;
try {
reader = new SequenceFile.Reader(fs, path, conf);
Writable key = (Writable)
ReflectionUtils.newInstance(reader.getKeyClass(), conf);
Writable value = (Writable)
ReflectionUtils.newInstance(reader.getValueClass(), conf);
long position = reader.getPosition();
while (reader.next(key, value)) {
String syncSeen = reader.syncSeen() ? "*" : "";
System.out.printf("[%s%s]\t%s\t%s\n", position, syncSeen, key, value);
position = reader.getPosition(); // beginning of next record
}
} finally {
IOUtils.closeStream(reader);
}
}
}
这个程序的另一特性是显示顺序文件中了同步点的位置(the positions of the sync points in the sequence file)。一个同步点(sync point)是流中的一个点,
如果 reader 丢失了一个记录边界,可用于与记录边界同步,例如,定位到流中任意一个位置之后就有可能丢失记录边界(after seeking to an arbitrary position
in the stream)。同步点由 SequenceFile.Writer 记录,在顺序文件被写入的时候每隔几条记录(every few records)插入一个特殊项来标记同步点。这些特殊项非常小
因此只造成非常小的存储开销 ———— 不到 1% 。同步点总是与记录的边界对齐。
运行 SequenceFileReadDemo 程序,将顺序文件中的同步点显示为星号:第一个 position 在 2021,第二个发生在4075
% hadoop SequenceFileReadDemo numbers.seq
[128] 100 One, two, buckle my shoe
[173] 99 Three, four, shut the door
[220] 98 Five, six, pick up sticks
[264] 97 Seven, eight, lay them straight
[314] 96 Nine, ten, a big fat hen
[359] 95 One, two, buckle my shoe
[404] 94 Three, four, shut the door
[451] 93 Five, six, pick up sticks
[495] 92 Seven, eight, lay them straight
[545] 91 Nine, ten, a big fat hen
[590] 90 One, two, buckle my shoe…
[1976] 60 One, two, buckle my shoe
[2021*] 59 Three, four, shut the door
[2088] 58 Five, six, pick up sticks
[2132] 57 Seven, eight, lay them straight
[2182] 56 Nine, ten, a big fat hen…
[4557] 5 One, two, buckle my shoe
[4602] 4 Three, four, shut the door
[4649] 3 Five, six, pick up sticks
[4693] 2 Seven, eight, lay them straight
[4743] 1 Nine, ten, a big fat hen
有两种方法定位顺序文件中一个给定的位置。第一个是 seek() method, 将 reader 定位到文件中一个给定的点。例如,定位到一个记录边界可以按如期的方式工作:
reader.seek(359);
assertThat(reader.next(key, value), is(true));
assertThat(((IntWritable) key).get(), is(95));
但如果定位的位置不是在文件中一个记录的边界, reader 调用 next() 方法时会失败。
reader.seek(360);
reader.next(key, value); // fails with IOException
第二个查找记录边界的方法是利用同步点。在 SequenceFile.Reader 上调用 sync(long position) method 把 reader 定位到 position 之后的下一个同步点。如果
文件中这个 position 之后没有同步点, reader 会被定位到文件的末尾。因此,可以在流中使用任意的 position 调用 sync() ———— 不必一定是一个记录的边界,
reader 会把它自己重新定位到下一个同步点,因而读取可以继续:
reader.sync(360);
assertThat(reader.getPosition(), is(2021L));
assertThat(reader.next(key, value), is(true));
assertThat(((IntWritable) key).get(), is(59));
WARNING
--------------------------------------------------------------------------------------------------------------------------------------------
SequenceFile.Writer 有一个 sync() 方法用于在流中当前位置插入一个同步点。不要与 Syncable interface 中定义的 hsync() 方法混淆,hsync() 方法用于
将 buffer 同步到底层设备中。
在使用顺序文件作为 MapReduce 输入时,同步点自行加入,因为它们允许文件被切分并且不同部分由单独的 map 任务独立处理。
通过命令行接口显示 SequenceFiles (Displaying a SequenceFile with the command-line interface)
--------------------------------------------------------------------------------------------------------------------------------------------
hadoop 的 fs command 有一个 -text option 以文本格式显示顺序文件。它查看文件的幻数代码来尝试检测出文件类型并适当地将它转换为文本。它能识别出
gzipped 文件,顺序文件以及 Avro 数据文件,否则,它假设输入是纯文本。
对于顺序文件,该命令仅当 key 和 value 是有具体含义的字符串表现形式时才真正有用(as defined by the toString() method)。如果有自己定义的键或值的
类,需要确保它们在 Hadoop 的类路径内。
运行:
-----------------------------------------------------------------------------------------------------------------------------------------
% hadoop fs -text numbers.seq | head
100 One, two, buckle my shoe
99 Three, four, shut the door
98 Five, six, pick up sticks
97 Seven, eight, lay them straight
96 Nine, ten, a big fat hen
95 One, two, buckle my shoe
94 Three, four, shut the door
93 Five, six, pick up sticks
92 Seven, eight, lay them straight
91 Nine, ten, a big fat hen
SequenceFiles 的排序与合并 (Sorting and merging SequenceFiles)
--------------------------------------------------------------------------------------------------------------------------------------------
对一个或多个顺序文件进行排序或合并最强大的方法是使用 MapReduce. MapReduce 天生具有并发能力并且可以指定要使用的 reducer 的数量,也就确定了输出
部分的数量。例如,指定一个 reducer, 会得到一个单一的输出文件。我们可以使用 Hadoop 自带的排序案例来指定输入和输出都是顺序文件,并设置 key 和
value 的类型:
% hadoop jar \
$HADOOP_HOME/share/hadoop/mapreduce/hadoop-mapreduce-examples-*.jar \
sort -r 1 \
-inFormat org.apache.hadoop.mapreduce.lib.input.SequenceFileInputFormat \
-outFormat org.apache.hadoop.mapreduce.lib.output.SequenceFileOutputFormat \
-outKey org.apache.hadoop.io.IntWritable \
-outValue org.apache.hadoop.io.Text \
numbers.seq sorted
% hadoop fs -text sorted/part-r-00000 | head
1 Nine, ten, a big fat hen
2 Seven, eight, lay them straight
3 Five, six, pick up sticks
4 Three, four, shut the door
5 One, two, buckle my shoe
6 Nine, ten, a big fat hen
7 Seven, eight, lay them straight
8 Five, six, pick up sticks
9 Three, four, shut the door
10 One, two, buckle my shoe
另一种使用 MapReduce 来 sort/merge 的方法是 SequenceFile.Sorter 类,它有一个 sort() 和 merge() 方法。这些方法是 MapReduce 早期提供的功能,
并且是比 MapReduce 低级的函数(例如,要获得并发性,需要手工对数据分区),因此通常情况下,使用 MapReduce 来对顺序文件进行排序和合并是更合适的方法。
SequenceFiles 的格式 (The SequenceFile format)
--------------------------------------------------------------------------------------------------------------------------------------------
一个顺序文件又一个文件头和它后面紧跟的一个或多个记录组成。一个顺序文件最开始的三个字节为 SEQ ,作为一个魔数;其后跟随一个字节表示版本号。
文件头还包含其他字段,包括 key 和 value 的类的名字,压缩细节,用户定义的元数据以及同步标识(the sync marker)。
SequenceFile Header:
version - 3 bytes of magic header SEQ, followed by 1 byte of actual version number (e.g. SEQ4 or SEQ6)
keyClassName -key class
valueClassName - value class
compression - A boolean which specifies if compression is turned on for keys/values in this file.
blockCompression - A boolean which specifies if block-compression is turned on for keys/values in this file.
compression codec - CompressionCodec class which is used for compression of keys and/or values (if compression is enabled).
metadata - SequenceFile.Metadata for this file.
sync - A sync marker to denote end of the header.
回顾同步标识用于reader 在文件中从任意位置同步到一个记录边界。每个文件有一个随机生成的同步标记,它的值存储在文件头中。同步标记出现在顺序文件的
记录之间。它们设计为占用小于 1% 的存储开销,因此没必要在每两个记录之间出现(特别是比较短的记录)。
记录的内部格式依赖于是否启用了压缩,如果启用了,是使用的记录压缩(record compression)还是块压缩(block compression)。
如果没有压缩启用(默认值),每个记录由记录长度(字节单位),key 长度, key, 然后是 value. 长度字段作为 4-byte 整数写入,遵循 java.io.DataOutput 的
writeInt() 方法协定(contract)。 key 和 value 使用 Serialization 定义的类写入到顺序文件中。
Uncompressed SequenceFile Format:
Header
Record
Record length
Key length
Key
Value
A sync-marker every few 100 bytes or so.
记录压缩(record compression)的格式几乎与没有压缩的格式相同,除了 value 的字节使用文件头定义的 codec 进行压缩之外。注意, key 是不被压缩的。
Record-Compressed SequenceFile Format:
Header
Record
Record length
Key length
Key
Compressed Value
A sync-marker every few 100 bytes or so.
块压缩(block compression)一次压缩多条记录;因其有机会利用相似的记录进行压缩,因此比记录压缩更加紧凑,并通常作为首选的压缩方式。记录不断地
添加到块中直到它达到了最小字节大小(a minimum size in bytes), 其值由 io.seqfile.compress.blocksize 属性定义,默认值为 1000000 。每个块开始之前写入
一个同步标记。块的格式为一个字段指明块中的记录数,后跟四个压缩的字段: key 的长度, keys , value 的长度,以及 values
Block-Compressed SequenceFile Format:
Header
Record Block
Uncompressed number of records in the block
Compressed key-lengths block-size
Compressed key-lengths block
Compressed keys block-size
Compressed keys block
Compressed value-lengths block-size
Compressed value-lengths block
Compressed values block-size
Compressed values block
A sync-marker every block.
2. MapFile 文件 (MapFile)
-----------------------------------------------------------------------------------------------------------------------------------------------
一个 MapFile 是一个已排序的 SequenceFile, 它有一个索引(an index) 允许按 key 查找。索引本身是一个 SequenceFile, 其中包含了 map 中一定比例的 key
(every 128th key, by default)。其思想是 index 可以载入到内存来提供从主数据文件中进行快速查找,主数据文件是另一个 SequenceFile, 包含了所有的按
key 次序排序的 map 记录( map entries) 。
MapFile 为读取数据和写入数据提供了和 SequenceFile 非常相似的接口。必须知道的是在使用 MapFile.Writer 进行写入的时候, map 记录是按次序添加的,否则
会抛出 IOException 。
MapFile 的变体 (MapFile variants)
--------------------------------------------------------------------------------------------------------------------------------------------
Hadoop 自带了几个在通用的 key-value MapFile 接口上的变体:
◇ SetFile:是一个特使的 MapFile,用于存储 Writable 类型的 key 的 set 集合。keys 必须以排序的次序添加。
◇ ArrayFile:是一个 MapFile, 其中 key 是一个整数,表示为元素在数组中的索引, value 是 Writable 类型的值。
◇ BloomMapFile:是一个 MapFile, 提供了一个快速版本的 get() method, 特别是对稀疏的文件非常有用。其实现是使用一个动态的 Bloom 过滤器来测试
一个给定的 key 是否存在与 map 文件中。测试非常快因为是在内存中(in-memory)操作的。只有 test 通过时(key 存在) 常规的 get()
method 才被调用。
其它文件格式 和 面向列的格式 (Other File Formats and Column-Oriented Formats)
--------------------------------------------------------------------------------------------------------------------------------------------
Sequence 文件和 map 文件是 Hadoop 中最早的二进制文件格式,但它们不是唯一的文件格式,事实上,对于新项目,还有更好的选择可以考虑。
Avro datafiles 像 sequence 文件一样设计用于大规模数据处理 ———— 它们是紧凑的并且可切分 ———— 但它们可以跨多种编程语言移植。存储在 Avro datafiles
中的对象由一个 schema 描述,而不是像 Writable 对象那样以 Java 代码实现(就像 sequence 文件中的情况)。Avro datafiles 在 Hadoop 生态系统中被广泛
支持,因此它们是一个二进制格式很好的默认选择。
Sequence files, map files, and Avro datafiles 都是面向行的(row-oriented)文件格式,意味着文件中每行的值都是连续存储的。在面向列的(column-oriented)
的格式中,一个文件中的行(or, equivalently, a table in Hive) 被打断为行的分片(row splits), 每个分片以面向列的方式存储:首先存储每行值的第一列,
然后在第二列中存储下一行的值,等等。
面向列的布局允许跳过一个查询中不访问的列。考虑一个表的查询只处理第二列。在面向行的存储中,比如一个顺序文件,整个行都要载入内存,即使实际读取的
只是第二行。延迟反序列化通过仅仅反序列化被访问的列字段节省了一些处理周期,但它不能避免从磁盘读取每一个行的字节的开销。
使用面向列的存储,只有文件中的第二列部分需要读入内存。通常,在查询仅仅访问表中很小数量的列时,面向列的格式工作得很好。相反,面向行的格式适用于在
同一时刻需要处理一行中大量的列时(when a large number of columns of a single row are needed for processing at the same time).
面向列的格式需要更多的内存进行读取和写入,因为它们必须在内存中缓存一个行分片(a row split),而不是仅仅一个单一的行。通常也不可能控制写操作什么时候
发生(via flush or sync operations), 因此面向列的格式不适合与流的写入,因为如果 writer 处理失败,当前的文件不可恢复。而另一方面,面向行的格式,如
sequence 文件和 Avro datafiles 在一个 writer 失败之后可以读到最后一个同步点。