map-reduce job执行过程和优化


本文将梳理一个MapReduce任务详细的执行过程,并通过图解的方式,说明中间我们可控的部分和常用的优化手段。

提交 job到Yarn的流程

在Hadoop1.x中map-reduce job通过job traker执行,多个任务便会有多个job traker。job traker 负责资源分配,任务执行状态的监控等等。它做了所有的事情,所以不利于扩展,并且job traker存在单点故障,一旦job traker崩溃。这个任务的信息就全丢了。所以在Hadoop2.x中引入了Yarn, 它不但可以支持HA(高可用)部署,而且可以支持不同的计算(不只是map-reduce job,还包括spark,graph计算等等符合规范的计算模型),所以Yarn是一个独立的资源调度框架,不仅仅智能用于map-reduce程序。

Yarn是一个资源调度平台,可以理解为一个分布式操作系统。而MapReduce程序相当于操作系统上的应用程序。

  • YARN组成

    ResourceManager:接收客户端请求,负责全局资源分配调度。

    NodeManager: 负责某个节点的资源调度

    Container : 资源的抽象。

    ApplicationMaster : 负责MapReduce作业的生命周期管理。其实现为MRAppMaster。

  • YARN工作机制

    • 1.用户向YARN中提交应用程序:

      Client向RM申请一个作业id。RM给Client返回该job资源的提交路径和作业id。Client提交jar包、切片信息和配置文件 (job.xml,job.split)到指定的资源提交路径。

    • 2.申请运行mrAppMaster:

      该请求会先封装成Tastk放入调度队列。其中一个NodeManager领取到Task任务,执行时ResourceManager与对应的Node-Manager通信,告知其分配第一个Container,要求它在这个Container中启动应用程序的ApplicationMaster。

    • 3.MRAppmaster向RM申请资源,创建MapTask:

      ApplicationMaster首先会检查分片等信息,判断是否采用uber job模式,如果不采用uber job模式(此模式,不会启动新的Container,会利用AM的container执行任务),则根据分片申请对应数量的Container资源,启动对应的MapTask. 并行计算。这个过程,ApplicationMaster会将jar包等任务文件拷贝到数据所在的NodeManager节点,然后进行任务启动计算。(这里就是到数据所在的地方计算,避免数据文件拷贝,只拷贝jar包等任务规划和计算程序). 计算过程中,Tasks会想AM 汇报进度等信息,如果失败,AM会将任务分配给其他节点。

    • 4.申请ReduceTask:

      MrAppMaster等待所有MapTask运行完毕后,向RM申请容器,运行ReduceTask。ReduceTask向MapTask获取相应分区的数据。

    • 程序运行完毕后,MR会向RM申请注销自己。

过程可以分为:提交——》启动MRAppMaster ——》任务执行——》完成销毁

Map-Reduce过程详解和优化

一个MapReduce任务,从HDFS读取文件开始。到进行切片创建任务,计算,其过程我画了一张图参照。同时,我们可以分析,在每一步,我们可以做哪些事,可以进行哪些优化。

在这里插入图片描述

  • ①:数据读取和切片
  • ②:map业务处理
  • ③:map的merge阶段
  • ④:mege和归并排序
  • ⑤:拷贝Map结果时,先拷入内存缓冲,内存不够,会溢写到磁盘。mergeSort阶段
  • ⑦:分组控制
  • ⑧:写出控制

shuffle指的是map()方法之后,reduce()方法之前的操作。所以包括了,map溢写时,分区排序规则设定,分区器设定,combiner处理,数据压缩,reduceTask进行的mergeSort,分组等等操作都是shuffle的阶段。

首先,HDFS在存储文件时,可能会出现一个问题,即小文件过多,这会为MapReduce阶段带来很多问题,比如切片过多导致的任务数量飙升,从而吃掉很多内存。在业务处理之前,存入HDFS阶段,我们可以通过下面方法,避免存入过多小文件。

  • 数据采集阶段将小文件合并后上传(利用Hardoop Archive 工具将小文件打包成HAR文件)
  • SequenceFile 存储,格式key/value ,可以将key设置为文件名,value设置为文件内容,即可将多个小文件合并为一个大文件。
  • 通过MapReduce程序合并小文件(MR除过业务处理,也可以做前期的数据清洗合并)

