开源组件系列(11):批处理引擎MapReduce

目录

(一)MapReduce设计目标

(二)MapReduce编程思想

(三)MapReduce模块

(四)MapReduce数据倾斜场景

(一)MapReduce设计目标

        MapReduce诞生于搜索领域,主要解决搜索引擎面临的海量数据处理扩展性差的问题,很大程度上借鉴了Google开源的论文思想,包括了简化编程接口、提高系统容错性等特征。如果我们总计一下MapReduce的设计目标,主要有以下几个:

  • 简化编程接口:传统的分布式程序设计非常复杂,用户需要关注的细节非常多,例如数据分片、传输、通信等问题,而MapReduce将以上过程极大的简化了,抽象成几个独立的公共模块,将底层交给系统实现,用户只需要关心自己的实现逻辑即可;
  • 良好的扩展性:MapReduce支持通过增加机器的方式实现现行扩展集群的能力;
  • 极高的容错性:MapReduce底层基于HDFS,运行依赖于Yarn,因而遇到常见的磁盘损坏、机器宕机、通信失败等软硬件故障时,能够依赖于其他框架实现比较好的容错性;
  • 不错的吞吐率:MapReduce通过分布式并行技术,能够利用多台机器的资源,一次性读取和写入海量数据。

(二)MapReduce编程思想

        MapReduce模型是针对大规模分布式处理问题而进行的抽象和总结,核心思想是:【分而治之】,即将一个分布式计算过程拆解成两个阶段:

  • 第一阶段:Map阶段。由多个可并行执行的Map Task构成,主要功能是,对前一阶段中各任务产生的结果进行规约,并得到最终结果。
  • 第二阶段:Reduce阶段。由多个可并行执行的Reduce Task构成,主要功能是,对前一阶段个任务产生的结果进行规约,并得到最终结果。

 

 

 

Map阶段:

        首先mapreduce会根据要运行的大文件来进行split,每个输入分片(input split)针对一个map任务,输入分片(input split)存储的并非数据本身,而是一个分片长度和一个记录数据位置的数组。输入分片(input split)往往和HDFS的block(块)关系很密切,假如我们设定HDFS的块的大小是64MB,我们运行的大文件是64x10M,mapreduce会分为10个map任务,每个map任务都存在于它所要计算block(块)的DataNode上。如果文件大小小于64M,则该文件不会被切片,不管文件多小都会是一个单独的切片,交给一个maptask处理。如果有大量的小文件,将导致产生大量的maptask,大大降低集群性能。

        经过map函数的逻辑处理后的数据输出之后,会通过OutPutCollector收集器将数据收集到环形缓存区保存。环形缓存区的大小默认为100M,当保存的数据达到80%时,就将缓存区的数据溢出到磁盘上保存。程序会对数据进行分区(默认HashPartition)和排序(默认根据key进行快排)。

        值得注意的是,对于数据分片,如果严格按照物理切分,会出现一条记录分成两部分的情况,因此第一个Split会读到该Record结束,第二个Split会从下一个Record开始读,因此Split并不是严格意义上数据块大小一致的。

        每个Mapper通过map()方法,读取自己的分片,生成键值对<Key, Value的形式>。

 

Shuffle阶段:

        MapReduce中的Shuffle更像是洗牌的逆过程,把一组无规则的数据尽量转换成一组具有一定规则的数据。我们都知道MapReduce计算模型一般包括两个重要的阶段:Map是映射,负责数据的过滤分发;Reduce是规约,负责数据的计算归并。从Map输出到Reduce输入的整个过程可以广义地称为Shuffle。Spill过程包括输出、排序、溢写、合并等步骤,如图所示:

 

 

        先把Kvbuffer中的数据按照partition值和key两个关键字升序排序,移动的只是索引数据,排序结果是Kvmeta中数据按照partition为单位聚集在一起,同一partition内的按照key有序。Spill线程为这次Spill过程创建一个磁盘文件:从所有的本地目录中轮训查找能存储这么大空间的目录,找到之后在其中创建一个类似于“spill12.out”的文件。Spill线程根据排过序的Kvmeta挨个partition的把数据吐到这个文件中,一个partition对应的数据吐完之后顺序地吐下个partition,直到把所有的partition遍历完。一个partition在文件中对应的数据也叫段(segment)。Map任务如果输出数据量很大,可能会进行好几次Spill,out文件和Index文件会产生很多,分布在不同的磁盘上。最后把这些文件进行合并的merge过程闪亮登场。然后为merge过程创建一个叫file.out的文件和一个叫file.out.Index的文件用来存储最终的输出和索引。

        这里使用的Merge和Map端使用的Merge过程一样。Map的输出数据已经是有序的,Merge进行一次合并排序,所谓Reduce端的sort过程就是这个合并的过程。一般Reduce是一边copy一边sort,即copy和sort两个阶段是重叠而不是完全分开的。

 

