MapReduce
MapReduce
为什么要学习MapReduce
单机版:内存受限,磁盘受限,运算能⼒受限 ⽽⼀旦将单机版程序扩展到集群来分布式运⾏,将极⼤增加程序的复杂度和开 发难度 引⼊MapReduce框架后,开发⼈员可以将绝⼤部分⼯作集中在业务逻辑的开发 上,⽽将分布式计算中的复杂性交由框架来处理
MapReduce简介
MapReduce是Apache Hadoop项⽬的⼀个核⼼模块 MapReduce是⼀个运⾏在hdfs上的分布式运算程序的编程框架,⽤于⼤规模数 据集(⼤于1TB)的并⾏运算 概念"Map(映射)"和"Reduce(归约)",是它们的主要思想,都是从函数式 编程语⾔⾥借来的,还有从⽮量编程语⾔⾥借来的特性 它极⼤地⽅便了编程⼈员在不会分布式并⾏编程的情况下,将⾃⼰的程序运⾏ 在分布式系统上
MapReduce优缺点
优点
1. MapReduce易于编程 它简单的实现⼀些接⼝,就可以完成⼀个分布式程序,这个程序可以分布到⼤量的 廉价的pc机器上运⾏。也就是说你写⼀个分布式程序,跟写⼀个简单的串⾏程序是⼀模 ⼀样的。就是因为这个特性使的MapReduce编程变得⾮常流⾏。 2. 良好的扩展性 当你的计算资源得不到满⾜的时候,你可以简单的通过增加机器来扩展它的计算能 ⼒ 3. ⾼容错性 MapReduce的设计初衷就是使程序能够部署在廉价的pc机器上,这就要求它具有 很⾼的容错性。⽐如⼀个机器挂了,它可以把上⾯的计算任务转移到另⼀个节点上运 ⾏,不⾄于这个任务运⾏失败,⽽且这个过程不需要⼈⼯参与,⽽完全是由hadoop内 部完成的。 4. 适合PB级以上海量数据的离线处理
缺点
1. 不适合实时计算 MapReduce⽆法做到像Mysql那样做到毫秒或者秒级的返回结果 2. 不适合流式计算 流式计算的输⼊数据是动态的,⽽MapReduce的输⼊数据集是静态的,不能流态变 化。这是MapReduce⾃身的设计特点决定了数据源必须是静态的。 3. 不适合DAG(有向图)计算 多个应⽤程序存在依赖关系,后⼀个应⽤程序的输⼊为前⼀个应⽤程序的输出,在 这种情况下,MapReduce并不是不能做,⽽是使⽤后每个MapReduce作业的输出结果都 会写⼊到磁盘,会造成⼤量的磁盘IO,导致性能⾮常低下。
MapReduce核⼼思想
1.MapReduce设计的⼀个理念是“计算向数据靠拢”(移动计算),⽽不是“数据向计 算靠拢”(移动数据) 2.将⽤户编写的业务逻辑代码和⾃带默认组件整合成⼀个完整的分布式运算程序,移 动到有数据存储的集群节点上,⼀是可以减少节点间的数据移动开销。⼆是在存储节 点上可以并⾏计算,⼤⼤提⾼计算效率问题。 因为移动数据需要⼤量的⽹络传输开销,尤其是在⼤规模数据环境下,这种开销 尤为惊⼈,所以移动计算要⽐移动数据更加经济。 3.MapReduce⼀个完整的运算分为Map和Reduce两个部分。Map会处理本节点的原 始数据,产⽣的数据会临时存储到本地磁盘。Reduce会跨节点fetch属于⾃⼰的数 据,并进⾏处理,产⽣的数据会存储到HDFS上。
MapReduce的阶段分类
第⼀阶段
第⼀阶段,也称之为Map阶段。这个阶段会有若⼲个MapTask实例,完全并⾏运 ⾏,互不相⼲。每个MapTask会读取分析⼀个InputSplit(输⼊分⽚,简称分⽚)对应 的原始数据。计算的结果数据会临时保存到所在节点的本地磁盘⾥。
数据扭转
该阶段的编程模型中会有⼀个map函数需要开发⼈员重写,map函数的输⼊是⼀个 <key,value>对,map函数的输出也是⼀个<key,value>对,key和value的类型需要开发 ⼈员指定。
第⼆阶段
第⼆阶段,也称为Reduce阶段。这个阶段会有若⼲个ReduceTask实例并发运⾏,互 不相⼲。但是他们的数据依赖于上⼀个阶段的所有maptask并发实例的输出。⼀个 ReudceTask会从多个MapTask运⾏节点上fetch⾃⼰要处理的分区数据。经过处理 后,输出到HDFS上。
数据扭转
该阶段的编程模型中有⼀个reduce函数需要开发⼈员重写,reduce函数的输⼊也是 ⼀个<key,value>对,reduce函数的输出也是⼀个<key,value>对。这⾥要强调的 是,reduce的输⼊其实就是map的输出,只不过map的输出经过shuffle技术后变成 了<key,List>⽽已。
注意: MapReduce编程模型只能包含⼀个map阶段和⼀个reduce阶段,如果⽤户的 业务逻辑⾮常复杂,那就只能多个MapReduce程序,串⾏运⾏。
MapReduce编程规范
⽤户编写的程序分为3个部分:Mapper、Reducer、Driver(提交mr程序的客户 端)
Mapper部分
1. ⾃定义类,继承Mapper类型 2. 定义K1,V1,K2,V2的泛型(K1,V1是Mapper的输⼊数据类型,K2,V2是Mapper的 输出数据类型) 3. 重写map⽅法(处理逻辑)
注意:map⽅法,每⼀个KV对都会调⽤⼀次。
Reducer部分
1. ⾃定义类,继承Reducer类型 2. 定义K2,V2,K3,V3的泛型(K2,V2是Reducer的输⼊数据类型,K3,V3是Reducer 的输出数据类型) 3. 重写reduce⽅法的处理逻辑
注意: reduce⽅法,默认按key分组,每⼀组都调⽤⼀次。
Driver部分
整个程序需要⼀个Driver来进⾏提交,提交的是⼀个描述了各种必要信息的job对 象,如下
1. 获取Job对象 2. 指定驱动类 3. 设置Mapper和Reducer类型 4. 设置Mapper的输出K2、V2的类型(如果类型和K3,V3相同,可省略) 5. 设置Reducer的输出K3、V3的类型 6. 设置Reduce的个数(默认为1) 7. 设置Mapper的输⼊数据的路径 8. 设置Reducer的输出数据的路径 9. 提交作业
打jar包 打包:点击Maven Project---->打开当前的⼯程------>Lifecycle----->package 架包位置:当前⼯程的target⽬录下可以看到xxx.jar⽂件
测试 别忘记开启yarn,yarn的配置参考⽂档zookeeper和yarn
1.在本地/root/wordcount/下创建a.txt,b.txt,c.txt并传⼊数据 2.将数据导⼊hdfs系统 hdfs dfs -put /root/wordcount/input/*.txt /wordcount/output 3.执⾏ hadoop jar xxx.jar wordcount /wordcount/input /wordcount/output
Partitioner组件的应⽤
Partitioner简介
1. partitioner的作⽤是将mapper 输出的key/value划分成不同的partition, 每个reducer对应⼀个 partition。 2. 默认情况下,partitioner先计算key的散列值(hash值)。然后通过reducer 个数执⾏取模运算: key.hashCode%(reducer个数)。这样能够随机地将整个key空间平均分发给每 个reducer,同时也能确保不同 mapper产⽣的相同key能被分发到同⼀个reducer。 3. ⽬的: 可以使⽤⾃定义Partitioner来达到reducer的负载均衡, 提⾼效率。 4. 适⽤范围: 需要⾮常注意的是:必须提前知道有多少个分区。⽐如⾃定义Partitioner会返 回4个不同int值,⽽reducer number设置了⼩于4,那就会报错。所以我们可以通过运⾏分析任务来确定分区 数。例如,有⼀堆包含时间戳的 数据,但是不知道它能追朔到的时间范围,此时可以运⾏⼀个作业来计算出时间范 围。 注意:在⾃定义partitioner时⼀定要注意防⽌数据倾斜。
IDE运⾏MapReduce模式
local模式测试本地⽂件
- Configuration对象会读取四个默认的配置⽂件。 - fs.defaultFS的值为file:///,获取的是本地⽂件系统对象。 - mapreduce.framework.name的值为local 所以,在Driver中的输⼊路径和输出路径指定为本地的路径即可
local模式测试集群⽂件
- 读取集群上的⽂件。 因此要修改属性fs.defaultFS的值为集群路径。因为我们使⽤的是HA,因此要配 置相应的那些属性信息 - 如果不想配置,可以把core-site.xml和hdfs-site.xml下载下来放置在src下 - 如果懒得做上⾯的操作,那么就直接将fs.defaultFS的值,设置为Active的地址和 端⼝即可 - 此时读取的是mapred-default.xml,⾥⾯的mapreduce.framework.name属性 的值是local。
yarn模式测试集群⽂件
mapreduce的核⼼思想,移动计算,分⽽治之
1. 打成jar包,放⼊项⽬的classpath中 2. 使⽤yarn配置:将mapred-site.xml和yarn-site.xml放⼊src中 3. 设置跨平台属性: conf.set("mapreduce.app-submission.cross-platform", "true"); 注意:如果碰到权限问题:改为777,或者 System.setProperty("HADOOP_USER_NAME", "root");
jar包上传到linux系统上运⾏
mapreduce的核⼼思想,移动计算,分⽽治之
将xxxx.jar包上传到linux虚拟机中
[hadoop@master data]$ hadoop jar xxxxx.jar 类全名 inpath outpath
Hadoop 序列化机制
为什么要进⾏序列化
在基于类的编程语⾔中,我们说需要的数据都会被封装成对象,在内存中进⾏管理。
需要将对象序列化成0和1组成的字节序列,字节序列就可以存储到磁盘中,或者进 ⾏⽹络传输了。当我们需要对象时,就可以读取磁盘上的字节序列,或者接收⽹络传 输过来的字节序列,进⾏反序列化成我们需要的对象就可以了。
序列化概念
序列化是指将具有结构化的内存对象转为0和1组成的字节序列,以便进⾏⽹络传输或持 久存储到设备的过程。 反序列化指的是将字节序列转为内存中具有结构化的对象的过程。
序列化应⽤的两个领域
-1. ⽹络传输(进程通信) -2. 永久存储
hadoop和java序列化的⽐较
hadoop会涉及到⼤量数据的传输(⽹络IO),⽐如进程之间的通信(RPC协议), reduceTask的fetch操作。⽽⽹络带宽是极其稀缺的,因此使⽤序列化机制迫不及 待。
Java的序列化是⼀个重量级序列化框架(Serializable),⼀个对象被序列化后,会 附带很多额外的信息(各种校验信息,header,继承体系......),这些数据不是我 们需要的,也不便于在⽹络中⾼效传输.
基于Hadoop在集群之间进⾏通讯或者RPC调⽤的时候,数据的序列化要快,体积要 ⼩,占⽤带宽要⼩的需求,因此,专⻔为hadoop单独开发了⼀套精简⾼效的序列化 机制(Writable)。此序列化机制要求具有以下特点:
1)紧凑:紧凑的格式能让我们充分利⽤⽹络带宽,⽽带宽是数据中⼼最稀缺的资; 2)快速:进程通信形成了分布式系统的⻣架,所以需要尽量减少序列化和反序列化的性 能开销,这是基本的; 3)可扩展:协议为了满⾜新的需求变化,所以控制客户端和服务器过程中,需要直接引 进相应的协议,这些是新协议,原序列化⽅式能⽀持新的协议报⽂; 4)互操作:能⽀持不同语⾔写的客户端和服务端进⾏交互;
需要注意的是: MapReduce的key和value,都必须是可序列化的。⽽针对于key⽽ ⾔,是数据排序的关键字,因此还需要提供⽐较接⼝:WritableComparable
常⽤类型的简介
Java类型 Hadoop Writable类型 释义 boolean BooleanWritable 标准布尔型数值 byte ByteWritable 单字节数值 int IntWritable 整型数值 float FloatWritable 单精度数 long LongWritable ⻓整型数值 double DoubleWritable 双精度数 string Text 使⽤UTF8格式存储的⽂本 map MapWritable 以键值对的形式存储Writable类型的数据 array ArrayWritable 以数组的形式存储Writable类型的数据 null NullWritable 当<key,value>中的key或value为空时使⽤
序列化接⼝之Writable
Writable的序列化反序列化
write() 是把每个对象序列化到输出流———Writable序列化
readFields()是把输⼊流字节反序列化-------Writable反序列化
排序接⼝之WritableComparable
public interface WritableComparable<T> extends Writable, Comparable<T> { }
⾃定义对象实现MR的序列化接⼝
如果需要将⾃定义的bean放在key中传输,则还需要实现comparable接⼝,因为 MapReduce框中的shuffle过程⼀定会对key进⾏排序,此时,⾃定义的bean实现的接 ⼝应该是WritableComparable
MapReduce 基础
MapReduce运⾏流程简述
⼀个完整的MapReduce程序在分布式运⾏时有三类实例进程
1) MRAppMaster:负责整个程序的过程调度及状态协调 2) MapTask:负责map阶段的整个数据处理流程 3) ReduceTask:负责reduce阶段的整个数据处理流程
当⼀个作业提交后(mr程序启动),⼤概流程如下
1) ⼀个mr程序启动的时候,会先启动⼀个进程Application Master,它的主类是 MRAppMaster 2) appmaster启动之后会根据本次job的描述信息,计算出inputSplit的数据,也 就是MapTask的数量 3) appmaster然后向resourcemanager来申请对应数量的container来执⾏ MapTask进程。 4) MapTask进程启动之后,根据对应的inputSplit来进⾏数据处理,处理流程如下 -利⽤客户指定的inputformat来获取recordReader读取数据,形成kv键值 对。 -将kv传递给客户定义的mapper类的map⽅法,做逻辑运算,并将map⽅法的输出 kv收集到缓存。 -将缓存中的kv数据按照k分区排序后不断的溢出到磁盘⽂件 5) appmaster监控maptask进程完成之后,会根据⽤户指定的参数来启动相应的 reduceTask进程,并告知reduceTask需要处理的数据范围 6) reducetask启动之后,根据appmaster告知的待处理的未知数据,从若⼲的已经 存到磁盘的数据中拿到数据,并在本地进⾏⼀个归并排序,然后,再按照相同的key的 kv为⼀组,调⽤客户⾃定义的reduce⽅法,并收集输出结果kv,然后按照⽤户指定的 outputFormat将结果存储到外部设备。
运⾏流程之分⽚机制
分⽚的概念
MapReduce在进⾏作业提交时,会预先对将要分析的原始数据进⾏划分处理,形成 ⼀个个等⻓的逻辑数据对象,称之为输⼊分⽚(inputSplit),简称“分⽚”。 MapReduce为每⼀个分⽚构建⼀个单独的MapTask,并由该任务来运⾏⽤户⾃定义 的map⽅法,从⽽处理分⽚中的每⼀条记录。
分⽚⼤⼩的选择
1. 拥有许多分⽚,意味着处理每个分⽚所需要的时间要⼩于处理整个输⼊数据所花的时 间(分⽽治之的优势)。 2. 并⾏处理分⽚,且每个分⽚⽐较⼩。负载平衡,好的计算机处理的更快,可以腾出时 间,做别的任务 3. 如果分⽚太⼩,管理分⽚的总时间和构建map任务的总时间将决定作业的整个执⾏时 间。 4. 如果分⽚跨越两个数据块,那么分⽚的部分数据需要通过⽹络传输到map任务运⾏的 节点,占⽤⽹络带宽,效率更低 5. 因此最佳分⽚⼤⼩应该和HDFS上的块⼤⼩⼀致。hadoop2.x默认128M.
分⽚总结
分⽚⼤⼩参数 通过分析源码,在FileInputFormat中,计算切⽚⼤⼩的逻辑:Math.max(minSize, Math.min(maxSize, blockSize)); 切⽚主要由这⼏个值来运算决定
参数 默认值 属性 minsize 1 mapreduce.input.fileinputformat.split.minsize maxsize Long.MAXVALUE mapreduce.input.fileinputformat.split.maxsize blocksize 块⼤⼩ dfs.blocksize:
可以看出,就是取minsize、maxsize、blocksize三者的中间的那个值。
1. 将maxsize调得⽐blocksize⼩,则会让切⽚变⼩,⽽且就等于配置的这个参数的 值. 2. 将minsize调得⽐blockSize⼤,则会让切⽚变得⽐blocksize还⼤ 3. 但是,不论怎么调参数,都不能让多个⼩⽂件"划⼊"⼀个split
创建过程总结
1. 获取⽂件⼤⼩及位置 2. 判断⽂件是否可以分⽚(压缩格式有的可以进⾏分⽚,有的不可以) 3. 获取分⽚的⼤⼩ 4. 剩余⽂件的⼤⼩/分⽚⼤⼩>1.1时,循环执⾏封装分⽚信息的⽅法,具体如下: 封装⼀个分⽚信息(包含⽂件的路径,分⽚的起始偏移量,要处理的⼤⼩,分⽚包含 的块的信息,分⽚中包含的块存在哪⼉些机器上) 5. 剩余⽂件的⼤⼩/分⽚⼤⼩<=1.1且不等于0时,封装⼀个分⽚信息(包含⽂件的路 径,分⽚的起始偏移量,要处理 的⼤⼩,分⽚包含的块的信息,分⽚中包含的块存在哪⼉些机器上)
分⽚的注意事项:1.1倍的冗余。
分⽚细节问题总结
如果有多个分⽚
- 第⼀个分⽚读到末尾再多读⼀⾏ - 既不是第⼀个分⽚也不是最后⼀个分⽚第⼀⾏数据舍弃,末尾多读⼀⾏ - 最后⼀个分⽚舍弃第⼀⾏,末尾多读⼀⾏ - 为什么:前⼀个物理块不能正好是⼀⾏结束的位置啊
分⽚与块的区别
1. 分⽚是逻辑数据,记录的是要处理的物理块的信息⽽已 2. 块是物理的,是真实存储在⽂件系统上的原始数据⽂件。
运⾏流程之MapTask
1. maptask调⽤FileInputFormat的getRecordReader读取分⽚数据 2. 每⾏数据读取⼀次,返回⼀个(K,V)对,K是offset,V是⼀⾏数据 3. 将k-v对交给MapTask处理 4. 每对k-v调⽤⼀次map(K,V,context)⽅法,然后context.write(k,v) 5. 写出的数据交给收集器OutputCollector.collector()处理 6. 将数据写⼊环形缓冲区,并记录写⼊的起始偏移量,终⽌偏移量,环形缓冲区默认 ⼤⼩100M 7. 默认写到80%的时候要溢写到磁盘,溢写磁盘的过程中数据继续写⼊剩余20% 8. 溢写磁盘之前要先进⾏分区然后分区内进⾏排序 9. 默认的分区规则是hashpatitioner,即key的hash%reduceNum 10. 默认的排序规则是key的字典顺序,使⽤的是快速排序 11. 溢写会形成多个⽂件,在maptask读取完⼀个分⽚数据后,先将环形缓冲区数据 刷写到磁盘 12. 将数据多个溢写⽂件进⾏合并,分区内排序(外部排序===》归并排序)
1) 如果硬件配置为2*12core + 64G,恰当的map并⾏度是⼤约每个节点20-100个 map,最好每个map的执⾏时间⾄少⼀分钟。 2) 如果job的每个map或者 reduce task的运⾏时间都只有30-40秒钟,那么就减少 该job的map或者reduce数,每⼀个task(map|reduce)的setup和加⼊到调度器中进 ⾏调度,这个中间的过程可能都要花费⼏秒钟,所以如果每个task都⾮常快就跑完了, 就会在task的开始和结束的时候浪费太多的时间。 3) 配置task的JVM重⽤可以改善该问题: (mapred.job.reuse.jvm.num.tasks,默认是1,表示⼀个JVM上最多可以顺序执⾏ 的task数⽬(属于同⼀个Job)是1。也就是说⼀个task启⼀个JVM) 4) 如果input的⽂件⾮常的⼤,⽐如1TB,可以考虑将hdfs上的每个block size设 ⼤,⽐如设成256MB或者512MB
运⾏流程之ReduceTask
1. 数据按照分区规则发送到reducetask 2. reducetask将来⾃多个maptask的数据进⾏合并,排序(外部排序===》归并排 序) 3. 按照key相同分组() 4. ⼀组数据调⽤⼀次reduce(k,iterable<v>values,context) 5. 处理后的数据交由reducetask 6. reducetask调⽤FileOutputFormat组件 7. FileOutputFormat组件中的write⽅法将数据写出
Reduce Task的并⾏度同样影响整个job的执⾏并发度和执⾏效率,但与Map Task的 并发数由切⽚数决定不同,Reduc Task数量的决定是可以直接⼿动设置:默认值是 1,⼿动设置为4
设置⽅法:job.setNumReduceTasks(4);
如果数据分布不均匀,就有可能在reduce阶段产⽣数据倾斜 注意: Reduce Task数量并不是任意设置,还要考虑业务逻辑需求,有些情况下,需 要计算全局汇总结果,就只能有1个Reduce Task 尽量不要运⾏太多的Reduce Task。对⼤多数job来说,最好reduce的个数最多和集 群中的reduce持平,或者⽐集群的 reduce slots⼩。这个对于⼩集群⽽⾔,尤其重 要。
MapReduce ⾼级
shuffle阶段
MapReduce会确保每个reducer的输⼊都是按键排序的。从map⽅法输出数据开 始、到作为输⼊数据传给reduce⽅法的过程称为shuffle。在此,我们将学习shuffle 是如何⼯作的,因为它有助于我们理解⼯作机制(如果需要优化MapReduce程 序)。shuffle属于不断被优化和改进的代码库的⼀部分,因此会随着版本的不同, 细节上可能会发⽣变量。
map 端
每个map任务都会有⼀个环形内存缓冲区⽤于存储map的输出数据。在默认情况 下,缓冲区的⼤⼩为100MB,这个值可以通过mapreduce.task.io.sort.mb属性来调 整。⼀旦缓冲区的内容达到阙值(默认是0.8,或者是80%,属性是 mapreduce.map.sort.spill.percent),⼀个后台线程便开始把内容溢写(spill)到磁 盘⾥,这个位置由属性mapreduce.cluster.local.dir来指定的。在将数据溢写到磁盘 过程中,map的输出数据继续写到缓冲区,但如果在此期间缓冲区被填满,map会 被阻塞直到写磁盘过程完成。
在写磁盘之前,线程会根据分区器的逻辑把数据划分为不同的分区(partition)。然 后,在每个分区中,后台线程会按键进⾏内存中排序(QuickSort,默认是字典顺 序)。如果指定了⼀个combiner函数,它就在排序后的输出上运⾏。运⾏combiner 函数使得map输出结果更紧凑,因此减少写到磁盘的数据和传递给reducer的数据。 每次内存缓冲区达到溢出阖值,就会新建⼀个溢出⽂件(spill file),因此在map任务写 完其最后⼀个输出记录之后,可能会有⼏个溢出⽂件。在MapTask任务完成之前, 多个溢出⽂件被合并成⼀个已分区且已排序的输出⽂件。配置属性 mapreduce.task.io.sort.factor控制着⼀次最多能合并多少个⽂件,默认值是10。 如果⾄少存在3个溢出⽂件(通过mapreduce.map.combine.minspills属性设置)时, 则combiner就会在输出⽂件写到磁盘之前再次运⾏。combiner可以在输⼊上反复运 ⾏,但并不影响最终结果。如果只有1或2个溢出⽂件,那么由于map输出规模减 少,因⽽不值得调⽤combiner产⽣开销,因此不会为该map输出再次运⾏ combiner。 为了使写磁盘的速度更快,节约磁盘空间,并且减少传给reducer的数据量,在溢写 到磁盘的过程中对数据进⾏压缩往往是个很好的主意。在默认情况下,输出是不压缩 的,但只要将mapreduce.map.output, compress设置为true,就可以轻松启⽤此功 能。使⽤的压缩库由mapreduce.map.output.compress.codec指定,要想进⼀步了 解压缩格式
Reduce端
reducer通过HTTP得到输出⽂件的分区。⽤于⽂件分区的⼯作线程的数量由任务的 mapreduce. shuffle.max. threads属性控制,此设置针对的是每⼀个节点管理器, ⽽不是针对每个map任务。 现在转到处理过程的reduce部分。map输出⽂件位于运⾏MapTask的本地磁盘(注 意,尽管map输出经常写到MapTask本地磁盘,但reduce输出并不这样)。现在, tasktracker需要为分区⽂件运⾏reduce任务。并且, reduce任务需要集群上若⼲个 map任务的map输出作为其特殊的分区⽂件。每个map任务的完成时间可能不同, 因此在每个任务完成时,reduce任务就开始复制其输出。这就是reduce任务的复制 阶段。reduce任务有少量复制线程,因此能够并⾏取得map输出。默认值是5个线 程,但这个默认值可以修改设置mapreduce.reduce.shuffle. parallelcopies 属性即 可。
map任务成功完成后,它们会使⽤⼼跳机制通知它们的application master。因此, 对于指定作业,application master知道map输出和主机位置之间的映射关系。 reducer中的⼀个线程定期询问master以便获取map输出主机的位置,直到获得所有输 出位置。 由于第⼀个reducer可能失败,因此主机并没有在第⼀个reducer检索到map输出时就 ⽴即从磁盘上删除它们。相反,主机会等待,直到application master告知它删除 map输出,这是作业完成后执⾏的。
如果map输出相当⼩,会被复制到reduce任务JVM的内存(缓冲区⼤⼩由 mapreduce.reduce.shuffle.input. buffer.percent 属性控制,指定⽤于此⽤途的堆 空间的百分⽐),否则,map输出被复制到磁盘。⼀旦内存缓冲区达到阖值⼤⼩(由 mapreduce.reduce.shuffle.merge.percent 决定)或达到 map 输出阖值(由 mapreduce. reduce. merge. inmem .threshold 控制),则合并后溢出写到磁盘 中。如果指定combiner,则在合并期间运⾏它以降低写⼊硬盘的数据量。 随着磁盘上的溢写⽂件数量增多,后台线程会将它们合并为更⼤的、排好序的⽂件。 这会为后⾯的合并节省⼀些时间。注意,为了合并,压缩的map输出(通过map任 务)都必须在内存中被解压缩。
复制完所有map输出后,reduce任务进⼊排序阶段(更恰当的说法是合并阶段,因 为排序是在map端进⾏的),这个阶段将合并map输岀,维持其顺序排序。这是循 环进⾏的。⽐如,如果有50个map输出,⽽合并因⼦是10(10为默认设置,由 mapreduce.task. io.sort.factor ,与 的合并类似),合并将进⾏ 5 趟 ,每趟将10个 ⽂件合并成⼀个⽂件,因此最后有5个中间⽂件。 在最后阶段,即reduce阶段,直接把数据输⼊reduce函数,从⽽省略了⼀次磁盘往 返⾏程,并没有将这5个⽂件合并成⼀个已排序的⽂件作为最后⼀趟。最后的合并可 以来⾃内存和磁盘⽚段。
shuffle流程总结
1. 从map函数输出到reduce函数接受输⼊数据,这个过程称之为shuffle. 2. map函数的输出,存储环形缓冲区(默认⼤⼩100M,阈值80M) 环形缓冲区:其实是⼀个字节数组kvbuffer. 有⼀个sequator标记,kv原始数 据从左向右填充(顺时针), kvmeta是对kvbuffer的⼀个封装,封装成了int数组,⽤于存储kv原始数据的对 应的元数据valstart, keystart,partition,vallen信息,从右向左(逆时针)。参考(环形缓冲区的 详解⼀张) 3. 当达到阈值时,准备溢写到本地磁盘(因为是中间数据,因此没有必要存储在HDFS 上)。在溢写前要进⾏对元数据分区(partition)整理,然后进⾏排序(quick sort, 通过元数据找到出key,同⼀分区的所有key进⾏排序,排序完,元数据就已经有序 了,在溢写时,按照元数据的顺序寻找原始数据进⾏溢写) 4. 如果有必要,可以在排序后,溢写前调⽤combiner函数进⾏运算,来达到减少数 据的⽬的 5. 溢写⽂件有可能产⽣多个,然后对这多个溢写⽂件进⾏再次合并(也要进⾏分区和排 序)。当溢写个数>=3时,可以再次调⽤combiner函数来减少数据。如果溢写个数<3 时,默认不会调⽤combiner函数。 6. 合并的最终溢写⽂件可以使⽤压缩技术来达到节省磁盘空间和减少向reduce阶段传 输数据的⽬的。(存储在本地磁盘中) 7. Reduce阶段通过HTTP写抓取属于⾃⼰的分区的所有map的输出数据(默认线程数是 5,因此可以并发抓取)。 8. 抓取到的数据存在内存中,如果数据量⼤,当达到本地内存的阈值时会进⾏溢写操 作,在溢写前会进⾏合并和排序(排序阶段),然后写到磁盘中, 9. 溢写⽂件可能会产⽣多个,因此在进⼊reduce之前会再次合并(合并因⼦是10),最 后⼀次合并要满⾜10这个因⼦,同时输⼊给reduce函数,⽽不是产⽣合并⽂件。 reduce函数输出数据会直接存储在HDFS上。
在Hadoop这样的集群环境中,⼤部分map task与reduce task的执⾏是在不同的节点 上。当然很多情况下Reduce执⾏时需要跨节点去拉取其它节点上的map task结果。如 果集群正在运⾏的job有很多,那么task的正常执⾏对集群内部的⽹络资源消耗会很严 重。这种⽹络消耗是正常的,我们不能限制,能做的就是最⼤化地减少不必要的消耗。还 有在节点内,相⽐于内存,磁盘IO对job完成时间的影响也是可观的。从最基本的要求来 说,我们对shuffle过程的期望可以有: a) 完整地从map task端拉取数据到reduce 端。 b) 在跨节点拉取数据时,尽可能地减少对带宽的不必要消耗。 c) 减少磁盘IO对task执⾏的影响。