MR介绍
MapReduce 是 Google 推广的一个简单的编程模型,它对以高度并行和可扩展的方式处理大数据集很有用。 MapReduce 的灵感来源于函数式编程,用户可将他们的计算表达为 map 和 reduce 函数,将数据作为键值对来处理。 Hadoop 提供了一个高级 API 来在各种语言中实现自定义的 map 和 reduce 函数。
Hadoop 基础架构负责处理分布式处理的所有复杂方面:并行化、调度、资源管理、机器间通信、软件和硬件故障处理等等。 得益于这种干净的抽象,实现处理数百(或者甚至数千)个机器上的数 TB 数据的分布式应用程序从未像现在这么容易过,甚至对于之前没有使用分布式系统的经验的开发人员也是如此。
MR执行前检查
MR提交到集群后,会做如下操作:
- 检查作业输出说明
- 计算作业输出划分Split
- 将作业所需要的资源(jar文件,配置文件,计算所得的输入划分)复制到集群中
备注:
- 关于Step 1:
判断输出文件是否存在的目的是为了保证数据的完整性. Hadoop设计思想认为: job都是庞大且耗时的, 如果能在执行前检查到错误并结束,会节省很多时间 - 关于Step 2: 计算 Split其实就是计算 Map 数
MR架构
MR将任务分割为 Map 端和 Reduce 端. 下图就是 Map/Reduce 过程图:
Map端
- 读取HDFS
- Split(见'Split(分割)')
- Map运算-具体业务
- Shuffle(见'Map端的Shuffle')
- Map端的所有工作都已结束, 最终生成的这个文件也存放在TaskTracker够得着的某个本地目录内
- 通知 JobTracker Task完成
当你想更改map的个数的时候:
1). 通过更改配置文件中block的size来增大或者减小map的个数
2). 通过 JobConf's conf.setNumMapTasks(int num)
但是就算你设置了数目在这里,它在实际运行中的数目不会小于它实际分割产生的数目。意思就是当你通过程序设置map为2个,但是在读入数据的时候,分割数据是需要3个,那么最后任务在实际运行的过程中map个数是3个而不是你设置的2个
Map的个数是由任务本身的数据量决定的
Reduce端
- Shuffle(见'Reduce端的Shuffle')
- Reducer 处理输入,写入 HDFS
- Reducer去Mapper中拉取文件
- Mapper会Merge出结果文件,直到文件生成之前,Reducer无法拉取
- 多个Mapper间的文件是可以被Reducer并行拉取的
当你想修改reduce的个数那么可以按照如下方法进行更改:
1). 在conf设置中调用conf.setStrings("mapred.reduce.tasks", values)
2). 调用job.setNumReduceTasks(tasks)
Reduce的个数hadoop是默认设置为1的,因为一个任务的输出的文件个数是由reduce的个数来决定的
总结:
- Split 决定Map数
- Shuffle 过程横跨 Map 和 Reduce 两端
- 排序与合并过程横跨 Map 和 Reduce 两端
- Map的个数为动态, Reduce的个数默认为1
- 很多人的误解如果不使用 Combiner便不会排序,这是错误的,不管你用不用 Combiner,Map Task 均会对产生的数据排序(如果没有 Reduce Task,则不会排序,实际上 Map 阶段的排序就是为了减轻 Reduce端排序负载)
- 在hadoop 1.x 中排序无法避免,也不可以关闭,但 hadoop2.x 是可以关闭的
- 据说: 排序是影响性能的要素!
- 据说: IO是影响性能的要素! IO确实不少且无法避免
Split(分割)
在执行mapreduce之前,原始数据被分割成若干split,每个split作为一个map任务的输入
FileInputFormat: 所有以文件作为数据源的InputFormat实现的基类,它有2个功能:
- List<InputSplit> getSplits(): 获取由输入文件计算出输入分片,解决数据或文件分割成片问题
- RecordReader<K,V> createRecordReader(): 创建RecordReader,从InputSplit中读取数据
SplitSize计算:
splitSize = max(minsize,min(maxSize,blockSize)) = 64M;
maxSize = mapred.max.split.size 默认最大值整数值
minSize = mapred.min.split.size 默认0
通过公式可以看出:
- FileInputFormat只划分比HDFS block大的文件
- 如果一个文件的大小比block小,将不会被划分
- 例如: 一个1G的文件, 会被划分成16个64MB的split,并分配16个map任务处理
- 例如: 10000个100kb的文件会被10000个map任务处理
为什么默认分片大小与分块大小是相同的原因??
hadoop在存储有输入数据(HDFS中的数据)的节点上运行map任务,可以获得高性能,这就是所谓的数据本地化。所以最佳分片的大小应该与HDFS上的块大小一样,因为如果分片跨越2个数据块,对于任何一个HDFS节点(基本不肯能同时存储这2个数据块),分片中的另外一块数据就需要通过网络传输到map任务节点,与使用本地数据运行map任务相比,效率则更低
小文件问题:
如果一个文件的大小比block小,将不会被划分,所以每一个小文件都会被当做一个split并分配一个map任务,这也是Hadoop处理大文件的效率要比处理很多小文件的效率高的原因(10000个100kb的文件会被10000个map任务处理,肯定排队呀!
Shuffle
Shuffle描述着数据从map task输出到reduce task输入的这段过程
Shuffle 过程横跨 Map 和 Reduce 两端
在Hadoop这样的集群环境中,大部分map task与reduce task的执行是在不同的节点上。 当然很多情况下Reduce执行时需要跨节点去拉取其它节点上的map task结果。 如果集群正在运行的job有很多,那么task的正常执行对集群内部的网络资源消耗会很严重。 这种网络消耗是正常的,我们不能限制,能做的就是最大化地减少不必要的消耗。 还有在节点内,相比于内存,磁盘IO对job完成时间的影响也是可观的。从最基本的要求来说, 我们对Shuffle过程的期望可以有:
- 完整地从map task端拉取数据到reduce 端
- 在跨节点拉取数据时,尽可能地减少对带宽的不必要消耗
- 减少磁盘IO对task执行的影响
Map端的Shuffle
先看看map端的情况, 通过下图希望让大家清晰地了解从map数据输入到map端所有数据准备好的全过程:
下面我对细节来一一说明(注意红色圆数字):
- 步骤1(输入/Split):
在map task执行时,它的输入数据来源于HDFS的block,当然在MapReduce概念中,map task只读取split。Split与block的对应关系可能是多对一,默认是一对一。 - 步骤2(Partition):
通过MapReduce提供的Partitioner接口, Mapper 输出映射到 Reducer 上
确定 Reducer 后, 将数据写入'内存缓冲区'中,缓冲区的作用是批量收集map结果,减少磁盘IO的影响
***写入之前,key 与value 值都会被序列化成字节数组
- 步骤3(Spill, Sort, Combiner):
本步骤会发生Spill, Sort, Combiner. 具体请见相应的部分~~~
当map task输出结果很多时。Map会将数据写入磁盘,保证缓冲区可以重复使用. 这个叫Spill(具体见'Spill')
当Spill时, MR程序默认会对写入内容按key排序, 然后合并(Combiner), 详情见'Combiner'
- 步骤4(Merge):
每次溢写会在磁盘上生成一个溢写文件, 如果map的输出结果真的很大就会生成多个溢写文件
当map task真正完成时,内存缓冲区中的数据也全部溢写到磁盘中形成一个溢写文件
故当map task真正完成时,会有一个或多个溢写文件,所以需要将这些溢写文件归并到一起,这个过程就叫做Merge
Merge过程中如果client设置过Combiner, 也会使用Combiner来合并相同的key
总结:
- Mapper 输出映射到 Reducer发生在 Partition 过程中
- Mapper 输出写入缓冲区时,未进行排序与合并
- 排序与合并发生在 Spill与Merge 两个阶段
- 序列化发生在 Partition 阶段
- 内存缓冲区中的数据最终要生成文件,这也是Hadoop采用文件中转的原因. 区别于Spark
Reduce端的Shuffle
通过下图描述Reduce端的Shuffle细节:
下面我对细节来一一说明(注意红色圆数字):
- 步骤1(Copy):
简单地拉取数据. Reduce进程启动一些数据copy线程(Fetcher),通过HTTP方式请求map task所在的TaskTracker获取map task的输出文件。
因为maptask早已结束,这些文件就归TaskTracker管理在本地磁盘中。 - 步骤2(Merge):
Copy 过来的数据会先放入内存缓冲区中,这里的缓冲区大小要比 map 端的更为灵活,它基于 JVM 的 heap size 设置,因为 Shuffle 阶段 Reducer 不运行,所以应该把绝大部分的内存都给 Shuffle 用。Merge 有三种形式:
- 内存到内存: 默认情况下不启用,让人比较困惑
- 内存到磁盘: 当内存中的数据量到达一定阈值就会写入磁盘. 与Map端Spill类似,也会排序和合并
- 磁盘到磁盘: 当Map端数据读取完成后. 启动磁盘到磁盘的Merge方式生成Reducer输入文件
- 步骤3(Reducer的输入):
Merge 的最后会生成一个文件,大多数情况下存在于磁盘中,但是需要将其放入内存中。 当reducer 输入文件已定,整个 Shuffle 阶段才算结束。然后就是 Reducer 执行,把结果放到 HDFS 上。
总结:
- Reducer真正运行之前,所有的时间都是在拉取数据,做Merge
- 在 Merge 过程中会发生排序与合并,包括 '内存到磁盘' 与 '磁盘到磁盘'
分区(Partition)
Mapper后的结果集该传递给哪个reduce去做下一步操作, 如何决定??? 答案就是通过MapReduce提供的Partitioner接口
它的作用就是根据key或value及reduce的数量来决定当前的输出数据最终应该交由哪个reduce task处理
Partition是shuffle的一部分
默认规则如下:
对key进行hash后,除以reduce task数量取模, 取模方式只是为了平均reduce的处理能力
如果用户自己对Partitioner有需求,可以订制并设置到job上
溢写(Spill)
当map task输出结果很多时就可能发生内存溢出,所以需要在一定条件下将缓冲区的数据临时写入磁盘,然后重新利用这块缓冲区。这个从内存往磁盘写数据的过程被称为 Spill,中文可译为溢写
溢写是由单独线程来完成,不影响往缓冲区写map结果的线程
整个缓冲区有个溢写的比例(spill.percent), 这个比例 默认是0.8 ,故溢写线程启动时不应该阻止map的结果输出
当缓冲区的数据已经达到阈值时,溢写线程启动锁定这80%的内存,执行溢写过程。 Map task的输出结果还可以往剩下的20%内存中写,互不影响
合并(Combine)
在 Spill 过程中, MR程序会将数据按key合并到一块,这个过程叫reduce也叫combine
但MapReduce的术语中,reduce只指reduce端执行从多个map task取数据做计算的过程。除reduce外,非正式地合并数据只能算做combine了
注意:非正式地合并叫combine,其实大家知道的,MapReduce中将Combiner等同于Reducer
- Combine 的作用是优化结果,减少溢写到磁盘的数据量, 所以它在整个模型中会多次使用
- Combiner的输出是Reducer的输入,Combiner绝不能改变最终的计算结果, Combiner的使用一定得慎重
- 累加,最大值等Combine操作不影响最终结果, 平均值则不行~~
感激: https://blog.csdn.net/zengxiaosen/article/details/73189207 的文章