Reduce阶段:

        reduce节点从各个map节点拉取存在磁盘上的数据放到Memory Buffer(内存缓冲区),同理将各个map的数据进行合并并存到磁盘,最终磁盘的数据和缓冲区剩下的20%合并传给reduce阶段。reduce对shuffle阶段传来的数据进行最后的整理合并。

        在Reduce端,输入可能来自不同的map输出,不同map任务的完成时间不同,只要有一个任务完成,reduce就会拷贝其输出,这个过程称之为“复制阶段”。和map端类似,每个reduce也持有一个内存缓冲区,如果map输出较小,会写到内存中,在溢出写到磁盘,如果map输出很大,会直接写临时文件。后台会维护一个线程,当临时文件太多时,会对磁盘文件进行合并,合并的结果文件可能是多个。

 

(三)MapReduce模块

        按照map/reduce执行流程中各个任务的时间顺序详细叙述map/reduce的各个任务模块,包括:输入分片(input split)、map阶段、combiner阶段、shuffle阶段和reduce阶段。

 

1:input & split

        input可以是本地文件,也可以是数据库的表, 提供数据源输入。

        Split是一个单独的Map任务需要处理的数据块,通常一个split就是一个block,这样做的好处是使得Map任务可以在存储有当前数据的节点上运行本地的任务,而不需要通过网络进行跨节点的任务调度。

        set odps.sql.mapper.split.size=512,可以控制block大小,一般默认是256M

 

2:Mapper

        Map是一类将输入记录集转换为中间格式记录集的独立任务,主要是读取InputSplit的每一个Key,Value对并进行处理.

        maps的数量通常取决于输入大小,也即输入文件的block数,对于那种有大量小文件输入的的作业来说,一个map处理多个文件会更有效率。如果输入的是打文件,那么一种提高效率的方式是增加block的大小(比如512M)

 

