深度理解篇之MapReduce-个人拙见

MapReduce

MapReduce是一个分布式计算框架,MR主要是来自原Google的那片论文,从名字上看只有两个阶段,两个阶段map阶段和reduce阶段。Mapreduce框架追求的是对过去计算特征的一种抽取,发现计算就是对数据一般都是过滤或者聚合的操作,比如我们写sql时的where条件,就是一种过滤,而select * 的查询没有聚合的操作,MapReduce中只有map就可以完成这种操作。如果我们写sql时不只有where ,而且有sum、group by 等函数,这样单个map无法解决,只有配合reduce才能解决这种聚合操作,通过以上总结可以知道mapreduce就是对于一些计算的总结,计算数据时要嘛是面向单条的数据,要嘛是面向多条的数据聚合,mapreduce覆盖了基本的计算。只不过这样的计算模式在遇到复杂的业务需求的时候两步完成不了,比如说入门级的WorldCoun统计的时候,我们通过单个的map和reduce,统计完毕后再进行新的需求,看看相同次数的单词有几个,这样我们还得利用新的map和reduce进行分组计算,对于复杂的计算,一个mapReuce是跑不出来的,需要多个mapReduce去跑,将次数作为key,单词作为value,进行统计这样,可以找出相同的key在一组,的有那些单词,即相同次数的单数有那些。由上面可以总结出mapreduce是大数据计算中最小的粒度,map是面向单条的,reduce是面向组的。

Map和reduce是线性发生的,对数据进行计算时,首先将数据抽取成K、V形式的键值对,k就是这条记录的特性,v就是值,抽取的这组数据的特征为一组,那么为后面的进行groupbykey的时候,将向相同的key为一组,当map发生完时,reduce才能做计算的处理,他们之间有着线性的执行顺序,执行顺序则是由任务调度层面来执行的,通过有序的调度才能线性的计算。

任务的调度:

1.x和2.x是有着不同的调度的,1.x中有着Job tracker 和task tracker,job tracker负责完成资源的调度和任务的调度,但是这个两个调度容易造成瓶颈和单点故障,而在企业中一般是MR一般是跑在Yarn资源调度框架上的,在2.x中,这里是将1.x中job tracker的资源调度”切分”出来了,两者进行解耦,资源的调度和任务的调度分离开来,而yarn进行了资源的调度和任务的调度。

在Yran资源调度框架中,角色由resourcemanager,负责资源的管理和nodemanager节点的管理,他们之间的数据的传递,只是节点直接的传递,基于yarn的MapReduce的任务调度由Application Master来执行的,AM来负责MR运行时的任务调度。Yran中的resourcemanager和nodemanager是像nameNode和datanode一样常服务,一直处于运行状态,在计算层的客户端 、AppMaster、task任务、reduce任务都不是常服务,每次提交一次请求,都会生成一个新的AppMaster。


MR的提交流程

当客户端开始提交jar包开始启动时,会启动一个客户端进程,这个客户端进程在MapReduce是举足轻重的,为什么?因为MapReduce是一种分布式的计算框架,这里面有并行的map,且大数据里有计算向数据移动的事实,map最终是要移动在那些数据节点上, 移动性是如何实现的呢?是由客户端来支撑起来的,这时候我们可以重视客户端这个jvm进程,那么这个jvm进程做了那些事呢?

如果看源码一共做了5步:

  1. 输入输出路径的检查
  2. 切片的计算
  3. 在hdfs中创建一个存放共享资源的目录
  4. 将配置文件、jar包等发送存储层
  5. 最后提交请求到resourcemanager去请求启动ApplicationMaster。