实际开发中,我们不可避免HDFS已经存入大量小文件,这时,我们就要在MapReduce阶段采取措施:

  • 在①阶段采用CombineTextInputFormat提高效率
  • 启用uber job模式,该模式会启动jvm重用。

其次,在数据输入时,我们可以根据业务需要调整InputFormat的类型,它可以控制切片的方式和读取数据的方式(默认是一行行读取,key是偏移量)。

protected void map(LongWritable key, Text value, Context context)

这里的key,value底层就是通过InputFormat类读取后作为参数,传入map方法。默认使用的TextInputFormat,每次读取一行,key是每行第一个字符在全文的偏移量,比如一个文本文件,第一次读取,key为0,vale为第一行字符串,假如第一行有10个字符,第二次读取时,key就是10 以此类推。如果我们想改变这种读取方式,我们可以自定义InputFormat。也可以使用KeyValueTextInputFormat

不同的InputFormat切片机制也不同,总结如下。

——|FileInputFormat

​ ——|TextInputFormat :读取一行,key是偏移量 (采用默认分片机制)

​ ——|NLineInputFormat :读取行,key是偏移量。(按设置的行数 分片)

​ ——|KeyValueTextInputFormat:读取一行的第一个字符串(\t标识)作为key,剩余内容作为value (分片采用默认分片机制 )

​ ——|CombineFileInputFormat :读取行,key是偏移量 (分片采用文件组合方式)

默认切片规则:按照文件和块大小切片。比如一个文件 200M,会切分一个128M和一个72M 两片。切片的数量,决定了MapTask的数量。

这时到了第②步,在这里,我们可以做下面的事情:

  • 修改分区内排序的规则

    默认分区内排序是安装字典顺序排序,我们可以根据业务需求,自定义排序规则。具体做法如下:

    1.先定义一个Bean对象,实现序列化,并且实现WritableComparable接口,然后重写排序方法。

    2.在map方法中读取内容后,封装为bean对象,写出。

  • 修改分区策略

    map端读取数据,会写入环形缓冲区,当达到80%容量时,会进行磁盘溢写,写到不同的分区文件上。默认分区策略时hash取余。我们可以根据需要自定义分区。步骤如下:

    1.写一个类继承Partitioner,在getPartition方法中编写分区逻辑,返回值从0开始,代表不同情况,数据写到不同的分区。

    2.在Driver类中设置分区类

    3.要显示设置对应梳理的ReduceTask

    第三步要设置,是因为,不同分区会交给不同的ReduceTask分别处理。而ReduceTask的数量,是根据默认分区策略计算的,为了保证所有分区都能被计算,必须显式声明。

    job.setPartitionerClass(MyPartitioner.class);
    //同时指定相应数量的reduce task
    job.setNumReduceTasks(5);
    
  • 环形缓冲区默认大小是100M,可以根据需要适当增加,减少溢写次数。

    具体设置是在mapred-site.xml通过mapreduce.task.io.sort.mb参数设定。

Mapper中的Kvbuffer的大小默认100M,可以通过mapreduce.task.io.sort.mb(default:100)参数来调整。可以根据不同的硬件尤其是内存的大小来调整,调大的话,会减少磁盘spill的次数此时如果内存足够的话,一般都会显著提升性能。spill一般会在Buffer空间大小的80%开始进行spill(因为spill的时候还有可能别的线程在往里写数据,因为还预留空间,有可能有正在写到Buffer中的数据),可以通过mapreduce.map.sort.spill.percent(default:0.80)进行调整,Map Task在计算的时候会不断产生很多spill文件,在Map Task结束前会对这些spill文件进行合并,这个过程就是merge的过程。mapreduce.task.io.sort.factor(default:10),代表进行merge的时候最多能同时merge多少spill,如果有100个spill个文件,此时就无法一次完成整个merge的过程,这个时候需要调大mapreduce.task.io.sort.factor(default:10)来减少merge的次数,从而减少磁盘的操作;