3:Shuffle

        一般把从map任务输出到reducer任务输入之间的map/reduce框架所做的工作叫做shuffle。这部分也是map/reduce框架最重要的部分。

        当Map程序开始产生结果的时候,并不是直接写到文件的,而是写到一个内存缓冲区(环形内存缓冲区)。

        每个map任务都有一个内存缓冲区,存储着map的输出结果,这个内存缓冲区是有大小限制的,默认是100MB(可以通过属性参数配置)。当map task的输出结果很多时,就可能会超过100MB内存的限制,所以需要在一定条件下将缓冲区中的数据临时写入磁盘,然后重新利用这块缓冲区。这个从内存往磁盘写数据的过程被称为“spill”,中文可译为溢写。

        在把map()输出数据写入内存缓冲区之前会先进行Partitioner操作。Partitioner用于划分键值空间(key space)。MapReduce提供Partitioner接口,它的作用就是根据key或value及reduce的数量来决定当前的这对输出数据最终应该交由哪个reduce task处理。默认对key hash后再以reduce task数量取模。

        Partitioner操作得到的分区元数据也会被存储到内存缓冲区中。当数据达到溢出的条件时,读取缓存中的数据和分区元数据,然后把属与同一分区的数据合并到一起。对于每一个分区,都会在内存中根据map输出的key进行排序(排序是MapReduce模型默认的行为,这里的排序也是对序列化的字节做的排序。最后实现溢出的文件内是分区的,且分区内是有序的。

        Combiner最主要的好处在于减少了shuffle过程从map端到reduce端的传输数据量。combiner阶段是程序员可以选择的,combiner其实也是一种reduce操作。Combiner是一个本地化的reduce操作,它是map运算的后续操作,主要是在map计算出中间文件前做一个简单的合并重复key值的操作

        每次spill操作也就是写入磁盘操作时候就会写一个溢出文件,也就是说在做map输出有几次spill就会产生多少个溢出文件,等map输出全部做完后,map会合并这些输出文件生成最终的正式输出文件,然后等待reduce任务来拉数据。将这些溢写文件归并到一起的过程叫做Merge。

 

4:Reduce

        reduce任务在执行之前的工作就是不断地拉取每个map任务的最终结果,然后对从不同地方拉取过来的数据不断地做merge,也最终形成一个文件作为reduce任务的输入文件。reduce的运行可以分成copy、merge、reduce三个阶段。

        由于job的每一个map都会根据reduce(n)数将数据分成map 输出结果分成n个partition,所以map的中间结果中是有可能包含每一个reduce需要处理的部分数据的。所以,为了优化reduce的执行时间,hadoop中是等job的第一个map结束后,所有的reduce就开始尝试从完成的map中下载该reduce对应的partition部分数据,因此map和reduce是交叉进行的。

        这里的merge如map端的merge动作类似,只是数组中存放的是不同map端copy来的数值。Copy过来的数据会先放入内存缓冲区中,然后当使用内存达到一定量的时候才刷入磁盘。

        当reduce将所有的map上对应自己partition的数据下载完成后,就会开始真正的reduce计算阶段。Reducer的输出是没有排序的。

(四)MapReduce数据倾斜场景

Map端:

        在Map 读数据阶段,可以通过“ set odps.mapper.split.size=256 ”来调节Map Instance 的个数,提高数据读人的效率,同时也可以通过“ set odps.mapper.merg e.limit.size=64 ”来控制Map Instance 读取文件的个数。

        在写人磁盘之前,线程首先根据Reduce Instance 的个数划分分区,数据将会根据Key 值Hash 到不同的分区上,一个Reduce Instance 对应一个分区的数据。Map 端也会做部分聚合操作,以减少输入Reduce 端的数据量。由于数据是根据Hash 分配的,因此也会导致有些Reduce Instance 会分配到大量数据,而有些Reduce Instance 却分配到很少数据,甚至没有分配到数据。

        在Map端读数据时,由于读人数据的文件大小分布不均匀,因此会导致有些Map Instance 读取并且处理的数据特别多,而有些Map Instance 处理的数据特别少,造成Map端长尾。以下两种情况可能会导致Map端长尾:

        1:上游表文件的大小特别不均匀,并且小文件特别多,导致当前表Map端读取的数据分布不均匀,引起长尾。

        2:Map端做聚合时,由于某些Map Instance读取文件的某个值特别多而引起长尾,主要是指Count Distinct操作。

        解决方案:

        第一种情况导致的Map 端长尾,可通过对上游合并小文件,同时调节本节点的小文件的参数来进行优化。

        第二种情况可以使用“ distribute by rand ()”来打乱数据分布,使数据尽可能分布均匀。

        Map 端长尾的根本原因是由于读人的文件块的数据分布不均匀,再加上UDF 函数性能、Join 、聚合操作等,导致读人数据量大的Map lnstance 耗时较长。在开发过程中如果遇到Map 端长尾的情况,首先考虑如何让Map Instance 读取的数据量足够均匀,然后判断是哪些操作导致Map Instance 比较慢,最后考虑这些操作是否必须在Map 端完成,在其他阶段是否会做得更好。

 

Join端:

        SQL在Join 执行阶段会将Join Key相同的数据分发到同一个执行Instance上处理。如果某个Key 上的数据量比较大,则会导致该Instance 执行时间较长。其表现为:在执行日志中该Join Task 的大部分Instance 都已执行完成,但少数几个Instance 一直处于执行中(这种现象称之为长尾)。

        这里主要讲述三种常见的倾斜场景:

        1:Join的某路输入比较小,可以采用Map Join,避免分发引起长尾(Map Join的原理是将Join操作提前到Map 端执行,将小表读人内存,顺序扫描大表完成Join,这样可以避免因为分发key不均匀导致数据倾斜,MapJoin 的使用方法非常简单,在代码中select 后加上“/*+mapjoin(a) */”即可,其中a 代表小表的别名)。

        2:Join的每路输入都较大,且长尾是空值导致的,可以将空值处理成随机值,避免聚集。

        3:Join的每路输入都较大,且长尾是热点值导致的,可以对热点值和非热点值分别进行处理,再合并数据。

 

Reduce端:

        Reduce端负责的是对Map端梳理后的有序key-value键值对进行聚合,即进行Count、Sum 、Avg等聚合操作,得到最终聚合的结果。Distinct是MaxCompute SQL中支持的语法,用于对字段去重。比如计算在某个时间段内支付买家数、访问UV等,都是需要用Distinct进行去重的。MaxCompute中Distinct的执行原理是将需要去重的宇段以及Group By字段联合作为key 将数据分发到Reduce端。

        1:因为Distinct 操作,数据无法在Map 端的Shuffle阶段根据Group By先做一次聚合操作,以减少传输的数据量,而是将所有的数据都传输到Reduce端,当key的数据分发不均匀时,就会导致Reduce端长尾。Reduce端产生长尾的主要原因就是key的数据分布不均匀。比如有些Reduce任务Instance处理的数据记录多,有些处理的数据记录少,造成Reduce端长尾。如下几种情况会造成Reduce 端长尾:

        2:对同一个表按照维度对不同的列进行Count Distinct操作,造成Map端数据膨胀,从而使得下游的Join和Reduce出现链路上的长尾。

        3:Map端直接做聚合时出现key值分布不均匀,造成Reduce端长尾。

        4:动态分区数过多时可能造成小文件过多,从而引起Reduce端长尾。

        5:多个Distinct同时出现在一段SQL代码中时,数据会被分发多次,不仅会造成数据膨胀N倍,还会把长尾现象放大N倍。

 

解决方案:

        1:嵌套编写,先Group By再Count(*);group by维度过小:采用sum() group by的方式来替换count(distinct)完成计算。

        2:可以对热点key进行单独处理,然后通过“ Union All ”合并;

        3:可以把符合不同条件的数据放到不同的分区,避免通过多次“Insert Overwrite”,写人表中,特别是分区数比较多时,能够很好地简化代码;

        4:在把不同指标Join 在一起之前, 一定要确保指标的粒度是原始表的数据粒度;当代码比较膝肿时,也可以将上述子查询落到中间表里。

原创文章 54 获赞 61 访问量 1万+

猜你喜欢

转载自blog.csdn.net/gaixiaoyang123/article/details/105288145
今日推荐