这五大步中最重要的第二步,会计算出切片清单,在mapreduce中切片的计算算是一种解耦,在hdfs中block是一种实际的物理层面的,是真的将物理的数据变成块, 在计算层中,为什么提出的split切片的概念呢?因为切片是一种动态可以调整大小的东西,默认切片大小是128M,我们也可以将数据块大小调整成不同的大小,这样由于它调整不同的大小,则在计算的时候带来了并行度的差异,以及每个map计算的时候数据量的大小的差异。一般计算的时候有两类计算,哪两类计算呢?

  1. CPU密集型计算
  2. IO密集型计算

什么叫CPU密集型计算呢?

比如说每读到一行,就循环50万次,也有50万次的计算,若一个块中有10条记录,1条记录有50万次,存到内存中要算一个小时,那整个块10条记录计算完毕就是10小时,如果计算层没有切片的概念,就是一个数据块,那么就有一个数据块对应一个map,那么10个块并行起来也就是消耗10个小时,如果想让他们的性能提升的话,怎么做呢?如果这时候,有一个切片,对一个数据块开启一个切片,最这些数据进行切片,那么对于这些切片,中只包含了一条记录,这样呢一条记录就包含着一个map,那么map的数量也就变多了,比起前面现在一个map处理的数据量也就变少了,那么所有的map都并行的话,处理这些数据只需要1小时,之前是线性阻塞处理完数据要耗费10小时,所以呢速度上是由很大的优势的。

什么叫IO密集型计算呢?

记录读到内存中,不需要太多的计算,就是使得一次IO读取到足够多的数据,那么这时我们可以将切片大小调到足够大,然后可以使得一个map读到足够的数据,使用一次IO就可以完成数据的拉取。IO密集型的场景呢比较少,一般块大小基本上都是覆盖的是IO密集型。

客户端最重要的就是最数据进行切片,切片的公式是什么公式呢?

人为干预的值minsize(1)和maxsize(long的最大值,),这两个值会和数据块blocksize的大小去做比较,怎么比较的呢?Max(minsize,min(maxsize,blocksize)),首先拿块的大小和maxsize做比较取出最小值,maxsize是long的最大值,blocksize是人为设置的,没有设置,默认是128M,第一步计算取到的最小值是blocksize的大小。第二部拿计算结果blocksize的大小和minsize(minsize的大小一般是1)的大小取最大值,一般数据肯定是大于1的,则计算下来一般都是blocksize数据切片的大小,则默认块的大小就是一个数据切片的大小。

切片有哪些属性呢?

  1. 切片是哪个文件的切片,path
  2. 切片是从文件的哪个偏移量,offset
  3. 切片的大小,也就是一个切片有多少数据,size
  4. 切片对应的是哪个block位置,location

正是因为location这个信息,才支持计算向数据移动的可能性,如果切片没有这个信息,那么计算就不知道向哪移动。在MapReduce计算框架中,一个切片对应的是一个map,

Mapreduce、Spark中并行度都是由于切片的数量来决定的,都是这个原理。

以上描述的这些方法都是归属于InputFormat这个类的,输入格式化类。无论是MapReduce还是Spark都是要使用输入格式化类,只不过子类的形式是不一样的,比如说TextInputFormat文本的输入格式化类。Spark中textFile()方法底层使用的就是这个输入格式化类,MapReduce默认的是TextInputFormat类,输入格式化类有两个核心的方法,一个就是切片的计算,就是以上的这些部分了。

客户端提交job:

当客户端计算完这些切片之后,将jar包、配置文件,切片清单(所有的完成的切片信都会序列化成一个文件),会将这三个提交到hdfs中的一个目录中,它的副本数时是10,副本数越多支持redcue端并发的拉取,当上传完毕之后。

客户端请求启动appMaster:

客户端就会去请求resourcemanager,要提交job,resourcemanager会选择一台磁盘不太满、CPU不太忙的节点启动面向作业的调度程序也就是ApplicationMaster,启动完AppMaster之后,AppMaster会从HDFS中把切片清单拿下来,放在共享层由任务层去拿去切片清单,寻找resourcemanager申请资源,应为只有resourcemanager知道集群中那些资源的健康的,那些资源是空闲的,resourcemanager会根据自己掌握的节点信息,根据切片清单,比如说请求时在datanode上进行提交的,那么resourcemanager会首先从所提交的本机架开始,寻找有没有空闲的节点服务器,如果有则直接分配,如果没有则随机寻找一台空闲的机架服务器节点分配资源。

在申请的时候,AppMaster会拿着切片清单,去寻找resourcemanager,请求resourcemanager将不同的切片储存到不同的位置,比如请求储存到1,3,5节点那么resorcemanager会查看1,3,5节点是否空闲,若是空闲的则直接分配,如果都没有空闲,就随机找一台空闲节点返回,对每一个map提交成一个优先级队列1,3,5,resourcemanager会从个优先级队里中寻找最合适资源节点,如果1节点所在的机架内有空闲的直接返回同机架的,如果没有只能出机架返回一台空闲的节点。这是调度程序和资源层在会话,因为调度程序只能调度任务,不管你资源,只有资源层才知道,那些节点是空闲的,哪个节点有空闲的资源。也就是我们的计算层调用了资源层.

资源层resourcemanager根据提交的切片清单,最后决定哪个节点需要返回。这些切片的清单,也决定了有多少的Container容器,这些Container容器也决定了归属于哪个nodemanager。资源都是一个抽象的container来代表的。将这个Container看做是一个对象,这个对象的属性有可用的CPU、可用的内存、IO等等,由这个资源对象容器来抽象的。当一些列Container容器被申请回来之后,由ApplicationMaster这个任务调度程序,来完成调度层该调度的map task任务,和reduce task任务,map task任务和reduce task 任务基本上是线程发生的,一般都是map task跑一段时间 之后reduce task就开始跑了,reduce task则只会拉取map task执行完毕的数据,但不会计算,当map task执行完毕的时候,reduce task才会做真正的计算。

数据在切分时,如何保证数据切分时数据的完整性能呢?

Map input:

在mapreduce中每一个map面向一个文件,拿到hdfs的IO,通过HDFS拿到这个输入流,但是之后每一个map,根据自己切片的偏移量,将InputStream输入流seek到指定的偏移量的数据块文件上,每个map对同一文件都有seek,但是由于map拿到的offset偏移量不同,因此不同的每个map读取到的数据时不同的。在初始化的时候除了将第一切片读取完整,其他的都会预读取一行,多读一行,然后将它丢弃不用,然后把第二行的offset更新,所以真正每个map计算的时候,都是从每个数据切片的第二行开始读取的。间接地表述每次读取数据的时候,对于每个数据块,都会往后多读取一行。切片是逻辑的是一个字节范围,在map计算的时候,有一个概念record记录,要从切片的数据块中格式化出一条记录,拿到一条记录调用一次map,map是面向记录的,在这个输入格式化类中第二个重要点就是输入格式化类的不同就会返回不同的记录读取器的对象,那么在框架默认的文本TextInputFormat中,返回的读取器的对象是lineRecordReader行记录读取器,输入格式化类中有两个重要方法一个是切片的计算,客户端用,另一个就是行读取器,在map启动的时候用,行读取器多读取一行,丢弃第一行,更新第二行的offset。

Map output:

