目录
前言__1
1. 目的__ 1
2. 前提条件__ 1
3. 概述__ 1
4. 输入和输出__ 2
5. 例子:WordCount v1.0_ 3
5.1 源代码_ 3
5.2 用法_ 6
5.3 分析_ 7
6. MapReduce——用户接口__ 9
6.1 有效载荷_ 9
6.2 工作配置_ 14
6.3 任务执行和环境_ 14
6.4 工作的提交和跟踪监视_ 23
6.5 工作输入_ 26
6.6 工作输出_ 28
6.7 其他有用的特点_ 30
7. 示例:WordCount v2.0_ 37
7.1 源代码_ 37
7.2 示例运行_ 42
7.3 重点_ 44
此教程是Hadoop 1.0.4自带文档“mapred_tutorial”的中文翻译。译者通过自身对Hadoop的学习和自己的编程知识力求做到意思准确、段落流畅、术语专业。
译文中对众多术语进行了“中文化”,例如“JobTacker”翻译为“工作跟踪器”,“IsolationRunner”翻译为“隔离运行器”等等。但是对于Hadoop程序中会直接出现的类名称,例如“DistributedCache”,“Tool”等词汇保留了英文词汇,目的是加深读者对这些关键词的印象,在自己动手实验的时候对这些关键词不会感到陌生。
由于译者水平限制和译文较长,译文出现谬误在所难免,请读者批评指正!
译者:TomHeaven
2013年06月04日
本文档全面描述了使用Hadoop MapReduce框架的用户所需注意的各个方面,因而作为MapReduce教程。
确保Hadoop已经正确安装、配置和运行。更多细节可以参看Hadoop文档:
l 《单节点建立》(SingleNode Setup);
l 《集群建立》(ClusterSetup)。
HadoopMapReduce是一个易于编程的软件框架,它能以可靠、容错的方式在有上千个商用硬件节点的大规模集群上并行地处理海量的数据(TB级的数据集)。
一个 MapReduce的工作(job)通常将输入数据集分为独立的数据块并由Map任务(tasks)进行完全并行的处理。软件框架将Map的输出排序,在作为Reduce任务的输入。工作的输入和输出都存储在一个文件系统里。软件框架还负责调度、监视计划的任务并重新执行失败的任务。
一般来说计算节点和存储节点是相同的,也就是说,MapReduce框架和Hadoop分布式文件系统(HDFS)是在相同的节点集群上运行的。这种配置可以使软件框架高效地将任务分配到数据所在的节点上,从而高效地利用集群的网络带宽。
MapReduce框架由每个节点上一个主节点JobTracker和一个从节点TraskTracker组成。主机负责调度组成工作的任务到各个从机,监视它们并重新调度失败的任务。从机根据主机的指令执行任务。
简单来说,应用程序指定输入和输出路径,并通过实现适当的抽象类或接口提供给map和reduce函数。这些,和其他的工作参数一起构成了工作配置(job configuration)。Hadoop的工作客户端(job client)将工作(jar或者其他可执行文件)和配置提交给工作跟踪器(JobTracker)。 而后者负责将它们分配到从节点,调度和监视任务,向工作客户端提供状态和诊断信息。
虽然Hadoop框架是由Java实现的,但MapReduce程序可以不用Java编写。
l Hadoop 流(Streaming)是一个允许用户使用任何可执行程序作为Mapper类和Reducer类的工具。
l Hadoop管道(Pipes)是一个兼容C++ API的MapReduce应用程序(不是基于JNI的)。
MapReduce框架处理的完全是
Key类和Value类都必须由框架串行化因而需要实现Writable接口。Key类还需要实现WritableComparable接口以由框架排序。
MapReduce工作的输入输出类型:
(input)
在我们进入细节之前,先来看一个MapReduce应用程序的示例,以便对它是如何运行的有一个直观的印象。
WordCount是一个简单的单词计数程序。它统计在文本文件中每个单词出现的频数。
示例代码可以在伪集群或者完全分布式的Hadoop集群上运行。(参看文档《单节点建立》, Single Node Setup)。
WordCount.java |
|
1. |
package org.myorg; |
2. |
|
3. |
import java.io.IOException; |
4. |
import java.util.*; |
5. |
|
6. |
import org.apache.hadoop.fs.Path; |
7. |
import org.apache.hadoop.conf.*; |
8. |
import org.apache.hadoop.io.*; |
9. |
import org.apache.hadoop.mapred.*; |
10. |
import org.apache.hadoop.util.*; |
11. |
|
12. |
public class WordCount { |
13. |
|
14. |
public static class Map extends MapReduceBase implements Mapper |
15. |
private final static IntWritable one = new IntWritable(1); |
16. |
private Text word = new Text(); |
17. |
|
18. |
public void map(LongWritable key, Text value, OutputCollector |
19. |
String line = value.toString(); |
20. |
StringTokenizer tokenizer = new StringTokenizer(line); |
21. |
while (tokenizer.hasMoreTokens()) { |
22. |
word.set(tokenizer.nextToken()); |
23. |
output.collect(word, one); |
24. |
} |
25. |
} |
26. |
} |
27. |
|
28. |
public static class Reduce extends MapReduceBase implements Reducer |
29. |
public void reduce(Text key, Iterator |
30. |
int sum = 0; |
31. |
while (values.hasNext()) { |
32. |
sum += values.next().get(); |
33. |
} |
34. |
output.collect(key, new IntWritable(sum)); |
35. |
} |
36. |
} |
37. |
|
38. |
public static void main(String[] args) throws Exception { |
39. |
JobConf conf = new JobConf(WordCount.class); |
40. |
conf.setJobName("wordcount"); |
41. |
|
42. |
conf.setOutputKeyClass(Text.class); |
43. |
conf.setOutputValueClass(IntWritable.class); |
44. |
|
45. |
conf.setMapperClass(Map.class); |
46. |
conf.setCombinerClass(Reduce.class); |
47. |
conf.setReducerClass(Reduce.class); |
48. |
|
49. |
conf.setInputFormat(TextInputFormat.class); |
50. |
conf.setOutputFormat(TextOutputFormat.class); |
51. |
|
52. |
FileInputFormat.setInputPaths(conf, new Path(args[0])); |
53. |
FileOutputFormat.setOutputPath(conf, new Path(args[1])); |
54. |
|
55. |
JobClient.runJob(conf); |
57. |
} |
58. |
} |
59. |
假设HADOOP_HOME是Hadoop的安装目录,HADOOP_VERSION是所安装Hadoop的版本号,编译WordCount.java并创建一个jar文件。
$ mkdir wordcount_classes
$ javac -classpath
${HADOOP_HOME}/hadoop-${HADOOP_VERSION}-core.jar-d
wordcount_classes WordCount.java
$ jar -cvf /usr/joe/wordcount.jar -Cwordcount_classes/ .
假设:
l /usr/joe/wordcount/input – HDFS中的输入文件夹;
l /usr/joe/wordcount/output – HDFS中的输出文件夹。
输入文件文件样例为:
$ bin/hadoop dfs -ls/usr/joe/wordcount/input/
/usr/joe/wordcount/input/file01
/usr/joe/wordcount/input/file02
$ bin/hadoop dfs -cat/usr/joe/wordcount/input/file01
Hello World Bye World
$ bin/hadoop dfs -cat/usr/joe/wordcount/input/file02
Hello Hadoop Goodbye Hadoop
运行应用程序:
$ bin/hadoop jar /usr/joe/wordcount.jarorg.myorg.WordCount
/usr/joe/wordcount/input/usr/joe/wordcount/output
输出:
$ bin/hadoop dfs -cat/usr/joe/wordcount/output/part-00000
Bye 1
Goodbye 1
Hadoop 2
Hello 2
World 2
应用可以通过:
l -files选项设定由逗号分隔的路径列表作为任务的当前工作目录(currentworkding directory);
l -libjars选项允许用户添加附加依赖的jar包到map和reduce任务的classpath中;
l -archives选项设置逗号分隔的归档文件列表作为参数。这些归档文件将被解压并在任务的当前工作目录下创建同名链接;
更多细节请参考Hadoop文档《命令指南》(CommandsGuide)。
加上-libjars,-files和-archives参数运行wordcount:
hadoop jar hadoop-examples.jar wordcount-files cachefile.txt -libjars mylib.jar -archives myarchive.zip input output
这里的“myarchive.zip”文件会被解压到一个名为“myarchive.zip”的文件夹中。
用户还可以通过以下命令指定一个不同的链接名称:
hadoop jar hadoop-examples.jar wordcount –filesdir1/dict.txt#dict1,dir2/dict.txt#dict2 –archives mytar.tgz#tgzdir input output
这里,任务可以分别通过链接名称“dict1”和“dict2”访问文件 “dir1/dict.txt”和“dir2/dict.txt”。归档文件“mytar.tgz”会被解压缩到名为“tgzdir”的目录中。
WordCount程序是简洁明了、顺序执行的。
Mapper的实现(第14-26行),通过map方法(第18-25行),由TextInputFormat(第49行)一次处理一行输入。然后,它通过StringTokenizer将输入行分解为空格分隔的符号,并产生一个<
对于样例输入,产生的键值对有:
< Hello, 1>
< World, 1>
< Bye, 1>
< World, 1>
对于第二个map,产生的键值对有:
< Hello, 1>
< Hadoop, 1>
< Goodbye, 1>
< Hadoop, 1>
在后面的章节,我们会更详细地学习更多的关于工作产生多少个map以及怎样控制它们的知识。
WordCount还指定了一个Combiner(第46行)。因此,每个map的输出在按键排序后,被传递给本地的Combiner(在这个工作里和Reducer一样)做本地聚合操作。
第一个map 的输出:
< Bye, 1>
< Hello, 1>
< World, 2>
第二个map的输出:
< Goodbye, 1>
< Hadoop, 2>
< Hello, 1>
Reducer 的实现(第28-36行),通过reduce方法(第29-35行)只是将每个出现单词的出现次数加起来。
工作的输出为:
< Bye, 1>
< Goodbye, 1>
< Hadoop, 2>
< Hello, 2>
< World, 2>
在JobConf中,run方法指定了工作很多方面,例如输入/输出路径(通过命令行传递),键值对类型,输入输出格式等等。然后它调用JobClient.runJob(第55行)方法提交工作并跟踪进度。
我们会在接下来的教程中学习JobConf,JobClient,Tool和其他接口和类。
本章将就面向用户的各个方面提供MapReduce框架的相当丰富的信息。这将帮助用户细致地实现,配置和调整他们的工作。但请注意每个类/接口的Java 文档(Doc)才是最细致完整的文档:本文只是一个教程。
首先我们介绍Mapper和Reducer两个接口。应用程序一般实现它们来提供map和reduce函数(Java中称为“方法”)。
然后我们将讨论其他核心接口,包括:JobConf, JobClient,Partitioner, OutputCollector, Reporter, InputFormat, OutputFormat,OutputCommiter和其他一些接口。
最后,我们讨论一些框架的实用特点,例如DistributedCache.IsolationRunner等,作为结束。
应用程序一般实现Mapper和Reducer两个接口来提供map和reduce函数。这形成了一个工作的核心。
Mapper将输入的
Map函数是将输入记录转换称过渡记录的独立的一些任务。被转换成的过渡记录不必与输入记录是相同的类型。一个指定的输入键值对可以被映射到许多的输出键值对。
Hadoop的MapReduce框架对于每一个InputSplit(输入分块),由工作的InputFormat(输入格式)类生成一个Map任务。
总的来说,Mapper是通过JobConfigurable.congfigure(JobConf)方法传递JobConf来实现的,并通过重载的这个方法会初始化。然后,框架会在InputSplit类中对每个任务的每一个键值对调用map(WritableComparable, Writable, OutputCollector Reporter)。应用程序可以重载Closeable.close()方法来执行任何需要的清理。
输出的键值对不需要是输入键值对对相同的类型。一个输入键值对可以映射到零个或者更多的输出键值对。输出键值对由OutputCollector.collect(WritableComparable, Writable)来收集。
应用可以用Reporter类来报告进度,设置应用程序级的消息和更新Counter(计数器),或者表明它们处于运行状态。
所有与指定的输出键相关的中间值随后由框架分组,并传递到Reducer中以计算最终的输出。用户可以通过JobConf.setOutputKeyComparatorClass(Class)来指定一个Comparator(比较器)。
Mapper的输出由每个Reducer排序和分组。分组的总数与Reducer的数目相同。用户可以通过实现一个自定义的Partitioner来控制哪些keys分组到那个Reducer。
用户还可以可选地通过JobConf.setCombinerClass(Class)来指定一个combiner,对输出的中间结果执行本地聚合操作,以减少从Mapper到Reducer的传输带宽。
排序过的中间结果总是被存储于一个简单的形式(键长度,键,值长度,值)。应用程序可以控制是否或者如何对中间的输出结果进行压缩。这点通过JobConf设置CompressionCodec就可以实现。
Map的总数通常是由输入数据的大小来决定的,也就是输入文件所占据的文件块(blocks)数目决定的。
每个节点比较合适的Map数量似乎在10到100之间,尽管对于CPU占用少的map任务可以设置到300个。任务的建立需要时间,所以每个任务的运行时间最好不少于一分钟。
因此,如果你的输入数据有10TB并且块大小(blocksize)为128MB,你会得到82000个map任务,除非你用setNumMapTasks(int)(此方法只框架的一个提示,并没有实际作用)设置更大的数。
Reducer聚合一组有共同键的中间结果的值。Reduce任务的数量由JobConf.setNumReduceTasks(int)方法来设置。
总的来说,Reducer的是由JobConfigurable.configure(JobConf)方法传递了JobConf来实现的,并通过重载它来实现初始化。接着框架对每一组<键,值列表>调用reduce(WritableComparable, Iterator, OutputCollector, Reporer)方法。应用程序可以重载Closeable.close()方法来执行任何需要的清理。
Reducer有三个主要的阶段:复制(shuffle)、排序(sort)和聚合(reduce)。
Reducer的输入是Mapper输出的排序结果。在这一阶段框架通过HTTP协议将相关的获取Mapper输出中相关的分组。
在这一阶段框架依据键对Reducer的输入排序(因为不同的mapper可能产生相同的键)。
对Mapper输出的复制与排序阶段是同时进行的;在map输出被获取的同时它们也被合并。
如果分组中间键的排序规则与reduce之前的排序规则不同,那么可以通过JobConf.setOutputValueGroupingComparator(Class)来指定一个Comparator。因为JobConf.setOutputKeyComparatorClass(Class)能用于控制中间键的分组,它可以被用于实现对值的二级排序。
在这一阶段,对于每个分组的输入对<键,键列表>,reduce(WritableComparabl, Iterator, OutputCollector, Reporter)方法被调用。
Reduce 任务的输出通常被写入通过OutputCollector.collect(WritableComparable,Writable)写入文件系统(FileSystem)。
应用程序可以用Reporter来报告进度,设置应用级状态消息以及更新计数器(Counters),或者只是表明它们还在运行。
正确的Reduce数量似乎是0.95或者1.75乘以(节点数量*mapred.tasktracker.reduce.tasks.maximum)。
取0.95时所有的reduce可以在map完成后立即启动并开始传输map的输出。取1.75时比较快速的节点可以完成它们第一轮reduce之后启动第二轮reduce而使得工作的调度更平衡。
增加reduce的数量也增加了框架的额外开销(Overhead),但同时也使负载更平衡和减小了任务失败的代价。
上面的系数比整个集群的数量略小是为了保留一定的reduce空位给偶然出现的任务或者失败的任务。
如果不需要聚合操作可以将reducer的数目设置为零。
在这种情况下map任务的输出直接写入文件系统。写入路径由setOutputPath(Path)设置。框架不对输出进行排序。
分组器用于分组键空间。
分组起控制着map输出的中间键值的分组。一个或者一组键被用于产生一个分组,通常是通过哈希(hash)函数。分组的数目等于reduce任务的数目。因此它控制着中间键(以及记录)被发送到哪m个reduce任务。
HashPartitioner是默认的分组器。
报告器用于MapReduce应用程序报告进度,设置应用级状态消息以及更新计数器。
Mapper和Reducer的实现可以使用报告器来报告进度或者只是表明它们在运行。在一些场景中,应用程序处理一组键值对需要很可观的时间,这很关键因为框架可能认为任务超时而杀死任务。另一个避免这种情况的方法是设置配置参数mapred.task.timeout为一个足够大的值(或者直接设置为0表示没有超时限制)。
应用程序还可以通过报告器更新计数器。
输出收集器是MapReduce框架提供的收集Mapper和Reducer输出数据(中间结果或者最终输出)的一般工具。
Hadoop MapReduce内置了一个库,它包含常用的mapper,reducer和partitioner。
JobConf表示一个MapReduce工作的配置。
JobConf是用户向Hadoop框架描述一个MapReduce工作的主要接口。框架会尽力按照JobConf描述的方式执行工作,然而:
l 一些配置参数可能被管理员设置为“final”属性,因此不能被修改
l 虽然一些配置可直接设置(例如setNumReduceTasks(int)),其他参数间接与框架的其他部分交互因此设置更为复杂。
JobConf通常被用来设置Mapper,Combinner(如果有),Reducer,Partitioner,Reducer,InputFormat,OutputFormat和OutputCommitter的实现。
JobConf也用于设置输入文件(setInputPaths(JobConf, Path…)/addInputPath(JobConf, Pth))和输出路径(setOutputPath(Path))。
可选地,JobConf可以被用于配置其他高级工作设置,例如所使用的比较器(Comparator),放入分布式缓存(DistributedCache)的文件,中间结果或工作输出是否以及如何被压缩,使用用户提供的脚本来调试(通过SetMapDebugScript(String)/setReduceDebugScript(String)),以及工作任务能否以推测方法(speculative manner)执行(通过setReduceSeculativeExecution(boolean)),每个任务的最大尝试数(setMaxMapAttempts(int)/setMaxReduceAttempts(int)),工作可以允许的最大任务失败率(setMaxMapTaskFailuresPercent(int)/setMaxReduceTaskFailuresPercent(int))等等。
当然,用户也可以通过set(String,String)/get(String,String)方法来随意设置或者获取应用所需的参数值。然而,对于大量(只读)数据应当使用DistributedCache。
任务跟踪器将Mapper和Reducer作为子进程在两个独立的jvm(Java虚拟机)中执行。
子任务继承任务跟踪器的环境。用户可以通过子jvm的mapred.{map|reduce}.child.java.opts在JobConf中指定额外的参数,例如通过-Djava.library.path=<>等运行时链接库的非标准路径。如果mapred.{map|reduce}.child.java.opts参数包含符号@taskid@则由MapReduce任务的TaskId替换。
这里有一个多参数和重替换的例子,展示了jvm的GC日志,一个无密码的JVM JMX代理的启动以便和jconsole连接并观测内存和线程变化。它分别将Map和Reduce子JVM的堆栈大小设置为512M和1024M,并且通过java.libraray.path设置了外部依赖库路径。
-Xmx512M-Djava.library.path=/home/mycompany/lib
-verbose:gc -Xloggc:/tmp/@[email protected]
-Dcom.sun.management.jmxremote.authenticate=false
-Dcom.sun.management.jmxremote.ssl=false
-Xmx1024M-Djava.library.path=/home/mycompany/lib
-verbose:gc-Xloggc:/tmp/@[email protected]
-Dcom.sun.management.jmxremote.authenticate=false
-Dcom.sun.management.jmxremote.ssl=false
用户或管理员可以通过mapred.{map|reduce}.child.ulimit指定每一个子任务和任何子进程的最大虚拟内存。注意这里设置的是每一个进程的限制,单位是KB。并且这个值应该大于或等于 JVM的-Xmx参数,否则JVM将不能启动。
注意:mapred.{map|reduce}.child.java.opts只用于配置TaskTracker启动的子任务。对于守护进程内存的设置在《配置Hadoop守护进程的环境》(Configuring the Environment of Hadoop Daemons)文档中详细阐述。
Hadoop框架的其他一些部分也是可配置的。在Map和Reduce任务中,性能可能受到控制并发操作的参数的影响,如读写磁盘操作等。监视一个工作的文件系统计数器,如从map到reduce的字节计数器对于调整这些参数是很有用的。
如果启用内存管理,用户可以选择覆盖任务虚拟内存和RAM的默认值。设置方法见下表:
名称 |
类型 |
描述 |
mapred.task.maxvmem |
Int |
单位B,表示每个任务的最大虚拟内存。如果一个任务消耗的内存超过此限制,该任务将被杀死。 |
mapred.task.maxpmem |
Int |
单位B,表示每个任务的最大RAM。这个值可能会被调度器用于防止对于一个任务分配过多的RAM。 |
Map任务输出的记录会被串行化存储在缓存(buffer)中,元数据会被存储在记录缓存(accounting buffer)中。如下表所示,一旦串行化的缓存超过一个阈值,缓存将被后台写入磁盘。如果缓存已满并且写入磁盘的操作正在进行,map任务将被阻塞。当map任务完成,所有剩余的记录被写入磁盘并且所有磁盘分段被合并为一文件。最小化记录分段数可以减少map任务的时间,但是更大的缓存将会降低mapper的可用内存。
名称 |
类型 |
描述 |
io.sort.mb |
Int |
单位MB,从map输出的数据累计大小。 |
io.sort.record.percent |
Float |
记录空间(accounting space)的串行化比率是可以调整的。每个串行化除了本身数据外,记录需要16字节的记录信息。这个从io.sort.mb申请空间的百分比影响着申请一个文件分段的概率。文件分段是由串行化缓存耗尽或者新空间申请导致的。显然,对于输出很多小记录的map任务,一个比默认值更大的值会减少文件分段的数目。 |
io.sort.spill.percent |
float |
这是记录和串行化的缓存阈值。当任何缓存超过这个阈值,将会产生一个新的分段。 令io.sort.record.percent为r,io.sort.mb为x,此值为q。新分段产生前记录数目的最大值为r*x*q*2^16。注意更大的阈值可能降低分段数,但是会增大map任务被阻塞的概率。精确估计map的输出大小可以获得最低的map运行时间并防止产生过多的分段。 |
其他注意事项:
l 如果任何一个阈值在进程中被超过,map的收集过程会继续知道这个分段完成。
l 一个比串行化缓存大的记录会首先触发分段,然后被分割称一个单独的文件。这个记录是否会先通过combiner不能确定。
如前所述,每个reduce任务从Partitioner通过HTTP获取map输出的相关数据,存储到内存,并周期性地合并这些数据并写入磁盘。如果map输出的“立即压缩”被启用,每一个map输出会被先解压缩在复制进内存。以下参数控制了这些合并的频率。
名称 |
类型 |
描述 |
io.sort.factor |
Int |
确定进行合并时磁盘分段的数据。它控制了合并时打开文件和压缩编解码器的数目。如果文件数超过了这个限制,合并将被分为几轮进行。虽然这个参数也会被用于map,但是大多数任务的配置使得map不大可能触及这一限制。 |
mapred.inmem.merge.threshold |
Int |
合并写入磁盘前从map获取的排序的输出数目。这个一个触发器而不是分区。在实际应用中,这个值往往设置得很大(如1000)或者被禁用(0),因为在内存中合并数据比在磁盘中快得多。这个值仅影响在内存中的合并。 |
mapred.job.shuffle.merge.percent |
Float |
内存中分段合并前的内存阈值,用map输出存储内存的百分比表示。这个值过高会降低获取数据和合并的并发度。相反,这个值设为1.0能让可以在完全内存中进行的reduce任务获得最好的性能。这个值也仅影响在内存中的合并。 |
mapred.jobj.shuffle.input.buffer.percent |
Float |
分组过程中最大堆栈内存的百分比。一般来说此值应该设得足够大。 |
mapred.job.reduce.input.buffer.percent |
float |
Reduce输入过程中最大堆栈内存的百分比。一般来说此值应该设得足够大。 |
其他注意事项:
l 当一个map的输出超过申请的内存空间的25%,它会被直接写入磁盘而跳过内存中的第一阶段合并。
l 当使用Combiner时,设置较大的合并阈值的理由可能不成立。对于所有map输出完成前启动的合并,combiner在map输出写入磁盘的同时运行。在某些情况下,可以通过合并map输出、创建磁盘分段并且并行化分段和获取而不是增大缓存来减少reduce的时间。
l 当reduce已经开始内存中的合并,此时由于map输出达到io.sort.factor的限制而需要立即启动一个合并,在内存中数据也会被合并到磁盘。
任务跟踪器(TaskTracker)有一个本地目录,${mapred.local.dir}/taskTracker/来创建本地缓存和工作。它可以被定义为多个本地目录(分布在多个磁盘上)并且每一个文件名是半随机的。当任务开始,任务跟踪器根据配置创建一个本地化的工作目录。这样,本地任务跟踪器目录结构为:
l ${mapred.local.dir}/taskTracker/distance/:所有用户共享的公共分布式缓存。这个目录保存了公共分布式缓存并被所有任务、所有用户共享。
l ${mapred.local.dir}/taskTracker/$user/distance/:私有分布式缓存。特定用户的被所有任务共享的分布式缓存。
l ${mapredlocal.dir}/taskTracker/$user/jobchache/$jobid/:本地工作目录:
n ${mapredlocal.dir}/taskTracker/$user/jobchache/$jobid/work/:工作相关的共享目录。这个目录可以通过配置属性job.local.dir访问或者通过API JobConf.getJobLocalDir()获取,也可以通过系统属性System.getProperty(“job.local.dir”)获取。
n ${mapred.local.dir}/taskTracker/$user/jobcache/$jobid/jars/:jar包目录,保存了工作jar包和其他依赖的jar包。job.jar文件路径可以通过JobConf.getJar()获取,父目录通过JobConf.gerJar().getParent()获取。
n ${mapred.local.dir}/taskTracker/$user/jobcache/$jobid/job.xml:工作配置文件,存储在本地。
n ${mapred.local.dir}/taskTracker/$user/jobcache/$jobid/$taskid:任务目录。每一个任务目录的结构为:
u ${mapred.local.dir}/taskTracker/$user/jobcache/$jobid/$taskid/job.xml:一个job.xml文件,本地化任务配置文件。“本地化任务”是指任务的属性本地化。
u ${mapred.local.dir}/taskTracker/$user/jobcache/$jobid/$taskid/output/:即时输出目录。包含框架产生的暂时的map、reduce数据,如map输出文件等。
u ${mapred.local.dir}/taskTracker/$user/jobcache/$jobid/$taskid/work/:任务的当前工作目录。当“jvm重用”被启用时,这个目录是jvm的启动目录。
u ${mapred.local.dir}/taskTracker/$user/jobcache/$jobid/$taskid/work/tmp:任务的临时输出目录。用户可以通过设置mapred.child.tmp来改变这个值,默认值为./tmp。如果是相对路径,会自动加上当前工作目录。否则会直接使用绝对路径。如果目录不存在,将会被创建。这样,子任务参数为:-Djava.io.tmpdir=’tmp目录的绝对路径’。
工作可以通过设置配置文件的“mapred.job.reuse.jvm.num.tasks”属性启用JVM重用。如果值为1(默认),JVM重用未被启动。如果值为-1,JVM重用被启用次数没有限制。用户还可以通过JobConf.setNumTasksToExecutePerJvm(int)设置一个大于1的值。
6.3.6 配置参数
下表中列出了每一个任务执行配置参数:
名称 |
类型 |
描述 |
mapred.job.id |
String |
工作编号 |
mapred.jar |
String |
job.jar所在目录 |
job.local.dir |
String |
工作相关的共享空间 |
mapred.tip.id |
String |
任务编号 |
mapred.task.id |
String |
任务尝试标号 |
mapred.task.is.map |
Boolean |
是否是一个任务 |
mapred.task.partition |
Int |
工作内部的任务编号 |
mapred.input.file |
String |
map读取的输入文件名 |
mapred.input.start |
Long |
map输入文件分块的初始偏移量 |
mapred.input.length |
Long |
map输入文件分块的长度 |
mapred.work.output.dir |
String |
任务的临时输出目录 |
任务的标准输出和错误输出流由任务跟踪器读取并存储于${HADOOP_LOG_DIR}/usrlogs中。
DistributedCache(分布式缓存)可以被用于分布式缓存在map/reduce任务中需要的jars包或者本地库。子jvm总会将当前工作目录添加到java.library.path和LD_LIBRARY_PATH中。因此,缓存的库可以通过System.LoadLibrary或者 System.Load加载。更多关于加载本地库的细节请参考《本地库的使用》(native_libraries)文档。
JobClient是用户工作与工作跟踪器(JobTracker)交互的基本接口。
JobClient提供了机制来允许用户提交工作,跟踪它们的进度,访问子任务的报告和日志,以及获取MapReduce集群状态信息等功能。
工作提交过程包括:
1. 检查输入、输出设置;
2. 计算输入分块大小;
3. 如果需要,获取所需的DistributedCache的信息;
4. 复制工作的jar包和配置文件到MapReduce 系统文件夹;
5. 提交工作给工作跟踪器并可选地监视工作的状态。
工作的历史信息被记录在特定目录hadoop.job.history.user.location下,它也是默认的工作输出目录。文件被存储在”_logs/history/”目录下。因此,默认情况下它们会被存储在mapred.output.dir/_logs/history目录下。用户可以通过将hadoop.job.history.user.location设为“none”停止日志功能。
用户可以通过如下命令查看特定目录下的历史日志摘要:
$ bin/Hadoop job –historyoutput-dir
此命令会返回工作的详细信息,以及失败和被杀死的任务的详细信息。
更多信息,如成功的任务的信息,可以通过如下命令获得:
$ bin/Hadoop job –historyall output-dir
用户可以通过OutputLogFilter过滤输出目录的日志信息。
一般地,用户通过JobConf创建一个应用,描述它的各个方面,通过JobClient提交工作并监视进度。
Hadoop集群支持工作水平授权和队列水平授权。只需配置mapred.acls.enabled设置为true即可。当授权被启用,访问控制检查将被:
a) 工作跟踪器将在允许用户提交工作到队列之前执行;
b) 工作跟踪器和任务跟踪器在允许用户通过API,CLI或网络接口查看工作细节信息或者修改工作之前执行。
一个工作提交器可以通过分别配置mapreduce.job.acl-view-job和mapreduce.job.acl-modify-job来指定查看和修改工作的权限列表。默认情况下,没有人享有这些权限。
然而,无论工作的ACL如何配置,工作的所有者,超级用户和集群管理员(mapreduce.cluster.administrators)以及工作所在队列的队列管理员(mapred.queue.queue-name.acl-administer-jobs)总是有查看和修改工作的权限。
工作在返回可能的敏感信息之前会查看用户的ACL(访问控制水平,Access Control Level)授权配置(mapreudce.job.acl-view-job)。这些敏感信息包括:
l 工作水平计数器
l 任务水平计数器
l 任务诊断信息
l 网络接口上的任务跟踪器的任务日志
l 网络接口上的工作跟踪器提供的job.xml
其他与工作有关的信息,如状态、概况,可以被所有用户查看而无需授权。
在修改工作之前, ACL通过检查mapreduce.job.acl-modify-job授权用户进行如下操作:
l 杀死一个工作;
l 杀死或者判定一个工作的子任务失败;
l 设置工作的优先级。
这些操作也可以通过配置mapred.queue.acls.xml的“mapred.queue.queue-name.acl-administer-jobs”授权队列水平的ACL实现。
也就是说,只要用户获得工作修改ACL或则队列视屏ACL就可以执行上述操作。
工作水平ACL和队列水平ACL格式相同,参见《集群建立指南》(Cluster Setup)。
用户可能需要串联MapReduce工作来实现复杂的功能,单个MapReduce工作不能完成的功能。这非常容易因为工作的输出一般被放入文件系统的DistributedCache中。而这些输出反过来又可以成为另一个工作的输入。
然而,这也意味着用户需要确保工作完成(成功或者失败)。工作控制的方法有:
l runJob(JobConf):提交工作并只在工作完成后返回;
l submitJob(JobConf):只提交工作,立即返回工作(RunningJob)的句柄用于查询运行状态和计划任务。
l JobConf.setJobEndNotificationURI(String):建立一个工作完成的通知,这样就避免了查询。
在一个安全的集群里,用户是由Kerberos’ kinit命令授权(Kerboros是一种网络授权协议,译者注)。因为考虑到可测量性,我们不在MapReduce工作中推送Kerberos参数(Kerberos’ ticket)。取而代之的是,我们获取每一个HDFS主节点(Namenode)标志的代表。工作将会保存它们并作为工作提交的一部分。这些标志代表是从HDFS自动获取的分段目录,这些目标包括工作文件的保存目录以及FileInputFormat、DistCp和DistributedCache中使用到的任何目录。其他应用需要为所有Namenode配置“mapreduce.job.hdfs-servers”,因为任务在执行过程中可能需要相互交流。这是一个逗号分割的示例:“hdfs://nn1/,hdfs://nn2/”。这些标志被传给工作跟踪器作为工作提交的一部分。它们被称为“工作证书”(Credentials)。
与HDFS标志代表相似,我们也有MapReduce标志代表。MapReduce标志用于在需要的时候帮助任务生成工作。任务通过MapReduce标志代表授权给工作跟踪器。标志代表可以通过API JobClient.etDelegationToken获得。获取到的标志必须被传给工作证书用于工作提交。API Credentials.addToken可以完成这一过程。
工作证书作为工作提交的一部分被传递给工作跟踪器。工作跟踪器在mapred.system.di/JOBID目录下的一个文件中维护这些标志和秘密。所有任务跟踪器将这个文件本地化作为工作本地化的一部分。任务可以访问一个名为HADOOP_TOKE_FILE_LOCATION的环境变量。Hadoop框架将其设为本地文件。为了从任务启动工作来完成任意的HDFS操作,任务必须配置“mapreduce.job.credentials.binary”指向这个文件。
在工作提交时传递给工作跟踪器的HDFS标志代表在工作完成时被撤销。这是默认行为除非“mapreduce.job.complete.cancel.delegation.tokens”在JobConf中被设置为“false”。对于子任务又生成工作的工作,这个值应该被设置为“false”。在JobClient端的多个工作中共享JobConf对象的应用程序,应该将mapreduce.job.complete.cancel.delegation.tokens设置为false。这是因为JobConf中的证书会被共享。所有的工作最后都共享一样的标志,所以在队列中所有工作完成前标志不应该被撤销。
除了HDFS代表标志之外,任何秘密也可以在工作提交时传递给子任务以便访问第三方服务。API JobConf.getCredentials或者JobContex.getCredentials()可以被用来获取证书对象并用Credentials.addSecretKey添加秘密。
对于使用旧版本MapReduceAPI应用,Mapper和Reducer类需要实现JobConfigurable以便在子任务中在访问证书。在JobConfigurable.configure中传递JobConf的引用应该被保存。在新版MapReduceAPI中,相似的事情可以在Mapper.setup方法中完成。API JobCnf.getCredentials()或JobContext.getCredentials()可以用于获取证书的引用(取决于使用新版还是旧版API)。任务可以通过Credentials中的API访问秘密。
InputFormat(Java类)描述了MapReduce工作的输入特性。
MapReduce框架依赖工作InputFormat来完成:
1. 确认工作的输入特性;
2. 将输入数据分块为逻辑上的InputSplit(输入分块)实例,每一个实例将被分配给一个Mapper;
3. 提供RecordReader的实现用于从逻辑InputSplit收集输入记录,提交给Mapper处理。
基于文件的InputFormat的默认行为,典型的是FileInputFormat的派生类,是将输入数据分块为基于输入文件总大小(字节)的逻辑上的InputSplit实例。但是,文件系统的块大小会被作为分块大小的上限。更小的文件系统分块可以通过mapred.min.split.size设置。
显然,基于输入总大小的逻辑分块无法满足众多应用的需求,因为必须考虑记录的边界。在这种情况下,应用程序应当实现一个RecordReader,负责处理记录边界并向每一个任务任务提供面向记录的逻辑InputSplit视图。
TextInputFormat是默认的InputFormat。
如果TextInputFormat作为工作的InputFormat,Hadoop框架会检测输入文件的.gz扩展名并自动用相应的解码器解压。然而,必须注意的是压缩文件不能被分块而且每一个压缩文件只能由一个单独的Mapper处理。
InputSplit代表一个单独的Mapper处理的数据。
一般来说,InputSplit是一个面向字节的输入视图。RecordReader有责任将它处理为一个面向记录的视图。
FileSplit是默认的InpuSplit。对于每一个逻辑分块,它设置map.input.file为输入文件的路径。
RecordReader从InputSplit读取<键,值>对。
一般地,RecordReader将InputSplit提供的、面向字节的输入视图转化成面向记录的视图,提供给Mapper的实现处理。这样,RecordReader担负起处理记录边界和向任务提供键值对的责任。
OutputFormat描述了一个MapReduce任务的输出特性。
MapReduce框架依赖工作的OutputFormat来:
1. 确认工作的输出特性:例如,检查输出目录是否存在;
2. 提供RecordWriter实现输出文件的写操作。输出文件被存储在文件系统(FileSystem)中。
TextOutputFormat是默认的OuputFormat。
输出提交器描述了一个工作的任务输出方式。
MapReduce框架依赖任务输出器来:
1. 在初始化阶段建立工作。例如,在工作初始化阶段创建临时目录。工作的建立是在工作处于“PRE”状态时和初始化其他任务后,由一个单独的任务来完成的。当建立工作的任务完成,工作会转移到“运行”(RUNNING)状态。
2. 在工作完成后进行清理。例如,在工作完成之后删除临时输出目录。工作清理由工作结束后一个单独的任务来完成。在清理完成后,工作状态转变为“成功”或“失败”或“被杀死”(SUCCEEDED/FAILED/KILLED)。
3. 建立任务的临时输出目录。任务的建立是在任务初始化时,作为任务本身的一部分。
4. 检查任务是否需要输出。这是为了在任务不需要输出时避免输出过程。
5. 处理任务输出。一旦任务完成并且需要输出,任务会立即提交输出数据。
6. 丢弃任务输出。如果任务失败或者被杀死,它的输出将会被清理。如果任务本身不能清理(任务处于异常块),一个单独的、具有相同任务编号的任务会被启动,用于清理任务。
FileOutputCommitter是默认的OutputCommitter。工作建立/清理任务会占据map/reduce槽。而槽在任务跟踪器上随时可用的。工作清理任务、任务清理任务和工作建立任务按依次具有最高优先级。
在一些应用中,组件任务需要创建或写入附属文件,这与实际的工作输出文件不同。
在这种情况下,两个同时运行的、同一个Mapper或Reducer的实例可能在读、写同一个文件的时候出现冲突。因此,每一个任务尝试(taskattempt)需要使用不同的尝试编号(例如attempt_200709221812_0001_m_00000_0),而不是每一个任务使用不同的编号。
为了避免这样的问题,当FileOutputCommitter作为OutputCommiter时,MapReduce框架维护一个特殊的${mapred.output.dir}/_temporary/_${taskid}子目录。这个子目录可以通过每个任务尝试的${mapred.work.output.dir}访问并在FileSystem上存储任务尝试的输出。对于成功完成的任务尝试,${mapred.output.dir}/_temporary/_${taskid}目录下的文件会被复制到${mapred.output.dir}目录下。当然,框架会丢弃不成功任务尝试的输出子目录。这一过程对于应用程序来说是透明的。
应用程序输出可以利用这一特点在${mapred.work.output.dir}目录下,通过FileOutputFormat.getWorkOutputPath(),创建任何所需的附属文件。框架会复制成功任务尝试的附属文件,这样不必再为每一个任务尝试选一个不同的路径。
注意:在运行时,特定任务尝试的${mapred.work.output.dir}的值实际上是${mapred.output.dir}/_temporary/_{$taskid},这个值是由MapReduce框架赋予的。因此,可以在FileOutputFormat.getWorkOutputPath()获取的路径下创建附属文件。
这里的讨论对于没有Reducer的情况依然成立,因为那种情况下,输出来自map,并直接写入HDFS。
RecordWriter将<键,值>对写入输出文件中。
RecordWriter的实现将工作的输出写入FileSystem。
用户向队列提交工作。队列,是工作的容器,允许系统提供特定的功能。例如,队列使用ACL(访问控制列表,Access Control List)来控制哪些用户可以提交工作。一般情况下,队列主要由Hadoop调度器使用。
Hadoop默认配置一个强制的队列,称为“默认队列”(default)。队列的名称由Hadoop网站(site)文件的mapred.queue.names属性配置。一些工作调度器,例如容量调度器(Capacity Scheduler),支持多队列。
工作通过mapred.job.queue.name或者通过APIsetQueueName(String)设置它需要提交到的队列。设置队列名称是可选的。如果一个工作在提交时没有设置队列名称,它会被提交到“默认队列”。
计数器表示全局计数器,由MapReduce框架或者应用程序定义。每个计数器可以是任意枚举类型 (Enum Type)。某一个特定枚举类型的计数器将成为Counters.Group的那个类型的分支。
应用程序可以定义任意(枚举类型的)计数器并map/reduce任务中通过Reporter.incrCounter(Enum.long)或Reporter.incrCounter(Strng,String, long)更新它们。这些计数器会由框架统一聚合。
DistributedCache能有效将应用指定的、大的、只读的文件分布到集群中。
DistributedCache是MapReduce框架提供的一个缓存应用所需的文件(文本,压缩文件,jar包等等)的设施。
应用程序在JobConf中指定的文件通过URL(hdfs://)被缓存。DistributedCache假设所有通过“hdfs://”指定的文件已经在FileSystem上了。
框架会在工作的任何任务在某子节点上执行前将需要的文件复制到该子节点上。它的效率源于每个工作的文件只被复制一次的事实以及缓存子节点上非归档文件为归档文件的能力。
DistributedCache跟踪缓存文件的修改时间戳。显然在工作运行期间,缓存文件不应被应用程序或者外部修改。
DistributedCache可被用于分布缓存简单的、只读的数据/文本文件和更复杂的类型,例如归档文件和jar包。归档文件(zip,tar,tgz,tar.gz)在子节点上是非归档的。文件具有执行权限集。
文件或者归档文件可以通过设置mapred.cache.{files|archives}来进行分布缓存。如果多于一个文件需要分布缓存,它们的路径可以通过逗号分隔。这个属性还可以通过API DistributedCache.addCacheFile(URI,conf)或DistributedCache.addCacheArchieve(URI,conf)和DitributedCache.setCachedFiles(URIs,conf)或DistributedCache.setCacheArchives(URIs,conf)设置。这里URI的形式为:“hdfs:/host:port/absolute-path$link-name”。在流(Streaming)中,文件可以通过如下命令分布缓存:
-cacheFile/-cacheArchive
可选地,用户可以通过DistributedCache.createSymlink(Configuration)直接定向DistributedCache链接到当前工作目录下的文件,或者设置“mapred.create.symlink”为“yes”。DistributedCache会用URI的片段作为系统链接的名称。例如,URI “hdfs://Namenode:port/lib.so.1#lib.so”将会使得分布式缓存中的“lib.so.1”作为任务中“lib.so”的链接。
DistributedCache也能被map/reduce作为初步的软件分布机制。它可以被用来分布jar包和本地库。API DistributedCache.addArchiveToClassPath(Path, Configuration)或DistributedCache.addFileToClassPath(Path,Configuration)可以用于分布缓存jar包和本地库文件并把它们添加到子JVM的classpath(类路径)中。这一过程也可以通过设置mapred.job.classpath.{files|archives}属性完成。类似地,缓存的文件作为任务工作目录下的链接时也可以是本地库并被程序加载。
DistributedCache文件可以是私有的或公有的,这里的区别在于它们是怎样被子节点共享的。
l “私有的”DistributedCache文件缓存在一个本地目录中,这个目录只用此用户及其工作能够访问,而其他用户的工作或任务无法共享这些文件。DistributedCache文件具有私有属性是因为文件系统(一般是HDFS)的权限限制。如果这个文件的访问权限不是所有人都可以访问,或者路径中的某一级目录不是所有人可以访问,这个文件就是私有的。
l “公有的”DistributedCache文件缓存在一个全局目录里并且所有用户都可以访问。这些文件可以被所有用户的工作和任务共享。DistributedCache文件具有公有属性也是因为文件系统(一般是HDFS)的权限限制。如果这个文件以及路径中的所有目录都是所有用户可访问的,这个文件就是公有的。换句话说,如果用户希望创建一个公有文件,应该确保该文件权限为所有人可访问,并且确保文件全路径中的所有层次的目录权限是所有人可访问。
Tool接口支持一般的Hadoop命令行选项的处理。
对于所有MapReduce工具或应用,Tool是标准化的。应用程序应当通过ToolRunner.run(Tool, String[])使用GenericOptionsParser委托处理标准命令行参数,从而只需处理自己应用特殊的参数。
一般的Hadoop命令行选项包括:
-conf
-D
-fs
-jt
IsolationRunner是调试MapReduce程序的工具。
要使用IsolationRunner,首先设置keep.faied.task.files为“true”(参看“keep.task.files.pattern”)。
其次,打开失败任务所在节点的任务跟踪器的本地目录并运行IsolationRunner:
$cd
$bin/Hadooporg.apache.hadoop.mapred.IsolationRunner ../job.xml
IsolationRunner会将失败的任务在一个单独的jvm中并且在同样的输入下运行,在这里就可以使用调试器。
注意:目前的IsolationRunner只能重复运行map任务。
Profiling是一个工具,它能针对map、reduce任务样本获取java内置的profiler的代表(2到3个)。
用户可以通过配置属性mapred.task.profile控制系统是否收集profiler信息。这个值也可以通过APIJobConf.setProfileEnabled(boolean)设置。如果值为“true”,Profiling就被启用。Profiler信息被存储与系统日志目录。默认情况下,工作的Profiling未被启用。
一旦用户配置需要Profiling,他(她)可以配置属性mapred.task.prfile.{maps|reduces}来设置需要收集概况信息的map和reduce任务。这个区间也可以通过API JobConf.setProfileTaskRange(boolean, String)来设置。默认情况下,这个区间是0到2。
用户还可以通过配置属性mapred.taks.profile.params设置Profiler的参数。APIJobConf.setProfileParams(String)也可以设置此值。如果字符串包含“%s”,在任务运行时,它将被Profiling输出文件名替换。这些Profiling参数将通过命令行传递给子jvm:
-agentlib:hprof=cpu=samples,heap=sites, force=n, thread=y, verbose = n, file=%s
MapReduce框架提供了一个设施来运行用户定义的脚本,以便调试。
例如,当一个任务失败时,用户可以运行一个调试脚本来处理任务日志。
脚本具有访问任务标准输出、错误输出、系统日志和JobConf的权限。调试脚本的标准输出和错误输出会被显示在命令行诊断信息中,也作为工作(网站)界面的一部分。
用户需要使用DistributedCache来分布缓存或者链接脚本文件。
一种快速提交脚本文件的方法是设置mapred.{map|reduce}.task.debug.script属性或者通过APIJobConf.setMapDebugScript(String)、JobConf.setReduceDebugScript(String)。在流(streaming)模式下,调试脚本可以通过命令行选项提交:
-mapdebug
-reducedebug
脚本的输入是任务额标准输出、错误输出、系统日志和JobConf文件。调试脚本命令运行在失败任务所在的节点上。也就是说,调试脚本运行如下:
$script$stdout $stderr $syslog $jobconf
对于管道(pipes),默认脚本是在gdb下处理核心错误信息,输出栈跟踪信息和运行的线程信息。
JobControl是一个封装了一套MapReduce工作和依赖关系的工具。
Hadoop MapReduce为应用程序输出提供了一套压缩map输出的中间结果和工作输出的设施。它还自带了压缩编码解码器,实现了zlib压缩算法,并支持gzip格式的文件。
Hadoop还提供了压缩编码解码器的本地化实现。这样做既考虑了性能(zlib),又考虑了Java库的不可用性。更多细节请参考文档《本地库》(Native Libraries)。
应用程序可以通过设置JobConf.setCompressMapOutput(boolean)设置是否压缩map的输出。使用的压缩编码解码器通过JobConf.setMapOutputCompressorClass(Class)设置。
应用程序可以通过设置JobConf.setCompressOutput(boolean)设置是否压缩工作的输出。使用的压缩编码解码器通过JobConf.setOutputCompressorClass(Class)设置。
如果工作输出被存储在SequenceFileOutputFormat中,所需的SequenceFile.ComressionType(RECORD/BLOCK)可以通过SequenceFileOutputFormat.setOutputComressionType(JobConf,SequenceFile.CompressionType)设置。
Hadoop提供了在处理map输入时跳过特定的损坏记录的功能。应用程序可以通过SkipBadRecords类使用这一功能。
这一功能可以被用于解决map遇到特定输入崩溃的问题。这通常是由于map中存在BUG导致的。通常,用户可以修正这些BUG。但是,有些时候这是不可能的。例如,这些BUG可能存在于第三方库中,源码不可获取。这种情况下,任务尝试重复很多次后仍然不成功,于是工作失败。有了这一功能,只有损坏记录附近的一小段数据丢失,对于有些应用来说是可接受的(例如大数据集的统计)。
默认情况下,这一功能是被禁用的。用户可以通过SkipBadRecords.setMapperMaxSkipRecords(Configuration, long)和SkipBadRecords.setReducerMaxSkipGroups(Configuration,long)启用。
当跳过损坏记录功能启用,框架会在一定数目map失败后进入“跳过模式”。更多细节请参考SkipBadRecords.setAttemptsToStartSkipping(Configuration, int)方法的文档。在“跳过模式”下,map任务维护一个被处理的记录的队列。框架依赖被处理记录的计数器完成这一功能。请参考关于SkipBadRecords.COUNTER_MAP_PROCESSED_RECORDS 和SkipBadRecords.COUNTER_REDUCE_PROCESSED_GROUPS的文档。这个计数器让框架知道有多少记录已经被成功处理,以及哪一段记录导致任务崩溃。在后续的任务尝试中,这些记录将会被跳过。
跳过记录的数量取决于应用程序让处理记录计数器自增的频率。建议每一条记录处理完后都让计数器增加。当然,这在某些应用中是不可能的。典型的如批处理。在这些情况下,框架可能会跳过损坏记录附近更多的记录。用户可以通过SkipBadRecords.setMapperMaxSkipRecords(Configuration, long)和SkipBakRecords.setReducerMaxSkipGroups(Configuration,long)来控制跳过的记录数。框架会尝试用二分查找缩小跳过的记录区间。被跳过的记录会被等分为两份,只有一半被执行。在后续的执行中,框架判断出哪一半记录包含了损坏记录并继续二分查找那一半记录,直到记录区间不大于可接受的值或者所有任务尝试数达到限制。如果需要提高任务尝试限制,可以用JobConf.setMaxMapAttempts(int)和JobConf.setMaxReduceAttempts(int)。
被跳过的记录会以SequenceFileFormat形式被写入HDFS,以便以后分析。存储路径可以通过SkipBadRecords.setSkipOutputPath(JobConf, Path)设置。
下面是一个更完整的WordCount示例。示例中使用了许多MapReduce框架提供的特性。
示例运行需要HDFS的建立和运行,特别是DistributedCache相关的特性。因此它只能运行于伪集群或者完全分布式的Hadoop集群。
WordCount.java |
|
1. |
package org.myorg; |
2. |
|
3. |
import java.io.*; |
4. |
import java.util.*; |
5. |
|
6. |
import org.apache.hadoop.fs.Path; |
7. |
import org.apache.hadoop.filecache.DistributedCache; |
8. |
import org.apache.hadoop.conf.*; |
9. |
import org.apache.hadoop.io.*; |
10. |
import org.apache.hadoop.mapred.*; |
11. |
import org.apache.hadoop.util.*; |
12. |
|
13. |
public class WordCount extends Configured implements Tool { |
14. |
|
15. |
public static class Map extends MapReduceBase implements Mapper |
16. |
|
17. |
static enum Counters { INPUT_WORDS } |
18. |
|
19. |
private final static IntWritable one = new IntWritable(1); |
20. |
private Text word = new Text(); |
21. |
|
22. |
private boolean caseSensitive = true; |
23. |
private Set |
24. |
|
25. |
private long numRecords = 0; |
26. |
private String inputFile; |
27. |
|
28. |
public void configure(JobConf job) { |
29. |
caseSensitive = job.getBoolean("wordcount.case.sensitive", true); |
30. |
inputFile = job.get("map.input.file"); |
31. |
|
32. |
if (job.getBoolean("wordcount.skip.patterns", false)) { |
33. |
Path[] patternsFiles = new Path[0]; |
34. |
try { |
35. |
patternsFiles = DistributedCache.getLocalCacheFiles(job); |
36. |
} catch (IOException ioe) { |
37. |
System.err.println("Caught exception while getting cached files: " + StringUtils.stringifyException(ioe)); |
38. |
} |
39. |
for (Path patternsFile : patternsFiles) { |
40. |
parseSkipFile(patternsFile); |
41. |
} |
42. |
} |
43. |
} |
44. |
|
45. |
private void parseSkipFile(Path patternsFile) { |
46. |
try { |
47. |
BufferedReader fis = new BufferedReader(new FileReader(patternsFile.toString())); |
48. |
String pattern = null; |
49. |
while ((pattern = fis.readLine()) != null) { |
50. |
patternsToSkip.add(pattern); |
51. |
} |
52. |
} catch (IOException ioe) { |
53. |
System.err.println("Caught exception while parsing the cached file '" + patternsFile + "' : " + StringUtils.stringifyException(ioe)); |
54. |
} |
55. |
} |
56. |
|
57. |
public void map(LongWritable key, Text value, OutputCollector |
58. |
String line = (caseSensitive) ? value.toString() : value.toString().toLowerCase(); |
59. |
|
60. |
for (String pattern : patternsToSkip) { |
61. |
line = line.replaceAll(pattern, ""); |
62. |
} |
63. |
|
64. |
StringTokenizer tokenizer = new StringTokenizer(line); |
65. |
while (tokenizer.hasMoreTokens()) { |
66. |
word.set(tokenizer.nextToken()); |
67. |
output.collect(word, one); |
68. |
reporter.incrCounter(Counters.INPUT_WORDS, 1); |
69. |
} |
70. |
|
71. |
if ((++numRecords % 100) == 0) { |
72. |
reporter.setStatus("Finished processing " + numRecords + " records " + "from the input file: " + inputFile); |
73. |
} |
74. |
} |
75. |
} |
76. |
|
77. |
public static class Reduce extends MapReduceBase implements Reducer |
78. |
public void reduce(Text key, Iterator |
79. |
int sum = 0; |
80. |
while (values.hasNext()) { |
81. |
sum += values.next().get(); |
82. |
} |
83. |
output.collect(key, new IntWritable(sum)); |
84. |
} |
85. |
} |
86. |
|
87. |
public int run(String[] args) throws Exception { |
88. |
JobConf conf = new JobConf(getConf(), WordCount.class); |
89. |
conf.setJobName("wordcount"); |
90. |
|
91. |
conf.setOutputKeyClass(Text.class); |
92. |
conf.setOutputValueClass(IntWritable.class); |
93. |
|
94. |
conf.setMapperClass(Map.class); |
95. |
conf.setCombinerClass(Reduce.class); |
96. |
conf.setReducerClass(Reduce.class); |
97. |
|
98. |
conf.setInputFormat(TextInputFormat.class); |
99. |
conf.setOutputFormat(TextOutputFormat.class); |
100. |
|
101. |
List |
102. |
for (int i=0; i < args.length; ++i) { |
103. |
if ("-skip".equals(args[i])) { |
104. |
DistributedCache.addCacheFile(new Path(args[++i]).toUri(), conf); |
105. |
conf.setBoolean("wordcount.skip.patterns", true); |
106. |
} else { |
107. |
other_args.add(args[i]); |
108. |
} |
109. |
} |
110. |
|
111. |
FileInputFormat.setInputPaths(conf, new Path(other_args.get(0))); |
112. |
FileOutputFormat.setOutputPath(conf, new Path(other_args.get(1))); |
113. |
|
114. |
JobClient.runJob(conf); |
115. |
return 0; |
116. |
} |
117. |
|
118. |
public static void main(String[] args) throws Exception { |
119. |
int res = ToolRunner.run(new Configuration(), new WordCount(), args); |
120. |
System.exit(res); |
121. |
} |
122. |
} |
123. |
首先创建样本输入文件:
$ bin/hadoop dfs -ls/usr/joe/wordcount/input/
/usr/joe/wordcount/input/file01
/usr/joe/wordcount/input/file02
$ bin/hadoop dfs -cat/usr/joe/wordcount/input/file01
Hello World, Bye World!
$ bin/hadoop dfs -cat /usr/joe/wordcount/input/file02
Hello Hadoop, Goodbye to hadoop.
其次运行命令:
$ bin/hadoop jar /usr/joe/wordcount.jarorg.myorg.WordCount
/usr/joe/wordcount/input/usr/joe/wordcount/output
启动工作。
最后用一下命令查看输出:
$ bin/hadoop dfs -cat /usr/joe/wordcount/output/part-00000
Bye 1
Goodbye 1
Hadoop, 1
Hello 2
World! 1
World, 1
hadoop. 1
to 1
注意输入与第一个WordCount版本不同。现在我们看一下这对输出有什么影响。
现在,通过DistributedCache定义需要被忽略的文本“黑名单”(patterns.txt)。
$ hadoop dfs -cat/user/joe/wordcount/patterns.txt
\.
\,
\!
to
用更多选项再次运行程序:
$ bin/hadoop jar /usr/joe/wordcount.jarorg.myorg.WordCount
-Dwordcount.case.sensitive=true/usr/joe/wordcount/input
/usr/joe/wordcount/output -skip
/user/joe/wordcount/patterns.txt
正如所期望的,输出为:
$ bin/hadoop dfs -cat /usr/joe/wordcount/output/part-00000
Bye 1
Goodbye 1
Hadoop 1
Hello 2
World 2
hadoop 1
设置格诺选项后再次运行:
$ bin/hadoop jar /usr/joe/wordcount.jarorg.myorg.WordCount
-Dwordcount.case.sensitive=false/usr/joe/wordcount/input
/usr/joe/wordcount/output -skip
/user/joe/wordcount/patterns.txt
当然,输出为:
$ bin/hadoop dfs -cat/usr/joe/wordcount/output/part-00000
bye 1
goodbye 1
hadoop 2
hello 2
world 2
第二个版本的WordCount使用MapReduce框架的某些特性对前一版本做了提升:
l 展示了应用程序如何在Mapper(或Reducer)的configure方法中访问配置参数;
l 展示了如何使用DistributedCache分布缓存工作所需的只读数据。这里它允许用户在计数单词的同时指定“黑名单”(第104行);
l 展示了Tool接口的用法和如何用GenericOptionsParser来处理一般的Hadoop命令行参数(第87-116行,第96行);
l 展示了应用程序如何使用计数器(第68行)以及如何利用它们设置应用程序的状态信息。方法是通过传递给map(或reduce)的Reporter实例(第72行);
Java和JNI是Sun Microsystems有限公司在美国和其他国家的商标或注册商标。