maptask并行度机制 : 逻辑规划 (逻辑切片)
时间 : 客户端提交mr程序之前 main(客户端) , job.submit()
地点 : 客户端
参与者 : 待处理数据目录 FileInputFormat.
getSplits()
规则 : 对待处理目录下
逐个
遍历文件 , 以切片大小对文件进行逻辑规划 split size == block size ==128M
结果 : 把形成的规划信息写在文件中
job.split
随着客户端提交程序 , 把规划文件提交yarn指定的路径下
问题1 : 如何决定分片大小的 ?
查看源码TextInputFormat中FileInputFormat中的getSplits()中的computeSplitSize()方法
Math.max(minSize, Math.min(maxSize, blockSize)) minSize 默认为1 blockSize 默认为128M maxSize 是Long.max
既然切片大小可以通过参数修改决定 , 有一个方向尽量保证
split size == block size
特殊1 : 小文件 , 如果存储了许多小文件该怎么办 ?
由于一个文件在小我们也需要用一个切片来进行存储 , 因此就会占用nn很大一部分内存
解决办法 :
- hdfs 的shell命令中的appendToFile()
本地java IO进行小文件合并
- 修改默认规则 , 源码 , combineInputFormat
我们一般先在本地把小文件合并成大文件(最好是block size)
特殊2 : 文件特别大
当文件特别大时 , 我们又该怎么去处理 ?
解决方案 : 改变hdfs上block size 256M 512M , 这时候默认切片大小等于block size , dfs.blicksize
特殊3 : 当文件大小刚刚超过128M , 怎么处理 ?
bytesRemaining)/splitSize > SPLIT_SLOP(1.1即超过10%才进行分片) 剩下未切的/切片大小 > 1.1 如果满足 , 继续切 如果不满足 , 剩下的全部作为最后一个切片 如 : 129/128 = 1.0078 < 1.1 故129M作为一个切片
特殊4 : 当数据在一部分在上一个切片的最后一行 , 另一部分在下一个切片的最上面一行时怎么办 ?
由于hdfs在存储文件的时候不会考虑数据的完整性 , 只要够128M就切一刀 , 因此
只能在mr读数据的层面解决 :
每个maptask都多读取下一个切片的第一行
如果maptask不是第一个 , 都舍弃自己的第一行 , 从第二行读取
特殊5 : 如果切片大小就是不等于block大小 , 怎么办 ?
如果切片大小不等于block大小 , 意味着有的文件需要跨块读取 , 相对麻烦
Reducetask 数量的决定是可以直接手动设置 : job.setNumReduceTasks(4)
, 如果分布不均匀 , 就可能产生数据倾斜
当数据产生倾斜 , 怎么处理?
哈希打散
具体操作可以看上图演示
注意 : reducetask 数量并不是任意设置,还要考虑业务逻辑需求,有些情况下,需要计算全局汇总结果,就只能有 1 个 reducetask。
如何评估每个task执行的时间
最终方向 : 最好每个task的执行事件至少一分钟
==>可以在setup()初始化方法中增加开始时间 , cleanup()结束方法中增加结束时间 , 两者进行相减获得task执行时间
由于map或reduce task申请资源都需要耗费很大一部分时间 , 若执行时间较短, 很快就结束了 , 如果在需要又得申请资源 , 导致申请资源的时间过于多 , 而运行的时间太短 , 性能浪费太大 .
读取数据组件InputFormat(默认TextInputFormat)会通过getSplits方法对输入目录中文件进行逻辑切片规划
得到splits , 有多少个 split 就对应启动多少个 MapTask。split 与 block 的对应关系默认是一对一。
将 输 入 文 件 切 分 为 splits 之 后 , 由 RecordReader
对 象 ( 默 认LineRecordReader)进行读取,以\n 作为分隔符,读取一行数据,返回
。Key 表示每行首字符偏移值,value 表示这一行文本内容
读取 split 返回用户重写的 map 函数
。RecordReader 读取一行这里调用一次。
map 逻辑完之后,将 map 的每条结果通过 context.write 进行 collect
数据收集。在 collect 中,会先对其进行分区
处理,默认使用 HashPartitioner
。
MapReduce 提供 Partitioner 接口,它的作用就是根据 key 或 value 及 reduce 的数量来决定当前的这对输出数据最终应该交由哪个 reduce task 处理。默认对 key hash 后再以reduce task 数量取模
。默认的取模方式只是为了平均 reduce 的处理能力,如果用户自己对 Partitioner 有需求,可以订制并设置到 job 上。
接下来,会将数据写入内存,内存中这片区域叫做环形缓冲区
,缓冲区的作用是批量收集 map 结果,减少磁盘 IO 的影响。我们的 key/value 对以及Partition 的结果都会被写入缓冲区。当然写入之前,key 与 value 值都会被序列化成字节数组。
环形缓冲区其实是一个数组
, 默认大小是100M , 当数据达到80%是开始溢出(即开始写入磁盘)
当溢写线程启动后,需要对这 80MB 空间内的 key 做排序(Sort)
。排序是MapReduce 模型默认的行为,这里的排序也是对序列化的字节做的排序。
每次溢写会在磁盘上生成一个临时文件
, 当整个数据处理结束之后开始对磁盘中的临时文件进行merge 合并
,因为最终的文件只有一个,写入磁盘,并且为这个文件提供了一个索引文件
,以记录每个 reduce 对应数据的偏移量。
Reduce 大致分为copy、sort、reduce
三个阶段,重点在前两个阶段。copy阶段包含一个eventFetcher来获取已完成的map列表 , 由Fetcher线程去copy数据 , 会启动inMemoryMerger 和onDiskMerger,分别将内存中的数据 merge到磁盘和将磁盘中的数据进行 merge。待数据copy完成之后 , copy阶段完成 , 进行sort阶段 , sort阶段主要是执行finalMerge操作 , 纯粹的sort阶段 , 完成之后就是reduce阶段 , 调用用户定义的reduce函数进行处理.
由上图可以得出 , 合并分为三种情况 :
map 阶段处理的数据如何传递给reduce 阶段
,是 MapReduce 框架中最关键的一个流程,这个流程就叫 shuffle
。
shuffle: 洗牌、发牌——(核心机制:数据分区,排序,合并)。
shuffle 是 Mapreduce 的核心,它分布在 Mapreduce 的 map 阶段和 reduce阶段。一般把从 p Map 产生输出开始到 e Reduce 取得数据 作为输入 之前 的过程 称作 作 shuffle
。
以下参数是在用户自己的 MapReduce 应用程序中配置就可以生效
mapreduce.map.memory.mb
: 一个 Map Task 可使用的内存上限(单位:MB),默认为1024。如果 Map Task 实际使用的资源量超过该值,则会被强制杀死。mapreduce.reduce.memory.mb
: 一个 Reduce Task 可使用的资源上限(单位:MB),默认为 1024。如果 Reduce Task 实际使用的资源量超过该值,则会被强制杀死。mapreduce.map.cpu.vcores
: 每个 Maptask 可用的最多 cpu core 数目, 默认值: 1mapreduce.reduce.cpu.vcores
: 每个 Reducetask 可用最多 cpu core 数目默认值: 1应该在 yarn 启动之前就配置在服务器的配置文件中才能生效
shuffle 性能优化的关键参数,应在 yarn 启动之前就配置好
mapreduce.task.io.sort.mb
100 shuffle 的环形缓冲区大小,默认 100mmapreduce.map.sort.spill.percent
0.8 环形缓冲区溢出的阈值,默认 80% , 即达到80%就溢出shuffle是一个过程 , 描述了mr中maptask的数据如何交给reducetask处理之前的种种阶段。
从maptask输出数据到内存缓冲区开始 到reducetask取得数据进行reduce聚合之前
。
在shuffle的过程中,涉及了数据内存到磁盘 、磁盘到内存、内存再到磁盘的过程、所以十分耗时 性能不高
见以前的图 , 我们可以看到shuffle的工作位置
推测执行机制
,默认为 true, 如果为 true,则可以并行执行一些 Map 任务的多个实例。推测执行机制(Speculative Execution):它根据一定的法则推测出“拖后腿”的任务,并为这样的任务启动一个备份任务,让该任务与原始任务同时处理同一份数据,并最终选用最先成功运行完成任务的计算结果作为最终结果。(通过算法找出拖后腿的短板任务,为其启动备份任务,备份任务跟原任务一模一样,最终谁先处理完,谁的结果就作为最终结果。)
MapReduce 计数器(Counter)为我们提供一个窗口,用于观察 MapReduce Job 运行期的各种细节数据。
内置计数器包括:
文件系统计数器(File System Counters)
作业计数器(Job Counters)
MapReduce 框架计数器(Map-Reduce Framework)
Shuffle 错误计数器(Shuffle Errors)
文件输入格式计数器(File Output Format Counters)
文件输出格式计数器(File Input Format Counters)
Hadoop也支持自定义计数-----全局计数
, 如果我们不自定义计数会造成线程的锁事务
mr自定义计数器 最大的用处在于 全局计数
Counter counter = context.getCounter("ItcastCounter", "helloCounter");
counter.increment(1);
public class WordCount{
static class WordCount Mapper extends Mapper<LongWritable, Text, Text, LongWritable> {
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException,InterruptedException {
Counter counter =context.getCounter(“SelfCounters”,”myCounters”);
String[] words = value.toString().split(",");
for (String word : words) {
if("hello".equals(word)){counter.increment(1)};
context.write(new Text(word), new LongWritable(1));
}
}
}
Apache Hadoop YARN (Yet Another Resource Negotiator,另一种资源协调者)是一种新的 Hadoop 资源管理器,它是一个通用资源管理系统和调度平台,可为上层应用提供统一的资源管理和调度,它的引入为集群在利用率、资源统一管理和数据共享等方面带来了巨大好处。
可以把 yarn 理解为相当于一个分布式的操作系统平台,而 mapreduce 等运算程序则相当于运行于操作系统之上的应用程序,Yarn 为这些程序提供运算所需的资源(内存、cpu)
yarn 并不清楚用户提交的程序的运行机制
yarn 只提供运算资源的调度(用户程序向 yarn 申请资源,yarn 就负责分配资源)
yarn 中的主管角色叫 ResourceManager
yarn 中具体提供运算资源的角色叫 NodeManager
yarn与运行的用户程序完全解耦,意味着yarn上可以运行各种类型的分布式运算程序,比如 mapreduce、storm,spark,tez ……
spark、storm 等运算框架都可以整合在 yarn 上运行,只要他们各自的框架中有符合yarn 规范的资源请求机制即可
yarn 成为一个通用的资源调度平台.企业中以前存在的各种运算集群都可以整合在一个物理集群上,提高资源利用率,方便数据共享
YARN 是一个资源管理、任务调度的框架,主要包含三大模块:ResourceManager(RM)、NodeManager(NM)、ApplicationMaster(AM)。
ResourceManager 负责所有资源的监控、分配和管理;
ApplicationMaster 负责每一个具体应用程序的调度和协调;
NodeManager 负责每一个节点的维护。
对于所有的 applications,RM 拥有绝对的控制权和对资源的分配权。而每个 AM 则会和RM 协商资源,同时和 NodeManager 通信来执行和监控 task。
注 :
RM 只负责监控 AM,并在 AM 运行失败时候启动它。RM 不负责 AM 内部任务的容错,任务的容错由 AM 完成。
在这种机制下,任何程序在yarn上执行变成如下模式:
由上图我们得知mr提交yarn的交互流程
- 客户端请求运行程序
hadoop jar wordcount.jar xxx
- 检查申请是否符合运行的需求 ,
生成一个jobid , 提交资源的路径
- 返回给客户端
- 请求资源用于运行本次程序的applicationMaster(MrAppMaster)
- RM收到请求 , 并且通过NM返回一个可用的资源容器所在的机器位置NM1
- 返回地址
- 客户端到指定的机器容器里启动本次程序的MrAPPMaster , 申请对应的container
- MrAPPMaster启动成功之后向
RM注册自己
, 并且保存通信- MrAppMaster通过读取本次任务切片信息 , 向RM申请对应数量的容器用于运行MapTask , 比如切片数3
- 向RM申请资源运行MapTask
- RM接收到请求 , 并且通过NM返回与申请数量对应的资源容器位置(NM1,NM2,NM2)
- 返回给MrAppMaster
- MrAppMaster到返回的容器里启动MapTask , 并且追踪task的执行情况
- maptask运行结束 , MrAppMaster向RM申请 , 容器资源使用完毕 , 请注销
- 如果程序还有下一个阶段ReduceTask , MrAppMaster继续去申请容器资源
- 当所有的task执行完毕 , MrAppMaster向RM申请注销自己 , 收回资源
官方语言解释
client 向 RM 提交应用程序,其中包括启动该应用的 ApplicationMaster 的必须信息,例如 ApplicationMaster 程序、启动 ApplicationMaster 的命令、用户程序等。
ResourceManager 启动一个 container 用于运行 ApplicationMaster。
启动中的 ApplicationMaster 向 ResourceManager 注册自己,启动成功后与 RM 保持心跳。
ApplicationMaster 向 ResourceManager 发送请求,申请相应数目的 container。
ResourceManager 返回 ApplicationMaster 的申请的 containers 信息。申请成功的container,由 ApplicationMaster 进行初始化。container 的启动信息初始化后,AM与对应的 NodeManager 通信,要求 NM 启动 container。AM 与 NM 保持心跳,从而对 NM上运行的任务进行监控和管理
container 运行期间,ApplicationMaster 对 container 进行监控。container 通过 RPC
协议向对应的 AM 汇报自己的进度和状态等信息。
应用运行期间,client 直接与 AM 通信获取应用的状态、进度更新等信息。
应用运行结束后,ApplicationMaster 向 ResourceManager 注销自己,并允许属于它的
container 被收回。
Yarn 中,负责给应用分配资源的就是 Scheduler
。
在Yarn中有三种调度器可以选择:FIFO Scheduler,Capacity Scheduler,FairScheduler
FIFO Scheduler
FIFO Scheduler 把应用按提交的顺序排成一个队列,这是一个 先进先出队列,在进行资源分配的时候,先给队列中最头上的应用进行分配资源,待最头上的应用需求满足后再给下一个分配,以此类推。就一个通道 , 先进先出 , 如果前面有耗时的大队列 , 会造成阻塞
Capacity Scheduler
Capacity 调度器允许多个组织共享整个集群,每个组织可以获得集群的一部分计算能力。分为大和小的两个通道 , 当小的多时 , 大的通道利用较低 , 因此造成资源利用不合理
Fair Scheduler
Fair 调度器会为所有运行的job 动态的调整
系统资源。
会动态的调整 , 但是当程序申请停止某些资源 , 在执行小文件 , 这时候是很费性能的
示例:Capacity 调度器 配置使用
调度器的使用是通过 yarn-site.xml 配置文件中的
yarn.resourcemanager.scheduler.class 参数进行配置, 默认采用 CapacityScheduler 调度器。
假设我们有如下层次的队列:
root
├── prod
└── dev
├── mapreduce
└── spark
文件名为 capacity-scheduler.xml
<configuration>
<property>
<name>yarn.scheduler.capacity.root.queuesname>
<value>prod,devvalue>
property>
<property>
<name>yarn.scheduler.capacity.root.dev.queuesname>
<value>mapreduce,sparkvalue>
property>
<property>
<name>yarn.scheduler.capacity.root.prod.capacityname>
<value>40value>
property>
<property>
<name>yarn.scheduler.capacity.root.dev.capacityname>
<value>60value>
property>
<property>
<name>yarn.scheduler.capacity.root.dev.maximum-capacityname>
<value>75value>
property>
<property>
<name>yarn.scheduler.capacity.root.dev.mapreduce.capacityname>
<value>50value>
property>
<property>
<name>yarn.scheduler.capacity.root.dev.spark.capacityname>
<value>50value>
property>
configuration>
我们可以看到,dev 队列又被分成了 mapreduce 和 spark 两个相同容量的子队列。dev的 maximum-capacity 属性被设置成了 75%,所以即使 prod 队列完全空闲 dev 也不会占用全部集群资源,也就是说,prod 队列仍有 25%的可用资源用来应急。我们注意到,mapreduce和 spark 两个队列没有设置 maximum-capacity 属性,也就是说 mapreduce 或 spark 队列中的 job 可能会用到整个 dev 队列的所有资源(最多为集群的 75%)。而类似的,prod 由于没有设置 maximum-capacity 属性,它有可能会占用集群全部资源。
关于队列的设置,这取决于我们具体的应用。比如,在 MapReduce 中,我们可以通过mapreduce.job.queuename 属性指定要用的队列。如果队列不存在,我们在提交任务时就会收到错误。如果我们没有定义任何队列,所有的应用将会放在一个 default 队列中。
注意 : 对于 Capacity 调度器,我们的队列名必须是队列树中的最后一部分
,如果我们使用队列树则不会被识别。
/root
/prod 40%
/dev 60% --->75%
蚕食
当集群设置队列以后 提交mr程序需要指定队列名
队列名必须是队列树中的最后一部分
mapreduce.job.queuename = /root/dev/mapreduce xxxxx
mapreduce.job.queuename = mapreduce 对
mapreduce.job.queuename = prod 对