hadoop生态系统之MR详解

笔者将以第一人称视角向各位阐述MR,从两个大方向描述MR旨在将自己所学所会融进这套知识体系。
1. 站在系统设计的角度讲讲MR在hadoop生态系统中上下游扮演的角色起到了什么作用及为什么需要MR
2.技术性细节,MR的整个工作流程
如有不到之处烦请指正

一 宏观剖析

1 MR是什么?

MapReduce是一种计算模型,用以进行大数据量的计算。其中Map对数据集上的独立元素进行指定的操作,生成键-值对形式中间结果。Reduce则对中间结果中相同“键”的所有“值”进行规约,以得到最终结果。MapReduce这样的功能划分,非常适合在大量计算机组成的分布式并行环境里进行数据处理。

2 MR在hadoop生态圈上游是谁?下游是谁?

既然MR是一种计算模型,那肯定需要有大量的数据才能计算对吧?如果没有数据,那MR可以说是巧妇难为无米之炊,在这个前提之上,那么谁能存下大量的数据呢?当然是HDFS,所以笔者认为MR在生态系统中对应的上游就是HDFS这种能存储大量离散数据的系统。
既然有了上游,那MR就可以开始干活了,干完活了问题来了,我干完活把处理完的东西交给谁?这东西肯定要存起来对吧
那么经过MR的计算聚合,得到的数据通常是结构化的即以KV对形式存在的数据,那我们想一想谁适合当这个下游呢?数据经过MR变成结构化数据了,但是结构化的数据可能还是不能满足要求啊,MR在处理大数据方面是优势,但是这并不能不代表MR的效率高对吧。这个效率为王的时代,笔者认为下游应该是可以解决海量结构化数据统计问题的数据库。可以将数据的存储和并行计算结合在一起。

3 为什么要学习MR?

如果前面两点还不能足够说明的话,那我们来看看Hadoop的官网介绍,hadoop之源。
在这里插入图片描述

二 微观剖析

先上一张MR输入输出的宏观流程图:
在这里插入图片描述

从图中我们可以看到,数据是从input端进来,经过Mapper—Shuffle—Reducer—最后在Output输出到我们指定的路径下。
接下来我们就从这几个阶段分别了解一下,一条数据在这条路上到底经历了什么。

现在假设我们现在有一个文件:
文件大小:200MB
HDFS默认Block大小:128MB
在这里插入图片描述

现在我要处理这个文件,那按照默认的文件块大小它被分成了两个文件块对吧,一个128MB一个72MB。
下一步客户端提交任务:
在客户端提交这个任务之后会创建一个内部的JobSummiter,调用其submitJobInternal()方法先进行一大堆的检查比如检查当前Job的状态,检查当前模式是集群模式还是本地模式,输出目录和作业输入分片等等一系列的操作,类似于你去坐飞机过安检一样~
校验之后呢它会被分配一个Joib然后创建Job路径将运行作业所需要的资源JAR包复制到一个以作业ID命名的目录下的共享文件系统中。
计算切片信息:
找到数据存储的路径
遍历处理规划切片(标记的索引元数据)下的每一个文件
遍历第一个文件
1)获取文件的大小fs.size
2)计算切片大小 computeSplitSize(Math.max(min.Size,Math.min(maxSize,blockSize)))
3)集群默认情况下切片大小等于blockSize = 128MB,本地模式下blockSize = 32MB
4)开始切片,第一个切片在0-128MB,第2个切片在128-256MB…每次切片时都要判断切完剩下的部分是否大于块的1.1倍,大于1.1倍就继续切片
5)将切片信息写入到切片规划文件中
6)整个切片的核心过程都在getSplit()方法中完成
7)InputSplit只记录了切片中的元数据,比如起始位置 长度以及所在的节点列表等
最后提交切片规划文件和配置文件到共享文件路径下,YARN上的MrAppMaster根据切片规划文件计算开启MapTask个数。

