MapReduce 整体的工作流程和原理介绍

一、MR 的整体工作流程

在这里插入图片描述

  • MapTask 阶段
  • Shuffle 阶段
  • ReduceTask 阶段

二、MapTask 阶段

1. 工作机制

在这里插入图片描述

  • Read 阶段:客户端根据待处理的数据进行接片,然后向 Yarn 提交作业信息申请 对应数量的 MapTask;MapTask 启动后通过 InputFormat 获得的 RecordReader,从输入 InputSplit 中解析出一个个的 key/value 读取到 Mapper 程序。
  • Map 阶段:将解析出的 key/value 交给用户编写的 map() 方法进行处理,并产生一系列新的 key/value。
  • Collect 阶段:在用户编写的 map() 方法中,当数据处理完成后,一般会调用 OutputCollector.collect() 输出结果。在该函数内部,它会将生成的 key/value 进行分区(调用Partitioner),并写入一个环形内存缓冲区中。
  • Spill 阶段:即“溢写”, 当环形缓冲区满后, MapReduce 会将数据写到本地磁盘上,生成一个临时文件。需要注意的是,将数据写入本地磁盘之前,先要对数据进行一次本地排序,并在必要时对数据进行合并、压缩等操作。
    • 步骤 1:利用快速排序算法对缓存区内的数据进行排序,排序方式是,先按照分区编号 Partition 进行排序,然后按照 key 进行排序。这样,经过排序后,数据以分区为单位聚集在一起,且同一分区内所有数据按照 key 有序。
    • 步骤 2: 按照分区编号由小到大依次将每个分区中的数据写入任务工作目录下的临时文件 output/spillN.out(N 表示当前溢写次数)中。如果用户设置了 Combiner,则写入文件之前,对每个分区中的数据进行一次聚集操作。
    • 步骤 3: 将分区数据的元信息写到内存索引数据结构 SpillRecord 中,其中每个分区的元信息包括在临时文件中的偏移量、压缩前数据大小和压缩后数据大小。如果当前内存索引大小超过 1MB,则将内存索引写到文件 output/spillN.out.index 中。
  • Merge 阶段:当所有数据处理完成后,MapTask 对所有临时文件进行一次合并,以确保最终只会生成一个数据文件。当所有数据处理完后,MapTask 会将所有临时文件合并成一个大文件,并保存到文件 output/file.out 中,同时生成相应的索引文件 output/file.out.index。在进行文件合并过程中, MapTask 以分区为单位进行合并。对于某个分区, 它将采用多轮递归合并的方式。 每轮合并 mapreduce.task.io.sort.factor (默认 10) 个文件,并将产生的文件重新加入待合并列表中,对文件排序后,重复以上过程,直到最终得到一个大文件。让每个 MapTask 最终只生成一个数据文件,可避免同时打开大量文件和同时读取大量小文件产生的随机读取带来的开销。

2. InputFormat

InputFormat 是 MapReduce 输入的基类,所有 MapReduce 输入都实现了 InputFormat 接口

一般常见的输入文件格式包括:基于行的日志文件、二进制格式文件、数据库表等,为了 MapReduce 程序能针对性的处理不同文件格式,FileInputFormat 常见的接口实现类包括:TextInputFormat、KeyValueTextInputFormat、NLineInputFormat、CombineTextInputFormat 和 自定义 InputFormat 等。

2.1 TextInputFormat

默认的 FileInputFormat 实现类

  • 按行读取每条记录

  • 键是存储该行在整个文件中的起始字节偏移量,LongWritable 类型。

  • 值是这行的内容,不包括任何行终止符(换行符和回车符),Text类型。

    文件内容:
    Rich learning form
    Intelligent learning engine
    Learning more convenient
    From the real demand for more close to the enterprise
    
    读取记录:
    (0,Rich learning form)
    (20,Intelligent learning engine)
    (49,Learning more convenient)
    (74,From the real demand for more close to the enterprise)
    
2.2 CombineTextInputFormat