Spill这个重要的过程是由Spill线程承担,Spill线程从Map任务接到“命令”之后就开始正式干活,干的活叫SortAndSpill,原来不仅仅是Spill,在Spill之前还有个颇具争议性的Sort。

然后,第④步完之后,map阶段工作就完成了。在收集map阶段各个分区到reduce阶段处理之前,我们还可以做两件事。

  • combiner

    combiner父类就是Reducer,所以其方法和reducer方法一样,但是区别是处理的节点是在MapTask运行所在的节点,

    而Reducer是收集全局的数据,所以combiner中的处理就相当于在全局收集之前,先做一些处理。这样可以减少网络传输量。

    Combiner存在的时候,此时会根据Combiner定义的函数对map的结果进行合并,什么时候进行Combiner操作呢???和Map在一个JVM中,是由min.num.spill.for.combine的参数决定的,默认是3,也就是说spill的文件数在默认情况下有三个的时候就要进行combine操作,最终减少磁盘数据;

  • 数据压缩

    数据压缩可以减少网络传输。可以启用。常用的Lzo压缩。

    减少磁盘IO和网络IO还可以进行:压缩,对spill,merge文件都可以进行压缩。中间结果非常的大,IO成为瓶颈的时候压缩就非常有用,可以通过mapreduce.map.output.compress(default:false)设置为true进行压缩,数据会被压缩写入磁盘,读数据读的是压缩数据需要解压,在实际经验中Hive在Hadoop的运行的瓶颈一般都是IO而不是CPU,压缩一般可以10倍的减少IO操作,压缩的方式Gzip,Lzo,BZip2,Lzma等,其中Lzo是一种比较平衡选择,mapreduce.map.output.compress.codec(default:org.apache.hadoop.io.compress.DefaultCodec)参数设置。但这个过程会消耗CPU,适合IO瓶颈比较大时。

之后,就到了第⑤、⑥步,ReduceTask会从各个节点将相同分区的数据拷贝到内存(多个ReduceTask会分别处各个节点对应相同的分区),如果内存不够,则溢写到磁盘,此时可以调整相关参数,进行优化

参数说明
mapreduce.reduce.shuffle.parallelcopies每个Reduce去Map中取数据的并行数。默认值是5
mapreduce.reduce.shuffle.merge.percentBuffer中的数据达到多少比例开始写入磁盘。默认值0.66
mapreduce.reduce.shuffle.input.buffer.percentBuffer大小占Reduce可用内存的比例。默认值0.7
mapreduce.reduce.input.buffer.percent指定多少比例的内存用来存放Buffer中的数据,默认值是0.0

由于job的每一个map都会根据reduce(n)数将数据分成map 输出结果分成n个partition,所以map的中间结果中是有可能包含每一个reduce需要处理的部分数据的。所以,为了优化reduce的执行时间,hadoop中是等job的第一个map结束后,所有的reduce就开始尝试从完成的map中下载该reduce对应的partition部分数据,因此map和reduce是交叉进行的,其实就是shuffle。Reduce任务通过HTTP向各个Map任务拖取(下载)它所需要的数据(网络传输),Reducer是如何知道要去哪些机器取数据呢?一旦map任务完成之后,就会通过常规心跳通知应用程序的Application Master。reduce的一个线程会周期性地向master询问,直到提取完所有数据(如何知道提取完?)数据被reduce提走之后,map机器不会立刻删除数据,这是为了预防reduce任务失败需要重做。因此map输出数据是在整个作业完成之后才被删除掉的。

reduce进程启动数据copy线程(Fetcher),通过HTTP方式请求maptask所在的TaskTracker获取maptask的输出文件。由于map通常有许多个,所以对一个reduce来说,下载也可以是并行的从多个map下载,那到底同时到多少个Mapper下载数据??这个并行度是可以通过mapreduce.reduce.shuffle.parallelcopies(default5)调整。默认情况下,每个Reducer只会有5个map端并行的下载线程在从map下数据,如果一个时间段内job完成的map有100个或者更多,那么reduce也最多只能同时下载5个map的数据,所以这个参数比较适合map很多并且完成的比较快的job的情况下调大,有利于reduce更快的获取属于自己部分的数据。 在Reducer内存和网络都比较好的情况下,可以调大该参数;

