在前面的博文中,我已经详细的讲述了MapReduce是如何格式化一个作业的输入数据的,也就是用户应该如何来定义作业的输入数据的key-value类型,即如何读取输入数据,并把它们包装成一个个key-value对,最后来交给map函数来处理。那么对应的,本文将主要重点介绍如何格式化保存作业的最后输出结果,很明显,作业的最后输出结果实际上就是reduce函数的输出:key-value。因此,这个问题的关键就转换成了如何保存这些key-value对了,例如如何组织这些key-value,组织之后又把它们存储在什么地方,是HDFS还是HBase还是传统的关系型数据库?对于作业的最后输出结果的处理,Hadoop是完全交给了用户自己来定义或者设置——OutputFormat的实现。
先来看看输出格式化器在Hadoop内部的设计与实现体系:
从上面的类图可以看出输出格式化器(OutputFormat)主要包括两大组件:记录写入器(RecordWriter)和任务提交器(OutputCommitter),因此,OutputFormat的实现主要需要完成三个任务:
public abstract class OutputFormat<K, V> {
/**
* 创建一个记录写入器
*/
public abstract RecordWriter<K, V> getRecordWriter(TaskAttemptContext context) throws IOException, InterruptedException;
/**
* 检查结果输出的存储空间是否有效
*/
public abstract void checkOutputSpecs(JobContext context) throws IOException, InterruptedException;
/**
* 创建一个任务提交器
*/
public abstract OutputCommitter getOutputCommitter(TaskAttemptContext context) throws IOException, InterruptedException;
}
接下来就来好好讲一讲与输出格式化器(OutputFormat)相关的两大组件:
1.记录写入器(RecordWriter<K,V>)
记录写入器主要负责根据存储环境的需要先对对一个key-value对进行组织,然后把这个组织好的key-value存储到某个存储环境中,如:分布式文件系统、数据库等。例如它的一个实现LineRecordWriter,就是把一个key-value组织成一行文本,然后把这一行文本存储到设置好个某一个文件系统下的某一个文件中。当然在LineRecordWriter中,每一行中的key和value的值是通过分隔符来区别的,这个分隔符默认是'\t',但也可以通过配置文件来设置,对应的配置项为:mapred.textoutputformat.separator。
2.任务提交器(OutputCommitter)
这里的任务提交器主要是用来对任务(包括map任何和rduce任务)的输出进行管理,具体的工作如下:
public abstract class OutputCommitter {
/**
* 启动作业
*/
public abstract void setupJob(JobContext jobContext) throws IOException;
/**
* 清理作业
*/
public abstract void cleanupJob(JobContext jobContext) throws IOException;
/**
* 启动任务
*/
public abstract void setupTask(TaskAttemptContext taskContext)
throws IOException;
/**
* 检查是否能够提交任务
*/
public abstract boolean needsTaskCommit(TaskAttemptContext taskContext)
throws IOException;
/**
* 提交任务
*/
public abstract void commitTask(TaskAttemptContext taskContext)
throws IOException;
/**
* 放弃任务
*/
public abstract void abortTask(TaskAttemptContext taskContext)
throws IOException;
}
现在以它的一个具体实现——FileOutputCommitter来详细的介绍用户应该如何来根据自己的实际情况自定义一个任务提交器。
public FileOutputCommitter(Path outputPath, TaskAttemptContext context) throws IOException {
if (outputPath != null) {
this.outputPath = outputPath;//最终存放任务输出结果的目录
outputFileSystem = outputPath.getFileSystem(context.getConfiguration());
//任务运行时的工作目录(这个工作目录是临时目录的子目录)
workPath = new Path(outputPath,(FileOutputCommitter.TEMP_DIR_NAME + Path.SEPARATOR + "_" + context.getTaskAttemptID().toString())).makeQualified(outputFileSystem);
}
}
/**
* 启动作业:为作业的最终输出创建一个中间临时目录(这个临时目录是最终目录的子目录)
*/
public void setupJob(JobContext context) throws IOException {
if (outputPath != null) {
Path tmpDir = new Path(outputPath, FileOutputCommitter.TEMP_DIR_NAME);
FileSystem fileSys = tmpDir.getFileSystem(context.getConfiguration());
if (!fileSys.mkdirs(tmpDir)) {
LOG.error("Mkdirs failed to create " + tmpDir.toString());
}
}
}
/**
* 清理作业:清空存放作业最终输出的临时目录
*/
public void cleanupJob(JobContext context) throws IOException {
if (outputPath != null) {
Path tmpDir = new Path(outputPath, FileOutputCommitter.TEMP_DIR_NAME);
FileSystem fileSys = tmpDir.getFileSystem(context.getConfiguration());
if (fileSys.exists(tmpDir)) {
fileSys.delete(tmpDir, true);
}
}
}
/**
* 启动任务:无需做任何处理
*/
@Override
public void setupTask(TaskAttemptContext context) throws IOException {
}
/**
* 移动输出结果
*/
private void moveTaskOutputs(TaskAttemptContext context, FileSystem fs, Path jobOutputDir, Path taskOutput) throws IOException {
TaskAttemptID attemptId = context.getTaskAttemptID();
context.progress();
if (fs.isFile(taskOutput)) {
Path finalOutputPath = getFinalPath(jobOutputDir, taskOutput, workPath);
if (!fs.rename(taskOutput, finalOutputPath)) {
if (!fs.delete(finalOutputPath, true)) {
throw new IOException("Failed to delete earlier output of task: " + attemptId);
}
if (!fs.rename(taskOutput, finalOutputPath)) {
throw new IOException("Failed to save output of task: " + attemptId);
}
}
LOG.debug("Moved " + taskOutput + " to " + finalOutputPath);
} else if(fs.getFileStatus(taskOutput).isDir()) {
FileStatus[] paths = fs.listStatus(taskOutput);
Path finalOutputPath = getFinalPath(jobOutputDir, taskOutput, workPath);
fs.mkdirs(finalOutputPath);
if (paths != null) {
for (FileStatus path : paths) {
moveTaskOutputs(context, fs, jobOutputDir, path.getPath());
}
}
}
}
/**
* 提交任务:1.把中间目录中的输出结果移动到最终目录;2.删除工作目录
*/
public void commitTask(TaskAttemptContext context) throws IOException {
TaskAttemptID attemptId = context.getTaskAttemptID();
LOG.debug("using [org.apache.hadoop.mapreduce.lib.output.FileOutputCommitter] to commit Task["+attemptId+"]..");
if (workPath != null) {
context.progress();
if (outputFileSystem.exists(workPath)) {
// Move the task outputs to their final place
moveTaskOutputs(context, outputFileSystem, outputPath, workPath);
// Delete the temporary task-specific output directory
if (!outputFileSystem.delete(workPath, true)) {
LOG.warn("Failed to delete the temporary output" + " directory of task: " + attemptId + " - " + workPath);
}
LOG.debug("Saved output of task '" + attemptId + "' to " + outputPath);
}
}
}
/**
* 放弃任务:删除工作目录
*/
public void abortTask(TaskAttemptContext context) {
try {
if (workPath != null) {
context.progress();
outputFileSystem.delete(workPath, true);
}
} catch (IOException ie) {
LOG.warn("Error discarding output" + StringUtils.stringifyException(ie));
}
}
/**
*根据工作目录是否存在来判断任务是否可以提交
*/
public boolean needsTaskCommit(TaskAttemptContext context) throws IOException {
return workPath != null && outputFileSystem.exists(workPath);
}
在上面的FileOutputCommitter实现中,任务处理过程中的输出结果存储在工作目录(在临时目录下)下,最后成功处理完之后才把结果移动到最终目录(临时目录是其子目录)。作业最终的输出目录可以通过配置文件或FileOutputFormat.setOutputPath()来设置,对应的配置项为:mapred.output.dir。
另外,用户在提交作业之前可以通过Job.setOuputFormatClass()或者配置文件来设置自己所需要的输出格式化器,配置项为:mapreduce.outputformat.class。