MapReduce V2 开发手册

Table of Contents

目的

前提条件

概述

输入输出

Example: WordCount v1.0

代码

使用

用户接口

Job input

InputSplit

RecordReader

Job Output

OutputCommitter

任务副文件

RecordWriter

其他有用的特性

提交作业到队列中

计数器

分布式缓存

分析

Debugging

数据压缩

跳过不良数据数据

Example: WordCount v2.0

Source Code

亮点


目的

这个文档全面描述了 Hadoop MapReduce 框架面向用户的所有方面,作为一个教程。

前提条件

确保 Hadoop 已安装、配置并正在运行。更多的细节:

概述

Hadoop MapReduce 是一个软件框架,可以轻松地编写在大型集群(数千个节点)上并行处理大量数据(兆兆字节数据集)的应用程序,以一种可靠的、容错的方式。

MapReduce 作业通常将输入数据集分割成独立的区块,由 map 任务以完全并行的方式处理。框架对 map 的输出进行排序,然后将其输入到 reduce 任务中。通常作业的输入和输出都存储在文件系统中。框架负责调度任务、监视任务并重新执行失败的任务。

通常计算节点和存储节点是相同的,也就是说,MapReduce 框架和 Hadoop 分布式文件系统运行在同一组节点上。这种配置允许框架在数据已经存在的节点上有效地调度任务,从而在整个集群中产生非常高的聚合带宽。

MapReduce 框架由单个主 ResourceManager、每个集群节点一个 worker NodeManager 和每个应用程序的 MRAppMaster 组成。

应用程序至少指定输入/输出位置,并通过适当接口和/或抽象类的实现提供 map 和 reduce 函数。这些和其他作业参数组成了作业配置。

Hadoop 作业客户端然后将作业(jar/executable等)和配置提交给 ResourceManager,后者负责将软件/配置分发给工人,调度任务并监控它们,向作业客户端提供状态和诊断信息。

尽管 Hadoop 框架是在 Java™中实现的,但 MapReduce 应用程序不需要用Java编写。

  • Hadoop Streaming 是一种实用工具,它允许用户使用任何可执行程序(例如 shell 实用程序)创建和运行作业,作为 mapper 和/或 reducer。

  • Hadoop Pipes是一个 swigi 兼容的 c++ API,用于实现 MapReduce 应用程序(非基于JNI™)。

输入输出

MapReduce 框架只对<key, value>对操作,也就是说,框架将作业的输入视为一组<key, value>对,并生成一组<key, value>对作为作业的输出,可能是不同类型的。

框架必须将键和值类序列化,因此需要实现可写接口。此外,关键类必须实现 WritableComparable接口,以方便框架排序。

MapReduce 作业的输入和输出类型:

(input) <k1, v1> -> map -> <k2, v2> -> combine -> <k2, v2> -> reduce -> <k3, v3> (output)

Example: WordCount v1.0

在我们进入细节之前,让我们通过一个示例 MapReduce 应用程序来了解它们是如何工作的。

WordCount 是一个简单的应用程序,它计算给定输入集中每个单词出现的次数。

这适用于本地独立、伪分布或完全分布的 Hadoop 安装(单节点设置)。

代码

import java.io.IOException;
import java.util.StringTokenizer;

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.Mapper;
import org.apache.hadoop.mapreduce.Reducer;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;

public class WordCount {

  public static class TokenizerMapper
       extends Mapper<Object, Text, Text, IntWritable>{

    private final static IntWritable one = new IntWritable(1);
    private Text word = new Text();

    public void map(Object key, Text value, Context context
                    ) throws IOException, InterruptedException {
      StringTokenizer itr = new StringTokenizer(value.toString());
      while (itr.hasMoreTokens()) {
        word.set(itr.nextToken());
        context.write(word, one);
      }
    }
  }