reduce的每一个下载线程在下载某个map数据的时候,有可能因为那个map中间结果所在机器发生错误,或者中间结果的文件丢失,或者网络瞬断等等情况,这样reduce的下载就有可能失败,所以reduce的下载线程并不会无休止的等待下去,当一定时间后下载仍然失败,那么下载线程就会放弃这次下载,并在随后尝试从另外的地方下载(因为这段时间map可能重跑)。reduce下载线程的这个最大的下载时间段是可以通过mapreduce.reduce.shuffle.read.timeout(default180000秒)调整的。如果集群环境的网络本身是瓶颈,那么用户可以通过调大这个参数来避免reduce下载线程被误判为失败的情况。一般情况下都会调大这个参数,这是企业级最佳实战。

第⑥ 步我们称为MegeSort

这里的merge和map阶段的merge类似,只是数组中的数据是不同map拷贝过来的。Copy过来后,会先放在内存缓冲区中,缓冲区大小的设置是基于JVM的heap size设置,是通过参数mapreduce.reduce.shuffle.input.buffer.percent(default 0.7f 源码里面写死了) 来设置。这是一个百分比参数,意思是说,shuffile在reduce内存中的数据最多使用内存量为:0.7 × maxHeap of reduce task。JVM的heapsize的70%。内存到磁盘merge的启动门限可以通过mapreduce.reduce.shuffle.merge.percent(default0.66)配置。也就是说,如果该reduce task的最大heap使用量(通常通过mapreduce.reduce.memory.mb来设置,比如设置为1024m)的一定比例用来缓存数据。默认情况下,reduce会使用其heapsize的70%来在内存中缓存数据。假设 mapreduce.reduce.shuffle.input.buffer.percent 为0.7,reducetask的max heapsize为1G,那么用来做下载数据缓存的内存就为大概700MB左右。这700M的内存,跟map端一样,也不是要等到全部写满才会往磁盘刷的,而是当这700M中被使用到了一定的限度(通常是一个百分比),就会开始往磁盘刷(刷磁盘前会先做sortMerge)。这个限度阈值也是可以通过参数 mapreduce.reduce.shuffle.merge.percent(default0.66)来设定。与map 端类似,这也是溢写的过程,这个过程中如果你设置有Combiner,也是会启用的,然后在磁盘中生成了众多的溢写文件。这种merge方式一直在运行,直到没有map端的数据时才结束,然后启动磁盘到磁盘的merge方式生成最终的那个文件。

当reduce将所有的map上对应自己partition的数据下载完成后,就会开始真正的reduce计算阶段。reducetask真正进入reduce函数的计算阶段,由于reduce计算时肯定也是需要消耗内存的,而在读取reduce需要的数据时,同样是需要内存作为buffer,这个参数是控制,reducer需要多少的内存百分比来作为reduce读已经sort好的数据的buffer大小??默认用多大内存呢??默认情况下为0,也就是说,默认情况下,reduce是全部从磁盘开始读处理数据。可以用mapreduce.reduce.input.buffer.percent(default 0.0)(源代码MergeManagerImpl.java:674行)来设置reduce的缓存。如果这个参数大于0,那么就会有一定量的数据被缓存在内存并输送给reduce,当reduce计算逻辑消耗内存很小时,可以分一部分内存用来缓存数据,可以提升计算的速度。所以默认情况下都是从磁盘读取数据,如果内存足够大的话,务必设置该参数让reduce直接从缓存读数据

最后到第⑦步,分组,默认会将相同key的数据,分为一组,进入reduce方法,我们可以根据业务自定义分组策略。

步骤:

1.编写一个类实现WritableComparator

package com.zd.hadoop.group;

import org.apache.hadoop.io.WritableComparable;
import org.apache.hadoop.io.WritableComparator;

/**
 * Copyright (C) zhongda
 *
 * @author zx
 * @date 2020/9/4 0004 07:58
 * @description: 分组排序,让map 阶段的对象分为一组,一同进入Reduce
 */