用于小文件过多的场景,可以将多个小文件从逻辑上规划到一个切片中,这样,多个小文件就可以交给一个 MapTask 处理。

  • 虚拟存储过程:

    1. 将输入目录下所有文件大小,依次和设置的 setMaxInputSplitSize 值比较,如果不大于设置的最大值,逻辑上划分一个块。如果输入文件大于设置的最大值且大于两倍,那么以最大值切割一块;当剩余数据大小超过设置的最大值且不大于最大值 2 倍,此时将文件平均分成 2 个虚拟存储块(防止出现太小切片)。
    2. 例如:setMaxInputSplitSize 值为 4M,输入文件大小为 8.02M,则先逻辑上分成一个 4M。剩余的大小为 4.02M,如果按照 4M 逻辑划分,就会出现 0.02M 的小虚拟存储文件,所以将剩余的 4.02M 文件切分成(2.01M和2.01M)两个文件。
  • 切片过程:

    1. 判断虚拟存储的文件大小是否大于 setMaxInputSplitSize 值,大于等于则单独形成一个切片。
    2. 如果不大于则跟下一个虚拟存储文件进行合并,再重复第一步。
  • 举例:有 4 个大小分别为 1.7M、5.1M、3.4M 以及 6.8M 的小文件,经过虚拟存储过程后形成 6 个虚拟文件块,大小分别为:1.7M,(2.55M、2.55M),3.4M 以及(3.4M、3.4M),经过切片过程最终会形成 3个切片,大小分别为:(1.7+2.55)M,(2.55+3.4)M,(3.4+3.4)M

3. 数据切片

数据切片是 MapReduce 在处理数据时在逻辑上对数据划分的基本单位,是计算时输入数据的单位

  • MapReduce 程序获取数据存储的目录
  • 遍历目录下的每一个文件(按每一个文件单独进行数据切片)
  • 获取一个文件后:
    1. 获取文件大小
    2. 计算切片大小(默认情况下为块大小 128M)
      // minSize 默认为 1,maxSize 默认为Long的最大值
      computerSplitSize(Math.max(minSize,Math.min(maxSize,blockSize)))
      // minSize 和 maxSize 配置:
      // mapred-site.xml 的 mapreduce.input.fileinputformat.split.minsize 和 mapreduce.input.fileinputformat.split.maxsize 参数
      
    3. 开始切片,计算出切片个数
      若文件为 300M,则第一个切片为 0-128;第二个切片为 128-256;第三个切片为 256-300
      每一次切片时,要判断剩下的部分是否大于块的 1.1 倍,若不大于 1.1 倍,则不再继续划分
      
    4. 将切片信息写入一个切片规划文件 Job.split
    5. 核心过程:FileInputFormat.getSplit() 方法
    6. 切片的元数据信息记录在 FileSplit 中
  • 提交切片规划文件到 Yarn 上,MrAppMaster 根据切片规划文件计算开启的 MapTask 数量,即 MapTask 的并行度

4. Job 提交流程

在这里插入图片描述

Job.waitForCompletion();

Job.submit();

// 1建立连接
	connect();	
		// 1)创建提交Job的代理
		new Cluster(getConfiguration());
			// (1)判断是本地运行环境还是yarn集群运行环境
			initialize(jobTrackAddr, conf); 

// 2 提交job
submitter.submitJobInternal(Job.this, cluster)

	// 1)创建给集群提交数据的Stag路径
	Path jobStagingArea = JobSubmissionFiles.getStagingDir(cluster, conf);

	// 2)获取jobid,并创建Job路径
	JobIDjobId = submitClient.getNewJobID();

	// 3)拷贝jar包到集群
	copyAndConfigureFiles(job, submitJobDir);	
	rUploader.uploadFiles(job, jobSubmitDir);

	// 4)计算切片,生成切片规划文件
	writeSplits(job, submitJobDir);
		maps = writeNewSplits(job, jobSubmitDir);
		input.getSplits(job);

	// 5)向Stag路径写XML配置文件
	writeConf(conf, submitJobFile);
	conf.writeXml(out);

	// 6)提交Job,返回提交状态
	status = submitClient.submitJob(jobId, submitJobDir.toString(), job.getCredentials());

三、Shuffle 阶段

