MapReduce概述
- 源自于Google的MapReduce论文,发表于2004年12月
- Hadoop MapReduce是Google MapReduce的克隆版
- MapReduce优点:海量数据的离线处理、易开发、易运行。
所谓海量数据,说明MapReduce可以处理的数据量非常大,离线处理说明MapReduce跟实时响应不同,用户将作业提交,系统按批次进行处理,由于数据量大,自然非常耗时。
所谓易开发,如果我们自己要实现这样的分布式应用程序的话需要考虑很多东西,比如如何对文件进行拆分、如何处理节点故障问题,MapReduce框架在内部已经封装好了这些问题,我们只要把重心关注在应用逻辑的处理上就行了。
所谓易运行,当然这个易运行是相对的。
- MapReduce缺点:无法满足实时流式计算
MapReduce的作业是非常耗时的,不可能实时处理数据;流式计算也是无法满足的,我们之前看过MapReduce的处理图,它的各个流程是由依赖关系的,也就是说如果map没做完,那么reduce也做不了。
Hadoop官网是这么介绍MapReduce框架的:
Overview
Hadoop MapReduce is a software framework for easily writing applications which process vast amounts of data (multi-terabyte data-sets) in-parallel on large clusters (thousands of nodes) of commodity hardware in a reliable, fault-tolerant manner.
Hadoop Mapreduce是一个易于编程并且能在大型集群(上千节点)快速地并行得处理大量数据的软件框架,以可靠,容错的方式部署在商用机器上。
A MapReduce job usually splits the input data-set into independent chunks which are processed by the map tasks in a completely parallel manner. The framework sorts the outputs of the maps, which are then input to the reduce tasks. Typically both the input and the output of the job are stored in a file-system. The framework takes care of scheduling tasks, monitoring them and re-executes the failed tasks.
MapReduce Job通常将输入的数据集切分成独立的切片,以完全并行(completely parallel)的方式在map任务中处理。该框架对maps输出的数据进行排序,这些排序后的数据将做为reduce任务的输入,一般Job的输入输出都是存储在文件系统中。该框架可以调度任务、监控任务和重启失效的任务。
Typically the compute nodes and the storage nodes are the same, that is, the MapReduce framework and the Hadoop Distributed File System are running on the same set of nodes. This configuration allows the framework to effectively schedule tasks on the nodes where data is already present, resulting in very high aggregate bandwidth across the cluster.
一般来说计算节点和存储节点都是同样的设置,MapReduce框架和HDFS运行在同组节点。这样的设定使得MapReduce框架能够在有数据的节点上有效地调度任务,这样可以在集群上实现高聚合(aggregate)的带宽(bandwidth)。
The MapReduce framework consists of a single master
ResourceManager
, one workerNodeManager
per cluster-node, andMRAppMaster
per application.
MapReduce 框架包含一个主ResourceManager,每个集群节点都有一个从NodeManager并且每个应用都有一个MRAppMaster。
Minimally, applications specify the input/output locations and supply map and reduce functions via implementations of appropriate interfaces and/or abstract-classes. These, and other job parameters, comprise the job configuration.
应用最少必须指定输入和输出的路径并且通过实现合适的接口或者抽象类来提供map和reduce功能。前面这部分内容和其他Job参数构成了Job的配置。
The Hadoop job client then submits the job (jar/executable etc.) and configuration to the
ResourceManager
which then assumes the responsibility of distributing the software/configuration to the workers, scheduling tasks and monitoring them, providing status and diagnostic information to the job-client.
Hadoop 客户端提交Job和配置信息给ResourceManger,它将负责把配置信息分配给从属节点,调度任务并且监控它们,把状态信息和诊断信息传输给客户端。
MapReduce编程模型
MapReduce执行步骤
在之前说明统计词频的例子中我们曾用到过这幅图:
可以很清楚的看到一个MapReduce作业会被拆成map阶段和reduce阶段,分别交给map task和reduce task进行处理。整个MapReduce的执行步骤从这张图中已经看得非常清晰了。接下来我们来看看官网文档中关于这部分的说明。
Inputs and Outputs
The MapReduce framework operates exclusively on
<key, value>
pairs, that is, the framework views the input to the job as a set of<key, value>
pairs and produces a set of<key, value>
pairs as the output of the job, conceivably of different types.
MapReduce 框架只操作键值对,MapReduce 将job的不同类型输入当做键值对来处理并且生成一组键值对作为输出。
The
key
andvalue
classes have to be serializable by the framework and hence need to implement the Writable interface. Additionally, the key classes have to implement the WritableComparable interface to facilitate sorting by the framework.
Key和Value类必须通过实现Writable接口来实现序列化。此外,Key类必须实现WritableComparable 来使得排序更简单。
至于什么是Writable接口和WritabelComparable接口,我们等会再说。
Input and Output types of a MapReduce job:
(input)
<k1, v1> ->
map-> <k2, v2> ->
combine-> <k2, v2> ->
reduce-> <k3, v3>
(output)
可以看出,在整个MapReduce阶段,整个过程中都是以键值对的方式进行数据传递的。
关于输入输出的介绍就看到这里,回到我们刚才提到的,什么是Writable接口?我们通过官网的链接点进去:
A serializable object which implements a simple, efficient, serialization protocol, based on DataInput and DataOutput.
这句话说,Writable接口是一个基于数据输入输出的实现的一个简单高效的序列化协议的接口。
这个接口中一共有两个方法:
void write(DataOutput out) throws IOException;
void readFields(DataInput input) throws IOException;
一个是写方法,将对象的字段序列化到out中,比如一个用户的字段可以有账号、密码、年龄等等。
另一个是读方法,把对象中的属性字段从in中反序列化出来。
官网还给了个例子:
public class MyWritable implements Writable {
// Some data
private int counter;
private long timestamp;
// Default constructor to allow (de)serialization
MyWritable() { }
public void write(DataOutput out) throws IOException {
out.writeInt(counter);
out.writeLong(timestamp);
}
public void readFields(DataInput in) throws IOException {
counter = in.readInt();
timestamp = in.readLong();
}
public static MyWritable read(DataInput in) throws IOException {
MyWritable w = new MyWritable();
w.readFields(in);
return w;
}
}
Writable接口看完了我们再来看看什么是WritableComparable接口。
WritableComparable接口是继承自Writable和Comparable接口的,Writable我们说过了,Comparable接口是干嘛的呢?其实这个接口主要用在排序中,比如定义了一个Student类,里面有一些姓名、年龄、成绩的属性,如果我要按照年龄给学生排序怎么办?解决办法就是让Student实现Comparable接口,而Comparable接口中有个compareTo方法,在Student中实现这个方法,将按年龄排序的逻辑写在这个方法中就可以对学生按照年龄进行排序了。
Writabel接口的描述是这样的:
WritableComparables can be compared to each other, typically via Comparators. Any type which is to be used as a key in the Hadoop Map-Reduce framework should implement this interface.
因为实现了Comparable接口,所以WriteComparable是可以相互比较的,一般是通过Comparators比较器进行比较。在MapReduce中会用到键值对的组合,所有的键(key)的都必须要实现这个接口,以便MapReduce可以根据键对键值对(key-value pairs)进行排序(后面运行过程的图中我们会看到有个sort的步骤)。
同样官网给我们提供了一个例子:
public class MyWritableComparable implements WritableComparable {
// Some data
private int counter;
private long timestamp;
public void write(DataOutput out) throws IOException {
out.writeInt(counter);
out.writeLong(timestamp);
}
public void readFields(DataInput in) throws IOException {
counter = in.readInt();
timestamp = in.readLong();
}
public int compareTo(MyWritableComparable o) {
int thisValue = this.value;
int thatValue = o.value;
return (thisValue < thatValue ? -1 : (thisValue==thatValue ? 0 : 1));
}
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + counter;
result = prime * result + (int) (timestamp ^ (timestamp >>> 32));
return result
}
}
MapReduce的运行过程
下面这张图展示了MapReduce在机器上的运行过程:
首先将文件从本地或者HDFS中读取出来,使用InputFormat将文件拆分成若干个splits,这个InputFormat又是个什么玩意儿?我们通过他的一个实现类TextInputFormat来更好的理解他,这个类的主要作用是处理文本型的文件。
TextInputFormat这个类中有这样的两个方法:
InputSplit[] getSplits(JobConf job, int numSplits) throws IOException;
这个方法的作用从命名就能看出来,他可以把输入文件拆分成一个InputSplit类型的数组,至于拆成几份可以用参数numSplits控制。
RecorderReader<K,V> getRecorderReader(InputSplit split, JobConf job, Reporter reporter) throws IOException;
这个方法可以得到一个RR(记录读取器),用于读取拆分后每个split的记录内容。
InputFormat将文件拆分成split后,再由RR把每个split上的数据读取出来,每读一行就交给一个map task进行处理,对于wordcount例子来说,经过map处理后得到的是每个单词出现的记录,下一步是将相同单词的记录扔到一台机器上去进行统计,这就是Partitioner要做的事情了。有可能会发到node2上,也有可能在本机上(node1)处理。排序后最终交由reduce处理,处理的结果由outputformat输出到本地文件系统或者HDFS上。
说到这里,我们会发现,上面提到的split是不是跟之前讨论的时候说到的block很相似,这两者是一样的东西么?
其实,split是交由MapReduce作业来处理的数据块,是MapReduce中最小的计算单元。而block的是HDFS中最小的存储单。默认情况下这两者是一一对应的,也就是说split的大小默认和hdfs的block块大小一致,当然也可以手动设置两者的关系,但是并不建议这么做。
如果设置过两者的对应关系,那么可能就会像下面这张图一样:
上面这张图就是一个block对应两个split,每个split交由一个mapper task进行处理。如果我们假设K1是文件的偏移量,那么V1就是每个偏移量对应的文件内容,比如K1取每行行首的偏移量,V1就是一行的文件内容。经过map阶段后,K2就是每个单词,V2就是这个单词出现的一条条记录,比如可能是["hello",{1,1,1,1,1}]
这种形式。把这样的数据交给reduce处理,得出来的就是hello出现的次数了。reduce处理完了后就会写到HDFS上去,因为有3个reduce task,因此会写三个文件。
MapReduce架构1.x
MapReduce1.x的框架的这张图我们在上一篇YARN的文章中已经见过了,现在我们来分析一下每个组件的具体功能。
- JobTracker(JT)
- 是作业的管理者。
- 将作业分解成一堆的任务(task),其中任务又分为map task和reduce task。
- 将任务分派给TaskTracker运行。
- 作业的监控、容错处理(task作业挂了,重启task的机制)。
- 在一定的时间间隔内,JT没有收到TT的心跳信息,那么说明TT很有可能是挂了,TT上运行的任务会被指派到其他TT上去执行。
- TaskTracker(TT)
- 任务的执行者。
- 在TT上运行task。
- 会与JT进行交互:执行/启动/作业,发送心跳信息给JT。
- map task
- 自己开发的map任务交由task进行处理。
- 解析每条记录的数据,交给自己的map方法处理。
- 将map的输出结果写到本地磁盘。
- redece task
- 将map task输出的数据进行读取。
- 按照数据进行分组传给编写的reduce方法处理。
- 输出结果写到HDFS。
MapReduce架构2.x
下面是MapReduce2.x的架构图,可以跟我们之前说YARN画的那张图做个比较,大体结构是一样的,只是把App Mstr换成了MapReduce App Mstr了,这儿也体现了我们之前说的YARN框架的通用性,只需要换个模块就可以直接运行其他的计算框架。
2.x的架构去掉了JobTracker和TaskTracker,用ResourceManager和NodeManager替代。Client的差别不大,Client先提交个请求给RM,RM启动一个NM,并在NM上启动MapReduce Application Master,然后App Mstr向RM提交资源申请,在获取到资源后给相应的NM发送请求并在相应的NM上启动map task和reduce task。