mapreduce之详细工作流程原理

        先给大家看看mapreduce的两个完整的工作流程图,然后下面再一步步把mapreduce的所有细节流程抛出来进行一个更详细的讲解!(请看下面A1和B2这两张完整图)

 A1:

这是一个map的工作原理

B2:

这是两个map和两个reduce的最终输出文件的原理

 图1:

  现在我们有一个单词本(如上图1所示),这个单词本是200m,我们现在hadoop2.7.2HDFS的默认文件块大小是128m,现在我有一个200m的文本,那他是不是应该分了两个文件块对吧?然后下一步客户端提交任务(如下图2所示)

 图2:

提交这个任务之后在submit这个任务真正运行之前,它先跟这个resourcemanager通信,resourcemanager根据当前yarn的这个状态信息然后给我们在任意一个nodemanager节点上开启一个applicationmaster,由resourcemanager帮我们创建一个applicationmaster进程。

  图3:

这个applicationmaster进程就负责上图三里面的这个任务的资源调度,举个简单的例子,我运行这个mapreduce任务我是不是要消耗CPU要消耗内存?那我现在有三台机器,我要消耗哪一台机器的cpu和消耗哪一台机器的内存呢?由applicationmaster来告诉我们,因为yarn这个框架它持有的是三台机器的状态对吧?每台机器剩余多少cpu剩余多少内存,而你这个任务需要多少cpu需要多少内存,你是不是要提前知道啊?所以说呢由applicationmaster根据当前集群的资源状态来选择你这个任务放在哪一台机器上去跑,由applicationmaster来负责。然后根据这个参数的配置形成一个任务的规划,现在我们这个单词本是200m,那么分成两个文件块,也就是第一个文件块0到128m,第二个文件块是128到200m。接着下一步提交切片信息(如下图4所示)

 图4:

我们现在这个数据,我这有一个inputform,由它来读取这个切片信息然后给map,那么这个inputform在读取这个数据的时候,它要对这个数据进行切片,那这个数据的切片信息它首先要拿到才能进行一个切片,就是说我们HDFS原本就已经把这个200m的文件给切成两块了,如果说现在假设没有把这个200m的文件切成两块,我们一个文件块就是128m,那后面128m这个数据我要产生几个map来处理呢?对于我们现在而言是不知道的,我们什么时候可以知道呢?谁来告诉我们呢?由inputform来告诉我们,inputform的切片目的是为了产生map来用的,inputform产生了几个切片就会产生几个map。注意的是:inputform的切片和我们HDFS的这个切片,它不是一个概念,而我们提交任务的时候这个resourcemanager提交的切片信息是HDFS的切片信息,总之我这个任务是要跟resourcemanager通信的,然后resourcemanager提供了这些切片信息,我的这个代码是谁?WordCount的这个代码打了个jar包对吧?我这个代码是谁?我这个数据都在哪放着的?你要告诉人家对吧?它要做这个任务的调度的。

图5:

计算出maptask的数量(如上图5红箭头指向所示),也就是说根据我们当前这个文本文档的这个大小然后估算一下我们需要几个map来处理,计算完之后我们现在是产生两个map(如下图6红箭头指向所示)

 图6:

你不是有两个切片吗?那么就是一个切片由一个map来处理,那么第一个map处理的是0到128m,第二个map处理的是128到200m(如下图7红箭头指向所示)

  图7:

上图7的RecorderReader这个东西才是本质上读取数据的东西,这个RecorderReader的实例化对象里面的reader()方法是用来读取数据的,就是在这个reader()方法里面我们创建了一个输入流,通过这个输入流我们把数据读出来了,然后RecorderReader这个类由inputformat这个类里面的某个线程来实例化,读出来之后和这个单词本里面的单词就会以k,v键值对的形式读取进来,为什么是键值对呢?(如下图8红箭头指向所示)

 图8:

回想一下,我们在map这个类的时候是不是有四个泛型?第一个泛型指的是偏移量对吧?这个偏移量的类型叫LongWritable,第二个泛型指的是这个第一个泛型偏移量对应的那一行数据,然后第二个泛型的类型是Text类型,那么这个第一个泛型这个LongWritable它第一次读的时候它偏移量是0,第二次读的时候它的偏移量是1,那么这个k就是0,1,2,3是第几行数据,这个v就是那每一行数据的具体字符串内容,拿过来之后我们要在map的map方法里面我们是不是做了一个切割?就是比如这行字符串line.split(“  ”),用空格去切分,把这行这个字符串切分成若干个单词,因为它本身就是多个单纯组成的,中间就是用空格隔开的,然后给它放到一个数组里面了。

        我们在map方法里面是做切片line.split(“  ”)这样的一个操作,这个切完之后我们再做什么操作?回想一下,我切完之后是一个数组,然后我们是不是for循环遍历这个数组?然后遍历这个数组的过程中我每拿到这个数组里面一个元素我都把它组合成这个元素<0,1>这样读的结构把它写到了上下文里面,也就是这个我拿到了,也跑到了map方法里面了,然后我们切了一下line.split(“  ”),切完之后然后组装成<0,1>这样的一个结构,然后写出去了(Context.write(k,v)),注意写出去之后,这个(Context.write(k,v))里面的k和v就不是这个上图8里面的mapTask里面的k和v了,mapTask的k和v是<0,line>或者是<1,line>这一行数据,也就是这里面放的是原生数据。然后(Context.write(k,v))里面放的k,v是我们处理后的这样的数据结构,这个k是word这个单词,这个v就是这个数字1。

   然后接下来出现了一个不太好理解的这么一个概念(如下图9红箭头指向所示)

图9:

这个叫环形数组,你的k和v是不是已经写到了上下文里面了(Context.write(k,v))?你写到上下文里面是不是意味着我们这个单词和数字这样的一个映射关系就要被序列化了?它是一个数据,是一个键值对,那这个键值对最后由这个mapTask这个阶段的这个outputCollector来收集,把这个数据收集过来之后把它写到环形内存里面。 

       这个环形内存也是个数组,这个数组是一个非常普通的数组,比如说我这个英文单词是字母a,这个字母a它在ascii码中对应的是97,97这个数字我如果做二进制的话,把它保存到一个int类型的这个变量里面,它是不是占用4个字节?那么a就是这个(Context.write(k,v))里面的这个k,a后面对着的这个v应该就是1对吧?1这个数字我直接也给它转成int,它对应的也是4个字节对吧?那么现在序列化的操作就相当于把我们这一块可以阅读的可以理解的数据给它变成了不可理解的数据,这就是叫序列化过程。

        就是给它序列化两个二进制数据,这两个二进制数据分别占四个字节,序列化成int类型的,那么outputCollector就相当于把这个(Context.write(k,v))里面的k和v进行序列化操作,序列化完成之后写到一个数组里面,这个数组是字节数组,那现在假如说我这边是这样的一个键值对数据,我这个这个数据往数组里面写的时候怎么写呢?应该是[0,0,0,0]这一块代表a,因为这是一个字节数组,字节数组就意味着这个数组里面每一个元素都是一个字节,而我们这个字母a或者是字符串a它序列化成了一个int类型的二进制的一个数字,一个字节是8位,那我们int类型的是4个字节,所以说光这个字符串a就在这个数组里面就占4个元素的位置([0,0,0,0]),那么a我就存到内存/数组里面了,接着就是要存这个(Context.write(k,v))里面的v对吧?这个v里面是个数字1对吧?这个数字1它也是统一序列化成了一个int类型的或者是int这个长度的二进制数据,那么是不是也应该占4个元素的位置?也就是[0,0,0,0=a,0,0,0,0=1]。

        那么这一个[0,0,0,0,0,0,0,0]数组里面的这一块东西就代表了(Context.write(k,v))里面的k和v这块数据对吧?那么(Context.write(k,v))这块数据序列化进来之后我们数组的偏移量是达到多少?是从0到7对吧?一共8个元素,那我们环形内存里面是保存这个数据的,但是再往后想比如[0,0,0,0,d,d,d,d]这是一个单词的,那么下一个单词进来可能是,那这个我是不是还要往[0,0,0,0,d,d,d,d]这数组里面的后面去添加啊?用数组去保存键值对那肯定是相邻去摆放的,那这里又来了个b单词,那就应该最后变成这样[0,0,0,0,d,d,d,d,k,k,k,k,e,e,e,e,e](0代表a,d代表1,k代表b,e代表1)。

        那么如果说我们就以这种方式去写下去的话,一会我要用这个数据的时候我怎么去用呢?比如你先想读第一个单词a,那么你就去读数组里面的前四个元素也就是0,0,0,0,然后接着你读完前四个之后,你开始读这个v的话你就再去读d,d,d,d这四个元素以此类推,那么如果我们的数据它用四个元素没法代表呢?比如说我们某一个单词是这样子的“abcdefg”,那按照刚才数组里面的元素这样做的话是不是就意味着这“abcdefg”整个这一串东西我们序列化成二进制数据的话用四个字节可能存不下对吧?那如果存不下的话那能不能存呢?其实当然也能存,你需要多少个字节你就往后摆多少个字节就可以了,那这个问题会造成什么影响呢?会意味着我们这个环形数组每一个单词它占据的元素位数不一样,第一个单词可能占四个元素的位置,第二个单词可能占八个元素的位置这是有可能的,所以我们如果把数据这样子去存放的话,压根就不知道我读到哪算读完第一个单词,这就没法读了,所以必须要建立数据的索引,那这个数据的索引怎么建立呢?

      我们继续以这个例子为例a是占四个元素,然后1也是占四个元素[0,0,0,0,d,d,d,d],这个[0,0,0,0,d,d,d,d]代表的是原数据本身,我们要对这个原数据建立索引,那么是不是意味着这个数组里面的a单词是从0到3的这个偏移量,那么这个数据1在这个数组里面是从4到7这个偏移量,那么就是0到3,4到7就是这个[0,0,0,0,d,d,d,d]数组中的索引,那么索引也是数据,那么这个数据它也保存到环形数组里面,这是一个绝妙的设计,那怎么放呢?它是这样子做的,这个数组从左边[--> 0这个位置开始写数据本身,然后从右边这个数组最大的这个位置上写元数据  <--],也就是说这个数组从头从0开始把这个数据往里面写,然后右边从最后开始写,写这个原数据对应的元数据信息也就是[-->    <--]这样的一个过程,那接着把这个数组首部到尾部连起来是不是就是一个圈了?这个概念就是叫环形数组,也就是(如下图10红箭头指向所示)

 图10:

从圈上面的中间是0这个位置和数组末尾的位置,数组0和数组末尾你想象一下是一个纸带,你把这个纸带给它拽一下给它连起来,它是不是就变成是一个环形的了?

        那么接下来看一下这个操作,这个数据本身往我们对着这个环形数组的右边开始写,也就是往这个环形数组的右边开始写,然后这个数据本身它对应的这个数组中存放的这个索引往我们对着环形数组的左边开始写,它是一个直线,但是这个数组肯定不是一个环形的,你想让他变成一个环形的怎么操作,你是不是直接求余就行啊?然后这个环形数组默认的是100m大小,数据本身往环形数组的右边开始写, 数据本身对应的元数据信息往左边开始写,然后这个环形数组出现了一个单词buffindex,buffindex的意思是这个数据本身它这个偏移量叫buffindex,这个偏移量是依次递增的,然后环形数组左边这个元数据的偏移量叫kvindex,它的这个在数组中的索引是依次下降的,元数据的长度它不是固定。

            那么它一直这样写,那么我这个环形数组的内存也就只有100m大小,你一直这样写的话就一下子会满了对吧?满了的话就会内存溢出,我们这是处理大数据的框架,你怎么可能让他内存溢出呢?我们的数据可能要几个T几百个T都有可能,那我现在只有100m怎么办呢?你是不是需要溢写啊?你需要把内存中的数据把这个中间结果给它临时的保存到磁盘上,那什么时机才去保存呢?当这个环形数组的数据存储量达到百分之八十的时候就开始执行split溢写这个操作,它会把环形数组里面存有的百分之八十的这个数据给它写到本地磁盘上,那写到本地磁盘上之后,它这个内存也不会立刻释放,因为它写的这个过程也是消耗时间的,那么新的数据是不是还可能往那个环形数组的内存里面去加啊?那能不能加呢?当然也是可以的,因为我们环形数组里面还有百分之二十呢,这就是为什么设计到百分之八十的时候就开始溢写而不是到百分之百的时候才开始溢写,因为如果我内存占用率达到百分之百再开始溢写的话,就会导致我们新的数据就要暂停要阻塞就不能继续往内存里面写了,这影响效率,那么这个默认的100m这个值是可以改的和百分之八十这个值也是可以改的。

            如果说这个环形数组内存设计的越大,那么这个任务就跑的就越快,为什么spark比mapreduce跑的快,那就是因为spark它内存消耗的多,内存占用多那么数据落盘就少,数据落盘少那数据处理的效率就会变快。

            那么在溢写的过程中新来的数据怎么往环形数组里面写呢?那么就是我进来一个新的数据在百分之二十的那个地方选择一个中间点,然后依靠于中间点的位置来往右手边和左手边写数据,中间点的右手边写新来的数据本身,中间点的左手边写新数据本身所对应的那个元数据信息。那么就是说溢写过程不耽误新数据往这个环形数组里面添加,因为这是一个同步操作,溢写线程和把这个数据写到环形数组里面的线程是两个线程,也属于并行操作。

            那么在溢写的时候它都干了什么呢?首先它会对这个数据进行分区(如下图11所示)

  图11:

比如说我们现在这个单词本不是200m吗?然后被分为两块对吧?一个是0到128m,一个是128到200m,那么就0到128m这个文件单词块里面,那么在环形数组内存里面拿到了80m的数据了,然后这个环形数组内存里面已经达到预值要落盘了,落盘的时候它会对这个80m的数据进行分区,那么按照什么来分区呢?默认就是按照k来分区,k就是我们的单词,那它默认的分区规则是,比如说这个字符串单词是niuk然后点hacode(niuk.hashcode),然后这个单词的hashcode之后是一个数字对吧?然后这个数字和int的最大值进行与运算,因为它害怕这个单词的hash值它超过了int类型的存储范围,所以它跟int的最大值进行与运算,与运算完成之后得到的肯定是这个长度的一个数字,然后再把这个结果和要启动的reduce的个数进行求余,reduce的个数是可以设置的,redude的个数默认是一个,那么这就是一个分区规则。���~ٟ��"

         那么第一个单词是,第二个单词是,第三个单词是,我将这三个单词分区,一个区就对应着一个文件,你现在不是要把环形数组内存里面的数据溢写到本地磁盘吗?那你一定要对应着文件的产生,那么这个文件是按照分区去产生的,这边有几个分区,就产生几个文件,那指的是仅仅针对于这一次的溢写过程。

 图12:

分区内部还要排序,先分区,分完区才会排序,那是不是分完区之后立刻就产生文件呢?其实也不是(如下图13红箭头指向所示)。

 图13:

举个例子:我们已经分好区了,那分好区之后,我们要排序,那么怎么排呢?是不是要对这个文件里面的内容进行排序?一个分区对应一个文件,分区内排序就相当于把某一个分区所对应的文件的所有的数据给它读出来,然后给它排个序。那么现在这个例子就是如上图13所示有a和c这两个英文单词,这两个英文单词a和c它hash之后和这个int类型的最大值与运算值之后,然后又和这个reduce的个数求余,求完余之后得到一个数字,然后这个数字正好一样,那么就意味着a和c这两个英文单词属于同一个分区,也就是一个文件里面。

        那么这两个单词在一个文件里面有可能是这个顺序,但是我们要排个序,要把放到前面,放到后面,这个排序默认是按照k进行排序的,如果是字符串的话,那么它的排序规则就是按位进行比较,就是这个字符串每一个这个英文的ascii的大小比较这样一个排序,也就是字典排序,那么现在这有两个分区文件(如下图14所示)

图14:

那么这第二个分区文件里面的数据都是b,那么肯定它这个hash值是一样,hash值一样那跟int类型的最大值与运算结果也是一样,最后跟reduce求余的结果肯定也还是一样,所以说这两个b只要是一样的,那么肯定是在同一个分区里面。

但是同一个分区里面有可能是不同的这个英文单词,然后接着(如下图15红箭头指向所示)。