1. 基本流程

  • MapTask 收集 map 方法输出的 kv 对,写入到内存环形缓冲区中
  • 当环形缓冲区中的数据写到 80% 时开始溢写数据到磁盘文件,同时进行反向写入数据,可能会溢写多个文件
  • 在溢写之前,要调用 Partitioner 进行分区和针对 key 索引进行排序
    • 排序方法:快速排序
    • 针对 key 的索引排序
    • 按照文件的字典顺序进行
  • 多个溢出文件会按照分区被合并成大文件,并对 key 进行归并排序,然后压缩写入磁盘
  • ReduceTask 根据自己的分区号,去各个 MapTask 机器上主动拉取相应的结果分区数据
  • ReduceTask 会抓取到同一个分区的来自不同 MapTask 的结果文件,ReduceTask 会将这些文件再进行合并和归并排序
  • 合并成大文件后,Shuffle 的过程也就结束了,后面进入 ReduceTask 的逻辑运算过程(从文件中取出一个一个的键值对 Group,调用用户自定义的 reduce 方法)

2. 分区

2.1 介绍
  • 使用分区可以将统计结果按条件输出到不同的文件中

  • 默认 Partitioner 分区:按照 key 的 Hash 值计算分区

    public class HashPartitioner<K,V> extends Partitioner<K,V> {
        public int getPartition(K key, V value, int numReduceTasks) {
            return (key.hashCode() & Integer.MAX_VALUE) % numReduceTasks;
        }
    }
    
  • 自定义 Partitioner 分区

    • 创建一个类继承 Partitioner 类并重写 getPartition 方法

      public class MyPartitioner<K,V> extends Partitioner<K,V> {
          @Override
          public int getPartition(K key, V value, int numReduceTasks) {
              
          }
      }
      
    • 在 Driver 类中设置分区为自定义类,并设置 ReduceTasks 值

      job.setPatitionerClass(MyPartitioner.class);
      job.setNumReduceTasks(5); // 默认为 1,不会使用自定义分区类
      
  • 分区的注意事项:
    • numReduceTasks > partition,会产生空的分区输出文件
    • 1< numReduceTasks < partition,会有部分分区文件没法被拉取,产生 IO 异常
    • numReduceTasks = 1,只会产生一个分区文件,不会走自定义分区类的逻辑
    • 分区号必须从 0 开始逐一累加
2.2 案例
  • 需求:将统计结果按照手机归属地不同省份输出到不同文件中(分区),手机号136、137、138、139 开头都分别放到一个独立的 4 个文件中,其他开头的放到一个文件中

    id   手机号        网络IP           域名         上行流量  下行流量  网络状态码   
    1	13736230513	192.196.100.1	www.apple.com	2481	24681	200
    2	13846544121	192.196.100.2			        264	      0	    200
    3 	13956435636	192.196.100.3			        132	     1512	200
    4 	13966251146	192.168.100.1			        240	      0	    404
    5 	18271575951	192.168.100.2	www.apple.com	1527	2106	200
    6 	84188413	192.168.100.3	www.apple.com	4116	1432	200
    7 	13590439668	192.168.100.4			        1116	954	    200
    8 	15910133277	192.168.100.5	www.hao123.com	3156	2936	200
    9 	13729199489	192.168.100.6			        240	      0	    200
    10 	13630577991	192.168.100.7	www.shouhu.com	6960	 690	200
    11 	15043685818	192.168.100.8	www.baidu.com	3659	3538	200
    12 	15959002129	192.168.100.9	www.apple.com	1938	 180	500
    13 	13560439638	192.168.100.10			        918	    4938	200
    14 	13470253144	192.168.100.11			        180	     180	200
    15 	13682846555	192.168.100.12	www.qq.com	    1938	2910	200
    16 	13992314666	192.168.100.13	www.gaga.com	3008	3720	200
    17 	13509468723	192.168.100.14	www.qinghua.com	7335	110349	404
    18 	18390173782	192.168.100.15	www.sogou.com	9531	2412	200
    19 	13975057813	192.168.100.16	www.baidu.com	11058	48243	200
    20 	13768778790	192.168.100.17			        120	     120	200
    21 	13568436656	192.168.100.18	www.alibaba.com	2481	24681	200
    22 	13568436656	192.168.100.19			        1116	 954	200
    
  • 编写程序

    • 自定义分区类:

      import org.apache.hadoop.io.Text;
      import org.apache.hadoop.mapreduce.Partitioner;
      
      public class ProvincePartitioner extends Partitioner<Text, FlowBean> {
      
          @Override
          public int getPartition(Text text, FlowBean flowBean, int numPartitions) {
              //获取手机号前三位prePhone
              String phone = text.toString();
              String prePhone = phone.substring(0, 3);
      
              //定义一个分区号变量partition,根据prePhone设置分区号
              int partition;
      
              if("136".equals(prePhone)){
                  partition = 0;
              }else if("137".equals(prePhone)){
                  partition = 1;
              }else if("138".equals(prePhone)){
                  partition = 2;
              }else if("139".equals(prePhone)){
                  partition = 3;
              }else {
                  partition = 4;
              }
      
              //最后返回分区号partition
              return partition;
          }
      }
      
    • 配置 Driver 类:

      import org.apache.hadoop.conf.Configuration;
      import org.apache.hadoop.fs.Path;
      import org.apache.hadoop.io.Text;
      import org.apache.hadoop.mapreduce.Job;
      import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
      import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
      import java.io.IOException;
      
      public class FlowDriver {
      
          public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
      
              //1 获取job对象
              Configuration conf = new Configuration();
              Job job = Job.getInstance(conf);
      
              //2 关联本Driver类
      		job.setJarByClass(FlowDriver.class);
      
              //3 关联Mapper和Reducer
              job.setMapperClass(FlowMapper.class);
              job.setReducerClass(FlowReducer.class);
      
              //4 设置Map端输出数据的KV类型
              job.setMapOutputKeyClass(Text.class);
              job.setMapOutputValueClass(FlowBean.class);
      
              //5 设置程序最终输出的KV类型
              job.setOutputKeyClass(Text.class);
              job.setOutputValueClass(FlowBean.class);
      
              //8 指定自定义分区器
      		job.setPartitionerClass(ProvincePartitioner.class);
      
              //9 同时指定相应数量的ReduceTask,必须与分区数一致
      		job.setNumReduceTasks(5);
      
              //6 设置输入输出路径
              FileInputFormat.setInputPaths(job, new Path("D:\\inputflow"));
              FileOutputFormat.setOutputPath(job, new Path("D\\partitionout"));
      
              //7 提交Job
              boolean b = job.waitForCompletion(true);
              System.exit(b ?0 : 1);
          }
      }
      