  public static class IntSumReducer
       extends Reducer<Text,IntWritable,Text,IntWritable> {
    private IntWritable result = new IntWritable();

    public void reduce(Text key, Iterable<IntWritable> values,
                       Context context
                       ) throws IOException, InterruptedException {
      int sum = 0;
      for (IntWritable val : values) {
        sum += val.get();
      }
      result.set(sum);
      context.write(key, result);
    }
  }

  public static void main(String[] args) throws Exception {
    Configuration conf = new Configuration();
    Job job = Job.getInstance(conf, "word count");
    job.setJarByClass(WordCount.class);
    job.setMapperClass(TokenizerMapper.class);
    job.setCombinerClass(IntSumReducer.class);
    job.setReducerClass(IntSumReducer.class);
    job.setOutputKeyClass(Text.class);
    job.setOutputValueClass(IntWritable.class);
    FileInputFormat.addInputPath(job, new Path(args[0]));
    FileOutputFormat.setOutputPath(job, new Path(args[1]));
    System.exit(job.waitForCompletion(true) ? 0 : 1);
  }
}

使用

假设环境变量设置如下:

export JAVA_HOME=/usr/java/default
export PATH=${JAVA_HOME}/bin:${PATH}
export HADOOP_CLASSPATH=${JAVA_HOME}/lib/tools.jar

编译 WordCount.java 并创建一个 jar:

$ bin/hadoop com.sun.tools.javac.Main WordCount.java
$ jar cf wc.jar WordCount*.class

假定

  • /user/joe/wordcount/input - input directory in HDFS
  • /user/joe/wordcount/output - output directory in HDFS

样本文本文件作为输入:

$ bin/hadoop fs -ls /user/joe/wordcount/input/
/user/joe/wordcount/input/file01
/user/joe/wordcount/input/file02

$ bin/hadoop fs -cat /user/joe/wordcount/input/file01
Hello World Bye World

$ bin/hadoop fs -cat /user/joe/wordcount/input/file02
Hello Hadoop Goodbye Hadoop

运行应用程序:

$ bin/hadoop jar wc.jar WordCount /user/joe/wordcount/input /user/joe/wordcount/output

输出:

$ bin/hadoop fs -cat /user/joe/wordcount/output/part-r-00000
Bye 1
Goodbye 1
Hadoop 2
Hello 2
World 2

应用程序可以使用选项 -files 指定一个以逗号分隔的路径列表,该列表将出现在任务的当前工作目录中。libjars 选项允许应用程序将 jar 添加到 map 和 reduce 的类路径中。选项 -archives 允许他们传递以逗号分隔的存档列表作为参数。这些存档是未归档的,并在任务的当前工作目录中创建带有存档名称的链接。

运行 wordcount 的例子与 -libjars, -files and -archives:

bin/hadoop jar hadoop-mapreduce-examples-<ver>.jar wordcount -files cachefile.txt -libjars mylib.jar -archives myarchive.zip input output

在这里,myarchive.zip 将被放置并解压缩到一个名为“myarchive.zip”的目录中。

用户可以为通过 -files 和 -archives 选项传递的文件和归档指定不同的符号名,使用#。

bin/hadoop jar hadoop-mapreduce-examples-<ver>.jar wordcount -files dir1/dict.txt#dict1,dir2/dict.txt#dict2 -archives mytar.tgz#tgzdir input output

在这里,文件 dir1/dict.txt和dir2/dict.txt可以被任务分别使用符号名dict1和dict2访问。存档mytar。tgz将被放在一个名为“tgzdir”的目录中并被解除归档。

 

用户接口

Job input

InputFormat 描述了 MapReduce 作业的输入规范。

MapReduce 框架依赖于工作的 InputFormat:

  1. 验证作业的输入规范。
  2. 将输入文件分解为逻辑 InputSplit 实例,然后将每个实例分配给单个 map。
  3. 提供用于从逻辑 InputSplit 收集输入记录以供映射器处理的 RecordReader 实现。