图15:

      第三个分区和第四个分区里面是这样的两个文件,这第三个分区和第四个分区文件是环形数组内存里面第二次达到80m了然后进行溢写之后的分区文件,那么也就是说前两个分区是环形数组内存第一次达到80m的时候溢写产生的两个分区文件,后两个是环形数组内存又一次达到80m的时候又溢写产生的另外两个分区文件。

 那么第一次达到80m的时候里面的数据可能是,这四个英文单词,我第二次又达到80m了,这个环形数组内存里面的数据有可能是,这样的数据,那么是在同一个分区里面,然后在第二次达到80m溢写之后是在同一个分区里面,那么这的hash肯定也是一致的,然后的hash值也肯定是一致的,那么是不是就意味着可能和这四个英文单词这四个分区其实应该在同一个分区里面对吧?但是它为什么就不在同一个分区里面呢?因为这是两次溢写的过程,第二次溢写的这个数据是不会直接利用第一次溢写生成的那个分区文件的,两次溢写产生了四个分区文件,这四个分区文件里面有些数据是应该在一个分区里面的。

                                接下来要进行一个归并排序:(如下图16红箭头指向所示)

 图16:

现在的第十次操作归并排序就是把和放到了一个分区里面了,那么也就是说把和这两个分区文件合并成一个文件,然后把原来的和这两个分区文件给删掉,那是不是意味着我们每次溢写过程,会产生若干个小文件,当整个溢写过程结束的时候,所有的小文件都会按照分区来合并成一个大文件。

图17:

这上图17红箭头指向的合并过程是一个可选过程,也就是说我们如果不配置的话,就是刚刚图16红箭头指向的排序过程这样走下去是没有错的,如果说我们这个合并过程配置的话,那么效率将会大大的提高。那么Combiner是什么意思呢?比如说在map阶段我输出的是,我如果按照这个内容去传给reduce的话,那应该到了reduce效果就是a,[1,1],b[1]这样的一个数据效果对吧?但是呢这两个1我传给reduce是不是有点消耗资源?那么我能不能在map阶段的这个shuffer过程中能不能把这两个1合并之后再传过去呢?其实是可以的,也就是说我想传给reduce的不是a,[1,1]这样的数据,而是传给reduce的是a[2]这样的一个数据效果这样也是可以的,那么这个Combiner就是来做这样的一个操作的。

  在map阶段提前做了一次聚合是会在传给reduce的过程中大大的提升了传输效率,就比如你在QQ传文件给对方的时候,一般你最好都要打成压缩包,然后再传给对方,其实这样的话是把全部溢写小的文件合成一个大文件去传输,将大大的提高的网络IO的传输效率。

    那么在map阶段这所有的过程变成了(如下图18红箭头指向的最终合并过程所示)

图18:

这个效果是配置了Combiner才有的操作,如果没有配置Combiner,那么就是这样的效果(如下图19红箭头指向的所示)

图19:

然后再接下来还有一个排序(如下图20红箭头指向的所示)

 图20:

为什么还有这么多排序呢?我们一开始在原数据200m这里是不是拆分了两个文件块?那么我们这上面一直讲的是以一个文件块一个map为例来做的详细工作流程,这个文件是200m,分了两个map来处理这200m的数据,那么这两个map是不是有可能在不同的机器上?那么比如第一个map在第一台linux上进行运算,第二个map在第二台linux上进行运算,那么这两个机器上面处理的数据都有共同的一个nick这个单词,两个不同的机器分别有一个相同的英文单词,那么后面该怎么操作呢?那么就是把这两个map里面的数据交给reduce进行聚合操作。那么上面写了的一大堆都是只针对于以一个map的整个工作流程

现在来说一下多个map之间怎么将数据给它归类,这里面有两个map,这两个map有可能在不同的机器上也有可能在同个机器上,即使在同个机器上的也是不同的map进程(如下图21红箭头指向的所示)

图21:

然后第一个map产生出了  这样的数据结果,第二个map产生出了 这样的数据结果(这是没有配置Combiner的效果),那么接下来要把这两个map处理的中间数据同时的给reduce。 

图22:

所有map任务完成后,启动相应数量的reduce(如上图22红箭头指向的所示),这句话的意思就是几个分区就几个reduce来进行处理,我们的reduce的个数最好等于分区个数,即使reduce不等于分区个数也要大于分区的个数,绝对不能小于分区个数,因为如果reduce一旦小于分区个数,那么就会报错!所以最好是reduce跟分区的个数保持一致,但也最好不要大于分区个数,因为这样等于是浪费资源。

        一个reduce必须是处理一个分区的数据,那么也就是和这些数据给ReduceTask1进行处理,和这些数据给ReduceTask2进行处理(如下图23红箭头指向的所示)