3. 排序

3.1 介绍
  • Hadoop 默认要求所有的 key 都能被排序

  • MapReduce 过程中默认会经历 3 次排序,Map 阶段 2 次,Reduce 阶段 1 次

  • 排序分类:

    • 部分排序:不同的分区内部进行排序
    • 全排序:结果输出一个文件且有序(效率低,不建议)
    • 辅助排序:GroupingComparator 分组
    • 二次排序:在自定义排序时,compareTo 方法的判断条件为 2 个
  • 自定义排序:实现 WritableComparable 接口,并重写 compareTo 方法;WritableComparable 接口继承 Writable 接口和 Comparable 接口

3.2 案例
3.2.1 全排序
  • 需求:统计每一个手机号耗费的总上行流量、总下行流量、总流量,并对总流量进行倒序排序

    id   手机号        网络IP           域名         上行流量  下行流量  网络状态码   
    1	13736230513	192.196.100.1	www.apple.com	2481	24681	200
    2	13846544121	192.196.100.2			        264	      0	    200
    3 	13956435636	192.196.100.3			        132	     1512	200
    4 	13966251146	192.168.100.1			        240	      0	    404
    5 	18271575951	192.168.100.2	www.apple.com	1527	2106	200
    6 	84188413	192.168.100.3	www.apple.com	4116	1432	200
    7 	13590439668	192.168.100.4			        1116	954	    200
    8 	15910133277	192.168.100.5	www.hao123.com	3156	2936	200
    9 	13729199489	192.168.100.6			        240	      0	    200
    10 	13630577991	192.168.100.7	www.shouhu.com	6960	 690	200
    11 	15043685818	192.168.100.8	www.baidu.com	3659	3538	200
    12 	15959002129	192.168.100.9	www.apple.com	1938	 180	500
    13 	13560439638	192.168.100.10			        918	    4938	200
    14 	13470253144	192.168.100.11			        180	     180	200
    15 	13682846555	192.168.100.12	www.qq.com	    1938	2910	200
    16 	13992314666	192.168.100.13	www.gaga.com	3008	3720	200
    17 	13509468723	192.168.100.14	www.qinghua.com	7335	110349	404
    18 	18390173782	192.168.100.15	www.sogou.com	9531	2412	200
    19 	13975057813	192.168.100.16	www.baidu.com	11058	48243	200
    20 	13768778790	192.168.100.17			        120	     120	200
    21 	13568436656	192.168.100.18	www.alibaba.com	2481	24681	200
    22 	13568436656	192.168.100.19			        1116	 954	200
    
  • 自定义 FlowBean 类实现 WritableComparable 接口

    import org.apache.hadoop.io.WritableComparable;
    import java.io.DataInput;
    import java.io.DataOutput;
    import java.io.IOException;
    
    public class FlowBean implements WritableComparable<FlowBean> {
    
        private long upFlow; //上行流量
        private long downFlow; //下行流量
        private long sumFlow; //总流量
    
        //提供无参构造
        public FlowBean() {
        }
    
        //生成三个属性的getter和setter方法
        public long getUpFlow() {
            return upFlow;
        }
    
        public void setUpFlow(long upFlow) {
    		this.upFlow = upFlow;
        }
    
        public long getDownFlow() {
            return downFlow;
        }
    
        public void setDownFlow(long downFlow) {
    		this.downFlow = downFlow;
        }
    
        public long getSumFlow() {
            return sumFlow;
        }
    
        public void setSumFlow(long sumFlow) {
    		this.sumFlow = sumFlow;
        }
    
        public void setSumFlow() {
    		this.sumFlow = this.upFlow + this.downFlow;
        }
    
        //实现序列化和反序列化方法,注意顺序一定要一致
        @Override
        public void write(DataOutput out) throws IOException {
            out.writeLong(this.upFlow);
            out.writeLong(this.downFlow);
            out.writeLong(this.sumFlow);
    
        }
    
        @Override
        public void readFields(DataInput in) throws IOException {
            this.upFlow = in.readLong();
            this.downFlow = in.readLong();
            this.sumFlow = in.readLong();
        }
    
        //重写ToString,最后要输出FlowBean
        @Override
        public String toString() {
            return upFlow + "\t" + downFlow + "\t" + sumFlow;
        }
    
        @Override
        public int compareTo(FlowBean o) {
    
            //按照总流量比较,倒序排列
            if(this.sumFlow>o.sumFlow){
                return -1;
            }else if(this.sumFlow<o.sumFlow){
                return 1;
            }else {
                return 0;
            }
        }
    }
    
  • 编写 Mapper 类:

    import org.apache.hadoop.io.LongWritable;
    import org.apache.hadoop.io.Text;
    import org.apache.hadoop.mapreduce.Mapper;
    import java.io.IOException;
    
    public class FlowMapper extends Mapper<LongWritable, Text, FlowBean, Text> {
        private FlowBean outK = new FlowBean();
        private Text outV = new Text();
    
        @Override
        protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
    
            //1 获取一行数据
            String line = value.toString();
    
            //2 按照"\t",切割数据
    		String[] split = line.split("\t");
    
            //3 封装outKoutV
            outK.setUpFlow(Long.parseLong(split[1]));
            outK.setDownFlow(Long.parseLong(split[2]));
            outK.setSumFlow();
            outV.set(split[0]);
    
            //4 写出outKoutV
    		context.write(outK,outV);
        }
    }
    
  • 编写 Reducer 类:

    import org.apache.hadoop.io.Text;
    import org.apache.hadoop.mapreduce.Reducer;
    import java.io.IOException;
    
    public class FlowReducer extends Reducer<FlowBean, Text, Text, FlowBean> {
        @Override
        protected void reduce(FlowBean key, Iterable<Text> values, Context context) throws IOException, InterruptedException {
    
            //遍历values集合,循环写出,避免总流量相同的情况
            for (Text value : values) {
                //调换KV位置,反向写出
                context.write(value,key);
            }
        }
    }
    
  • 编写 Driver 类:

    import org.apache.hadoop.conf.Configuration;
    import org.apache.hadoop.fs.Path;
    import org.apache.hadoop.io.Text;
    import org.apache.hadoop.mapreduce.Job;
    import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
    import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
    import java.io.IOException;
    
    public class FlowDriver {
    
        public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
    
            //1 获取job对象
            Configuration conf = new Configuration();
            Job job = Job.getInstance(conf);
    
            //2 关联本Driver类
    		job.setJarByClass(FlowDriver.class);
    
            //3 关联Mapper和Reducer
            job.setMapperClass(FlowMapper.class);
            job.setReducerClass(FlowReducer.class);
    
            //4 设置Map端输出数据的KV类型
            job.setMapOutputKeyClass(FlowBean.class);
            job.setMapOutputValueClass(Text.class);
    
            //5 设置程序最终输出的KV类型
            job.setOutputKeyClass(Text.class);
            job.setOutputValueClass(FlowBean.class);
    
            //6 设置输入输出路径
            FileInputFormat.setInputPaths(job, new Path("D:\\inputflow2"));
            FileOutputFormat.setOutputPath(job, new Path("D:\\comparout"));
    
            //7 提交Job
            boolean b = job.waitForCompletion(true);
            System.exit(b ?0 : 1);
        }
    }
    