在这里插入图片描述调用submitApplication()方法。
跟resourcemanager通信,是主动找老大通信不是老大找你哦~,然后老大将请求初始化为一个Task,进入一个任务调度队列
在这里插入图片描述
此时老大手下的NodeManager会定时向老大轮询,问:老大有没有任务呀。。。老大有没有任务,对,就像这样的反复轮询。如果这时所有的小弟都在干活,那在队列中的Task就处于等待状态,当其中一个NM领取到Task任务以后,就会创建一个容器Container,并产生一个MRAppmaster。
在这里插入图片描述
这个AM进程就负责我们接下来任务的资源调度。
什么是资源调度?那我简单举个栗子:1,我运行MR任务需要消耗CPU消耗内存对吧,那我现在在一个集群模式下,我应该消耗那一台机器的CPU和那一台机器的内存呢?由AM来告诉我们。
那AM是怎么知道的呢?RM,YARN框架持有所有机器的状态对吧,每台机器剩余多少CPU,内存以及你跑这个MR任务需要多少CPU需要多少内存,是不是都要提前知道对吧,所以老大把当前集群的资源状态信息告诉AM了,然后手下的小弟AM根据当前集群的资源状态来选择我们这个任务应该放在那一台机器上去跑。
然后AM根据参数的配置形成一个任务规划,我们现在这个单词本是200M,分成两个文件块,也就是一个是0-128,一个是128-200。
AM现在需要向RM申请运行MR任务所需要的资源,既然是帮我们申请,那肯定需要知道我们的各种任务信息对吧,这里就由Container从HDFS上拷贝资源到本地,然后AM来读取任务资源信息,根据提交的切片规划文件开启MapTask个数,分配任务给map,
在这里插入图片描述
第一个切片处理0-128m,第二个map处理128-200m。
那么我们现在看其中一个map是如何处理数据的:
在这里插入图片描述
上图中的RecorderReader这个东西才是本质上读取数据的东西,这个RecorderReader的实例化对象里面的reader()方法是用来读取数据的,就是在这个reader()方法里面我们创建了一个输入流,通过这个输入流我们把数据读出来了,然后RecorderReader这个类由inputformat这个类里面的某个线程来实例化,读出来之后和这个文本里面的单词就会以k,v键值对的形式读取进来,为什么是键值对呢?
回想一下,我们在map这个类的时候是不是有四个泛型?第一个泛型指的是偏移量对吧?这个偏移量的类型叫LongWritable,第二个泛型指的是这个第一个泛型偏移量对应的那一行数据,类型是Text类型,那么这个第一个泛型这个LongWritable它第一次读的时候它偏移量是0,第二次读的时候它的偏移量是1,那么这个k表示的是第几行数据,这个v是每一行数据的具体字符串内容,拿过来之后我们在Mapper的map()方法里面是不是做了一个切割?就是比如把一行行字符串,用空格去切分,把这行这个字符串切分成若干个单词,因为它本身就是由多个单词组成的,中间就是用空格隔开,然后把处理后的数据放到一个数组里面了。
我们在map方法里切割单词这个操作做完以后还做了什么?回想一下,切完以后是一个数组,接着我们是不是for循环遍历这个数组,遍历过程中没拿到这个数组里面的一个元素都把它组合成了一个<k,v>对,最后通过上下文context.write(k,v)写出去了,注意这里写出去的<k,v>和我们传进来的k,v就不是一个kv了,maptesk的k和v是一行一行数据,我们处理后的kv是一组一组的按照我们想要的方式处理过的数据。拿wordcount案例来说,k就代表一个单词,v就代表1用来做增量累加统计。
接下来数据会进入到一个环形缓冲区,那什么是环形缓冲区呢?
在这里插入图片描述
上图这个就叫环形缓冲区,本质这个环形内存就是一个非常普通的数组,数据被我们封装成kv对以后进入到环形数组内,比如说我的kv对是(a,1),这个字母a在ASCII码中对应的码值是97对吧,97这个数字我做二进制的话,把他保存到一个int类型的变量里面,它是不是占用4个字节?那么97就是这个Context.write(a,1)里的这个k,后面对应的1给给它转成int,对应的也是4个字节对吧。那么序列化的操作,就是在这个过程发生的,简单来说序列化操作就相当于把我们可以阅读和理解的一块数据以二进制的形式给它变成了不可理解的数据,这就叫序列化过程。序列化完成以后写到数组里面,这个数组是一个字节数组,那现在我们这个(a,1)在写的时候怎么写呢?
应该是[0,0,0,0]这一块代表a,因为这是一个字节数组,字节数组就意味着这个数组里面每一个元素都是一个字节,而我们这个字母a或者是字符串a它序列化成了一个int类型的二进制的一个数字,那我们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]这是一个单词的<a,1>,那么下一个单词进来可能是<b,1>,那这个<b,1>我是不是还要往[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>这个例子为例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开始把这个数据往里面写,然后右边从最后开始写,写这个原数据对应的元数据信息也就是[–> <–]这样的一个过程,那接着把这个数组首部到尾部连起来是不是就是一个圈了?这个概念就是叫环形数组,也就是最开始说的环形缓冲区。
从圈上面的中间是0这个位置和数组末尾的位置,数组0和数组末尾你想象一下是一个纸带,你把这个纸带给它拽一下给它连起来,它是不是就变成是一个环形的了?
那么接下来看一下这个操作,这个数据本身往我们对着这个环形数组的右边开始写,也就是<a,1>往这个环形数组的右边开始写,然后这个数据本身它对应的这个数组中存放的这个索引往我们对着环形数组的左边开始写,它是一个直线,但是这个数组肯定不是一个环形的,你想让他变成一个环形的怎么操作,你是不是直接求余就行啊?然后这个环形数组默认的是100m大小,数据本身往环形数组的右边开始写, 数据本身对应的元数据信息往左边开始写,然后这个环形数组出现了一个单词buffindex,buffindex的意思是这个数据本身它这个偏移量叫buffindex,这个偏移量是依次递增的,然后环形数组左边这个元数据的偏移量叫kvindex,它的这个在数组中的索引是依次下降的,元数据的长度它不是固定的。
那么它一直这样写,那么我这个环形数组的内存也就只有100m大小,你一直这样写的话就一下子会满了对吧?满了的话就会内存溢出,我们这是处理大数据的框架,你怎么可能让他内存溢出呢?我们的数据可能要几个T几百个T都有可能,那我现在只有100m怎么办呢?你是不是需要溢写啊?你需要把内存中的数据把这个中间结果给它临时的保存到磁盘上,那什么时机才去保存呢?当这个环形数组的数据存储量达到百分之八十的时候就开始执行split溢写这个操作,它会把环形数组里面存有的百分之八十的这个数据给它写到本地磁盘上,那写到本地磁盘上之后,它这个内存也不会立刻释放,因为它写的这个过程也是消耗时间的,那么新的数据是不是还可能往那个环形数组的内存里面去加啊?那能不能加呢?当然也是可以的,因为我们环形数组里面还有百分之二十呢,这就是为什么设计到百分之八十的时候就开始溢写而不是到百分之百的时候才开始溢写,因为如果我内存占用率达到百分之百再开始溢写的话,就会导致我们新的数据就要暂停要阻塞就不能继续往内存里面写了,这影响效率,这个默认的100m这个值和百分之八十这个值是可以改的。
如果说这个环形数组内存设计的越大,这个任务跑的就越快,那就能明白为什么spark比mapreduce跑的快,那就是因为spark它内存消耗的多,内存占用多那么数据落盘就少,数据落盘少那数据处理的效率就会变快。
那么在溢写的过程中新来的数据怎么往环形数组里面写呢?那就是我进来一个新的数据在百分之二十的那个地方选择一个中间点,然后依靠于中间点的位置来往右手边和左手边写数据,中间点的右手边写新来的数据本身,中间点的左手边写新数据本身所对应的那个元数据信息。那么就是说溢写过程不耽误新数据往这个环形数组里面添加,因为这是一个同步操作,溢写线程和把这个数据写到环形数组里面的线程是两个线程,也属于并行操作。
在溢写的时候都干了什么呢?
在这里插入图片描述
首先它会对数据进行分区,如上图所示。
比如说我们现在这个单词本不是200m吗?然后被分为两块对吧?一个是0到128m,一个是128到200m,那么就0到128m这个文件单词块里面,在环形数组内存里面拿到了80m的数据了,然后这个环形数组内存里面已经达到阈值要落盘了,落盘的时候它会对这个80m的数据进行分区,那么按照什么来分区呢?默认就是按照key来分区,key就是我们的单词,那它默认的分区规则是,比如说这个字符串单词是a然后点hacode(a.hashcode),然后这个单词的hashcode之后是一个数字对吧?然后这个数字和int的最大值进行与运算,因为它害怕这个单词的hash值它超过了int类型的存储范围,所以它跟int的最大值进行与运算,与运算完成之后得到的肯定是这个长度的一个数字,然后再把这个结果和要启动的reduce的个数进行求余,reduce的个数是可以设置的,redude的个数默认是一个,那么这就是默认是一个分区规则。
那么第一个单词是<a,1>,第二个单词是<b,1>,第三个单词是<c,1>,我将这三个单词分区,一个区就对应着一个文件,你现在不是要把环形数组内存里面的数据溢写到本地磁盘吗?那你一定要对应着文件的产生,那么这个文件是按照分区去产生的,这边有几个分区,就产生几个文件,这里仅仅针对于这一次的溢写过程。
在这里插入图片描述
分区内部还要排序,先分区,分完区才会排序,那是不是分完区之后立刻就产生文件呢?其实也不是
举个例子:我们已经分好区了,那分好区之后,我们要排序,那么怎么排呢?是不是要对这个文件里面的内容进行排序?一个分区对应一个文件,分区内排序就相当于把某一个分区所对应的文件的所有的数据给它读出来,然后给它排个序。那么现在这个例子就是如上图所示。
如果我们现在有a和c这两个英文单词,这两个英文单词a和c它hash之后和这个int类型的最大值与运算值之后,然后又和这个reduce的个数求余,求完余之后得到一个数字,然后这个数字正好一样,那么就意味着a和c这两个英文单词属于同一个分区,也就是一个文件里面。
那么这两个单词在一个文件里面有可能是<c,1><a,1>这个顺序,但是我们要排个序,要把<a,1>放到前面,<c,1>放到后面,这个排序默认是按照k进行排序的,如果是字符串的话,那么它的排序规则就是按位进行比较,就是这个字符串每一个这个英文的ascii的大小比较这样一个排序,也就是字典排序。
现在有两个分区文件
在这里插入图片描述
这第二个分区文件里面的数据都是b,那么肯定它这个hash值是一样,hash值一样那跟int类型的最大值与运算结果也是一样,最后跟reduce求余的结果肯定也还是一样,所以说这两个b只要是一样的,那么肯定是在同一个分区里面。
但是同一个分区里面有可能是不同的这个英文单词,接着如下
在这里插入图片描述
第三个分区和第四个分区里面是这样的两个文件,这第三个分区和第四个分区文件是环形数组内存里面第二次达到80m了然后进行溢写之后的分区文件,那么也就是说前两个分区是环形数组内存第一次达到80m的时候溢写产生的两个分区文件,后两个是环形数组内存又一次达到80m的时候又溢写产生的另外两个分区文件。
那么第一次达到80m的时候里面的数据可能是<a,1><c,1>,<b,1><b,1>这四个英文单词,我第二次又达到80m了,这个环形数组内存里面的数据有可能是<a,1><e,1>,<b,1><f,1> 这样的数据,那么<a,1><c,1>是在同一个分区里面,然后在第二次达到80m溢写之后<a,1><e,1>是在同一个分区里面,那么这<a,1><c,1>的hash肯定也是一致的,然后<a,1><e,1>的hash值也肯定是一致的,那么是不是就意味着可能<a,1><c,1>和<a,1><e,1>这四个英文单词两个分区其实应该在同一个分区里面对吧?但是它为什么就不在同一个分区里面呢?因为这是两次溢写的过程,第二次溢写的这个数据是不会直接利用第一次溢写生成的那个分区文件的,两次溢写产生了四个分区文件,这四个分区文件里面有些数据是应该在一个分区里面的。
接下来要进行一个归并排序:
在这里插入图片描述
归并排序就是把<a,1><c,1>和<a,1><e,1>放到了一个分区里面了,那么也就是说把<a,1><c,1>和<a,1><e,1>这两个分区文件合并成一个文件,然后把原来的<a,1><c,1>和<a,1><e,1>这两个分区文件给删掉,那是不是意味着我们每次溢写过程,会产生若干个小文件,当整个溢写过程结束的时候,所有的小文件都会按照分区来合并成一个大文件。
在这里插入图片描述
上图红箭头指向的合并过程就是一个可选过程,如果说我们这个合并过程配置的话,那么效率将会大大的提高。那么Combiner是什么意思呢?比如说在map阶段我输出的是<a,1><a,1><b,1>,我如果按照<a,1><a,1><b,1>这个内容去传给reduce的话,那应该到了reduce效果就是a,[1,1],b[1]这样的一个数据效果对吧?但是呢<a,1><a,1>这两个1我传给reduce是不是有点消耗资源?那么我能不能在map阶段的这个shuffer过程中能不能把这两个1合并之后再传过去呢?其实是可以的,也就是说我想传给reduce的不是a,[1,1]这样的数据,而是传给reduce的是a[2]这样的一个数据效果这样也是可以的,那么这个Combiner就是来做这样的一个操作的。
在map阶段提前做了一次聚合是会在传给reduce的过程中大大的提升了传输效率,就比如你在QQ传文件给对方的时候,一般你最好都要打成压缩包,然后再传给对方,其实这样的话是把全部溢写小的文件合成一个大文件去传输,将大大的提高的网络IO的传输效率。
然后接下来还有一个排序
为什么还有这么多排序呢?我们一开始在原数据200m这里是不是拆分了两个文件块?那么我们这上面一直讲的是以一个文件块一个map为例来做的详细工作流程,这个文件是200m,分了两个map来处理这200m的数据,那么这两个map是不是有可能在不同的机器上?那么比如第一个map在第一台linux上进行运算,第二个map在第二台linux上进行运算,那么这两个机器上面处理的数据都有共同的一个a这个单词,两个不同的机器分别有一个相同的英文单词,那么后面该怎么操作呢?就是把这两个map里面的数据交给reduce进行聚合操作。其实上面写了的一大堆都是只针对于以一个map为例的整个工作流程。
现在来说一下多个map之间怎么将数据归类,这两个map有可能在不同的机器上也有可能在同个机器上,即使在同个机器上也是不同的map进程。
在这里插入图片描述
第一个map产生出了<a,1><a,1><c,1><e,1> <b,1><b,1><b,1><f,1>这样的数据结果,第二个map产生出了<g,1> <h,1>这样的数据结果(这是没有配置Combiner的效果),那么接下来要把这两个map处理的中间数据同时的给reduce。
在这里插入图片描述
启动对应的ReduceTask(如上图所示)
根据自己的分区号,去各个MapTask机器上取相应的结果分区数据,
在这里插入图片描述
一个reduce必须是处理一个分区的数据,那么也就是<a,1><a,1><c,1><e,1>和<g,1>这些数据给ReduceTask1进行处理,<b,1><b,1><b,1><f,1>和<h,1>这些数据给ReduceTask2进行处理(如上图所示)
注意:我们的reduce的个数最好等于分区个数,如果reduce一旦小于分区个数且不等于1,那么就会报错!所以最好是reduce跟分区的个数保持一致,但也最好不要大于分区个数,因为这样等于是浪费资源。
那我们继续往下走,当ReduceTask1从不同的map中拉取到的本来应该同一个分区中的数据在reduce的shuffle过程会再一次进行归并,这个归并排序和上面所讲的那个map那个shuffle过程中的归并排序是一致的,那个时候map阶段的shuffle过程用到归并排序的原因是因为多次溢写产生了本来应该在一个分区里面的数据写到了多个分区里面。那么在reduce的shuffle过程是将不同map里面的同一个分区里的数据进行合并,reduce的shuffle过程合并完成之后之后是直接给真正的reduce。
在这里插入图片描述
当归并结束也就意味着我们跳出了reduce的shuffle过程来到真正的reduce这里是我们在写代码涉及到的那个过程,reduce里面有个reduce()方法,那么这个reduce()方法会回调,它有几组(key,value)就会调几次。
刚才提到了分区这个概念,分区指的是同一个分区里面的数据肯定要交给同一个reduce这个类的实例化对象去处理,那么这个分组就是这个分区里面有几个分组,比如说可能一个分区里面有两个分组,
再比如a[1,1,1],b[1,1]我们拿这个字符串做默认分组的话,那么就是a[1,1,1]是一个组,b[1,1]是一个组。如果a和b它们的两个hash值最终一样的话那么它们两个就是在同一个分区里面,那么它们在同一个分区里面,我的reduce()方法是不是就没有办法去区分和聚合对吧?所以在分区里面要进一步划分,a和b这两个单词虽然是一个区里面,但是它是在不同的组里面(如下图红箭头指向所示)
在这里插入图片描述
分完组以后reduce聚合
在这里插入图片描述
聚合完成以后写到上下文里面也就是Context.write(k,v),写到上下文之后,要把k和v里面的数据带到outputformat里面了,带到outputformat里面之后,这个outputformat是一个类,类肯定是要实例化的,在做WordCount的需求时,这个outputformat默认的实现子类是TextOutputFormat,outputformat的实例化对象负责来实例化RecordWriter,刚才我们说map读取数据本质上是由inputformat的RecorderReader那个对象的实例化来读取数据的,而写出数据是靠outputformat的实例化对象去实例化RecordWriter,然后这里面有一个Writer()方法,这个方法是真正的将数据输出的一个方法,那么输出的时候其实用到了FSDataoutputStream这个输出流,输出到HDFS。

package com.atguigu.outputformat;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FSDataOutputStream;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.RecordWriter;
import org.apache.hadoop.mapreduce.TaskAttemptContext;

import java.io.IOException;

public class Recordwriter extends RecordWriter<Text,NullWritable> {

    private final FSDataOutputStream atguiguout;
    private final FSDataOutputStream otherout;

    public Recordwriter(TaskAttemptContext job) throws IOException {
        Configuration conf = job.getConfiguration();
        FileSystem fs = FileSystem.get(conf);
        //创建文件输出路径
        Path path = new Path("d:/atguigu.log");
        Path path1 = new Path("d:/other.log");
        //创建输出流
        atguiguout = fs.create(path);
        otherout = fs.create(path1);
    }

    @Override
    public void write(Text key, NullWritable value) throws IOException, InterruptedException {
        if(key.toString().contains("atguigu")){
            atguiguout.write(key.toString().getBytes());
        }else {
            otherout.write(key.toString().getBytes());
        }
    }

    @Override
    public void close(TaskAttemptContext context) throws IOException, InterruptedException {
        atguiguout.close();
        otherout.close();
    }
}

最终是数据输出的结果
在这里插入图片描述
第一个reduce产生的文件叫Part-r-00000,第二个reduce跟第一个reduce的过程 是一样的,产生的文件叫Part-r-00001

最后附上三张图,望各位看官有所收获。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

  • 6
    点赞
  • 34
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值