public class OrderSortComparator extends WritableComparator {

    protected  OrderSortComparator(){
        super(OrderBean.class,true);
    }

    @Override
    public int compare(WritableComparable a, WritableComparable b) {
        OrderBean aBean = (OrderBean) a;
        OrderBean bBean = (OrderBean) b;

        int result ;
        if(aBean.getOrderId()>bBean.getOrderId()){
            result = 1;
        }else if(aBean.getOrderId()<bBean.getOrderId()){
            result = -1;
        }else{
            result = 0;
        }

        return result;
    }
}

2.在Driver中设置

job.setGroupingComparatorClass(OrderSortComparator.class);
通用优化

Hadoop默认使用4KB作为缓冲,这个算是很小的,可以通过io.file.buffer.size来调高缓冲池大小。

常用优化参数列表

(1)以下参数是在用户自己的MR应用程序中配置就可以生效(mapred-default.xml)

配置参数参数说明
mapreduce.map.memory.mb一个MapTask可使用的资源上限(单位:MB),默认为1024。如果MapTask实际使用的资源量超过该值,则会被强制杀死。
mapreduce.reduce.memory.mb一个ReduceTask可使用的资源上限(单位:MB),默认为1024。如果ReduceTask实际使用的资源量超过该值,则会被强制杀死。
mapreduce.map.cpu.vcores每个MapTask可使用的最多cpu core数目,默认值: 1
mapreduce.reduce.cpu.vcores每个ReduceTask可使用的最多cpu core数目,默认值: 1
mapreduce.reduce.shuffle.parallelcopies每个Reduce去Map中取数据的并行数。默认值是5
mapreduce.reduce.shuffle.merge.percentBuffer中的数据达到多少比例开始写入磁盘。默认值0.66
mapreduce.reduce.shuffle.input.buffer.percentBuffer大小占Reduce可用内存的比例。默认值0.7
mapreduce.reduce.input.buffer.percent指定多少比例的内存用来存放Buffer中的数据,默认值是0.0

(2)应该在YARN启动之前就配置在服务器的配置文件中才能生效(yarn-default.xml)

配置参数参数说明
yarn.scheduler.minimum-allocation-mb给应用程序Container分配的最小内存,默认值:1024
yarn.scheduler.maximum-allocation-mb给应用程序Container分配的最大内存,默认值:8192
yarn.scheduler.minimum-allocation-vcores每个Container申请的最小CPU核数,默认值:1
yarn.scheduler.maximum-allocation-vcores每个Container申请的最大CPU核数,默认值:32
yarn.nodemanager.resource.memory-mb给Containers分配的最大物理内存,默认值:8192

(3)Shuffle性能优化的关键参数,应在YARN启动之前就配置好(mapred-default.xml)

配置参数参数说明
mapreduce.task.io.sort.mbShuffle的环形缓冲区大小,默认100m
mapreduce.map.sort.spill.percent环形缓冲区溢出的阈值,默认80%

2.容错相关参数(MapReduce性能优化)

配置参数参数说明
mapreduce.map.maxattempts每个Map Task最大重试次数,一旦重试参数超过该值,则认为Map Task运行失败,默认值:4。
mapreduce.reduce.maxattempts每个Reduce Task最大重试次数,一旦重试参数超过该值,则认为Map Task运行失败,默认值:4。
mapreduce.task.timeoutTask超时时间,经常需要设置的一个参数,该参数表达的意思为:如果一个Task在一定时间内没有任何进入,即不会读取新的数据,也没有输出数据,则认为该Task处于Block状态,可能是卡住了,也许永远会卡住,为了防止因为用户程序永远Block住不退出,则强制设置了一个该超时时间(单位毫秒),默认是600000。如果你的程序对每条输入数据的处理时间过长(比如会访问数据库,通过网络拉取数据等),建议将该参数调大,该参数过小常出现的错误提示是“AttemptID:attempt_14267829456721_123456_m_000224_0 Timed out after 300 secsContainer killed by the ApplicationMaster.”。
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值