3.2.2 部分排序
  • 需求:统计每个省份手机号耗费的总上行流量、总下行流量、总流量,并对总流量进行倒序排序

    id   手机号        网络IP           域名         上行流量  下行流量  网络状态码   
    1	13736230513	192.196.100.1	www.apple.com	2481	24681	200
    2	13846544121	192.196.100.2			        264	      0	    200
    3 	13956435636	192.196.100.3			        132	     1512	200
    4 	13966251146	192.168.100.1			        240	      0	    404
    5 	18271575951	192.168.100.2	www.apple.com	1527	2106	200
    6 	84188413	192.168.100.3	www.apple.com	4116	1432	200
    7 	13590439668	192.168.100.4			        1116	954	    200
    8 	15910133277	192.168.100.5	www.hao123.com	3156	2936	200
    9 	13729199489	192.168.100.6			        240	      0	    200
    10 	13630577991	192.168.100.7	www.shouhu.com	6960	 690	200
    11 	15043685818	192.168.100.8	www.baidu.com	3659	3538	200
    12 	15959002129	192.168.100.9	www.apple.com	1938	 180	500
    13 	13560439638	192.168.100.10			        918	    4938	200
    14 	13470253144	192.168.100.11			        180	     180	200
    15 	13682846555	192.168.100.12	www.qq.com	    1938	2910	200
    16 	13992314666	192.168.100.13	www.gaga.com	3008	3720	200
    17 	13509468723	192.168.100.14	www.qinghua.com	7335	110349	404
    18 	18390173782	192.168.100.15	www.sogou.com	9531	2412	200
    19 	13975057813	192.168.100.16	www.baidu.com	11058	48243	200
    20 	13768778790	192.168.100.17			        120	     120	200
    21 	13568436656	192.168.100.18	www.alibaba.com	2481	24681	200
    22 	13568436656	192.168.100.19			        1116	 954	200
    
  • 自定义分区类:

    import org.apache.hadoop.io.Text;
    import org.apache.hadoop.mapreduce.Partitioner;
    
    public class ProvincePartitioner2 extends Partitioner<FlowBean, Text> {
    
        @Override
        public int getPartition(FlowBean flowBean, Text text, int numPartitions) {
            //获取手机号前三位
            String phone = text.toString();
            String prePhone = phone.substring(0, 3);
    
            //定义一个分区号变量partition,根据prePhone设置分区号
            int partition;
            if("136".equals(prePhone)){
                partition = 0;
            }else if("137".equals(prePhone)){
                partition = 1;
            }else if("138".equals(prePhone)){
                partition = 2;
            }else if("139".equals(prePhone)){
                partition = 3;
            }else {
                partition = 4;
            }
    
            //最后返回分区号partition
            return partition;
        }
    }
    
  • 配置 Driver 类:

    // 设置自定义分区器
    job.setPartitionerClass(ProvincePartitioner2.class);
    
    // 设置对应的ReduceTask的个数
    job.setNumReduceTasks(5);
    