基于文件的 InputFormat 实现(通常是 FileInputFormat 的子类)的默认行为是根据输入文件的总大小(以字节为单位)将输入分割成逻辑 InputSplit 实例。但是,输入文件的文件系统块大小被视为输入分割的上限。拆分大小的下界可以通过 mapreduce.input.fileinputformat.split.minsize 设置。

显然,基于输入大小的逻辑分割对于许多应用程序来说是不够的,因为必须尊重记录边界。在这种情况下,应用程序应该实现一个记录器,负责记录边界,并向单个任务展示逻辑 InputSplit 的面向记录的视图。

TextInputFormat 是默认的 InputFormat。

如果 TextInputFormat 是给定作业的输入格式,那么框架会检测带有 .gz 扩展名的输入文件,并使用适当的 CompressionCodec 自动解压缩它们。但是,必须注意的是,具有上述扩展名的压缩文件不能被分割,每个压缩文件都由单个映射器完整地处理。

InputSplit

InputSplit 表示要由单个映射器处理的数据。

典型的 InputSplit 提供了一个面向字节的输入视图,记录器负责处理和提供一个面向记录的视图。

FileSplit 是默认的 InputSplit。它设置 mapreduce.map.input.file 到用于逻辑分割的输入文件的路径。

RecordReader

记录器从 InputSplit 中读取<key, value>对。

通常,记录器转换由 InputSplit 提供的输入的面向字节的视图,并将面向记录的视图呈现给 Mapper 实现进行处理。RecordReader 因此承担了处理记录边界的责任,并使用键和值表示任务。

Job Output

OutputFormat 描述了 MapReduce 作业的输出规范。

MapReduce 框架依赖于工作的输出格式:

  1. 验证工作的输出规范;例如,检查输出目录是否已经存在。
  2. 提供用于写入作业输出文件的 RecordWriter 实现。输出文件存储在文件系统中。

TextOutputFormat 是默认的输出格式。

OutputCommitter

OutputCommitter 描述了提交 MapReduce 作业的任务输出。

MapReduce 框架依赖于 Job 的 OutputCommitter:

  1. 在初始化期间设置作业。例如,在作业初始化期间为作业创建临时输出目录。作业设置在作业处于准备状态时和在初始化任务之后由一个单独的任务完成。安装任务完成后,该作业将转移到运行状态。
  2. 工作完成后清理工作。例如,在作业完成后删除临时输出目录。作业清理由一个单独的任务在作业结束时完成。清理任务完成后,作业被声明为成功 / 失败 / 终止。
  3. 设置任务临时输出。任务设置是在任务初始化期间作为同一任务的一部分完成的。
  4. 检查任务是否需要提交。这是为了避免在任务不需要提交时执行提交过程。
  5. 提交任务输出。任务完成后,如果需要,任务将提交它的输出。
  6. 放弃任务 commit。如果任务失败 / 终止,输出将被清除。如果任务不能清除(在异常块中),将启动一个单独的任务,使用相 同的 attempt-id 来执行清除。

FileOutputCommitter 是默认的 OutputCommitter。作业设置 / 清理任务占用 map 或 reduce 容器(以 NodeManager 上可用的任意一个)。而 JobCleanup 任务、TaskCleanup 任务和 JobSetup 任务具有最高的优先级,并按此顺序排列。

任务副文件

在某些应用程序中,组件任务需要创建和 / 或写入侧文件,这与实际的作业输出文件不同。

在这种情况下,试图打开和 / 或写入文件系统上相同的文件(路径)时,可能会出现同时运行的同一映射器或 Reducer 的两个实例(例如,推测任务)的问题。因此,应用程序编写器必须为每个任务尝试选择唯一的名称(使用attempt_200709221812_0001_m_000000_0),而不仅仅是为每个任务。

