本文档全面描述了Hadoop MapReduce框架中面向用户的方方面面,并将其作为教程。
确保已安装Hadoop,配置好并且是正在运行。想要了解更多细节:
Hadoop MapReduce是一个可以轻松地编写应用程序,以可靠,容错的方式并行处理大型硬件集群(数千个节点)上的大量数据(多TB数据集)的软件框架。
MapReduce作业通常将输入数据集拆分为独立的块,这些任务由map tasks以完全并行的方式进行处理。该框架对maps的输出进行排序,然后将其输入到reduce tasks中。通常情况下,作业的输入和输出都存储在文件系统中。该框架负责安排任务,监控任务的执行并重新执行失败的任务。
通常,计算节点和存储节点是相同的,也就是说,MapReduce框架和Hadoop分布式文件系统(请参阅HDFS架构指南)在同一组节点上运行。这个配置使框架可以在已经存在数据的节点上有效地调度任务,从而在整个群集中产生很高的聚合带宽。
MapReduce框架由一个主资源管理器(ResourceManager),每个集群节点一个工作器NodeManager和每个应用程序一个MRAppMaster组成(请参阅YARN体系结构指南)。
应用程序指定了输入和输出位置,通过适当的接口或抽象类的实现来提供map和reduce功能。这些以及其他job的参数一起构成了job的配置。
然后,Hadoop作业客户端提交作业(jar包或者是可执行文件等)和配置给ResourceManager,然后由ResourceManager负责将软件/配置分发给工作节点,安排任务并对其进行监控,为job客户端提供状态和诊断信息。
尽管Hadoop框架是用Java™实现的,但MapReduce应用程序可以不用Java编写。
MapReduce框架仅在
作为键和值的类必须由框架实现可序列化,因此需要实现Writable接口。此外,作为键的类必须实现WritableComparable接口,以利于框架进行排序。
MapReduce作业的输入和输出类型:
(input)
-> map -> -> combine -> -> reduce -> (output)
在进入细节之前,让我们来看一个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
$ bin/hadoop com.sun.tools.javac.Main WordCount.java
$ jar cf wc.jar WordCount*.class
$ 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和reduces的类路径。-archives
选项允许他们将逗号分隔的存档列表作为参数传递。这些归档文件是未归档的,并且在当前任务工作目录中创建了带有归档文件名称的链接。有关命令行选项的更多详细信息,请参见命令指南。
使用-libjars
,-files
和-archives
运行wordcount示例:
bin/hadoop jar hadoop-mapreduce-examples-.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-.jar wordcount -files dir1/dict.txt#dict1,dir2/dict.txt#dict2 -archives mytar.tgz#tgzdir input output
这里,任务可以分别使用符号名称dict1和dict2访问文件dir1/dict.txt和dir2/dict.txt。归档文件mytar.tgz将被放置tgzdir这个目录,并取消归档。
应用程序可以通过在命令行上分别使用-Dmapreduce.map.env,-Dmapreduce.reduce.env和-Dyarn.app.mapreduce.am.env选项在命令行上指定mapper,reducer和application master tasks的环境变量。
例如,以下为mappers和reducers设置环境变量FOO_VAR = bar
和LIST_VAR = a,b,c
:
bin/hadoop jar hadoop-mapreduce-examples-<ver>.jar wordcount -Dmapreduce.map.env.FOO_VAR=bar -Dmapreduce.map.env.LIST_VAR=a,b,c -Dmapreduce.reduce.env.FOO_VAR=bar -Dmapreduce.reduce.env.LIST_VAR=a,b,c input output
下面的WordCount应用程序非常简单:
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);
}
}
Mapper实现通过map方法一次处理一行,这由指定的TextInputFormat提供。然后,它通过StringTokenizer根据空格将行进行分隔,并生成键值对<
。
对于给定的样本输入,第一个map生成的键值对:
< Hello, 1>
< World, 1>
< Bye, 1>
< World, 1>
第二个map生成的键值对:
< Hello, 1>
< Hadoop, 1>
< Goodbye, 1>
< Hadoop, 1>
在本教程的后面部分,我们将详细了解为给定任务生成的Map数量,以及如何以精细的方式控制它们。
job.setCombinerClass(IntSumReducer.class);
WordCount还指定一个聚合器。因此,在对键进行排序之后,每个Map的输出都将通过本地聚合器(参数输入的类型与每一个作业配置的Reducer相同)进行本地聚合。
第一个Map的输出:
< Bye, 1>
< Hello, 1>
< World, 2>
第二个Map的输出:
< Goodbye, 1>
< Hadoop, 2>
< Hello, 1>
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);
}
Reducer接口的实现中的reduce方法只是对值进行求和,这些值是每个键的出现次数(即本示例中的单词)。
因此,Job的输出为:
< Bye, 1>
< Goodbye, 1>
< Hadoop, 2>
< Hello, 2>
< World, 2>
main方法指定作业的各个方面,例如作业中的输入输出路径(通过命令行传递),键值类型,输入输出格式等。 然后,它调用job.waitForCompletion提交作业并监控其进度。
我们将在本教程稍后的部分中详细了解Job,InputFormat,OutputFormat和其他接口和类。
本节提供有关MapReduce框架每个面向用户方面的合理数量的详细信息。这应该可以帮助用户以细粒度的方式实现,配置和调整作业。但是,请注意每个类或接口的javadoc仍然是最全面的文档,这仅仅是一个教程。
首先让我们使用Mapper接口和Reducer接口。应用程序通常实现它们以提供map方法和reduce方法。
然后,我们将讨论其他核心接口,包括Job,Partitioner,InputFormat,OutputFormat等。
最后,我们将讨论框架的一些有用功能,例如DistributedCache,IsolationRunner等,作为总结。
应用程序通常实现Mapper和Reducer接口以提供map方法和reduce方法,这些构成了工作的核心。
Mapper接口将输入键值对映射到一组中间键值对。
Maps是将输入数据转换为中间数据的单个任务。转换后的中间数据的类型可以不用和输入数据的类型相同。给定的输入键值对可以映射为零或多个输出键值对。
Hadoop MapReduce框架为作业的InputFormat生成的每个InputSplit生成一个map任务。
总体而言,Mapper的实现是通过Job.setMapperClass(Class)方法传递给Job作业的。然后,框架针对该任务的InputSplit中的每个键值对调用map(WritableComparable,Writable,Context)。然后,应用程序可以重写cleanup(Context)
方法以执行任何必需的清理。
输出键值对的数据类型可以和输入键值对不同。给定的输入键值对可以映射为零或多个输出键值对。通过对context.write(WritableComparable,Writable)的调用来收集输出键值对。
应用程序可以使用计数器报告其统计信息。
随后,与给定输出键关联的所有中间值都由框架进行分组,并传递给Reducer,以确定最终输出。用户可以通过Job.setGroupingComparatorClass(Class)指定一个Comparator来控制分区。
Mapper的输出会进行排序,然后按每个Reducer进行分区。分区总数与作业的reduce任务总数相同。用户可以通过实现自定义分区程序来控制将哪些键(从而记录)转到哪个Reducer。
用户可以选择通过Job.setCombinerClass(Class)指定一个聚合器,以执行中间输出的本地聚合,这有助于减少从Mapper传递给Reducer的数据量。
排序的中间输出始终以简单的格式(key-len,key,value-len,value)存储。应用程序可以通过配置控制是否以及如何压缩中间输出,以及使用CompressionCodec。
maps数通常由输入的总大小也即输入文件的块总数决定。
maps的正确并行度级别似乎是每个节点10-100个maps,尽管已经为非常cpu-light的map任务设置了300个maps 。任务设置需要一段时间,因此最好执行map至少一分钟。
因此,如果您期望输入的数据为10TB,块大小为128MB,则最终将获得82,000个maps,除非使用Configuration.set
(MRJobConfig.NUM_MAPS,int)(仅向框架提供提示)进行设置它甚至更高。
Reducer对一组中间值进行归并操作,这些中间值共享一个较小值集的key。
用户通过Job.setNumReduceTasks(int)设置作业的reduces数量。
总体而言,Reducer实现是通过Job.setReducerClass(Class)方法传递作业的Job的,并且可以重写它来初始化自己。然后,框架为分组输入中的每个
Reducer 分为三个主要阶段:shuffle,sort和reduce。
Reducer的输入是mappers的排序输出。在此阶段,框架通过HTTP获取所有mappers的输出的相关分区。
在此阶段,框架根据键将Reducer的输入数据进行排序(因为不同的mappers可能输出相同的键)。
Shuffle阶段和排序阶段会同时进行; 在提取map的输出时会将它们进行合并。
如果在Reducer之前要求用于分组中间键的等效规则与用于分组键的等效规则不同,则可以通过Job.setSortComparatorClass(Class)指定一个Comparator。由于Job.setGroupingComparatorClass(Class)可用于控制中间键的分组方式,因此可以结合使用这些键来模拟对值的二次排序。
在此阶段,将对分组输入中的每个
reduce任务的输出通常通过Context.write(WritableComparable,Writable)方法写入文件系统。
应用程序可以使用计数器报告其统计信息。
Reducer的输出未排序
reduces的正确数量似乎是0.95或1.75乘以(<节点数> * <每个节点的最大容器数>)。
使用0.95时,所有reduce都可以立即启动,并在maps完成时开始传输map的输出。当使用1.75时,更快的节点将完成其第一轮reduces,并发起第二次reduces,从而更好地完成负载平衡。
增加reduces的数量会增加框架开销,但会增加负载平衡并降低故障成本。
上面的缩放因子略小于整数,以在框架中为推测性任务和失败任务保留一些reduce的时间。
如果不需要Reduces,则将Reduces任务的数量设置为零是合法的。
在这种情况下,map任务的输出将直接转到文件系统,进入FileOutputFormat.setOutputPath(Job,Path)设置的输出路径。该框架不会在将map输出写入文件系统之前对其进行排序。
分区程序对key空间进行分区。
分区器控制中间map的输出的键的分区。Key(或Key的子集)通常用于通过hash函数计算得出分区。分区总数与作业的reduce任务总数相同。因此,这控制了将中间键(以及记录)发送到m个reduce任务中的哪个reduce任务以进行reduction。
HashPartitioner是默认的分区器。
计数器是MapReduce应用程序报告其统计信息的工具。
Mapper和Reducer接口的实现可以使用Counter报告统计信息。
Hadoop MapReduce绑定了一个包含通常有用的mappers,reducers和partitioners的库。
Job代表MapReduce作业配置。
Job是用户向Hadoop框架描述MapReduce作业以执行的主要接口。该框架尝试按照Job的配置忠实地执行作业,然而:
Job通常用于指定Mapper,combiner(如果有),Partitioner,Reducer,InputFormat,OutputFormat
的实现。 FileInputFormat指示输入文件集(FileInputFormat.setInputPaths(Job,Path…)/ FileInputFormat.addInputPath(Job,Path…))和(FileInputFormat.setInputPaths(Job,String…)/ FileInputFormat.addInputPaths(Job,String…))和输出文件应被写入的位置(FileOutputFormat.setOutputPath(Path))。
这是可选地,作业用于指定作业的其他高级方面,例如要使用的比较器,要放置在DistributedCache中的文件,是否要压缩中间和/或作业输出(以及如何压缩),是否可以执行作业任务 以推测方式执行(setMapSpeculativeExecution(boolean)/ setReduceSpeculativeExecution(boolean)),每个任务的最大尝试次数(setMaxMapAttempts(int)/ setMaxReduceAttempts(int))等。
当然,用户可以使用Configuration.set(String,String)/ Configuration.get(String)设置/获取应用程序所需的任意参数。但是,对大量(只读)数据使用DistributedCache。
MRAppMaster在单独的jvm中将Mapper / Reducer任务作为子进程执行。
子任务继承了父MRAppMaster的环境。用户可以通过mapreduce.{map | reduce} .java.opts
向child-jvm指定额外的选项和在Job配置参数,例如运行时链接程序通过非标准路径-Djava.library.path=<>
搜索共享依赖库等。如果mapreduce.{map | reduce} .java.opts
参数包含符号@ taskid @
,则将其插入MapReduce任务的taskid值。
这是一个包含多个参数和替换项的示例,显示了jvm GC日志记录以及启动了无密码JVM JMX代理,以便它可以与jconsole等连接以监视子内存、线程并获取线程转储。它还将map和reduce的子jvm最大堆大小分别设置为512MB和1024MB。它还向child-jvm的java.library.path添加了一条额外路径。
<property>
<name>mapreduce.map.java.optsname>
<value>
-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
value>
property>
<property>
<name>mapreduce.reduce.java.optsname>
<value>
-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
value>
property>
用户或管理员还可以使用mapreduce.{map | reduce} .memory.mb
指定启动的子任务以及该子任务以递归方式启动的任何子进程的最大虚拟内存。需要注意的是,此处设置的值是针对每个进程进行限制的。mapreduce.{map | reduce} .memory.mb
的值应以兆字节(MB)为单位来进行指定。并且该值必须大于或等于传递给JavaVM的-Xmx,否则VM可能无法启动。
注意:mapreduce.{map | reduce} .java.opts
仅用于配置从MRAppMaster启动的子任务。Hadoop守护程序的环境配置中介绍了配置守护程序的内存选项。
框架某些部分可用的内存也是可配置的。在map和reduce任务中,并发性的操作和数据撞击磁盘频率的参数的调整可能会使性能受到影响。监视文件系统中Job的计数特别是相对于从map到reduce的字节数的记数相关参数的调整而言是无价的。
从map发出的数据将被序列化到缓冲区中,而元数据将被存储到accounting缓冲区中。如以下选项中所述,当序列化缓冲区或元数据超过阈值时,当map继续输出数据时,缓冲区的内容将被排序并在后台写入磁盘。如果在溢出过程中任一个缓冲区已完全填满,则map线程将阻塞。map过程完成后,所有剩余的数据都将写入到磁盘,并且所有磁盘上的数据片段都将合并到一个文件中。
Name | Type | Description |
---|---|---|
mapreduce.task.io.sort.mb | int | The cumulative size of the serialization and accounting buffers storing records emitted from the map, in megabytes. |
mapreduce.map.sort.spill.percent | float | The soft limit in the serialization buffer. Once reached, a thread will begin to spill the contents to disk in the background. |
其他注意事项:
mapreduce.map.sort.spill.percent
设置为0.33,并且在溢出运行时填充了缓冲区的其余部分,则下一个溢出将包括所有收集的数据或缓冲区的0.66,并且不会产生额外的泄漏。换句话说,阈值是定义触发器,而不是阻塞。如前所述,每个reduce都会将分区程序通过HTTP分配给它的输出提取到内存中,并定期将这些输出合并到磁盘上。如果打开了map输出的中间压缩,则将每个输出解压缩到内存中。以下选项会影响在还原之前这些合并到磁盘的频率以及在reduce期间分配给map输出的内存。
Name | Type | Description |
---|---|---|
mapreduce.task.io.soft.factor | int | Specifies the number of segments on disk to be merged at the same time. It limits the number of open files and compression codecs during merge. If the number of files exceeds this limit, the merge will proceed in several passes. Though this limit also applies to the map, most jobs should be configured so that hitting this limit is unlikely there. |
mapreduce.reduce.merge.inmem.thresholds | int | The number of sorted map outputs fetched into memory before being merged to disk. Like the spill thresholds in the preceding note, this is not defining a unit of partition, but a trigger. In practice, this is usually set very high (1000) or disabled (0), since merging in-memory segments is often less expensive than merging from disk (see notes following this table). This threshold influences only the frequency of in-memory merges during the shuffle. |
mapreduce.reduce.shuffle.merge.percent | float | The memory threshold for fetched map outputs before an in-memory merge is started, expressed as a percentage of memory allocated to storing map outputs in memory. Since map outputs that can’t fit in memory can be stalled, setting this high may decrease parallelism between the fetch and merge. Conversely, values as high as 1.0 have been effective for reduces whose input can fit entirely in memory. This parameter influences only the frequency of in-memory merges during the shuffle. |
mapreduce.reduce.shuffle.input.buffer.percent | float | The percentage of memory- relative to the maximum heapsize as typically specified in mapreduce.reduce.java.opts- that can be allocated to storing map outputs during the shuffle. Though some memory should be set aside for the framework, in general it is advantageous to set this high enough to store large and numerous map outputs. |
mapreduce.reduce.input.buffer.percent | float | The percentage of memory relative to the maximum heapsize in which map outputs may be retained during the reduce. When the reduce begins, map outputs will be merged to disk until those that remain are under the resource limit this defines. By default, all map outputs are merged to disk before the reduce begins to maximize the memory available to the reduce. For less memory-intensive reduces, this should be increased to avoid trips to disk. |
其他注意事项:
mapreduce.task.io.sort.factor
片段而需要进行中间合并,则内存中的map输出将成为中间合并的一部分。以下属性已在作业配置中本地化,以执行每个任务:
Name | Type | Description |
---|---|---|
mapreduce.job.id | String | The job id |
mapreduce.job.jar | String | job.jar location in job directory |
mapreduce.job.local.dir | String | The job specific shared scratch space |
mapreduce.task.id | String | The task id |
mapreduce.task.attempt.id | String | The task attempt id |
mapreduce.task.is.map | boolean | Is this a map task |
mapreduce.task.partition | int | The id of the task within the job |
mapreduce.map.input.file | String | The filename that the map is reading from |
mapreduce.map.input.start | long | The offset of the start of the map input split |
mapreduce.map.input.length | long | The number of bytes in the map input split |
mapreduce.task.output.dir | String | The task’s temporary output directory |
注意:在执行流作业期间,将转换“ mapreduce”参数的名称。点(.)变成下划线(_)。例如,mapreduce.job.id变为mapreduce_job_id,而mapreduce.job.jar变为mapreduce_job_jar。要在流作业的mapper和reducer中获取值,请在参数名称下加上下划线。
NodeManager读取标准输出(stdout)和错误(stderr)流以及任务的syslog,并将其记录到$ {HADOOP_LOG_DIR}/userlogs
中。
DistributedCache也可以用于分发jar和本地依赖库,以供在map任务或reduce任务中使用。child-jvm始终将其当前工作目录添加到java.library.path和LD_LIBRARY_PATH中。因此,可以通过System.loadLibrary或System.load加载缓存中的依赖库。本地库中记录了有关如何通过分布式缓存加载共享库的更多详细信息。
Job是用户作业与ResourceManager交互的主要接口。
Job提供了提交作业,跟踪其进度,访问组件任务的报告和日志,获取MapReduce集群的状态信息等功能。
作业提交过程涉及:
作业历史记录文件也记录到用户指定的目录mapreduce.jobhistory.intermediate-done-dir
和mapreduce.jobhistory.done-dir
,该目录默认为作业输出目录。
用户可以使用以下命令查看指定目录中的历史日志摘要:$ mapred job -history output.jhist
,这个命令将打印作业详细信息,失败和终止的提示详细信息。可以使用以下命令查看有关作业的更多详细信息,例如成功的任务和为每个任务进行的任务尝试,如下所示:$ mapred job -history all output.jhist
用户可能需要MapReduce作业链以完成无法通过单个MapReduce作业完成的复杂任务。这是相当容易的,因为作业的输出通常转到分布式文件系统,并且该输出又可以用作下一个作业的输入。
但是,这也意味着确保工作完成(成功/失败)的责任完全落在客户端身上。在这种情况下,各种作业控制选项是:
InputFormat描述了MapReduce作业的输入规范。
MapReduce框架依靠作业的InputFormat来:
基于文件的InputFormat实现(通常是FileInputFormat的子类)的默认行为是根据输入文件的总大小(以字节为单位)将输入拆分为逻辑InputSplit实例。但是,输入文件的FileSystem块大小被视为输入拆分的上限。可以通过mapreduce.input.fileinputformat.split.minsize
设置拆分大小的下限。
显然,对于许多应用程序而言,基于输入大小的逻辑拆分是不够的,因为必须遵守数据上下限边界。在这种情况下,应用程序应实现RecordReader这个接口,用来负责遵守数据上下限边界,并为单个任务提供逻辑InputSplit的面向数据的视图。
TextInputFormat是默认的InputFormat的实现。
如果TextInputFormat是给定作业的InputFormat的实现,则框架将检测带有.gz扩展名的输入文件,并使用适当的CompressionCodec自动将其解压缩。但是,必须注意,具有上述扩展名的压缩文件无法拆分,并且每个压缩文件均由单个mapper完整处理。
InputSplit表示要由单个Mapper处理的数据。
通常,InputSplit呈现输入的面向字节的视图,RecordReader负责处理和呈现面向数据的视图。
FileSplit是默认的InputSplit的实现。它将mapreduce.map.input.file
设置为逻辑拆分的输入文件的路径。
RecordReader从InputSplit读取
通常,RecordReader会转换InputSplit提供的输入的面向字节的视图,并将面向数据的形式呈现给Mapper实现以进行处理。因此,RecordReader承担处理数据边界的责任,并为任务提供键和值。
OutputFormat描述MapReduce作业的输出规范。
MapReduce框架依靠作业的OutputFormat来:
TextOutputFormat是OutputFormat接口的默认实现。
OutputCommitter描述了MapReduce作业的任务输出的提交。
MapReduce框架依赖于作业的OutputCommitter来:
FileOutputCommitter是OutputCommitter接口的默认实现。 作业设置或清除任务会占用map或reduce容器,无论哪个NodeManager可用。 并且JobCleanup任务,TaskCleanup任务和JobSetup任务具有最高优先级,并按照这个顺序进行执行。
在某些应用程序中,组件任务需要创建或写入附带文件,这些文件与实际的作业输出文件是不同的。
在这种情况下,试图同时打开或写入文件系统上同一文件(路径)的同一Mapper或Reducer的两个实例同时运行(例如,推测性任务)可能会出现问题。因此,应用程序编写者将不得不为每个尝试执行任务的用户选择唯一的名称(使用attemptid,例如try_200709221812_0001_m_000000_0),而不仅仅是每个任务。
为避免这些问题,当OutputCommitter
是FileOutputCommitter
时,MapReduce框架维护一个特殊的${mapreduce.output.fileoutputformat.outputdir}/_temporary/_${taskid}
子目录,可通过${mapreduce.task.output.dir}
访问对于存储任务尝试输出的文件系统上的每个任务尝试。成功完成任务尝试后,$ {mapreduce.output.fileoutputformat.outputdir}/_ temporary/_ $ {taskid}
(仅仅是)中的文件将升级为$ {mapreduce.output.fileoutputformat.outputdir}
。当然,该框架会丢弃尝试失败的子目录。此过程对应用程序完全透明。
注意:在执行特定任务尝试期间,${mapreduce.task.output.dir}
的值实际上是${mapreduce.output.fileoutputformat.outputdir}/_ temporary/_ {$ taskid}
,
该值由MapReduce框架设置。 因此,只需在MapReduce任务的FileOutputFormat.getWorkOutputPath(Conext)返回的路径中创建任何辅助文件,即可利用此功能。
整个讨论对于具有reducer = NONE(即0reduces)的作业的map都是正确的,因为在这种情况下,map的输出直接进入HDFS。
RecordWriter将输出
RecordWriter接口的实现将作业输出写入FileSystem。
用户将作业提交到队列。队列作为作业的集合,允许系统提供特定的功能。例如,队列使用ACL来控制哪些用户可以向其提交作业。队列预计主要由Hadoop Scheduler使用。
Hadoop配备了一个强制性队列,称为“default”。队列名称在Hadoop site configuration的mapreduce.job.queuename
属性中定义。某些作业调度程序(例如Capacity Scheduler)支持多个队列。
作业定义了需要通过mapreduce.job.queuename
属性或通过Configuration.set(MRJobConfig.QUEUE_NAME,String)API提交到的队列。设置队列名称是可选的。如果提交的作业没有关联的队列名称,则将其提交到“default”队列。
计数器代表由MapReduce框架或应用程序定义的全局计数器。每个记数器可以是任何Enum类型。特定Enum的计数器被分成Counters.Group类型的组。
应用程序可以定义任意计数器(类型为Enum),并通过map或reduce方法中的Counters.incrCounter(Enum,long)或Counters.incrCounter(String,String,long)更新它们。然后,这些计数器由框架全局汇总。
DistributedCache有效地分发特定于应用程序的大型只读文件。
DistributedCache是MapReduce框架提供的一种工具,用于缓存应用程序所需的文件(文本,档案,jars等)。
应用程序通过作业中的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/absolute-path#link-name
。在流式传输中,可以通过命令行选项-cacheFile/-cacheArchive分发文件。
DistributedCache文件可以是私有的也可以是公共的,这决定了如何在工作节点上共享它们。
Profiling是一种实用程序,用于获取代表性的(2或3个)内置Java分析器示例,以获取map和reduce的示例。
用户可以通过设置配置属性mapreduce.task.profile
来指定系统是否应收集作业中某些任务的分析器信息。可以使用api Configuration.set(MRJobConfig.TASK_PROFILE,boolean)
来设置该值。如果将该值设置为true,则启用任务分析。分析器信息存储在用户日志目录中。默认情况下,作业不会启用分析。
一旦用户配置了需要分析的用户,就可以使用配置属性mapreduce.task.profile.{maps|reduces}
设置要分析的MapReduce任务的范围。可以使用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=samples,heap=sites,force=n,thread=y,verbose=n,file=%s.
。
MapReduce框架提供了一种运行用户提供的脚本进行调试的功能。当MapReduce任务失败时,用户可以运行调试脚本来处理,例如任务日志。该脚本可以访问任务的stdout和stderr输出,syslog和jobconf。调试脚本的stdout和stderr的输出显示在诊断控制台中,也作为作业UI的一部分显示。
在以下各节中,我们讨论如何通过作业提交调试脚本。脚本文件需要分发并提交给框架。
用户需要使用DistributedCache来分发和链接到脚本文件。
提交调试脚本的一种快速方法是为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任务失败的节点上运行的debug命令是: $script $stdout $stderr $syslog $jobconf
管道程序将c ++程序名称作为命令的第五个参数。因此,对于管道程序,命令为$script $stdout $stderr $syslog $jobconf $program
对于管道,将运行默认脚本来处理gdb下的核心转储,打印堆栈跟踪并提供有关正在运行的线程的信息。
Hadoop MapReduce为应用程序编写器提供了便利,以便为中间map输出和作业输出(即reduces的输出)指定压缩。它还与zlib压缩算法的CompressionCodec实现捆绑在一起。还支持gzip,bzip2,snappy和lz4文件格式。
出于性能(zlib)和Java库不可用的原因,Hadoop还提供了上述压缩编解码器的本地实现。有关其用法和可用性的更多详细信息,请参见此处。
应用程序可以通过Configuration.set(MRJobConfig.MAP_OUTPUT_COMPRESS,boolean) api和通过Configuration.set(MRJobConfig.MAP_OUTPUT_COMPRESS_CODEC,Class) api使用的CompressionCodec来控制中间map输出的压缩。
应用程序可以通过FileOutputFormat.setCompressOutput(Job,boolean) api控制作业输出的压缩,可以通过FileOutputFormat.setOutputCompressorClass(Job,Class)api指定要使用的CompressionCodec。
如果作业输出要存储在SequenceFileOutputFormat中,则可以通过SequenceFileOutputFormat.setOutputCompressionType(Job,SequenceFile.CompressionType)api指定所需的SequenceFile.CompressionType (i.e. RECORD / BLOCK - defaults to RECORD)。
Hadoop提供了一个选项,可以在处理map输入时跳过某些不良输入数据集。应用程序可以通过SkipBadRecords类控制此功能。
当map任务在某些输入上确定性崩溃时,可以使用此功能。这通常是由于map函数中的错误引起的。通常,用户必须修复这些错误。但是,有时这是不可能的。该错误可能在第三方库中,例如,该第三方库的源代码不可用。在这种情况下,即使多次尝试,任务也永远无法成功完成,并且作业也会失败。使用此功能,仅丢失了坏数据周围的一小部分数据,这对于某些应用程序(例如那些对非常大的数据执行统计分析的应用程序)可以接受。
默认情况下,此功能处于禁用状态。要启用它,请参阅SkipBadRecords.setMapperMaxSkipRecords(Configuration,long)和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)和SkipBadRecords.setReducerMaxSkipGroups(Configuration,long)控制跳过的记录数。该框架尝试使用类似于二分搜索的方法来缩小跳过记录的范围。跳过的范围分为两半,只有一半被执行。在后续失败时,框架会找出其中一半包含不良数据。将重新执行任务,直到达到可接受的跳过值或用尽所有任务尝试为止。要增加任务尝试次数,请使用Job.setMaxMapAttempts(int)和Job.setMaxReduceAttempts(int)。
跳过的数据以序列文件格式写入HDFS,以供后续的分析。可以通过SkipBadRecords.setSkipOutputPath(JobConf,Path)更改位置。
这是一个更完整的WordCount,它使用了到目前为止我们讨论的MapReduce框架提供的许多功能。
这需要HDFS能够启动并运行,尤其是对于与DistributedCache相关的功能。因此,它仅适用于伪分布式或完全分布式Hadoop安装。
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 [-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
$ 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框架提供的一些功能对前一个版本进行了改进:
Java和JNI是Oracle America,Inc.在美国和其他国家/地区的商标或注册商标。