4. Combiner

4.1 介绍
  • Combiner 是 MapReduce 程序中除 Mapper 和 Reducer 之外的一种组件
  • Combiner 的父类是 Reducer
  • Combiner 与 Reducer 的区别:
    • Combiner 在每一个 MapTask 节点上运行
    • Reducer 接收全局的 MapTask 结果运行
  • Combiner 用于对每个 MapTask 进行局部汇总,以减少网络传输量,但不能影响最终的业务逻辑(如求平均数时不能使用 Combiner)
4.2 案例
  • 需求:统计文件中单词个数时在 Map 阶段进行局部汇总后再输出

  • 增加一个 WordCountCombiner 类继承 Reducer

    import org.apache.hadoop.io.IntWritable;
    import org.apache.hadoop.io.Text;
    import org.apache.hadoop.mapreduce.Reducer;
    import java.io.IOException;
    
    public class WordCountCombiner extends Reducer<Text, IntWritable, Text, IntWritable> {
    
    	private IntWritable outV = new IntWritable();
    
        @Override
        protected void reduce(Text key, Iterable<IntWritable> values, Context context) throws IOException, InterruptedException {
    
            int sum = 0;
            for (IntWritable value : values) {
                sum += value.get();
            }
    
            //封装outKV
    		outV.set(sum);
    
            //写出outKV
    		context.write(key,outV);
        }
    }
    
  • 在驱动类中指定 Combiner

    // 指定需要使用combiner,以及用哪个类作为combiner的逻辑
    job.setCombinerClass(WordCountCombiner.class);
    
  • 或者直接指定 Reducer 类作为 Combiner

    // 指定需要使用Combiner,以及用哪个类作为Combiner的逻辑
    job.setCombinerClass(WordCountReducer.class);
    

