在 Hadoop 问世之前,其实已经有了分布式计算,只是那个时候的分布式计算都是专用的系统,只能专门处理某一类计算,比如进行大规模数据的排序。很显然,这样的系统无法复用到其他的大数据计算场景,每一种应用都需要开发与维护专门的系统。而 Hadoop MapReduce 的出现,使得大数据计算通用编程成为可能。我们只要遵循 MapReduce 编程模型编写业务处理逻辑代码,就可以运行在 Hadoop 分布式集群上,无需关心分布式计算是如何完成的。也就是说,我们只需要关心业务逻辑,不用关心系统调用与运行环境,这和我们目前的主流开发方式是一致的。
大数据计算的核心思路是移动计算比移动数据更划算。既然计算方法跟传统计算方法不一样,移动计算而不是移动数据,那么用传统的编程模型进行大数据计算就会遇到很多困难,因此 Hadoop 大数据计算使用了一种叫作 MapReduce 的编程模型。
其实 MapReduce 编程模型并不是 Hadoop 原创,甚至也不是 Google 原创,但是 Google 和 Hadoop 创造性地将 MapReduce 编程模型用到大数据计算上,立刻产生了神奇的效果,看似复杂的各种各样的机器学习、数据挖掘、SQL 处理等大数据计算变得简单清晰起来。
今天我们就来聊聊Hadoop 解决大规模数据分布式计算的方案——MapReduce。
在我看来,MapReduce 既是一个编程模型,又是一个计算框架。也就是说,开发人员必须基于 MapReduce 编程模型进行编程开发,然后将程序通过 MapReduce 计算框架分发到 Hadoop 集群中运行。我们先看一下作为编程模型的 MapReduce。
为什么说 MapReduce 是一种非常简单又非常强大的编程模型?
简单在于其编程模型只包含 Map 和 Reduce 两个过程,map 的主要输入是一对
同时,MapReduce 又是非常强大的,不管是关系代数运算(SQL 计算),还是矩阵运算(图计算),大数据领域几乎所有的计算需求都可以通过 MapReduce 编程来实现。
下面,我以 WordCount 程序为例,一起来看下 MapReduce 的计算过程。
WordCount 主要解决的是文本处理中词频统计的问题,就是统计文本中每一个单词出现的次数。如果只是统计一篇文章的词频,几十 KB 到几 MB 的数据,只需要写一个程序,将数据读入内存,建一个 Hash 表记录每个词出现的次数就可以了。这个统计过程你可以看下面这张图。
如果用 Python 语言,单机处理 WordCount 的代码是这样的。
# 文本前期处理
strl_ist = str.replace('\n', '').lower().split(' ')
count_dict = {}
# 如果字典里有该单词则加1,否则添加入字典
for str in strl_ist:
if str in count_dict.keys():
count_dict[str] = count_dict[str] + 1
else:
count_dict[str] = 1
简单说来,就是建一个 Hash 表,然后将字符串里的每个词放到这个 Hash 表里。如果这个词第一次放到 Hash 表,就新建一个 Key、Value 对,Key 是这个词,Value 是 1。如果 Hash 表里已经有这个词了,那么就给这个词的 Value + 1。
小数据量用单机统计词频很简单,但是如果想统计全世界互联网所有网页(数万亿计)的词频数(而这正是 Google 这样的搜索引擎的典型需求),不可能写一个程序把全世界的网页都读入内存,这时候就需要用 MapReduce 编程来解决。
WordCount 的 MapReduce 程序如下。
public class WordCount {
public static class TokenizerMapper
extends Mapper
你可以从这段代码中看到,MapReduce 版本 WordCount 程序的核心是一个 map 函数和一个 reduce 函数。
map 函数的输入主要是一个
public void map(Object key, Text value, Context context
)
map 函数的计算过程是,将这行文本中的单词提取出来,针对每个单词输出一个
MapReduce 计算框架会将这些
public void reduce(Text key, Iterable values,
Context context
)
这里 reduce 的输入参数 Values 就是由很多个 1 组成的集合,而 Key 就是具体的单词 word。
reduce 函数的计算过程是,将这个集合里的 1 求和,再将单词(word)和这个和(sum)组成一个
一个 map 函数可以针对一部分数据进行运算,这样就可以将一个大数据切分成很多块(这也正是 HDFS 所做的),MapReduce 计算框架为每个数据块分配一个 map 函数去计算,从而实现大数据的分布式计算。
假设有两个数据块的文本数据需要进行词频统计,MapReduce 计算过程如下图所示。
以上就是 MapReduce 编程模型的主要计算过程和原理,但是这样一个 MapReduce 程序要想在分布式环境中执行,并处理海量的大规模数据,还需要一个计算框架,能够调度执行这个 MapReduce 程序,使它在分布式的集群中并行运行,而这个计算框架也叫 MapReduce。
所以,当我们说 MapReduce 的时候,可能指编程模型,也就是一个 MapReduce 程序;也可能是指计算框架,调度执行大数据的分布式计算。关于 MapReduce 计算框架.
小结
总结一下,今天我们学习了 MapReduce 编程模型。这个模型既简单又强大,简单是因为它只包含 Map 和 Reduce 两个过程,强大之处又在于它可以实现大数据领域几乎所有的计算需求。这也正是 MapReduce 这个模型令人着迷的地方。
思考题
对于这样一张数据表
如果存储在 HDFS 中,每一行记录在 HDFS 对应一行文本,文本格式是
1,25
2,25
1,32
2,25
根据上面 WordCount 的示例,请你写一个 MapReduce 程序,得到下面这条 SQL 的计算结果。
SELECT pageid, age, count(1) FROM pv_users GROUP BY pageid, age;
MapReduce如何让数据完成一次旅行?
前面我们讲到MapReduce 编程模型将大数据计算过程切分为 Map 和 Reduce 两个阶段,先复习一下,在 Map 阶段为每个数据块分配一个 Map 计算任务,然后将所有 map 输出的 Key 进行合并,相同的 Key 及其对应的 Value 发送给同一个 Reduce 任务去处理。通过这两个阶段,工程师只需要遵循 MapReduce 编程模型就可以开发出复杂的大数据计算程序。
那么这个程序是如何在分布式集群中运行起来的呢?MapReduce 程序又是如何找到相应的数据并进行计算的呢?答案就是需要 MapReduce 计算框架来完成。上一期我讲了 MapReduce 既是编程模型又是计算框架,我们聊完编程模型,接下来就来讨论 MapReduce 如何让数据完成一次旅行,也就是 MapReduce 计算框架是如何运作的。
首先我想告诉你,在实践中,这个过程有两个关键问题需要处理
如何为每个数据块分配一个 Map 计算任务,也就是代码是如何发送到数据块所在服务器的,发送后是如何启动的,启动以后如何知道自己需要计算的数据在文件什么位置(BlockID 是什么)。
处于不同服务器的 map 输出的
,如何把相同的 Key 聚合在一起发送给 Reduce 任务进行处理。 处于不同服务器的 map 输出的
,如何把相同的 Key 聚合在一起发送给 Reduce 任务进行处理。
那么这两个关键问题对应在 MapReduce 计算过程的哪些步骤呢?根据我上一期所讲的,我把 MapReduce 计算过程的图又找出来,你可以看到图中标红的两处,这两个关键问题对应的就是图中的两处“MapReduce 框架处理”,具体来说,它们分别是 MapReduce 作业启动和运行,以及 MapReduce 数据合并与连接。
MapReduce 作业启动和运行机制
我们以 Hadoop 1 为例,MapReduce 运行过程涉及三类关键进程。
- 大数据应用进程。这类进程是启动 MapReduce 程序的主入口,主要是指定 Map 和 Reduce 类、输入输出文件路径等,并提交作业给 Hadoop 集群,也就是下面提到的 JobTracker 进程。这是由用户启动的 MapReduce 程序进程,比如我们上期提到的 WordCount 程序。
2.JobTracker 进程。这类进程根据要处理的输入数据量,命令下面提到的 TaskTracker 进程启动相应数量的 Map 和 Reduce 进程任务,并管理整个作业生命周期的任务调度和监控。这是 Hadoop 集群的常驻进程,需要注意的是,JobTracker 进程在整个 Hadoop 集群全局唯一。
3.TaskTracker 进程。这个进程负责启动和管理 Map 进程以及 Reduce 进程。因为需要每个数据块都有对应的 map 函数,TaskTracker 进程通常和 HDFS 的 DataNode 进程启动在同一个服务器。也就是说,Hadoop 集群中绝大多数服务器同时运行 DataNode 进程和 TaskTracker 进程。
JobTracker 进程和 TaskTracker 进程是主从关系,主服务器通常只有一台(或者另有一台备机提供高可用服务,但运行时只有一台服务器对外提供服务,真正起作用的只有一台),从服务器可能有几百上千台,所有的从服务器听从主服务器的控制和调度安排。主服务器负责为应用程序分配服务器资源以及作业执行的调度,而具体的计算操作则在从服务器上完成。
具体来看,MapReduce 的主服务器就是 JobTracker,从服务器就是 TaskTracker。还记得我们讲 HDFS 也是主从架构吗,HDFS 的主服务器是 NameNode,从服务器是 DataNode。后面会讲到的 Yarn、Spark 等也都是这样的架构,这种一主多从的服务器架构也是绝大多数大数据系统的架构方案。
可重复使用的架构方案叫作架构模式,一主多从可谓是大数据领域的最主要的架构模式。主服务器只有一台,掌控全局;从服务器有很多台,负责具体的事情。这样很多台服务器可以有效组织起来,对外表现出一个统一又强大的计算能力。
讲到这里,我们对 MapReduce 的启动和运行机制有了一个直观的了解。那具体的作业启动和计算过程到底是怎样的呢?我根据上面所讲的绘制成一张图,你可以从图中一步一步来看,感受一下整个流程。
如果我们把这个计算过程看作一次小小的旅行,这个旅程可以概括如下:
应用进程 JobClient 将用户作业 JAR 包存储在 HDFS 中,将来这些 JAR 包会分发给 Hadoop 集群中的服务器执行 MapReduce 计算。
应用程序提交 job 作业给 JobTracker。
3.JobTracker 根据作业调度策略创建 JobInProcess 树,每个作业都会有一个自己的 JobInProcess 树。
4.JobInProcess 根据输入数据分片数目(通常情况就是数据块的数目)和设置的 Reduce 数目创建相应数量的 TaskInProcess。
5.TaskTracker 进程和 JobTracker 进程进行定时通信。
- 如果 TaskTracker 有空闲的计算资源(有空闲 CPU 核心),JobTracker 就会给它分配任务。分配任务的时候会根据 TaskTracker 的服务器名字匹配在同一台机器上的数据块计算任务给它,使启动的计算任务正好处理本机上的数据,以实现我们一开始就提到的“移动计算比移动数据更划算”。
7.TaskTracker 收到任务后根据任务类型(是 Map 还是 Reduce)和任务参数(作业 JAR 包路径、输入数据文件路径、要处理的数据在文件中的起始位置和偏移量、数据块多个备份的 DataNode 主机名等),启动相应的 Map 或者 Reduce 进程。
8.Map 或者 Reduce 进程启动后,检查本地是否有要执行任务的 JAR 包文件,如果没有,就去 HDFS 上下载,然后加载 Map 或者 Reduce 代码开始执行。
- 如果是 Map 进程,从 HDFS 读取数据(通常要读取的数据块正好存储在本机);如果是 Reduce 进程,将结果数据写出到 HDFS。
通过这样一个计算旅程,MapReduce 可以将大数据作业计算任务分布在整个 Hadoop 集群中运行,每个 Map 计算任务要处理的数据通常都能从本地磁盘上读取到。现在你对这个过程的理解是不是更清楚了呢?你也许会觉得,这个过程好像也不算太简单啊!
其实,你要做的仅仅是编写一个 map 函数和一个 reduce 函数就可以了,根本不用关心这两个函数是如何被分布启动到集群上的,也不用关心数据块又是如何分配给计算任务的。这一切都由 MapReduce 计算框架完成!是不是很激动,这也是我们反复讲到的 MapReduce 的强大之处。
MapReduce 数据合并与连接机制
MapReduce 计算真正产生奇迹的地方是数据的合并与连接。
让我先回到上一期 MapReduce 编程模型的 WordCount 例子中,我们想要统计相同单词在所有输入数据中出现的次数,而一个 Map 只能处理一部分数据,一个热门单词几乎会出现在所有的 Map 中,这意味着同一个单词必须要合并到一起进行统计才能得到正确的结果。
事实上,几乎所有的大数据计算场景都需要处理数据关联的问题,像 WordCount 这种比较简单的只要对 Key 进行合并就可以了,对于像数据库的 join 操作这种比较复杂的,需要对两种类型(或者更多类型)的数据根据 Key 进行连接。
在 map 输出与 reduce 输入之间,MapReduce 计算框架处理数据合并与连接操作,这个操作有个专门的词汇叫 shuffle。那到底什么是 shuffle?shuffle 的具体过程又是怎样的呢?请看下图。
每个 Map 任务的计算结果都会写入到本地文件系统,等 Map 任务快要计算完成的时候,MapReduce 计算框架会启动 shuffle 过程,在 Map 任务进程调用一个 Partitioner 接口,对 Map 产生的每个
map 输出的
/** Use {@link Object#hashCode()} to partition. */
public int getPartition(K2 key, V2 value, int numReduceTasks) {
return (key.hashCode() & Integer.MAX_VALUE) % numReduceTasks;
}
讲了这么多,对 shuffle 的理解,你只需要记住这一点:分布式计算需要将不同服务器上的相关数据合并到一起进行下一步计算,这就是 shuffle。
shuffle 是大数据计算过程中最神奇的地方,不管是 MapReduce 还是 Spark,只要是大数据批处理计算,一定都会有 shuffle 过程,只有让数据关联起来,数据的内在关系和价值才会呈现出来。如果你不理解 shuffle,肯定会在 map 和 reduce 编程中产生困惑,不知道该如何正确设计 map 的输出和 reduce 的输入。shuffle 也是整个 MapReduce 过程中最难、最消耗性能的地方,在 MapReduce 早期代码中,一半代码都是关于 shuffle 处理的。
小结
MapReduce 编程相对说来是简单的,但是 MapReduce 框架要将一个相对简单的程序,在分布式的大规模服务器集群上并行执行起来却并不简单。理解 MapReduce 作业的启动和运行机制,理解 shuffle 过程的作用和实现原理,对你理解大数据的核心原理,做到真正意义上把握大数据、用好大数据作用巨大。
FAQ
您好,有个问题,当某个key聚集了大量数据,shuffle到同一个reduce来汇总,考虑数据量很大的情况,这个会不会把reduce所在机器节点撑爆?这样任务是不是就失败了?
会的,数据倾斜,会导致任务失败。严重的数据倾斜可能是数据本身的问题,需要做好预处理
请问一下,map和reduce有绝对的先后关系吗,还是说可以一边map一边reduce
绝对先后关系,一个reduce必须要他前置的所有map执行完才能执行。
MapReduce框架会在85%的map程序执行完成时,开始启动reduce程序,reduce程序一边shuffle已完成的map数据,一边等待未完成的map继续执行,直到全部map完成,reduce才开始执行计算。