MapReduce的输入输出
数据以文件的形式存储在HDFS中,在MapReduce程序中,数据是怎么从HDFS传给Mapper的?Reducer处理完数据之后,又是怎么把数据存储到HDFS中的?1、将数据从HDFS传到Mapper是由InputFormat类实现的,2、将数据从Reducer存储到HDFS是由OutputFormat类实现的。
一、输入流
InputFormat类是一个抽象类,InputFormat类定义了两个抽象函数。这两个抽象函数是:
abstract List<InputSplit> getSplits(JobContext context); abstract RecordReader<K,V> createRecordReader(InputSplit split,TaskAttemptContext context);
函数getSplits的功能是将输入的HDFS文件切分成若干个split。在Hadoop集群里的每个节点做MapRed处理的时候,每次只处理一个split,所以split是MapReduce处理的最小单元。
函数createRecordReader是创建RecordReader对象,这个对象根据split的内容,将split解析成若干个键值对。在做MapReduce的时候,Mapper会不断地调用RecordReader的功能,从RecordReader里读取键值对,然后用map函数处理。
InputFormat是一个抽象类。她有3个继承类,DBInputFormat类,DelegatingInputFormat类和FileInputFormat类。其中,DBInputFormat类是处理从数据库输入,DelegationInputFormat类是用在多个输入处理,FileInputFormat类是处理基于文件的输入。
以 fileInputFormat 类为例。FileInputFormat 类是一个抽象类,它在 InputFormat 类的基础上,增加一些跟文件操作相关的函数。它实现了 getSplits 函数,但没实现 createRecordReader 函数,它把 createRecordReader 的实现留给继承类去做。
在 getSplits 函数里,最重要的是这段:
1 // generate splits 2 //这是返回值 3 List<InputSplit> splits = new ArrayList<InputSplit>(); 4 //获取 HDFS 文件的信息,FileStatus 在前面的章节使用过。 5 List<FileStatus>files = listStatus(job); 6 //对作业的每个文件都进行处理 7 for (FileStatus file: files) { 8 //获取文件路径 9 Path path = file.getPath(); 10 FileSystem fs = path.getFileSystem(job.getConfiguration()); 11 //获取文件长度 12 long length = file.getLen(); 13 //获取文件在 HDFS 上存储的文件块的位置信息 14 BlockLocation[] blkLocations = fs.getFileBlockLocations(file, 0, length); 15 if ((length != 0) && isSplitable(job, path)) { 16 //获取文件块的大小 17 long blockSize = file.getBlockSize(); 18 //根据文件块大小,最小尺寸,最大尺寸,计算出 split 的大小 19 long splitSize = computeSplitSize(blockSize, minSize, maxSize); 20 //这段代码是根据 splitSize,每次计算一个 split 的块位置和所在主机的位置。 21 //然后生成 split 对象存储。 22 long bytesRemaining = length; 23 while (((double) bytesRemaining)/splitSize > SPLIT_SLOP) { 24 int blkIndex = getBlockIndex(blkLocations, length-bytesRemaining); 25 splits.add(new FileSplit(path, length-bytesRemaining, splitSize, 26 blkLocations[blkIndex].getHosts())); 27 bytesRemaining -= splitSize; 28 } 29 //最后剩下的不够一个 splitSize 的数据单独做一个 split。 30 if (bytesRemaining != 0) { 31 splits.add(new FileSplit(path, length-bytesRemaining, bytesRemaining, 32 blkLocations[blkLocations.length-1].getHosts())); 33 } 34 } else if (length != 0) { 35 //如果文件很小,就直接做成一个 split 36 splits.add(new FileSplit(path, 0, length, blkLocations[0].getHosts())); 37 } else { 38 //如果文件尺寸是 0,空文件,就创建一个空主机,主要是为了形式上一致。 39 splits.add(new FileSplit(path, 0, length, new String[0])); 40 } 41 }
FileInputFormat 有 5 个 继 承 类 , 包 括 CombineFileInputFormat 类 , KeyValueTextInputFormat 类 ,NLineInputFormat 类,SequenceFileInputFormat 类和 TextInputFormat 类。这几个有抽象类,也有具体类。以 TextInputFormat 类为例,它实现了 createRecordReader 函数,非常简单,函数体只有一个语句,返回一个LineRecordReader。LineRecordReader 类继承了抽象类 RecordReader。 抽象类 RecordReader 定义的全是抽象函数。 LineRecordReader 每次从一个 InputSplit 里读取一行文本,以这行文本在文件中的偏移量为键,以这行文本为值,组成一个键值对,返回给 Mapper 处理。
二、输出流
OutputFormat 类将键值对写入存储结构。一般来说,Mapper 类和 Reducer 类都会用到 OutputFormat 类。 Mapper 类用它存储中间结果,Reducer 类用它存储最终结果。OutputFormat 是个抽象类,这个类声明了 3 个抽象函数:
public abstract RecordWriter<K, V> getRecordWriter(TaskAttemptContext context); public abstract void checkOutputSpecs(JobContext context); public abstract OutputCommitter getOutputCommitter(TaskAttemptContext context);
其中,最主要的函数是 getRecordWriter 返 回 RecordWriter , 它 负 责 将 键 值 对 写 入 存 储 部 件 。 函 数checkOutputSpecs 检查输出参数是 否 合 理,一 般 是检查输出目录是 否存 在,如果已经 存 在就 报错 。 函 数
getOutputCommitter 获取 OutputCommitter , OutputCommitter 类是负责做杂活的,诸如初始化临时文件,作业完成后清理临时目录临时文件,处理作业的临时目录临时文件等等。OutputFormat 类 4 个继承类,有 DBOutputFormat,FileOutputFormat,FilterOutputFormat,NullOutputFormat。 顾名思义,DBOutputFormat 是将键值对写入到数据库, FileOutputFormat 将键值对写到文件系统, FilterOutputFormat将其实是提供一种将 OutputFormat 进行再次封装,类似 Java 的流的 Filter 方式, NullOutputFormat 将键值对写入/dev/null,相当于舍弃这些值。以 FileOutputFormat 为例。FileOutputFormat 是一个抽象类。它有两个继承类,SequenceFileOutputFormat 和TextOutputFormat。 SequenceFileOutputFormat 将键值对写入 HDFS 的顺序文件。 TextOutputFormat 将数据写入 HDFS 的文本文件。