一、MapReduce应用场景
Hadoop的Mapreduce是一个使用简单的框架,基于它写出来的程序可以运行在上千个商用机器组成的大型集群上,并以一种可靠容错式并行处理TB级别的数据集。
MapReduce的优缺点:
优点:易于编程、良好的扩展性、高容错性、离线处理
缺点:无法实时计算、无法流式计算、不适合DAG(有向图)计算
二、MapReduce的编程模型
1.MapReduce的执行流程
假如有一批文件(规模为TB级或者PB级),我们要如何统计这些文件中所有单词出现的次数?首先,我们要分别统计每个文件中单词出现的次数,然后,累加不同文件中同一单词出现的次数。那么“首先”就是对应的MapReduce的Map过程,“然后”对应的就是MapReduce的Reduce过程。下面我们通过示意图来分析MapReduce的过程。
上图的流程大概分为以下几步。
第一步:假设一个文件有三行英文单词作为 MapReduce 的Input(输入),这里经过 Splitting 过程把文件分割为3块。分割后的3块数据就可以并行处理,每一块交给一个 map 线程处理。
第二步:每个 map 线程中,以每个单词为key,以1作为词频数value,然后输出。
第三步:每个 map 的输出要经过 shuffling(混洗),将相同的单词key放在一个桶里面,然后交给 reduce 处理。
第四步:reduce 接受到 shuffling 后的数据, 会将相同的单词进行合并,得到每个单词的词频数,最后将统计好的每个单词的词频数作为输出结果。
上述就是 MapReduce 的大致流程,前两步可以看做 map 阶段,后两步可以看做 reduce 阶段。 MapReduce 将 作业的整个运行过程分为两个阶段:Map 阶段和Reduce 阶段。
1、Map 阶段
Map 阶段是由一定数量的 Map Task 组成。这些 Map Task 可以同时运行,每个 Map Task又是由以下三个部分组成。
1)对输入数据格式进行解析的一个组件:InputFormat。因为不同的数据可能存储的数据格式不一样,这就需要有一个 InputFormat 组件来解析这些数据的存放格式。默认情况下,它提供了一个 TextInputFormat 来解释数据格式。TextInputFormat 就是我们前面提到的文本文件输入格式,它会将文件的每一行解释成(key,value),key代表每行偏移量,value代表每行数据内容。 通常情况我们不需要自定义 InputFormat,因为 MapReduce 提供了很多种InputFormat的实现,我们根据不同的数据格式,选择不同的 InputFormat 来解释就可以了。这一点我们后面会讲到。
2)输入数据处理:Mapper。这个 Mapper 是必须要实现的,因为根据不同的业务对数据有不同的处理。
3)数据分组:Partitioner。Mapper 数据处理之后输出之前,输出key会经过 Partitioner 分组或者分桶选择不同的reduce。默认的情况下,Partitioner 会对 map 输出的key进行hash取模,比如有6个Reduce Task,它就是模(mod)6,如果key的hash值为0,就选择第0个 Reduce Task,如果key的hash值为1,就选择第一个 Reduce Task。这样不同的 map 对相同单词key,它的 hash 值取模是一样的,所以会交给同一个 reduce 来处理。
2、Reduce 阶段
Reduce 阶段由一定数量的 Reduce Task 组成。这些 Reduce Task 可以同时运行,每个 Reduce Task又是由以下四个部分组成。
数据运程拷贝。Reduce Task 要运程拷贝每个 map 处理的结果,从每个 map 中读取一部分结果。每个 Reduce Task 拷贝哪些数据,是由上面 Partitioner 决定的。
数据按照key排序。Reduce Task 读取完数据后,要按照key进行排序。按照key排序后,相同的key被分到一组,交给同一个 Reduce Task 处理。
数据处理:Reducer。以WordCount为例,相同的单词key分到一组,交个同一个Reducer处理,这样就实现了对每个单词的词频统计。
数据输出格式:OutputFormat。Reducer 统计的结果,将按照 OutputFormat 格式输出。默认情况下的输出格式为 TextOutputFormat,以WordCount为例,这里的key为单词,value为词频数。
InputFormat、Mapper、Partitioner、Reducer和OutputFormat 都是用户可以实现的。通常情况下,用户只需要实现 Mapper和Reducer,其他的使用默认实现就可以了。
2.MapReduce 内部逻辑
下面我们通过 MapReduce 的内部逻辑,来分析 MapReduce的数据处理过程。我们以WordCount为例,来看一下mapreduce 内部逻辑,如下图所示。
MapReduce 内部逻辑的大致流程主要由以下几步完成。
1、首先将 HDFS 中的数据以 Split 方式作为 MapReduce 的输入。前面我们提到,HDFS中的数据是以 block存储,这里怎么又变成了以Split 作为输入呢?其实 block 是 HDFS 中的术语,Split 是 MapReduce 中的术语。默认的情况下,一个 Split 可以对应一个 block,当然也可以对应多个block,它们之间的对应关系是由 InputFormat 决定的。默认情况下,使用的是 TextInputFormat,这时一个Split对应一个block。 假设这里有4个block,也就是4个Split,分别为Split0、Split1、Split2和Split3。这时通过 InputFormat 来读每个Split里面的数据,它会把数据解析成一个个的(key,value),然后交给已经编写好的Mapper 函数来处理。
2、每个Mapper 将输入(key,value)数据解析成一个个的单词和词频,比如(a,1)、(b,1)和(c,1)等等。
3、Mapper解析出的数据,比如(a,1),经过 Partitioner之后,会知道该选择哪个Reducer来处理。每个 map 阶段后,数据会输出到本地磁盘上。
4、在reduce阶段,每个reduce要进行 shuffle 读取它所对应的数据。当所有数据读取完之后,要经过Sort全排序,排序之后再交给 Reducer 做统计处理。比如,第一个Reducer读取了两个的(a,1)键值对数据,然后进行统计得出结果(a,2)。
5、将 Reducer 的处理结果,以OutputFormat数据格式输出到 HDFS 的各个文件路径下。这里的OutputFormat默认为TextOutputFormat,key为单词,value为词频数,key和value之间的分割符为"\tab"。 由上图所示,(a 2)输出到Part-0,(b 3)输出到Part-1,(c 3)输出到Part-2。
3.MapReduce的基本架构
和HDFS一样,MapReduce也是采用Master/Slave的架构,其架构图如下所示。
MapReduce包含四个组成部分,分别为Client、JobTracker、TaskTracker和Task,下面我们详细介绍这四个组成部分。
1)Client 客户端
每一个 Job 都会在用户端通过 Client 类将应用程序以及配置参数 Configuration 打包成 JAR 文件存储在 HDFS,并把路径提交到 JobTracker 的 master 服务,然后由 master 创建每一个 Task(即 MapTask 和 ReduceTask) 将它们分发到各个 TaskTracker 服务中去执行。
2)JobTracker
JobTracke负责资源监控和作业调度。JobTracker 监控所有TaskTracker 与job的健康状况,一旦发现失败,就将相应的任务转移到其他节点;同时,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 处理。Split 和 Block的关系如下图所示:
Map Task 执行过程如下图 所示:由该图可知,Map Task 先将对应的split 迭代解析成一个个key/value 对,依次调用用户 自定义的map() 函数进行处理,最终将临时结果存放到本地磁盘上, 其中临时数据被分成若干个partition,每个partition 将被一个Reduce Task 处理。
Reduce Task 执行过程下图所示。该过程分为三个阶段:
①从远程节点上读取Map Task 中间结果(称为“Shuffle 阶段”);
②按照key 对key/value 对进行排序(称为“Sort 阶段”);
③依次读取< key, value list>,调用用户自定义的reduce() 函数处理,并将最终结果存到HDFS 上(称为“Reduce 阶段”)。
4.MapReduce的数据本地性
在介绍数据本地性之前,我们首先介绍网络拓扑的概念。在一个 Hadoop 集群里面,通常我们把这些机器按照机架来组织,比如说每个机架一般有16-64个节点。每个机架通过 Switch 交换机来通信,不同的机架又通过总的Switch交换机来交互。其架构图如下所示。
在上图的架构中,我们标注了A、B、C、D四个节点,其中A、B、C三个节点存储了block1数据块。我们假设节点A要读取block1数据块,那么它的最佳选择就是读取它本身存储的block1,此时读取的速度最快、效率最高。如果A本身的block1数据块丢失或者损坏,它就会选择读取同一个Switch机架下的B节点上的block1,这是因为在同一个机架下面读取数据相对较快,所以不会选择跨机架读取C节点上面的block1,因为跨机架读取数据的效率最差。除非A、B中的block1都丢失或者损坏,才会选择跨机架读取C节点上面的block1。
数据本地性(data locality)
那到底什么是数据本地性呢?如果一个任务运行在它将处理的数据所在的节点,我们就称该任务具有“数据本地性”。数据的本地性可避免跨节点或者机架进行数据传输,提高运行效率。
数据本地性分为三个类别:
1、同节点(node-local)。
2、同机架(rack-local)。
3、跨机架(off-switch)。
为了深入理解数据本地性的三个类别,我们下面举个示例,如下图所示
从上图可以看出一共有四个机架:R1、R2、R3和R4,每个机架上有三个节点,每个节点上面存储有不同的数据块。比如,R1机架下有三个节点:H1、H2和H3,H1节点上有b1和b8数据块,H2节点上有b1和b2数据块,H3节点上有b2和b3数据块。从上图可以看出,每个数据块有3个备份,按照 HDFS 的备份机制将这3个数据块存储到不同的节点上。下面我们来看一下数据本地性的三种类别。
1、我们假设Task1需要处理b1,H1节点正好有空闲资源得到Task1,H1节点存储有b1,这时Task1在本节点上直接读取b1,效率最高。这种情况下数据本地性称之为node-local。
2、我们假设Task2需要处理b2,H4节点正好有空闲资源得到Task2,b2不在H4节点上,那么Task2会找到存储有b2且距离最近的H5节点,H4和H5处于同一个机架,这时Task2会读取同机架节点上的b2。这种情况下数据本地性称之为rack-local。
3、我们假设Task3需要处理b3,H7节点正好有空闲资源得到Task3,b3并不在H7节点上,也不在H7同机架的节点上,而是在R1和R2机架下面的节点上,这时Task3就需要跨机架读取b3,效率非常低下。这种情况下数据本地性称之为off-switch。
三、MapReduce框架的容错性
1、JobTracker
很不幸,JobTracker 存在单点故障,一旦出现故障,整个集群就不可用。这个是1.0里面出现的问题,在2.0里面这个问题已经得到了解决。 不过大家放心,即使在1.0中,MapReduce也不会经常出现故障。它可能一年也就是出现几次故障,出现故障之后,你重启一下,再把作业重新提交就可以了,它不会像 HDFS 那样出现数据的丢失。 因为 MapReduce 是一个计算框架,计算过程是可以重现的,即使某个服务挂掉了,你重启一下服务,然后把作业重新提交,也是不会影响你的业务的。
2、TaskTracker
TaskTracker 周期性的向 JobTracker 汇报心跳,如果一定的时间内没有汇报这个心跳,JobTracker 就认为该TaskTracker 挂掉了,它就会把上面所有任务调度到其它TaskTracker(节点)上运行。这样即使某个节点挂了,也不会影响整个集群的运行。
3、MapTask和ReduceTask
MapTask和ReduceTask 也可能运行挂掉。比如内存超出了或者磁盘挂掉了,这个任务也就挂掉了。 这个时候 TaskTracker 就会把每个MapTask和ReduceTask的运行状态回报给 JobTracker,JobTracker 一旦发现某个Task挂掉了,它就会通过调度器把该Task调度到其它节点上。这样的话,即使任务挂掉了,也不会影响应用程序的运行。
MapReduce的错误处理机制
- 硬件故障
在 Hadoop Cluster 中,只有一个 JobTracker,因此,JobTracker 本身是存在单点故 障的。如何解决 JobTracker 的单点问题呢?我们可以采用主备部署方式,启动 JobTracker 主节点的同时,启动一个或多个 JobTracker 备用节点。当 JobTracker 主节点出现问题时, 通过某种选举算法,从备用的 JobTracker 节点中重新选出一个主节点。 机器故障除了 JobTracker 错误就是 TaskTracker 错误。TaskTracker 故障相对较为常 见,MapReduce 通常是通过重新执行任务来解决该故障。 在 Hadoop 集群中,正常情况下,TaskTracker 会不断的与 JobTracker 通过心跳机制 进行通信。如果某 TaskTracker 出现故障或者运行缓慢,它会停止或者很少向 JobTracker 发送心跳。如果一个 TaskTracker 在一定时间内(默认是 1 分钟)没有与 JobTracker 通信, 那么 JobTracker 会将此 TaskTracker 从等待任务调度的 TaskTracker 集合中移除。同时 JobTracker 会要求此 TaskTracker 上的任务立刻返回。 如果此 TaskTracker 任务仍然在 mapping 阶段的 Map 任务,那么 JobTracker 会要求其他的 TaskTracker 重新执行所有原 本由故障 TaskTracker 执行的 Map 任务。如果任务是在 Reduce 阶段的 Reduce 任务,那 么 JobTracker 会要求其他 TaskTracker 重新执行故障 TaskTracker 未完成的 Reduce 任务。 比如:一个 TaskTracker 已经完成被分配的三个 Reduce 任务中的两个,因为 Reduce 任务 一旦完成就会将数据写到 HDFS 上,所以只有第三个未完成的 Reduce 需要重新执行。但 是对于 Map 任务来说,即使 TaskTracker 完成了部分 Map,Reduce 仍可能无法获取此节 点上所有 Map 的所有输出。所以无论 Map 任务完成与否,故障 TaskTracker 上的 Map 任 务都必须重新执行。 - 任务执行失败引发的故障
在实际任务中,MapReduce 作业还会遇到用户代码缺陷或进程崩溃引起的任务失败等 情况。用户代码缺陷会导致它在执行过程中抛出异常。此时,任务 JVM 进程会自动退出,并 向 TaskTracker 父进程发送错误消息,同时错误消息也会写入 log 文件,最后 TaskTracker 将此次任务尝试标记失败。对于进程崩溃引起的任务失败,TaskTracker 的监听程序会发现 进程退出,此时 TaskTracker 也会将此次任务尝试标记为失败。对于死循环程序或执行时间 太长的程序,由于 TaskTracker 没有接收到进度更新,它也会将此次任务尝试标记为失败, 并杀死程序对应的进程。 在以上情况中,TaskTracker 将任务尝试标记为失败之后会将 TaskTracker 自身的任务 计数器减 1,以便想 JobTracker 申请新的任务。TaskTracker 也会通过心跳机制告诉 JobTracker 本地的一个任务尝试失败。JobTracker 接到任务失败的通知后,通过重置任务 状态,将其加入到调度队列来重新分配该任务执行(JobTracker 会尝试避免将失败的任务 再次分配给运行失败的 TaskTracker)。如果此任务尝试了 4 次(次数可以进行设置)仍没 有完成,就不会再被重试,此时整个作业也就失败了。
四、MapReduce资源组织方式
MapReduce 计算框架并没有直接调用 CPU和内存等多维度资源,它把多维度资源抽象为 “slot”,用 “slot” 来描述资源的数量。管理员可以在每个节点上单独配置slot个数。slot可以分为map slot和reduce slot。从一定程度上,slot可以看做“任务运行并行度”。如果某个节点配置了5个map slot,那么这个节点最多运行5个Map Task;如果某个节点配置了3个reduce slot,那么该节点最多运行3个Reduce Task。下面我们分别介绍 Map slot和Reduce slot。
1、Map slot
1)Map slot 可用于运行 Map Task 的资源,而且只能运行 Map Task。
2)每个 Map Task 通常使用一个map slot。而比如像容量调度器,它可以有比较大的 MapTask。这样的MapTask使用内存比较多,那么它可能使用多个map slot。
2、Reduce slot
1)Reduce slot 可用于运行ReduceTask,而且只能运行ReduceTask。
2)每个ReduceTask通常使用一个reduce slot。而比如像容量调度器,它可以有比较大的 ReduceTask。这样的ReduceTask使用内存比较多,那么它可能使用多个reduce slot。
五、MapReduce的数据倾斜优化
数据分布:正常的数据分布理论上都是倾斜的,就是我们所说的20-80原理:80%的财富集中在20%的人手中, 80%的用户只使用20%的功能 , 20%的用户贡献了80%的访问量 。
产生原因:
Mapreduce程序在运行的时候,运行了大部分,但是还有部分reduce还在运行,甚至长时间运行,最终导致整个程序运行时间很长才结束。
造成这种现象的主要原因是:
reduce程序处理的key的条数比其他key的条数大很多,这也就造成了分配到数据巨大的key的节点长时间运行。本质讲数据倾斜就是数据分布不均。出现场景:
不同的数据字段可能的数据倾斜一般有两种情况:
一种是唯一值非常少,极少数值有非常多的记录值(唯一值少于几千)
一种是唯一值比较多,这个字段的某些值有远远多于其他值的记录数,但是它的占比也小于百分之一或千分之一
解决方案:
方式1:增加reduce 的jvm内存
既然reduce 本身的计算需要以合适的内存作为支持,在硬件环境容许的情况下,增加reduce 的内存大小显然有改善数据倾斜的可能,这种方式尤其适合数据分布第一种情况,单个值有大量记录, 这种值的所有纪录已经超过了分配给reduce 的内存,无论你怎么样分区这种情况都不会改变。
方式2: 增加reduce 个数
这个对于数据分布第二种情况有效,唯一值较多,单个唯一值的记录数不会超过分配给reduce 的内存. 如果发生了偶尔的数据倾斜情况,增加reduce 个数可以缓解偶然情况下的某些reduce 不小心分配了多个较多记录数的情况. 但是对于第一种数据分布无效。
方式3: 自定义partition
如果map输出键的单词来源于一本书。其中大部分必然是省略词(stopword: a,the,or )。那么就可以将自定义分区将这部分省略词发送给固定的一部分reduce实例。而将其他的都发送给剩余的reduce实例。
方式4:设定combiner
减少流向reduce的文件数量,从而减轻reduce数据倾斜。