四、ReduceTask 阶段

1. 工作机制

在这里插入图片描述

  • Copy 阶段:ReduceTask 从各个 MapTask 上远程拷贝一片数据,并针对某一片数据,如果其大小超过一定阈值,则写到磁盘上,否则直接放到内存中。
  • Sort 阶段:在远程拷贝数据的同时,ReduceTask 启动了两个后台线程对内存和磁盘上的文件进行合并,以防止内存使用过多或磁盘上文件过多。按照 MapReduce 语义,用户编写 reduce() 函数输入数据是按 key 进行聚集的一组数据。为了将 key 相同的数据聚在一起,Hadoop 采用了基于排序的策略。由于各个 MapTask 已经实现对自己的处理结果进行了局部排序,因此,ReduceTask 只需对所有数据进行一次归并排序即可。
  • Reduce 阶段:reduce() 函数将计算结果写到 HDFS 上

2. OutputFormat

OutputFormat 是 MapReduce 输出的基类,所有 MapReduce 输出都实现了 OutputFormat 接口

2.1 常见实现类
  • TextOutputFormat:默认实现类
  • MapFileOutputFormat
2.2 自定义实现类
  • 步骤:定义一个类继承 FileOutputFormat 并重写 RecordWriter getRecordWriter() 方法

  • 需求:过滤输入的 log 日志,包含 apple 的网站输出到 e:/apple.log,不包含 apple 的网站输出到e:/other.log

    http://www.baidu.com
    http://www.google.com
    http://cn.bing.com
    http://www.apple.com
    http://www.sohu.com
    http://www.sina.com
    http://www.sin2a.com
    http://www.sin2desa.com
    http://www.sindsafa.com
    
  • 编写 Mapper 类:

    import org.apache.hadoop.io.LongWritable;
    import org.apache.hadoop.io.NullWritable;
    import org.apache.hadoop.io.Text;
    import org.apache.hadoop.mapreduce.Mapper;
    import java.io.IOException;
    
    public class LogMapper extends Mapper<LongWritable, Text, Text, NullWritable> {
        @Override
        protected void map(LongWritable key, Text value, Context context)  throws IOException, InterruptedException {
            //不做任何处理,直接写出一行 log 数据
            context.write(value, NullWritable.get());
        }
    }
    
  • 编写 Reducer 类:

        import org.apache.hadoop.io.NullWritable;
        import org.apache.hadoop.io.Text;
        import org.apache.hadoop.mapreduce.Reducer;
        import java.io.IOException;
        
        public class LogReducer extends Reducer<Text, NullWritable, Text, 
        NullWritable> {
            @Override
            protected void reduce(Text key, Iterable<NullWritable> values, Context context) throws IOException, InterruptedException {
            	//  防止有相同的数据,迭代写出
                for (NullWritable value : values) {
                	context.write(key, NullWritable.get());
                }
            }
        }
    
  • 自定义 OutputFormat 类:

        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 org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
        import java.io.IOException;
        
        public class LogOutputFormat extends FileOutputFormat<Text, NullWritable> {
            @Override
            public RecordWriter<Text, NullWritable>  getRecordWriter(TaskAttemptContext job) throws IOException,  InterruptedException {
                //创建一个自定义的 RecordWriter 返回
                LogRecordWriter logRecordWriter = new LogRecordWriter(job);
                return logRecordWriter;
            }
        }
    
  • 编写 RecordWriter 类:

    import org.apache.hadoop.fs.FSDataOutputStream;
    import org.apache.hadoop.fs.FileSystem;
    import org.apache.hadoop.fs.Path;
    import org.apache.hadoop.io.IOUtils;
    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 LogRecordWriter extends RecordWriter<Text, NullWritable> {
            
        private FSDataOutputStream appleOut; 
        private FSDataOutputStream otherOut;
            
        public LogRecordWriter(TaskAttemptContext job) {
            try {
                //获取文件系统对象
                FileSystem fs = FileSystem.get(job.getConfiguration());
                //用文件系统对象创建两个输出流对应不同的目录
                appleOut = fs.create(new Path("d:/hadoop/apple.log"));
                otherOut = fs.create(new Path("d:/hadoop/other.log"));
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
            
        @Override
        public void write(Text key, NullWritable value) throws IOException, InterruptedException {
            String log = key.toString();
            //根据一行的 log 数据是否包含 apple,判断两条输出流输出的内容
            if (log.contains("apple")) {
               appleOut.writeBytes(log + "\n");
            } else {
               otherOut.writeBytes(log + "\n");
            }
        }
            
        @Override
        public void close(TaskAttemptContext context) throws IOException, InterruptedException {
            //关流
            IOUtils.closeStream(appleOut);
            IOUtils.closeStream(otherOut);
        }
    }
    
  • 配置 Driver 类:

    import org.apache.hadoop.conf.Configuration;
    import org.apache.hadoop.fs.Path;
    import org.apache.hadoop.io.NullWritable;
    import org.apache.hadoop.io.Text;
    import org.apache.hadoop.mapreduce.Job;
    import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
    import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
    import java.io.IOException;
    
    public class LogDriver {
        public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
            Configuration conf = new Configuration();
            Job job = Job.getInstance(conf);
            job.setJarByClass(LogDriver.class);
            job.setMapperClass(LogMapper.class);
            job.setReducerClass(LogReducer.class);
            job.setMapOutputKeyClass(Text.class);
            job.setMapOutputValueClass(NullWritable.class);
    
            job.setOutputKeyClass(Text.class);
            job.setOutputValueClass(NullWritable.class);
            //设置自定义的 outputformat
            job.setOutputFormatClass(LogOutputFormat.class);
            FileInputFormat.setInputPaths(job, new Path("D:\\input"));
            // 虽然自定义了outputformat,但是因为自定义的outputformat继承自fileoutputformat
            //而 fileoutputformat 要输出一个_SUCCESS 文件,所以还得指定一个输出目录
            FileOutputFormat.setOutputPath(job, new Path("D:\\logoutput"));
            boolean b = job.waitForCompletion(true);
            System.exit(b ? 0 : 1);
        }
    }
    

3. ReduceTask 并行度决定机制

  • MapTask 并行度由切片个数决定,切片个数由输入文件和切片规则决定
  • ReduceTask 并行度可以直接手动设置:job.setNumReduceTasks(n);
  • 注意事项:
    • ReduceTask=0,表示没有 Reduce 阶段,输出文件个数和 MapTask 个数一致
    • ReduceTask 默认值是 1,输出 1 个文件
    • 如果数据分布不均匀,有可能在 Reduce 阶段产生数据倾斜
    • ReduceTask数量并不是任意设置,还要考虑业务逻辑需求,如需要计算全局汇总结果则只能有 1 个 ReduceTask
    • 生产上设置多少个 ReduceTask,需要根据集群性能而定,经过实验测试找到正态分布的最佳点
    • 如果分区数不是 1,但是 ReduceTask 为 1,则不执行分区过程。因为在 MapTask 的源码中,执行分区的前提是先判断 ReduceNum 个数是否大于 1。不大于 1 则不执行自定义分区逻辑
  • 25
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值