概述
Hadoop Map-Reduce是一个使用简易的软件框架,基于它写出来的应用程序能够运行在由上千个商用机器组成的大型集群上,并以一种可靠容错的方式并行处理上T级别的数据集。
一个Map-Reduce 作业(job) 通常会把输入的数据集切分为若干独立的数据块,由 map任务(task)以完全并行的方式处理它们。框架会对map的输出先进行排序,然后把结果输入给reduce任务。通常作业的输入和输出都会被存储在文件系统中。 整个框架负责任务的调度和监控,以及重新执行已经失败的任务。
通常,Map-Reduce框架和分布式文件系统是运行在一组相同的节点上的,也就是说,计算节点和存储节点通常在一起。这种配置允许框架在那些 已经存好数据的节点上高效地调度任务,这可以使整个集群的网络带宽被非常高效地利用。
Map-Reduce框架由单独一个master JobTracker 和每个集群节点一个 slave TaskTracker共同组成。这个master负责调度构成一个作业的所有任务, 这些任务分布在不同的slave上,master监控它们的执行,重新执行已经失败的任务。而slave 仅负责执行由master指派的任务。
应用程序至少应该指明输入/输出的位置(路径),并通过实现合适的接口或抽象类提供map和reduce函数。再加上其他作业的参数,就构成了作业配置(job configuration)。然后,Hadoop的job client 提交作业(jar包/可执行程序等)和配置信息给JobTracker,后者负责分发这些软件和配置信息 给slave、调度任务且监控它们的执行,同时提供状态和诊断信息给job-client。
虽然Hadoop框架是用JavaTM实现的,但Map-Reduce应用程序则不一定要用 Java来写 。
· Hadoop Streaming是一种运行作业的实用工具,它允许用户创建和运行任何可执行程序 (例如:Shell工具)来做为mapper和reducer。
· Hadoop Pipes是一个与SWIG兼容的C++ API (没有基于JNITM技术),它也可用于实现Map-Reduce应用程序。
Map-Reduce框架运转在<key, value> 键值对上,也就是说, 框架把作业的输入看为是一组<key, value> 键值对,同样也产出一组 <key, value> 键值对做为作业的输出,这两组键值对的类型可能不同。
框架需要对key和value的类(classes)进行序列化操作,因此,这些类需要实现 Writable接口。另外,为了方便框架执行排序操作,key类必须实现 WritableComparable接口。
一个Map-Reduce 作业的输入和输出类型如下所示:
(input) <k1, v1> -> map -> <k2, v2> -> combine -> <k2, v2> -> reduce -> <k3, v3> (output)
作业配置
老接口JobConf代表一个Map-Reduce作业的配置。JobConf的功能已被新的类Configuration和Job替换。Configuration类描述了资源,这些资源大多都是从XML配置文件中读取的属性和值组成。比如来自core-default.xml和core-site.xml。Job描述了用户角度的视图,它允许用户配置、提交、控制它的执行和查询状态。只有在作业提交之后才能使用set方法。
setNumReduceTasks() 设置Reduce的任务数;
setJarByClass() 设置Jar包的类来源;
setMapperClass() 设置实现的Mapper类;
setCombinerClass() 设置实现的Combiner类;
setReducerClass() 设置实现的Reducer类;
setInputFormatClass() 设置实现的InputFormat类;
setOutputKeyClass() 设置最终的输出key类;
setOutputValueClass() 设置最终的输出Value类。
默认情况下使用的是FileInputFormat类作为InputFormat类。
MapReduce编程接口
参数 | 作用 | 缺省值 | 其它实现 |
InputFormat | 将输入的数据集切割成小数据集 InputSplits, 每一个 InputSplit 将由一个 Mapper 负责处理。此外 InputFormat 中还提供一个 RecordReader 的实现, 将一个 InputSplit 解析成 <key,value> 对提供给 map 函数。 | TextInputFormat | SequenceFileInputFormat |
OutputFormat | 提供一个 RecordWriter 的实现,负责输出最终结果 | TextOutputFormat | SequenceFileOutputFormat |
OutputKeyClass | 输出的最终结果中 key 的类型 | LongWritable |
|
OutputValueClass | 输出的最终结果中 value 的类型 | Text |
|
MapperClass | Mapper 类,实现 map 函数,完成输入的 <key,value> 到中间结果的映射 | IdentityMapper | LongSumReducer, |
CombinerClass | 实现 combine 函数,将中间结果中的重复 key 做合并 | null |
|
ReducerClass | Reducer 类,实现 reduce 函数,对中间结果做合并,形成最终结果 | IdentityReducer | AccumulatingReducer, LongSumReducer |
InputPath | 设定 job 的输入目录, job 运行时会处理输入目录下的所有文件 | null |
|
OutputPath | 设定 job 的输出目录,job 的最终结果会写入输出目录下 | null |
|
MapOutputKeyClass | 设定 map 函数输出的中间结果中 key 的类型 | 如果用户没有设定的话,使用 OutputKeyClass |
|
MapOutputValueClass | 设定 map 函数输出的中间结果中 value 的类型 | 如果用户没有设定的话,使用 OutputValuesClass |
|
OutputKeyComparator | 对结果中的 key 进行排序时的使用的比较器 | WritableComparable |
|
PartitionerClass | 对中间结果的 key 排序后,用此 Partition 函数将其划分为R份,每份由一个 Reducer 负责处理。 | HashPartitioner | KeyFieldBasedPartitioner PipesPartitioner |
在这里,我们重点讨论如何去实现InputFormat、InputSplit、RecordReader、Mapper和Reducer类。新接口可在org.apache.hadoop.mapreduce.*名字空间下找到。
相关类的工作流程:(1)首先是InputFormat类,该类的getSplits()方法生成元素为InputSplit的List,这个类是在Job提交的时候被执行的,并且createRecordReader()方法创建相应的读记录类RecordReader;(2)对于每个InputSplit都由一个RecordReader类来处理,产生<K, V>对,然后将结果交由Mapper类来处理,一个InputSplit对应于一个RecordReader和一个Mapper;(3)通过RecordReader类中的nextKeyValue()的迭代,每个RecordReader产生的<K, V>对,执行一次Mapper类中的map();(4)Mapper产生的<K, V>对经过组合形成<K, List of V>, 然后Reducer的reduce()对这样的组合进行操作,并产生新的<K’, V’>对,有R个Reducer就对所有的<K, List of V>分成R份,每份将产生一个输出,由OutputFormat类来处理。
InputFormat<K, V>
用户可以定制自己的InputFormat类,也就是实现一个InputFormat子类,需要注意的是K类型必须实现WritableComparable接口,V类型必须实现Writable接口。对于文件处理可以继承FileInputFormat类,再细一点,如果文本文件,可以直接使用TextInputFormat类,如果二进制文件,可以直接使用SequenceFileInputFormat类。对于InputFormat的子类,必须实现public List<InputSplit> getSplits(JobContext context)方法和public RecordReader<K, V> createRecordReader(InputSplit split, TaskAttemptContext context)方法。这里牵涉到另外四个类InputSplit、RecordReader、JobContext和TaskAttemptContext。JobContext是运行期的作业上下文,TaskAttemptContext是运行期的尝试任务上下文。
InputSplit
用户可以定制自己的InputSplit类,也就是实现一个InputSplit子类,并且实现Writable接口。InputSplit子类必须实现getLength()和String[] getLocation()方法,前者获取当前Split的长度,后者获取Split分布的主机。当然还必须实现Writable接口的readFields()和write()。对于文件类型的数据,可以使用FileSplit,该类是以不超过分布在主机的单个块的大小为Split块大小。
RecordReader<K, V>
用户可以定制自己的RecordReader类,也就是实现一个RecordReader子类。RecordReader子类必须实现void initialize(InputSplit split, TaskAttemptContext context),boolean nextKeyValue(), K getCurrentKey(), V getCurrentValue(), float getProgress()和void close()。在initialize()函数的参数split就是上面的InputSplit子类,这类面最关键的就是nextKeyValue()函数,它对当前split中的Key和Value进行迭代查找,对于每个<K, V>执行一次Mapper类的Map()函数。对于文本文件,可以使用LineRecordReader类,该类按行来读取split块中的内容。
Mapper<KIN, VIN, KOUT, VOUT>
用户可以定制自己的Mapper类,也就是实现一个Mapper子类。Mapper子类必须实现void map(K, V, Context)方法,每处理一个<K, V>,交由Context类来处理,一般情况下有Context.write()来写结果。
Reducer<KIN, VIN, KOUT, VOUT>
用户可以定制自己的Reducer类,也就是实现一个Reducer子类。Reducer子类必须实现void reduce(K, V, Iterable<V> values, Context context)方法,每处理一个<K, List of V>,交由Context类来处理,一般情况下有Context.write()来写结果。