MapReduce是一个分布式计算框架,主要由两部分组成:编程模型和运行时环境。其中,编程模型为用户提供了非常易用的编程接口,用户只需要像编写串行程序一样实现几个简单的函数即可实现一个分布式程序,而其他比较复杂的工作,如节点间的通信、节点失效、数据切分等,全部由MapReduce运行时环境完成.
MapReduce设计目标,主要有以下几个:
易于编程:传统的分布式程序设计(如MPI)非常复杂,用户需要关注的细节非常多,比如数据分片、数据传输、节点间通信等,因而设计分布式程序的门槛非常高。
Hadoop的一个重要设计目标便是简化分布式程序设计,将所有并行程序均需要关注的设计细节抽象成公共模块并交由系统实现,而用户只需专注于自己的应用程序逻辑实现,这样简化了分布式程序设计且提高了开发效率。
良好的扩展性:随着业务的发展,积累的数据量(如搜索公司的网页量)会越来越大,当数据量增加到一定程度后,现有的集群可能已经无法满足其计算能力和存储能力,这时候管理员可能期望通过添加机器以达到线性扩展集群能力的目的。
高容错性:在分布式环境下,随着集群规模的增加,集群中的故障率(这里的“故障”包括磁盘损坏、机器宕机、节点间通信失败等硬件故障和坏数据或者用户程序bug产生的软件故障)会显著增加,进而导致任务失败和数据丢失的可能性增加。为此,Hadoop通过计算迁移或者数据迁移等策略提高集群的可用性与容错性。
从MapReduce自身的命名特点可以看出,MapReduce由两个阶段组成:Map和Reduce。用户只需编写map()和reduce()两个函数,即可完成简单的分布式程序的设计。
map()函数以key/value对作为输入,产生另外一系列key/value对作为中间输出写入本地磁盘。MapReduce框架会自动将这些中间数据按照key值进行聚集,且key值相同(用户可设定聚集策略,默认情况下是对key值进行哈希取模)的数据被统一交给reduce()函数处理。
reduce()函数以key及对应的value列表作为输入,经合并key相同的value值后,产生另外一系列key/value对作为最终输出写入HDFS。
下面以MapReduce中的“hello world”程序—WordCount为例介绍程序设计方法。
分布式处理框架也有自己的“hello world”程序:WordCount。它完成的功能是统计输入文件中的每个单词出现的次数。在MapReduce中,可以这样编写(伪代码)。
其中Map部分如下:
// key:字符串偏移量
// value:一行字符串内容
map(String key, String value) :
// 将字符串分割成单词
words = SplitIntoTokens(value);
for each word w in words:
EmitIntermediate(w, "1");
Reduce部分如下:
// key:一个单词
// values:该单词出现的次数列表
reduce(String key, Iterator values):
int result = 0;
for each v in values:
result += StringToInt(v);
Emit(key, IntToString(result));
编写完MapReduce程序后,按照一定的规则指定程序的输入和输出目录,并提交到Hadoop集群中。作业在Hadoop中的执行过程如图所示。Hadoop将输入数据切分成若干个输入分片(input split,后面简称split),并将每个split交给一个Map Task处理;
Map Task不断地从对应的split中解析出一个个key/value,并调用map()函数处理,处理完之后根据Reduce Task个数将结果分成若干个分片(partition)写到本地磁盘;同时,每个Reduce Task从每个Map Task上读取属于自己的那个partition,然后使用基于排序的方法将key相同的数据聚集在一起,调用reduce()函数处理,并将结果输出到文件中。
上面的程序还缺少三个基本的组件,功能分别是:①指定输入文件格式,将输入数据切分成若干个split,且将每个split中的数据解析成一个个map()函数要求的key/value对。②确定map()函数产生的每个key/value对发给哪个Reduce Task函数处理。③指定输出文件格式即每个key/value对以何种形式保存到输出文件中。
在Hadoop MapReduce中,这三个组件分别是InputFormat、Partitioner和OutputFormat,它们均需要用户根据自己的应用需求配置。而对于上面的WordCount例子,默认情况下Hadoop采用的默认实现正好可以满足要求,因而不必再提供。
综上,Hadoop MapReduce对外提供了5个可编程组件,分别是InputFormat、Mapper、Partitioner、Reducer、OutputFormat.
同HDFS一样,Hadoop MapReduce也采用了Master/Slave(M/S)架构,具体如图所示。它主要由以下几个组件组成:Client、JobTracker、 TaskTracker和Task。下面分别对
这几个组件进行介绍。
(1)Client
用户编写的MapReduce程序通过Client提交到JobTracker端;同时,用户可通过Client提供的一些接口查看作业运行状态。在Hadoop内部用“作业”(Job)表示MapReduce程序。一个MapReduce程序可对应若干个作业,而每个作业会被分解成若干个Map/Reduce任务(Task)。
(2)JobTracker
JobTracker主要负责资源监控和作业调度。JobTracker监控所有TaskTracker与作业的健康状况,一旦发现失败情况后,其会将相应的任务转移到其他节点;同时,JobTracker会跟踪任务的执行进度、资源使用量等信息,并将这些信息告诉任务调度器,而调度器会在资源出现空闲时,选择合适的任务使用这些资源。在Hadoop中,任务调度器是一个可插拔的模块,用户可以根据自己的需要设计相应的调度器。
(3)TaskTracker
TaskTracker会周期性地通过Heartbeat将本节点上资源的使用情况和任务的运行进度汇报给JobTracker,同时接收JobTracker发送过来的命令并执行相应的操作(如启动新任务、杀死任务等)。TaskTracker使用“slot”等量划分本节点上的资源量。“slot”代表计算资源(CPU、内存等)。一个Task获取到一个slot后才有机会运行,而Hadoop调度器的作用就是将各个TaskTracker上的空闲slot分配给Task使用。slot分为Map slot和Reduce slot两种,分别供Map Task和Reduce Task使用。TaskTracker通过slot数目(可配置参数)限定Task的并发度。
(4)Task
Task分为Map Task和Reduce Task两种,均由TaskTracker启动。HDFS以固定大小的block为基本单位存储数据,而对于MapReduce而言,其处理单位是split。split是一个逻辑概念,它只包含一些元数据信息,比如数据起始位置、数据长度、数据所在节点等。它的划分方法完全由用户自己决定。但需要注意的是,split的多少决定了Map Task的数目,因为每个split会交由一个Map Task处理。
Map Task执行过程如图所示。由下图可知,Map Task先将对应的split迭代解析成一个个key/value对,依次调用用户自定义的map()函数进行处理,最终将临时结果存放到本地磁盘上,其中临时数据被分成若干个partition,每个partition将被一个Reduce Task处理。
Reduce Task执行过程如下图所示。该过程分为三个阶段①从远程节点上读取MapTask中间结果(称为“Shuffle阶段”);②按照key对key/value对进行排序(称为“Sort阶段”);③依次读取
假设用户编写了一个MapReduce程序,并将其打包成xxx.jar文件,然后使用以下命
令提交作业:
$HADOOP_HOME/bin/hadoop jar xxx.jar \
-D mapred.job.name="xxx" \
-D mapred.map.tasks=3 \
-D mapred.reduce.tasks=2 \
-D input=/test/input \
-D output=/test/output
则该作业的运行过程如图所示:
这个过程分为以下5个步骤:
步骤1 作业提交与初始化。用户提交作业后,首先由JobClient实例将作业相关信
息,比如将程序jar包、作业配置文件、分片元信息文件等上传到分布式文件系统(一般为
HDFS)上,其中,分片元信息文件记录了每个输入分片的逻辑位置信息。然后JobClient
通过RPC通知JobTracker。JobTracker收到新作业提交请求后,由作业调度模块对作业进
行初始化:为作业创建一个JobInProgress对象以跟踪作业运行状况,而JobInProgress则会为每个Task创建一个TaskInProgress对象以跟踪每个任务的运行状态,TaskInProgress可能
需要管理多个“Task运行尝试”(称为“Task Attempt”)。
步骤2 任务调度与监控。前面提到,任务调度和监控的功能均由JobTracker完成。
TaskTracker周期性地通过Heartbeat向JobTracker汇报本节点的资源使用情况,一旦出现
空闲资源,JobTracker会按照一定的策略选择一个合适的任务使用该空闲资源,这由任务调
度器完成。任务调度器是一个可插拔的独立模块,且为双层架构,即首先选择作业,然后
从该作业中选择任务,其中,选择任务时需要重点考虑数据本地性。此外,JobTracker跟踪
作业的整个运行过程,并为作业的成功运行提供全方位的保障。首先,当TaskTracker或者
Task失败时,转移计算任务;其次,当某个Task执行进度远落后于同一作业的其他Task时,
为之启动一个相同Task,并选取计算快的Task结果作为最终结果。
步骤3 任务运行环境准备。运行环境准备包括JVM启动和资源隔离,均由
TaskTracker实现。TaskTracker为每个Task启动一个独立的JVM以避免不同Task在运行过程中相互影响;同时,TaskTracker使用了操作系统进程实现资源隔离以防止Task滥用资源。
步骤4 任务执行。TaskTracker为Task准备好运行环境后,便会启动Task。在运行过
程中,每个Task的最新进度首先由Task通过RPC汇报给TaskTracker,再由TaskTracker
汇报给JobTracker。
步骤5 作业完成。待所有Task执行完毕后,整个作业执行成功。