为了避免这些问题,在 MapReduce 框架中,当 OutputCommitter 是 FileOutputCommitter 时,维护一个特殊的 ${MapReduce .output.fileoutputformat。通过${mapreduce.task.output访问outputdir}/_temporary/_${taskid}子目录。用于存储 task-attempt 输出的文件系统上的每个task-attempt。当 task-attempt 成功完成时,${mapreduce.output.fileoutputformat中的文件。outputdir}/_temporary/_${taskid} (only)提升为${mapreduce.output.fileoutputformat.outputdir}。当然,框架会丢弃 unsuccessful 的子目录。

应用程序编写器可以通过在 ${mapreduce.task.output 中创建所需的任何侧文件来利用这个特性。在通过 FileOutputFormat.getWorkOutputPath(Conext) 执行任务的过程中,框架将以类似的方式对成功的 task-attempt 进行提升,从而消除了为每个 task-attempt 选择唯一路径的需要。

注意:${mapreduce.task.output 的值。在执行特定任务时,尝试的值实际上是 ${mapreduce.output.fileoutputformat。outputdir}/_temporary/_{$taskid},这个值由 MapReduce 框架设置。因此,只需在 MapReduce 任务的FileOutputFormat.getWorkOutputPath(Conext) 返回的路径中创建任何侧文件,就可以利用这个特性。

对于 reduce =NONE(即0 reduce)的作业的映射,整个讨论都是正确的,因为在这种情况下,map 端的输出将直接转到 HDFS。

RecordWriter

RecordWriter 将输出 <key, value> 对写入一个输出文件。
RecordWriter 实现将作业输出写入文件系统。

其他有用的特性

提交作业到队列中

用户将作业提交到队列。队列作为作业的集合,允许系统提供特定的功能。例如,队列使用 acl 来控制哪些用户可以向它们提交作业。队列预计主要由 Hadoop 调度器使用。

Hadoop 配置了一个强制队列,称为“default”。在 mapreduce.job 中定义队列名称。Hadoop 站点配置的 queuename 属性。一些作业调度器(如容量调度器)支持多个队列。

作业定义了需要通过 mapreduce.job 提交到的队列。queuename 属性,或通过 Configuration.set(MRJobConfig)。QUEUE_NAME字符串) API。设置队列名称是可选的。如果提交的作业没有关联的队列名称,则将其提交到“default”队列。

计数器

计数器表示由 MapReduce 框架或应用程序定义的全局计数器。每个计数器可以是任何枚举类型。特定枚举的计数器被捆绑成计数器类型。

应用程序可以定义任意的计数器(枚举类型)并通过计数器更新它们。Counters.incrCounter(Enum, long) or Counters.incrCounter(String, String, long)在 map 和/或 reduce 方法。然后,框架对这些计数器进行全局聚合。

分布式缓存

DistributedCache 有效地分发特定于应用程序的大型只读文件。

DistributedCache 是 MapReduce 框架提供的一种工具,用于缓存应用程序所需的文件(文本、存档、jar等)。

应用程序在作业中通过 url (hdfs://)指定要缓存的文件。DistributedCache 假设通过 hdfs:// url 指定的文件已经存在于文件系统中。

框架将在工作节点上执行任何任务之前将必要的文件复制到该工作节点。它的效率源于这样一个事实,即每个作业只复制一次文件,并且能够缓存在工作人员上未存档的存档。

DistributedCache 跟踪缓存文件的修改时间戳。显然,在作业执行时,应用程序或外部不应该修改缓存文件。

DistributedCache 可用于分发简单的、只读的数据 / 文本文件和更复杂的类型,如归档和jar。归档文件(zip、tar、tgz和tar.gz文件)在工作节点上被解压。文件已设置执行权限。

可以通过设置属性 mapreduce.job.cache.{files |archives} 分发档案文件。如果必须分发多个文件/归档,可以将它们添加为逗号分隔的路径。属性也可以通过api Job.addCacheFile(URI)Job.addCacheArchive(URI) 和 Job.setCacheFiles(URI[])Job.setCacheArchives(URI[]) 其中URI的形式为 hdfs://host:port/ absoluent -path#link-name。在流媒体中,文件可以通过命令行选项 -cacheFile / -cacheArchive分发。

DistributedCache 还可以用作在 map 和/或 reduce 任务中使用的基本软件分发机制。它可以用于分发jar和本机库。可以使用Job.addArchiveToClassPath(Path) 或 Job.addFileToClassPath(Path) api 缓存文件 jar,也可以将它们添加到子 jvm 的类路径中。通过设置配置属性 mapreduce.job.classpath.{files |archives},也可以做到这一点。类似地,可以使用符号链接到任务工作目录中的缓存文件分发本机库并加载它们。

私有和公共分发的缓存文件

DistributedCache 文件可以是私有的,也可以是公共的,这决定了如何在工作节点上共享它们。

  • “Private”DistributedCache 文件缓存在 localdirectory 私有中,用户的作业需要这些文件。这些文件仅由特定用户的所有任务和作业共享,不能由 Worker 上的其他用户的作业访问。DistributedCache 文件之所以成为私有文件,是因为它在文件上传所在的文件系统(通常是 HDFS )上具有权限。如果文件没有世界可读访问权限,或者如果指向该文件的目录路径没有世界可执行访问权限来进行查找,那么该文件将成为私有文件。
  • “Public”DistributedCache 文件被缓存在一个全局目录中,并且文件访问设置为对所有用户都公开可见。这些文件可以由工作者上的所有用户的任务和作业共享。DistributedCache 文件通过其在文件上传的文件系统(通常是HDFS)上的权限成为公有文件。如果文件具有全局可读访问权限,并且指向该文件的目录路径具有全局可执行访问权限以进行查找,则该文件将成为公共的。换句话说,如果用户打算让一个文件对所有用户公开可用,则必须将文件权限设置为全世界可读,并且指向该文件的路径上的目录权限必须是全世界可执行的。

分析

Profiling 是一个实用工具,它可以为 map 和 reduce 样本获取一个典型的内置 java profiler 样本(2或3个)。

用户可以通过设置配置属性 mapreduce.task.profile 来指定系统是否应该为作业中的某些任务收集 profiler 信息。可以使用 api Configuration.set(MRJobConfig.TASK_PROFILE, boolean) 设置该值。如果该值设置为 true,则启用任务分析。分析器信息存储在用户日志目录中。默认情况下,不为作业启用分析。

一旦用户配置,她/他可以使用配置属性 mapreduce.task.profile.{maps|reduces} 将 MapReduce 任务的范围设置为 profile。可以使用api Configuration.set(MRJobConfig.NUM_{MAP|REDUCE}_PROFILES, String) 设置该值。默认情况下,指定的范围是0-2。

用户还可以通过设置配置属性 mapreduce.task.profile.params 来指定配置分析器配置参数。可以使用api Configuration.set(MRJobConfig.TASK_PROFILE_PARAMS, String) 指定该值。如果字符串包含 %s,则在任务运行时将其替换为分析输出文件的名称。这些参数在命令行上传递给任务子 JVM。分析参数的默认值是-agentlib:hprof=cpu=sample,heap=sites,force=n,thread=y,verbose=n,file=%s。

Debugging

MapReduce 框架提供了一个工具来运行用户提供的脚本进行调试。当一个 MapReduce 任务失败时,用户可以运行一个调试脚本,例如处理任务日志。脚本可以访问任务的 stdout 和 stderr 输出、syslog 和 jobconf。来自调试脚本的 stdout 和 stderr 的输出显示在控制台诊断信息中,同时也作为作业 UI 的一部分。

在下面的部分中,我们将讨论如何提交带有作业的调试脚本。脚本文件需要分发并提交给框架。

如何提交脚本:

提交调试脚本的一种快速方法是设置属性 mapreduce.map.debug.script 和属性 mapreduce.reduce.debug.script 的值,分别用于调试 map 和 reduce 任务。还可以使用api Configuration.set(MRJobConfig.MAP_DEBUG_SCRIPT, String) 和 Configuration.set(MRJobConfig.REDUCE_DEBUG_SCRIPT, String)。在流模式下,可以通过命令行选项 -mapdebug和 -reducedebug 提交调试脚本,这两个选项分别用于调试 map 和 reduce 任务。

脚本的参数是任务的 stdout、stderr、syslog 和 jobconf 文件。在 MapReduce 任务失败的节点上运行的调试命令为:$script $stdout $stderr $syslog $jobconf $program

默认行为

对于管道,运行一个默认脚本来处理在 gdb 下的核心转储,打印堆栈跟踪并给出运行线程的信息。

数据压缩

Hadoop MapReduce 为应用程序编写者提供了工具,可以指定中间映射输出和作业输出的压缩,即 reduce 的输出。它还与 zlib 压缩算法的 CompressionCodec 实现绑定在一起。还支持 gzip、bzip2、snappy 和 lz4 文件格式。

由于性能(zlib)和 Java 库不可用的原因,Hadoop 还提供了上述压缩编解码器的本地实现。

中间输出

应用程序可以通过 Configuration.set(MRJobConfig.MAP_OUTPUT_COMPRESS, boolean) 和  Configuration.set(MRJobConfig.MAP_OUTPUT_COMPRESS_CODEC, Class) api 控制中间映射输出的压缩。

Job Outputs

应用程序可以通过 FileOutputFormat 控制作业输出的压缩。setCompressOutput(Job, boolean) api和要使用的CompressionCodec可以通过 FileOutputFormat.setOutputCompressorClass(Job, Class)指定。

如果作业输出要存储在 SequenceFileOutputFormat 中,则需要 SequenceFile.CompressionType (即RECORD / BLOCK - defaults to RECORD)可以通过 SequenceFileOutputFormat.setOutputCompressionType(Job, SequenceFile.CompressionType) 指定。

跳过不良数据数据

Hadoop 提供了一个选项,在处理 map 输入时可以跳过某些错误的输入记录。应用程序可以通过 SkipBadRecords 类控制该特性。

当 map 任务在某些输入上确定崩溃时,可以使用此特性。这通常是由于 map 函数中的 bug 造成的。通常,用户必须修复这些错误。然而,这有时是不可能的。bug 可能存在于第三方库中,例如,其源代码不可用。在这种情况下,即使经过多次尝试,任务也不会成功完成,并且任务失败。使用这个特性,只有坏记录周围的一小部分数据会丢失,这对于某些应用程序(例如,对非常大的数据执行统计分析的应用程序)来说是可以接受的。

默认情况下该特性是禁用的。要启用它, 参考:SkipBadRecords.setMapperMaxSkipRecords(Configuration, long) and SkipBadRecords.setReducerMaxSkipGroups(Configuration, long)

启用此功能后,在一定数量的 map 失败后,框架会进入“skipping mode”。更多细节,请参见 SkipBadRecords.setAttemptsToStartSkipping(Configuration, int)。在“skipping mode“中,map 任务维护正在处理的记录范围。为此,框架依赖于处理过的记录计数器SkipBadRecords.COUNTER_MAP_PROCESSED_RECORDS 和 SkipBadRecords.COUNTER_REDUCE_PROCESSED_GROUPS。这个计数器使框架能够知道已经成功处理了多少条记录,从而知道哪些记录范围导致任务崩溃。在进一步尝试时,将跳过此记录范围。

跳过的记录数量取决于应用程序增加已处理的记录计数器的频率。建议在处理每个记录之后增加这个计数器。这在一些通常批处理它们的应用程序中可能不可能实现。在这种情况下,框架可能跳过围绕坏记录的其他记录。用户可以通过 SkipBadRecords.setMapperMaxSkipRecords(Configuration, long) and SkipBadRecords.setReducerMaxSkipGroups(Configuration, long) 控制跳过记录的数量。框架尝试使用类似二进制搜索的方法缩小跳过记录的范围。跳过的范围被分成两部分,只有一半被执行。在随后的失败中,框架会找出哪一半包含了坏记录。将重新执行任务,直到满足可接受的跳过值或耗尽所有任务尝试为止。要增加任务尝试的次数,请使用 Job.setMaxMapAttempts(int) and Job.setMaxReduceAttempts(int)

跳过的记录将以顺序文件格式写入 HDFS,以便稍后进行分析。可以通过 SkipBadRecords.setSkipOutputPath(JobConf, Path) 更改位置。

Example: WordCount v2.0

下面是一个更完整的 WordCount,它使用了我们到目前为止讨论过的 MapReduce 框架提供的许多特性。

这需要 HDFS 启动并运行,特别是与 distributedcache 相关的特性。因此,它只适用于伪分布式或完全分布式的 Hadoop 安装。

Source Code

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.net.URI;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.StringTokenizer;

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.Mapper;
import org.apache.hadoop.mapreduce.Reducer;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
import org.apache.hadoop.mapreduce.Counter;
import org.apache.hadoop.util.GenericOptionsParser;
import org.apache.hadoop.util.StringUtils;

public class WordCount2 {

  public static class TokenizerMapper
       extends Mapper<Object, Text, Text, IntWritable>{

    static enum CountersEnum { INPUT_WORDS }

    private final static IntWritable one = new IntWritable(1);
    private Text word = new Text();

    private boolean caseSensitive;
    private Set<String> patternsToSkip = new HashSet<String>();

    private Configuration conf;
    private BufferedReader fis;

    @Override
    public void setup(Context context) throws IOException,
        InterruptedException {
      conf = context.getConfiguration();
      caseSensitive = conf.getBoolean("wordcount.case.sensitive", true);
      if (conf.getBoolean("wordcount.skip.patterns", false)) {
        URI[] patternsURIs = Job.getInstance(conf).getCacheFiles();
        for (URI patternsURI : patternsURIs) {
          Path patternsPath = new Path(patternsURI.getPath());
          String patternsFileName = patternsPath.getName().toString();
          parseSkipFile(patternsFileName);
        }
      }
    }

    private void parseSkipFile(String fileName) {
      try {
        fis = new BufferedReader(new FileReader(fileName));
        String pattern = null;
        while ((pattern = fis.readLine()) != null) {
          patternsToSkip.add(pattern);
        }
      } catch (IOException ioe) {
        System.err.println("Caught exception while parsing the cached file '"
            + StringUtils.stringifyException(ioe));
      }
    }

    @Override
    public void map(Object key, Text value, Context context
                    ) throws IOException, InterruptedException {
      String line = (caseSensitive) ?
          value.toString() : value.toString().toLowerCase();
      for (String pattern : patternsToSkip) {
        line = line.replaceAll(pattern, "");
      }
      StringTokenizer itr = new StringTokenizer(line);
      while (itr.hasMoreTokens()) {
        word.set(itr.nextToken());
        context.write(word, one);
        Counter counter = context.getCounter(CountersEnum.class.getName(),
            CountersEnum.INPUT_WORDS.toString());
        counter.increment(1);
      }
    }
  }

  public static class IntSumReducer
       extends Reducer<Text,IntWritable,Text,IntWritable> {
    private IntWritable result = new IntWritable();

    public void reduce(Text key, Iterable<IntWritable> values,
                       Context context
                       ) throws IOException, InterruptedException {
      int sum = 0;
      for (IntWritable val : values) {
        sum += val.get();
      }
      result.set(sum);
      context.write(key, result);
    }
  }

  public static void main(String[] args) throws Exception {
    Configuration conf = new Configuration();
    GenericOptionsParser optionParser = new GenericOptionsParser(conf, args);
    String[] remainingArgs = optionParser.getRemainingArgs();
    if ((remainingArgs.length != 2) && (remainingArgs.length != 4)) {
      System.err.println("Usage: wordcount <in> <out> [-skip skipPatternFile]");
      System.exit(2);
    }
    Job job = Job.getInstance(conf, "word count");
    job.setJarByClass(WordCount2.class);
    job.setMapperClass(TokenizerMapper.class);
    job.setCombinerClass(IntSumReducer.class);
    job.setReducerClass(IntSumReducer.class);
    job.setOutputKeyClass(Text.class);
    job.setOutputValueClass(IntWritable.class);

    List<String> otherArgs = new ArrayList<String>();
    for (int i=0; i < remainingArgs.length; ++i) {
      if ("-skip".equals(remainingArgs[i])) {
        job.addCacheFile(new Path(remainingArgs[++i]).toUri());
        job.getConfiguration().setBoolean("wordcount.skip.patterns", true);
      } else {
        otherArgs.add(remainingArgs[i]);
      }
    }
    FileInputFormat.addInputPath(job, new Path(otherArgs.get(0)));
    FileOutputFormat.setOutputPath(job, new Path(otherArgs.get(1)));

    System.exit(job.waitForCompletion(true) ? 0 : 1);
  }
}

示例运行

$ bin/hadoop fs -ls /user/joe/wordcount/input/
/user/joe/wordcount/input/file01
/user/joe/wordcount/input/file02

$ bin/hadoop fs -cat /user/joe/wordcount/input/file01
Hello World, Bye World!

$ bin/hadoop fs -cat /user/joe/wordcount/input/file02
Hello Hadoop, Goodbye to hadoop.

运行应用程序:

$ bin/hadoop jar wc.jar WordCount2 /user/joe/wordcount/input /user/joe/wordcount/output

输出:

$ bin/hadoop fs -cat /user/joe/wordcount/output/part-r-00000
Bye 1
Goodbye 1
Hadoop, 1
Hello 2
World! 1
World, 1
hadoop. 1
to 1

注意,输入与我们看到的第一个版本有所不同,以及它们如何影响输出。

现在,让我们通过 DistributedCache 插件一个模式文件,该文件列出要忽略的单词模式。

$ bin/hadoop fs -cat /user/joe/wordcount/patterns.txt
\.
\,
\!
to

再次运行它,这一次有更多的选项:

$ bin/hadoop jar wc.jar WordCount2 -Dwordcount.case.sensitive=true /user/joe/wordcount/input /user/joe/wordcount/output -skip /user/joe/wordcount/patterns.txt

如预期,输出:

$ bin/hadoop fs -cat /user/joe/wordcount/output/part-r-00000
Bye 1
Goodbye 1
Hadoop 1
Hello 2
World 2
hadoop 1

再次运行它,这次关闭大小写敏感性:

$ bin/hadoop jar wc.jar WordCount2 -Dwordcount.case.sensitive=false /user/joe/wordcount/input /user/joe/wordcount/output -skip /user/joe/wordcount/patterns.txt

果然,输出如下:

$ bin/hadoop fs -cat /user/joe/wordcount/output/part-r-00000
bye 1
goodbye 1
hadoop 2
hello 2
horld 2

亮点

WordCount 的第二个版本在上一个版本的基础上,使用了 MapReduce 框架提供的一些功能:

  • 演示应用程序如何在Mapper(和Reducer)实现的 setup 方法中访问配置参数。
  • 演示如何使用 DistributedCache 分发作业所需的只读数据。在这里,它允许用户指定在计数时跳过的单词模式。
  • 演示 GenericOptionsParser 处理通用 Hadoop 命令行选项的实用程序。
  • 演示应用程序如何使用计数器,以及如何设置传递给 map (和reduce)方法的特定于应用程序的状态信息。

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值