在初始化完毕之后,开始跑map,也就是说有了行记录读取器了,也就意味了拿到一行数据,调用一次map,拿到一行读取一行map,map启动之后,map收到的就是一行一行的记录,map中是我们的逻辑代码,每个人的逻辑代码可能是不一样的,但是map都是有先后输出的。输出的时候是K、V格式的,map中K是什么呢? 是根据我们的需求决定将什么样的特征抽取成为Key,map有并行度是切片对应的map并行度,reduce也可以并行,也可以设置调用多个reduce,默认是5个,reduce的别名对应着四个reduce分区,语义是分区分组,他们的关系是什么关系呢?一个分区可以包含若干组分组,一组只能到一个分区中。所以map输出的是K,V格式的,K决定着组,这条记录到哪一个分区呢?是通过计算出这个键值对的分区号,分区号怎么算呢?通过key的哈希对reduce取余,则最后格式就成了K、(V、P)这种格式了K即使抽取的特征,V就是这条数据,P就是分区号。Map输出完毕之后会调用分区器,通过调用getpartition()方法,拿key取hash值对reduce的个数取摸,这样就可以算出了哪条记录应该去往那个分区。谈到分区器,就是稳定,意思就是每个节点上相同的key计算出来的分区号是一致的。分区器的性能不能太低,应为每个方法都要掉它,所以分区器极大影响着mapreduce的执行的性能。代码量要精简、速度要快、稳定,相同的key必须要到一个分区中,当map的数据被处理成K,(v,p)这样的K、V格式之后,最终进入到缓冲区中,缓冲区的大小默认是100M,也可以调这个大小,调这个缓冲区的大小有一个概念就是,在JVM中有内存模型,堆的划分有新生代和老年代,新生代又分为,s0,s1,Eden,一般提起堆得内存模型都会提起GC,垃圾回收器,GC由新生代的GC和Full GC,Full GC满了之后会触发.......,由于GC量的增大,我们会调整Buffer缓冲区的大小,我们调整buffer缓冲区大小的时候,尽量调大一点。这样将数据格式从K、V通过分区处理转为K、(V,P)格式的类型对象。然后进入buffer缓冲区,进入缓冲区中的数据都已经经过了序列化,当buffer达到80%阈值的时候,就会触发溢写操作,加这个buffer缓冲区的目的,就是缓冲,防止频繁被吊起。一般在溢写之前都有sort排序和合并,但是sort排序着这个阶段都是可有可无的,但是一旦有了排序,对后续的操作都有一定的影响,就是在mapreduce中没有排序对reduce拉取数据的影响是非常大的。比如一个文件大小1T,有两组数据在这个文件里面,如果这个文件是乱序的话,那么读取数据的IO量就是两次的全量IO,都是从头开始读取的。但是如果文件是有序的,只有一次IO,打开文件,只需读取到一半的时候,就吧文件读取完毕了,继续第二个reducer进读取,这里意味着一次IO就可以读取完毕了。一次IO就可以读取完毕两组数据。如图是20组数据,乱序那么就需要20次IO量,但是如果使用的分区器,对数据进行了分区,那么值需要一次IO就可以将20组数据全部读取出来了。所以说排序在分布式计算框架中是可有可无的,但是一旦有了排序,那么对后续的操作有很大的影响,所以在MapReduce中缓存里面的map数据形成积压,这时候达到80%的时候,在溢写产生IO时候,由于内存的速度是磁盘的10万倍,所以如果需要内存的时候,会在内存的中进行sort排序,在MR源码中,reduce的数量是0的话,那么在计算过程中只有map task 在工作,没有reduce的执行过程,一旦有一个又或者多个reduce时,在map端就会分为两部分,一部分是map的计算,另一部分是sort排序,map占66%左右,reduce占33%左右。当内存缓冲区达到80%阈值的时候,开始溢写时,溢写时肯定会触发sort排序操作,这个排序是二次排序,由于处理后的数据是K、(V,P)格式的数据,先将分区号相同的排在一起,然后将分区向同里面的K进行排序,将K相同的进行排序。这个溢写的小文件特点就是分区有序,分区内的K有序。一次又一次的溢写,会溢写很多个小文件,这些小文件都是内部有序外部无序的文件,想到内部有序外部无序,则就会想到归并排序算法,所以面向这些小文件,只需要触发一次IO就会得到一个全排序的文件,这个全排序的文件特征依然是分区有序且分区内Key有序,分区有序方便各个分区对应的reduce拉取这些数据,分区内key有序就是reduce拉取到属于自己分区内对应的数据,这堆文件又是内部有序外部无序,这些文件又触发归并。。