图23:

那么我们继续看后面的流程(如下图24红箭头指向的所示)

图24:

当ReduceTask1从不同的map中拉取到的本来应该同一个分区中的数据在reduce的shuffle过程会再一次进行归并,这个归并排序和上面所讲的那个map那个shuffle过程中的归并排序是一致的,那个时候map阶段的shuffle过程用到归并排序的原因是因为多次溢写产生了本来应该在一个分区里面的数据写到了多个分区里面。那么在reduce的shuffle过程将不同map里面的同一个分区里面的数据进行合并,那么reduce的shuffle过程合并完成之后之后直接给真正的reduce(如下图25红箭头指向所示)

图25:

那么怎么理解整个map的shuffle过程和reduce的shuffle过程和再和真正的reduce呢?我举个例子吧!

        map的shuffle过程和reduce的shuffle过程可以理解为我在广东广州这个地方要去北京这个地方,然后map的shuffle过程意思就是在准备离开广东省境内了,然后到了reduce的shuffle的地方可以理解为我马上要到北京境内了,然后shuffle完了到了最终的reduce进程里面就是等于到了公司的最终办公地点了。就是这个意思。

        那么我们跳出这个reduce的shuffle过程到真正的reduce这里是真正的我们在写代码涉及到的那个过程,真正的reduce里面有个reduce()方法,那么这个reduce()方法会回调,它有几组就回调几次。

        刚才提到了分区这个概念,分区指的是同一个分区里面的数据肯定要交给同一个reduce这个类的实例化对象去处理,那么这个分组就是这个分区里面有几个分组,比如说可能一个分区里面有两个分组(如下图26红箭头指向所示)

图26:

再比如a[1,1,1],b[1,1]我们拿这个字符串做默认分组的话,那么就是a[1,1,1]是一个组,b[1,1]是一个组。如果a和b它们的两个hash值最终一样的话那么它们两个就是在同一个分区里面,那么它们在同一个分区里面,我的reduce()方法如果只调用一次的话是不是就没有办法去区分和聚合对吧?所以在分区里面要进一步划分,a和b这两个单词虽然是一个区里面,但是它应该是在不同的组里面(如下图27红箭头指向所示)

图27:

这个GroupingComparator(k,knext)这个类也可以说就是分组的类,但是其实也不是这个类GroupingComparator(k,knext),你先暂时的理解成是这个类,只是留一个概念,它真正分组的这个类不是GroupingComparator()这个类。 分完组以后reduce聚合(如下图28红箭头指向所示)

 图28:

聚合完成以后写到上下文里面也就是Context.write(k,v),写到上下文之后,要把k和v里面的数据带到outputformat里面了,带到outputformat里面之后,这个outputformat是一个类,那么肯定要实例化的,那么做WordCount的需求,这个outputformat默认的实现子类是TestOutputFormat,outputformat的实例化对象负责来实例化RecordWriter,刚才我们说map读取数据本质上是由inputformat的RecorderReader那个对象的实例化来读取数据的,而写出数据是靠outputformat的实例化对象去实例化RecordWriter,然后这里面有一个Writer方法,这个方法是真正的将数据输出的一个方法,那么输出的时候其实用到了FSDataoutputStream这个输出流,输出到HDFS。

那么我们可以不输出到HDFS上面,只要我们自己来实现这个RecordWriter,并且自己来实现这个Writer方法里面的逻辑就可以了,你想往哪写就可以往哪写!

                                然后最后是数据输出的最终结果(如下图29红箭头指向所示)

 图29:

第一个reduce产生的文件叫Part-r-00000

图30:

然后第二个reduce跟第一个reduce的过程是一样的,那么第二个reduce产生的文件叫Part-r-00001 ,这是最终的一个结果图!

       如果把上面所细讲的整个mapreduce工作流程理解透了,那么对我们学习大数据的其他组件就是轻而易举的事情了!!

你可能感兴趣的:(mapreduce之详细工作流程原理)