在了解MapReduce之前,需要了解批处理的概念,批处理模式是一种最早进行大规模数据处理的模式。批处理主要操作大规模静态数据集,并在整体数据处理完毕后返回结果。批处理非常适合需要访问整个数据集合才能完成的计算工作。
例如,在计算总数和平均数时,必须将数据集作为一个整体加以处理,而不能将其视作多条记录的集合。这些操作要求在计算进行过程中数据维持自己的状态。
需要处理大量数据的任务通常最适合用批处理模式进行处理,批处理系统在设计过程中就充分考虑了数据的量,可提供充足的处理资源。
由于批处理在应对大量持久数据方面的表现极为出色,因此经常被用于对历史数据进行分析。为了提高处理效率,对大规模数据集进行批处理需要借助分布式并行程序。
传统的程序基本是以单指令、单数据流的方式按顺序执行的。这种程序开发起来比较简单,符合人们的思维习惯,但是性能会受到单台计算机的性能的限制,很难在给定的时间内完成任务。而分布式并行程序运行在大量计算机组成的集群上,可以同时利用多台计算机并发完成同一个数据处理任务,提高了处理效率,同时,可以通过增加新的计算机扩充集群的计算能力。
Google 最先实现了分布式并行处理模式 MapReduce,并于 2004 年以论文的方式对外公布了其工作原理,Hadoop MapReduce 是它的开源实现。Hadoop MapReduce 运行在 HDFS 上。
MapReduce是一个分布式运算程序的编程框架,是用户开发“基于Hadoop的数据分析应用”的核心框架。
MapReduce核心功能是将用户编写的业务逻辑代码和自带默认组件整合成一个完整的分布式运算程序,并发运行在一个Hadoop集群上。
举例: 如果我们想知道相当厚的一摞牌中有多少张红桃,最直观的方式就是一张张检查这些牌,并且数出有多少张是红桃,最终数完这一摞牌后得出结果;这种方法的缺陷是速度太慢,特别是在牌的数量特别高的情况下,比如有两万张牌,一张张数过去获取结果的时间会很长。
按照MapReduce 方法的规则如下。
显而易见,MapReduce 方法通过让所有玩家同时并行检查牌来找出一摞牌中有多少红桃,可以大大加快得到答案的速度。
MapReduce 方法使用了拆分任务的思想,合并结果两种经典函数。
1)映射(Map)
对集合中的每个元素进行同一个操作。如果想把表单里每个单元格乘以二,那么把这个函数单独地应用在每个单元格上的操作就属于映射(Map)。
上述例子中对集合中的每个元素进行同一个操作即对每张牌都在判断是否是红桃
2)化简(Reduce)
遍历集合中的元素来返回一个综合的结果。如果想找出表单里所有数字的总和,那么输出表单里一列数字的总和的任务就属于化简(Reduce)。
下面使用 MapReduce 数据分析的基本方法来重新审视前面分散纸牌找出红桃总数的例子。在这个例子里,玩家代表计算机,因为他们同时工作,所以他们是个集群。
把牌分给多个玩家并且让他们各自数数,就是在并行执行运算,此时每个玩家都在同时计数。这就把这项工作变成了分布式的,因为多个不同的人在解决同一个问题的过程中并不需要知道他们的邻居在干什么。
告诉每个人去数数,实际上就是对一项检查每张牌的任务进行了映射。不是让玩家把红桃牌递回来,而是让他们把想要的东西化简为一个数字。
需要注意的是牌分配得是否均匀。如果某个玩家分到的牌远多于其他玩家,那么他数牌的过程可能比其他人要慢很多,从而会影响整个数牌的进度。
进一步还可以问一些更有趣的问题,例如,“一摞牌的平均值是什么?”。我们可以通过合并“所有牌的值的和是什么?”及“我们有多少张牌?”这两个问题来得到答案。用这个“和”除以“牌的张数”就得到了平均值。
MapReduce 算法的机制要远比数牌复杂得多,但是主体思想是一致的,即通过分散计算来分析大量数据。无论是 Google、百度、腾讯、NASA,还是小创业公司,MapReduce 都是目前分析互联网级别数据的主流方法。
1.MapReduce 易于编程
它简单的实现一些接口,就可以完成一个分布式程序,这个分布式程序可以分布到大量廉价的PC机器上运行。也就是说你写一个分布式程序,跟写一个简单的串行程序是一模一样的。就是因为这个特点使得MapReduce编程变得非常流行。
2.良好的扩展性
当你的计算资源不能得到满足的时候,你可以通过简单的增加机器来扩展它的计算能力。
3.高容错性
MapReduce设计的初衷就是使程序能够部署在廉价的PC机器上,这就要求它具有很高的容错性。比如其中一台机器挂了,它可以把上面的计算任务转移到另外一个节点上运行,不至于这个任务运行失败,而且这个过程不需要人工参与,而完全是由Hadoop内部完成的。
4.适合PB级以上海量数据的离线处理
可以实现上千台服务器集群并发工作,提供数据处理能力。
不擅长实时计算
MapReduce无法像MySQL一样,在毫秒或者秒级内返回结果。
不擅长流式计算
流式计算的输入数据是动态的,而MapReduce的输入数据集是静态的,不能动态变化。这是因为MapReduce自身的设计特点决定了数据源必须是静态的。
不擅长DAG(有向图)计算
多个应用程序存在依赖关系,后一个应用程序的输入为前一个的输出。在这种情况下,MapReduce并不是不能做,而是使用后,每个MapReduce作业的输出结果都会写入到磁盘,会造成大量的磁盘IO,导致性能非常的低下。
使用 MapReduce 处理大数据的基本思想包括 3 个层面。首先,对大数据采取分而治之的思想。对相互间不具有计算依赖关系的大数据实现并行处理,最自然的办法就是采取分而治之的策略。
其次,把分而治之的思想上升到抽象模型。为了克服 MPI 等并行计算方法缺少高层并行编程模型这一缺陷,MapReduce 借鉴了 Lisp 函数式语言中的思想,用 Map 和 Reduce 两个函数提供了高层的并行编程抽象模型。
最后,把分而治之的思想上升到架构层面,统一架构为程序员隐藏系统层的实现细节。
MPI 等并行计算方法缺少统一的计算框架支持,程序员需要考虑数据存储、划分、分发、结果收集、错误恢复等诸多细节,为此,MapReduce 设计并提供了统一的计算框架,为程序员隐藏了绝大多数系统层面的处理细节。
1. 大数据处理思想:分而治之
并行计算的第一个重要问题是如何划分计算任务或者计算数据以便对划分的子任务或数据块同时进行计算。但是,一些计算问题的前后数据项之间存在很强的依赖关系,无法进行划分,只能串行计算。
对于不可拆分的计算任务或相互间有依赖关系的数据无法进行并行计算。一个大数据若可以分为具有同样计算过程的数据块,并且这些数据块之间不存在数据依赖关系,则提高处理速度的最好办法就是并行计算。
例如,假设有一个巨大的 2 维数据,大得无法同时放进一个计算机的内存,如图 2 所示,现在需要求每个元素的开立方。因为对每个元素的处理是相同的,并且数据元素间不存在数据依赖关系,因此可以考虑将其划分为子数组,由一组计算机并行处理。
2. 构建抽象模型:Map 函数和 Reduce 函数
Lisp 函数式程序设计语言是一种列表处理语言。Lisp定义了可对列表元素进行整体处理的 各种操作。例如,(add #(1 2 3 4) #(4 3 2 1)) 产生的结果为 #(5 5 5 5)。
Lisp 中还提供了类似于 Map 函数和 Reduce 函数的操作。如:
Map 函数对一组数据元素进行某种重复式的处理,Reduce 函数对 Map 函数的中间结果进行某种进一步的结果整理。
MapReduce 通过借鉴 Lisp 的思想,定义了 Map 和 Reduce 两个抽象的编程接口,为程序员提供了一个清晰的操作接口抽象描述,由用户去编程实现。
1) Map:
输入:键值对
处理:数据记录将以“键值对”形式传入 Map 函数;Map 函数将处理这些键值对,并以另一种键值对形式输出中间结果 List(
输出:键值对List(
2) Reduce:
输入:由 Map 输出的一组键值对 List(
处理:对传入的中间结果列表数据进行某种整理或进一步的处理,并产生最终的输出结果List(
输出:最终输出结果List(
基于 MapReduce 的并行计算模型如图 3 所示。各个 Map 函数对所划分的数据并行处理,从不同的输入数据产生不同的中间结果。
各个 Reduce 函数也各自并行计算,负责处理不同的中间结果。进行 Reduce 函数处理之前,必须等到所有的 Map 函数完成。
因此,在进入 Reduce 函数前需要有一个同步屏障;这个阶段也负责对 Map 函数的中间结果数据进行收集整理处理,以便 Reduce 函数能更有效地计算最终结果,最终汇总所有 Reduce 函数的输出结果即可获得最终结果。
3)上升到架构:并行自动化并隐藏底层细节
MapReduce 提供了一个统一的计算框架,来完成计算任务的划分和调度,数据的分布存储和划分,处理数据与计算任务的同步,结果数据的收集整理,系统通信、负载平衡、计算性能优化、系统结点出错检测和失效恢复处理等。
MapReduce 通过抽象模型和计算框架把需要做什么与具体怎么做分开了,为程序员提供了一个抽象和高层的编程接口和框架,程序员仅需要关心其应用层的具体计算问题,仅需编写少量的处理应用本身计算问题的程序代码。
与具体完成并行计算任务相关的诸多系统层细节被隐藏起来,交给计算框架去处理:从分布代码的执行,到大到数千个,小到单个的结点集群的自动调度使用。
MapReduce 计算架构提供的主要功能包括以下几点。
1)任务调度
提交的一个计算作业(Job)将被划分为很多个计算任务(Tasks)。
任务调度功能主要负责为这些划分后的计算任务分配和调度计算结点(Map 结点或 Reduce 结点),同时负责监控这些结点的执行状态,以及 Map 结点执行的同步控制,也负责进行一些计算性能优化处理。例如,对最慢的计算任务采用多备份执行,选最快完成者作为结果。
2)数据/程序互定位
为了减少数据通信量,一个基本原则是本地化数据处理,即一个计算结点尽可能处理其本地磁盘上分布存储的数据,这实现了代码向数据的迁移。
当无法进行这种本地化数据处理时,再寻找其他可用结点并将数据从网络上传送给该结点(数据向代码迁移),但将尽可能从数据所在的本地机架上寻找可用结点以减少通信延迟。
3)出错处理
在以低端商用服务器构成的大规模 MapReduce 计算集群中,结点硬件(主机、兹盘、内存等)出错和软件有缺陷是常态。因此,MapReduce 架构需要能检测并隔离出错结点,并调度分配新的结点接管出错结点的计算任务。
4)分布式数据存储与文件管理
海量数据处理需要一个良好的分布数据存储和文件管理系统作为支撑,该系统能够把海量数据分布存储在各个结点的本地磁盘上,但保持整个数据在逻辑上成为一个完整的数据文件。
为了提供数据存储容错机制,该系统还要提供数据块的多备份存储管理能力。
5)Combiner 和 Partitioner
为了减少数据通信开销,中间结果数据进入 Reduce 结点前需要进行合并(Combine)处理,即把具有同样主键的数据合并到一起避免重复传送。
一个 Reduce 结点所处理的数据可能会来自多个 Map 结点,因此,Map 结点输出的中间结果需使用一定的策略进行适当的划分(Partition)处理,保证相关数据发送到同一个 Reduce 结点上。
2)第一个阶段的MapTask并发实例,完全并行运行,互不相干。
3)第二个阶段的ReduceTask并发实例互不相干,但是他们的数据依赖于上一个阶段的所有MapTask并发实例的输出。
4)MapReduce编程模型只能包含一个Map阶段和一个Reduce阶段,如果用户的业务逻辑非常复杂,那就只能多个MapReduce程序,串行运行。
一个完整的MapReduce程序在分布式运行时有三类实例进程:
1)MrAppMaster:
负责整个程序的过程调度及状态协调。
2)MapTask:
负责Map阶段的整个数据处理流程。
3)ReduceTask:
负责Reduce阶段的整个数据处理流程。
用户编写的 MapReduce 程序通过 JobClient 提交给 JobTracker。
JobTracker 主要负责资源监控和作业调度,并且监控所有 TaskTracker 与作业的健康情况,一旦有失败情况发生,就会将相应的任务分配到其他结点上去执行。
TaskTraker 会周期性地将本结点的资源使用情况和任务进度汇报给 JobTracker,与此同时会接收 JobTracker 发送过来的命令并执行操作。
Task 分为 Map Task 和 Reduce Task 两种,由 TaskTracker 启动,分别执行 Map 和 Reduce 任务。一般来讲,每个结点可以运行多个 Map 和 Reduce 任务。
在 Input 阶段,框架根据数据的存储位置,把数据分成多个分片(Splk),在多个结点上并行处理。
Map 任务通常运行在数据存储的结点上,也就是说,框架是根据数据分片的位置来启动 Map 任务的,而不是把数据传输到 Map 任务的位置上。这样,计算和数据就在同一个结点上,从而不需要额外的数据传输开销。
在 Map 阶段,框架调用 Map 函数对输入的每一个
在 Sort 阶段,当 Map 任务结束以后,会生成许多
在 Combine 阶段,框架对于在 Sort 阶段排序之后有相同键的中间结果进行合并。合并所使用的函数可以由用户进行定义。在图 1 中,就是把 K2 相同(也就是同一个字母)的 V2 值相加的。这样,在每一个 Map 任务的中间结果中,每一个字母只会出现一次。
在 Partition 阶段,框架将 Combine 后的中间结果按照键的取值范围划分为 R 份,分别发给 R 个运行 Reduce 任务的结点,并行执行。分发的原则是,首先必须保证同一个键的所有数据项发送给同一个 Reduce 任务,尽量保证每个 Reduce 任务所处理的数据量基本相同。
在图 1 中,框架把字母 a、b、c 的键值对分别发给了 3 个 Reduce 任务。框架默认使用 Hash 函数进行分发,用户也可以提供自己的分发函数。
在 Reduce 阶段,每个 Reduce 任务对 Map 函数处理的结果按照用户定义的 Reduce 函数进行汇总计算,从而得到最后的结果。在图 1 中,Reduce 计算每个字母在整个文件中出现的次数。只有当所有 Map 处理过程全部结束以后,Reduce 过程才能开始。
在 Output 阶段,框架把 Reduce 处理的结果按照用户指定的输出数据格式写入 HDFS 中。
在 MapReduce 的整个处理过程中,不同的 Map 任务之间不会进行任何通信,不同的 Reduce 任务之间也不会发生任何信息交换。用户不能够显式地从一个结点向另一个结点发送消息,所有的信息交换都是通过 MapReduce 框架实现的。
MapReduce 计算模型实现数据处理时,应用程序开发者只需要负责 Map 函数和 Reduce 函数的实现。MapReduce 计算模型之所以得到如此广泛的应用就是因为应用开发者不需要处理分布式和并行编程中的各种复杂问题。
单词计数是最简单也是最能体现 MapReduce 思想的程序之一,可以称为 MapReduce 版“Hello World”。单词计数的主要功能是统计一系列文本文件中每个单词出现的次数。本节通过单词计数实例来阐述采用 MapReduce 解决实际问题的基本思路和具体实现过程。
首先,检查单词计数是否可以使用 MapReduce 进行处理。因为在单词计数程序任务中,不同单词的出现次数之间不存在相关性,相互独立,所以,可以把不同的单词分发给不同的机器进行并行处理。因此,可以采用 MapReduce 来实现单词计数的统计任务。
其次,确定 MapReduce 程序的设计思路。把文件内容分解成许多个单词,然后把所有相同的单词聚集到一起,计算出每个单词出现的次数。
最后,确定 MapReduce 程序的执行过程。把一个大的文件切分成许多个分片,将每个分片输入到不同结点上形成不同的 Map 任务。每个 Map 任务分别负责完成从不同的文件块中解析出所有的单词。
Map 函数的输入采用
Map 阶段结束以后,会输出许多 <单词,1> 形式的中间结果,然后 Sort 会把这些中间结果进行排序并把同一单词的出现次数合并成一个列表,得到
如果使用 Combine,那么 Combine 会把每个单词的 List(value) 值进行合并,得到
在 Partition 阶段,会把 Combine 的结果分发给不同的 Reduce 任务。Reduce 任务接收到所有分配给自己的中间结果以后,就开始执行汇总计算工作,计算得到每个单词出现的次数并把结果输出到 HDFS 中。
下面通过一个实例对单词计数进行更详细的讲解。
1)将文件拆分成多个分片。
该实例把文件拆分成两个分片,每个分片包含两行内容。在该作业中,有两个执行 Map 任务的结点和一个执行 Reduce 任务的结点。每个分片分配给一个 Map 结点,并将文件按行分割形成
2)将分割好的
图 1 分割过程
图 2 执行Map函数
**3)**在实际应用中,每个输入分片在经过 Map 函数分解以后都会生成大量类似
**4)**Reduce 先对从 Map 端接收的数据进行排序,再交由用户自定义的 Reduce 方法进行处理,得到新的
图 4 Reduce 端排序及输出结果
整个 Hadoop MapReduce 的作业执行流程如图 所示,共分为 10 步。
Hadoop MapReduce的作业执行流程
客户端向 JobTracker 提交作业。首先,用户需要将所有应该配置的参数根据需求配置好。作业提交之后,就会进入自动化执行。在这个过程中,用户只能监控程序的执行情况和强制中断作业,但是不能对作业的执行过程进行任何干预。提交作业的基本过程如下。
**1)**客户端通过 Runjob() 方法启动作业提交过程。
**2)**客户端通过 JobTracker 的 getNewJobId() 请求一个新的作业 ID。
**3)**客户端检查作业的输出说明,计算作业的输入分片等,如果有问题,就抛出异常,如果正常,就将运行作业所需的资源(如作业 Jar 文件,配置文件,计算所得的输入分片等)复制到一个以作业 ID 命名的目录中。
**4)**通过调用 JobTracker 的 submitjob() 方法告知作业准备执行。
JobTracker 在 JobTracker 端开始初始化工作,包括在其内存里建立一系列数据结构,来记录这个 Job 的运行情况。
**1)**JobTracker 接收到对其 submitJob() 方法的调用后,就会把这个调用放入一个内部队列中,交由作业调度器进行调度。初始化主要是创建一个表示正在运行作业的对象,以便跟踪任务的状态和进程。
**2)**为了创建任务运行列表,作业调度器首先从 HDFS 中获取 JobClient 已计算好的输入分片信息,然后为每个分片创建一个 MapTask,并且创建 ReduceTask。
JobTracker 会向 HDFS 的 NameNode 询问有关数据在哪些文件里面,这些文件分别散落在哪些结点里面。JobTracker 需要按照“就近运行”原则分配任务。
TaskTracker 定期通过“心跳”与 JobTracker 进行通信,主要是告知 JobTracker 自身是否还存活,以及是否已经准备好运行新的任务等。
JobTracker 接收到心跳信息后,如果有待分配的任务,就会为 TaskTracker 分配一个任务,并将分配信息封装在心跳通信的返回值中返回给 TaskTracker。
对于 Map 任务,JobTracker 通常会选取一个距离其输入分片最近的 TaskTracker,对于 Reduce 任务,JobTracker 则无法考虑数据的本地化。
**1)**TaskTracker 分配到一个任务后,通过 HDFS 把作业的 Jar 文件复制到 TaskTracker 所在的文件系统,同时,TaskTracker 将应用程序所需要的全部文件从分布式缓存复制到本地磁盘。TaskTracker 为任务新建一个本地工作目录,并把 Jar 文件中的内容解压到这个文件夹中。
**2)**TaskTracker 启动一个新的 JVM 来运行每个任务(包括 Map 任务和 Reduce 任务),这样,JobClient 的 MapReduce 就不会影响 TaskTracker 守护进程。任务的子进程每隔几秒便告知父进程它的进度,直到任务完成。
一个作业和它的每个任务都有一个状态信息,包括作业或任务的运行状态,Map 任务和 Reduce 任务的进度,计数器值,状态消息或描述。任务在运行时,对其进度保持追踪。
这些消息通过一定的时间间隔由 ChildJVM 向 TaskTracker 汇聚,然后再向 JobTracker 汇聚。JobTracker 将产生一个表明所有运行作业及其任务状态的全局视图,用户可以通过 Web UI 进行查看。JobClient 通过每秒查询 JobTracker 来获得最新状态,并且输出到控制台上。
当 JobTracker 接收到的这次作业的最后一个任务已经完成时,它会将 Job 的状态改为“successful”。当 JobClient 获取到作业的状态时,就知道该作业已经成功完成,然后 JobClient 打印信息告知用户作业已成功结束,最后从 Runjob() 方法返回。
在Hadoop这样的集群环境中,大部分map task与reduce task的执行是在不同的节点上。当然很多情况下Reduce执行时需要跨节点去拉取其它节点上的map task结果。如果集群正在运行的job有很多,那么task的正常执行对集群内部的网络资源消耗会很严重。这种网络消耗是正常的,我们不能限制,能做的就是最大化地减少不必要的消耗。还有在节点内,相比于内存,磁盘IO对job完成时间的影响也是可观的。从最基本的要求来说,我们对Shuffle过程的期望可以有:
Hadoop MapReduce 的 Shuffle 阶段是指从 Map 的输出开始,包括系统执行排序,以及传送 Map 输出到 Reduce 作为输入的过程。
排序阶段是指对 Map 端输出的 Key 进行排序的过程。不同的 Map 可能输出相同的 Key,相同的 Key 必须发送到同一个 Reduce 端处理。Shuffle 阶段可以分为 Map 端的 Shuffle 阶段和 Reduce 端的 Shuffle 阶段。Shuffle 阶段的工作过程,如图 2 所示。
**1)**每个输入分片会让一个 Map 任务来处理,默认情况下,以 HDFS 的一个块的大小(默认为 64MB)为一个分片。Map 函数开始产生输出时,并不是简单地把数据写到磁盘中,因为频繁的磁盘操作会导致性能严重下降。它的处理过程是把数据首先写到内存中的一个缓冲区, 并做一些预排序,以提升效率。
**2)**每个 Map 任务都有一个用来写入输出数据的循环内存缓冲区(默认大小为 100MB),当缓冲区中的数据量达到一个特定阈值(默认是 80%)时,系统将会启动一个后台线程,把缓冲 区中的内容写到磁盘中(即 Spill 阶段)。在写磁盘过程中,Map 输出继续被写到缓冲区中,但如果在此期间缓冲区被填满,那么 Map 任务就会阻塞直到写磁盘过程完成。
**3)**在写磁盘前,线程首先根据数据最终要传递到的 Reduce 任务把数据划分成相应的分区(Partition)。在每个分区中,后台线程按 Key 进行排序,如果有一个 Combiner,便会在排序后 的输出上运行。
**4)**一旦内存缓冲区达到溢出写的阈值,就会创建一个溢出写文件,因此在 Map 任务完成其最后一个输出记录后,便会有多个溢出写文件。在 Map 任务完成前,溢出写文件被合并成一个索引文件和数据文件(多路归并排序)(Sort 阶段)。
**5)**溢出写文件归并完毕后,Map 任务将删除所有的临时溢出写文件,并告知 TaskTracker 任务已完成,只要其中一个 Map 任务完成,Reduce 任务就会开始复制它的输出(Copy 阶段)。
**6)**Map 任务的输出文件放置在运行 Map 任务的 TaskTracker 的本地磁盘上,它是运行 Reduce 任务的 TaskTracker 所需要的输入数据。
**1)**Reduce 进程启动一些数据复制线程,请求 Map 任务所在的 TaskTracker 以获取输出文件(Copy 阶段)。
**2)**将 Map 端复制过来的数据先放入内存缓冲区中,Merge 有 3 种形式,分别是内存到内存,内存到磁盘,磁盘到磁盘。默认情况下,第一种形式不启用,第二种形式一直在运行(Spill 阶段),直到结束,第三种形式生成最终的文件(Merge 阶段)。
**3)**最终文件可能存在于磁盘中,也可能存在于内存中,但是默认情况下是位于磁盘中的。当 Reduce 的输入文件已定,整个 Shuffle 阶段就结束了,然后就是 Reduce 执行,把结果放到 HDFS 中(Reduce 阶段)。