只有在前面map获取的数据序列化到buffer缓冲区中,溢写的那些小文件是无序的,然后通过快速排序,得到了分区有序分区内key有序的数据文件。其他的都是通过内部key有序,外部文件无序然后通过归并排序进行排序的形式。

 调优点:设置combiner

如果map端相同分区内的数据key是相同的并且数量很大,那么在reduce进行拉取的时候,会拉取很多次,比如相同的key有:beijing,那么reduce通过http拉取“beijing”的时候会网络间传输很多次,相同的key过多那么拉取次数也就过多,这样大的拉取会影响MR效率,并且增大shuffle的IO量。如果设置combiner,那么将相同分区中key相同的数据进行combiner进行压缩,比如100个<beijing,1>这样的数据,压缩成<beijing,100>这样的数据,那么一个分区内的数据reducer拉取数据的时候只拉取一次就可以了,大大减少了shuffle量。

Combiner使用的环境:

  1. 内存缓冲区达到80%溢写文件的时候,调用一次combiner
  2. 如果溢写的文件超过三个,在归并这些文件的时候,又要触发一次combiner,map端有一次或者多次的combiner使用。

map主要阐述重点:

  1. 切片
  2. 输入格式化类InputFormat中的lineRecordReader,
  3. Seek到文件的位置,多读取一行
  4. 缓冲区
  5. 排序(二次排序)、合并
  6. 调优,combiner

Reduce:

Map一部分跑完之后,就可以跑reduce了。前期reduce只是拉取map端分区中的对应的数据,如果map task没有跑完,reduce task拉取到了一批数据那么reduce端会先进行一次预归并,将拉取到的数据进行归并一次。当map task指定完毕之后,属于reduce分区内的数据都拉取回来之后,这里如果有大量的数据倾斜,就像一个分区中的数据很多,达到了很大的数据量,但是其他的数据量很小,这样会造成reduce端内存溢出吗?不会的,应为这里充分利用的迭代器的模式,而且是一种嵌套的迭代器模式,reduce方法收到的不是一个物理的数据对象集合,并不是将1T的数据变成对象,而是仅仅是一个嵌套的迭代器,迭代器里不存数据的,只是存储着数据的应用,数据只存在磁盘里,如果磁盘有一个1T的文件。打开它得到一个输入流,这个输入流一定有一个方法,叫readLine,调用一次拿一行数据,调用一次拿一行数据,这些流程分别可以利用一个对象,这个对象有hasNext和next两个方法,调用一次hasnext,就会readline返回一次next数据,这就是迭代器模式了,但是对于一个迭代器,就像游标一样,一直到数据结束,成了全量遍历,但是如果文件中有两组数据,一个迭代器只能迭代出一组数据,怎么一次性读取出两组呢?在这个迭代器的前提下再包装一个迭代器,即两个迭代器进行嵌套,这两个迭代器中间夹着一个while判断,这个判断关键属性名字iskeyissame 下一个key是否和上一个key相同,如果把外面的迭代器传递给reduce,reduce外面迭代一条数据,里面就会迭代两条数据,一条拿来用,第二条用来判断key是否相同,如果是相同的while判断为真,那么进行next取数据如果预判断下一条和自己不是一组数据,reduce方法的while循环hasnext为false,则reduce既可以结束里面的这个迭代器了。则这样可以解释为什么reduce中不会出现OOM了。这样的行为就是嵌套迭代器模式了。其实Spark一系列窄依赖的算子,一定会被划分成一个stage,每个RDD之间的计算,也是涉及到的嵌套迭代器模式了,而这种迭代器模式也决定了Spark的在运行过程中,对于内存的占用不是很大。

Reduce主要阐述的问题:

  1. 利用迭代器模式,解决OOM--技术
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值