三-中下, 大数据基础架构Hadoop- MapReduce框架原理和工作流程剖析

三, MapReduce框架原理


MapReduce 工作流程(InputFormat(逻辑切片, 定义读取数据的方式)–>Mapper(用户自定义的逻辑, 对输出数据进行处理)–>shuffle(分区–>排序–>溢写到磁盘–>合并------->拷贝同时进行归并排序))–>Reducer(用户自定义逻辑,对Mapper来的数据进行计算归并)–>outputFormat(决定数据的输出方式))

在MapReduce可分为两个阶段, 一个是Map阶段,我们统称为MapTask; 另一个是Reduce阶段, 我们统称为ReduceTask;

  1. 在MapTask中, 根据Input数据源, 通过InputFormat对数据进行处理(1.对输入数据切片 2. 实现以某种方式读取切片数据)), 读取的数据在Mapper(用户实现的逻辑)中进行处理.
  2. 在Mapper中, 主要是实现对输入数据的处理逻辑(比如按行读取, 按K-V读取), 并且还定义了写出的K-V数据的具体数据类型
  3. 在Mapper之后, Reducer之前, 进行Shuffle(混洗),
  4. 在ReduceTask中, 用户自定义逻辑,对Mapper来的数据进行计算归并
  • 下面每一个小节都是以上步骤中的具体深入介绍:

3.1 InputFormat 数据输入

[问题1]: 什么是InputFormat? 其作用是什么?

  • 平时我们写MapReduce程序的时候,在设置输入格式的时候,默认是采用(我们不需要设置)InputFormat 的孙子类TextInputFormat对输入数据进行处理, 有时候我们想自定义设置处理方式, 会用到 job.setInputFormatClass(CombineTextInputFormat.class)来保证输入文件按照我们想要的格式被读取。
  • 所有的输入格式都继承于InputFormat,这是一个抽象类,其子类有专门用于读取普通文件的FileInputFormat,用来读取数据库的DBInputFormat等等。
  • 其实,一个输入格式InputFormat,主要无非就是要解决如何将数据切片[比如多少数据/文件为一个切片],以及如何读取切片的数据[比如按行读取], 生成相应的K-V供Mapper读取。前者由getSplits()完成,后者由RecordReader完成。

3.1.0 切片与MapTask并行度决定机制

  1. 问题引入:
    MapTask的并行度决定Map阶段的任务处理并发度, 进而影响到整个Job的处理速度.
    Q: 1G的数据, 启动8个MapTask, 可以提高集群的并发处理能力. 那么1K的数据也启动8个MpaTask, 会提高集群性能吗? MapTask并发任务是否越多越好呢? 哪些因素影响了MapTask并行度?

  2. MapTask并行度决定机制

    • 数据块: Block是HDFS物理上把数据分为一块一块. 数据块是HDFS的物理存储数据单元.
    • 数据切片: 数据切片只是在逻辑上对输入进行分片, 并不会在磁盘上将其切成片进行存储. 数据切片是Mapreduce程序计算输入数据的单位, 一个切片会对应启动一个MapTask.

3.1.1 Job提交流程源码和切片源码详解

在深入了解InputFormat接口的作用及它的各种子类之间的区别之前, 我们先理解一下Job提交和数据切片的过程(这里先拿FileInputFormat 的子类TextInputFormat的切片为例):

详见此文: MapReduce - Job提交和切片流程源码详解

3.1.2 FileInputFormat 切片机制

3.1.2.1 FileInputFormat类的切片过程

[切片过程]

3.1.2.2 FileInputFormat 切片大小的参数配置

  1. 源码中计算切片大小的代码和配置项
    protected long computeSplitSize(long blockSize, long minSize, long maxSize) {
    
    
        return Math.max(minSize, Math.min(maxSize, blockSize));
    }
    // mapred-default.xml配置文件中的 切片minsize设置项
    mapreduce.input.fileInputformat.split.minsize=1, 默认值为1
    //mapred-default.xml配置文件中的 切片 maxsize设置项
    mapreduce.input.fileinputformat.split.maxsize=Long.MAXValue, 默认值为 Long.MAXValue.
  • 因此, 默认情况下, 切片大小=blockSize;
  1. 切片大小的设置
  • maxSize(切片最大值)–使切片变小:

    • maxSize调的比blockSize小, 则会让切片变小, 而且实际切片大小就等于maxSize
  • minSize(切片最小值)–使切片变大

    • minSize调的比BlockSize(此时blockSize > maxSize)大, 则可以让实际切片大小=minSize
  1. 获取切片信息的API
//获取切片的文件名称
String name = inputSplit.getPath().getName();

//根据文件类型获取切片信息
FileSplit inputSplit = (FileSPlit) context.getInputSplit();

3.1.5 结构梳理: InputFormat 抽象类和它的各种子类

[InputFormat 子类]

  • 在运行 MapReduce 程序时,输入的文件格式包括:基于行的日志文件、二进制格式文件、数据库表等。为了处理这些文件, Hadoop设计了InputFormat抽象类, 前面我们提过一嘴, 这个抽象类的作用是对输入的数据进行切片getSplits()并规定如何读取输入数据createRecordReader(), 由于输入文件类型的不同, 比如处理数据库表的DBInputFormat, 处理普通文本文件的FileInputFormat, 切片方式的不同, 读取切片数据的方式不同, InputFormat抽象类有着各种各样的子类. 我们重点应掌握的是InputFormat抽象子类, FileInputFormat及其子类TextInputFormat, 孙子类CombineTextInputFormat

  • Input接口及其子类结构如下图所示:

  • 我们重点关注框图内的类;

tips: IDEA下,

  1. ctrl+h 查看接口/抽象类的类结构
  2. ctrl+F12 查看类中实现的方法

[框内常用类及方法的类图]
在这里插入图片描述

[类图中涉及到的类的完整变量, 方法]

  1. InputFormat(抽象类, 抽象方法)

  2. FileInputFormat(抽象类, 定义了切片方式(即对普通文件进行默认切片(单文件单独切片)), 未定义如何读取切片)

  3. TextInputFormat(定义了读取切片的方式K,距首行偏移量, V-一行)

  4. CombineFileInputFormat(抽象类, 定义了切片方式(多个文件切片等))

  5. CombineTextInputFormat(定义了读取切片的方式)

[FIleInputFormat 实现类]

  • FileInputFormat 常见的子类包括:TextInputFormat、ConbineFileInputFormat(抽象类), KeyValueTextInputFormat、NLineInputFormat、CoTextInputFormat 和自定义 InputFormat 等。
  • 其中需要熟练掌握TextInputFormat 和 CombineFileInputFormat(抽象类)中的子类CombineTextInputFormat.

3.1.5.1 TextInputFormat

特点:

  • TextInputFormat 是默认的FIleInputFormat实现类.
  • createRecordReader(), 定义了读取切片的方式, 按行读取每条记录.
  • 返回值, <KEYIN, VALUEIN>=<LongWritable, Text>, 键是存储在该行在整个文件中的起始字符偏移量, Longwritable类型; 值是这一行的内容, 不包括任何行终止符号(换行, 回车符号), Text类型.

举个栗子:

实际应用: 经典WordCount案例实操

3.1.5.2 CombineTextInputFormat

  • Mapreduce中默认的TextInputFormat切片机制是对任务按文件规划切片, 不管文件多小, 都会是单独进行切片, 由于一个切片对应于一个MapTask. 所以, 如果有大量的小文件, 就会产生大量的MapTask, 处理效率及其低下.
  1. 应用场景

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

  2. 虚拟存储切片最大值设置

    CombineTextInputFormat.setMaxInputSplkitSize(job, 4194304);//4MB

注意: 虚拟存储切片最大值设置最好根据实际的小文件大小情况来设置具体的值.

  1. CombineTextInputFormat 切片机制

生成切片的过程: 分为虚拟存储过程切片过程.

[虚拟存储过程]

将输入目录下所有文件大小, 依次和设置的 setMaxInputSplitSize值比较:

  • 如果不大于设置的最大值, 逻辑上划分为一个块,
  • 如果大于设置的最大值且大于两倍, 以设置最大值切割出一块
  • 当剩余数据大小大于设置的最大值却不大于两倍, 将剩余文件均分为2个虚拟存储块(防止出现太小切片)

举个栗子:
setMaxInputSplitSize 值为 4MB,输入文件大小为 8.02MB, 则虚拟存储过程如下:

  1. 8.02MB > 设置最大值的两倍, 切割出 4MB一块;
  2. 剩余 4.02MB, 大于设置最大值却不大于两倍, 均分即可, 切割出2.01MB, 2.01MB 两块.
  3. 所以8.02MB总共分成了 4MB, 2.01MB, 2.01MB 三个虚拟存储块

[切片过程]

  1. 判断虚拟存储的文件大小是否大于 setMaxInputSplitSize 值,大于等于则单独形成一个切片。
  2. 如果不大于则跟下一个虚拟存储文件进行合并,共同形成一个切片。

举个栗子:

再举个栗子:

3.1.5.2.1 CombineInputFormat案例实操

[准备工作]

  • 准备四个普通文本文件放到输入目录, 大小如图所示

[代码编写]

前面我们学习了TextInputFormatwordCount案例:
对于专门用于处理文本文件的 FileInputFormat接口, 我们在编写WordCount时都是默认实现其中的TextInputFormat子类(即按文件单独切片)

  • 但是, 由于大量小文件单独切片会造成集群中开启大量的MapTask, 从而使集群资源占用高甚至集群瘫痪, 所以我们引入了CombineTextInputFormat, 它能够把小文件集中起来按照设置的切片最大值(setMaxInputSplitSize)进行切片.

而且, 实现CombineTextInputFormat只需要在原有的WordCount基础上对Driver类添加下面的语句即可

       //2-1. 设置InputFormat的实现类, 不设置的话默认就是TextInputFormat.class
    job.setInputFormatClass(CombineTextInputFormat.class);
        //虚拟存储切片最大值设置 4M
    CombineTextInputFormat.setMaxInputSplitSize(job, 4194304);

[与TextInputFormat的比较]

实现TextInputFormat类的WordCount切片数目:

  • 在控制台比较靠前的位置, 我们可以查看number of splits: 4, 即 前面的栗子, a,b,c,d 4个文件分成了4个切片.

实现CombineTextInputFormat类的WordCount切片数目:

  • 删除输出目录, 把前面介绍的两句代码添加到Driver类中, 重新执行WordCount程序可以发现切片变成了3片,
  • 为什么是3片呢? 再来回顾一下:
    • 已知输入的文件大小:1.7, 5.2, 3.4, 6.9, 最大块大小 4MB
    • 虚拟存储块: 1.7, 2.6, 2.6(因为5.2>4, 但是小于4的2倍嘛), 3.4, 3.45, 3.45
    • 切片: 4.3(1.7+2.6), 6, 6.9MB, 所以是3片

[拓展]

  • 如果我们想把这四个文件都放到一个切片来处理, 应该如何设置呢?
  • 简单, 把setMaxSplitSize(job, size)的size值设置的比所有输入文件总大小还大就可以了.

3.2 Mapper(实现对输入数据的处理逻辑(比如按行读取, 按K-V读取), 并且还定义了写出的K-V数据的具体数据类型)

  • 详见前面的几篇文章, 对Mapper编写的案例实操:
  1. WordCount
  2. 序列化

3.3 Shuffle机制中的部分操作

3.3.1 Partition 分区

[默认的Partition的分区方式]

默认分区是根据key的hashCode对ReduceTasks个数取模得到的. 用户没法控制哪个key存储到哪个分区

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;
    }
}

[问题引出]

  • 要求将统计结果按照条件输出到不同文件中(分区)。比如:将统计结果按照手机归属地不同省份输出到不同文件中(分区)

3.3.1.1 自定义Partition步骤

  1. 自定义类去继承Partitioner抽象类,重写抽象方法getPartition()
public class CustomPartitioner extends Partitioner<Text, FlowBean>{
    
    
    @Override
    public int getPartition(Text key, FlowBean value, int numPartiotions){
    
    
        //控制分区的代码逻辑
        ....
        return partition;
    }
}
  1. 在Job驱动中, 设置自定义Partitioner
job. setPartitionerClasee(CustomPartitioner.class);
  1. 自定义Partition后, 要根据自定义Partitioner的逻辑设置相应数量的ReduceTask;

因为啥? 因为一个partition分区, 对应于一个ReduceTask, 并且对应输出一个part文件和一个part.crc文件.

//设置ReduceTask的个数
job.setNumReduceTask(x);

3.3.1.2 自定义分区案例实操

[需求]

  • 将统计结果按照手机归属地不同省份输出到不同文件中(分区)

  • 期望输出数据:

    • 手机号136、137、138、139开头都分别放到一个独立的4个文件中,其他开头的放到一个文件中

[需求分析]

请添加图片描述

[代码实现]

  1. 新建partitontest包, 把序列化文章中的相关类(FlowBean, FlowMapper, FlowReducer, FLowBean 类)复制进去
  2. 新建FlowPartitioner类, 去继承Partitioner类, 并导入相关的jar包, 编写代码如下:
package partitiontest;

import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Partitioner;
// 分区是在Mapper执行之后进行的, 所以我们得到分区的输入数据是Mapper 的输出数据,<Text, FlowBean>=<手机号, 包装流量相关的信息的FlowBean类>
public class FlowPartitioner extends Partitioner<Text, FlowBean> {
    
    
    @Override
    public int getPartition(Text key, FlowBean bean, int numPartitions) {
    
    
        //需求: 4个分区=4个ReduceTask=4个part输出文件
        int partition = 4;
        
        String phoneNum = key.toString().substring(0,3);
        
        if(phoneNum.equals("136")){
    
    
            partition = 0;
        }else if(phoneNum.equals("137")){
    
    
            partition = 1;
        }else if(phoneNum.equals("138")){
    
    
            partition = 2;
        }else{
    
    
            partition = 3;
        }
        return partition;
    }

}
  1. 在FlowDriver类中添加以下语句:

        //指定自定义分区方法
        job.setPartitionerClass(FlowPartitioner.class);
        //指定ReduceTask个数
        job.setNumReduceTasks(4);
  1. 运行, 得到输出目录文件及其内容如下图所示:
    请添加图片描述

3.3.1.2 自定义分区总结

[总结一]

自定义分区个数: getPartition()的结果
如何自定义分区?

  1. 自定义一个类, 继承Patitioner类并重写其方法getPartition()
  2. 在Driver类中, 设置job. setPartitionerClasee(自定义类);指定运行自定义分区方法.
  3. 在Driver类中, 设置job.setNumReduceTasks(个数), 指定跟分区数一样的ReduceTask个数

[总结二]

自定义的分区数 与 ReduceTasks个数的关系:

  1. 如果ReduceTask数量 > getPartition()结果数, 就会多产上几个空的输出文件part-r-000xx;
  2. 如果1 < ReduceTask的数量 < getPartition()结果数, 则会有一部分数据无处安放, 会Exceptionp;
  3. 如果ReduceTask数量=1, 则不管MapTask端输出多少个分区文件, 最终结果都交给这一个ReduceTask, 最终也就只产生一个结果文件 part-r-00000;

当job.setNumReduceTask(1); //默认也就是这个设置
Debug 看一下执行流程

可以看到, 当reducetask的个数=1时候, 返回的分区号为reducetask的数目 -1, 也就是一个0号分区, 即只有一个分区, 输出文件也相应的只有一个;

请添加图片描述

  • 分区号必须从零开始, 逐一累加;

  • 举个栗子:
    假设自定义分区数getPartition()=5, 则:
    1.job.setNumReduceTasks(1); //会正常运行, 只不过值产生一个输出文件

    1. job.setNumReduceTasks(2); //会报错
    2. job.setNumRedcueTasks(6); //大于5, 程序正常运行, 输出文件中多出了一个空文件

3.3.2 WritableComparable排序

3.3.2.1 排序概述

排序是MapReduce框架中最重要的操作之一.
MapTask和ReduceTask均会对数据按照Key进行排序, 该操作属于Hadoop的默认行为. 任何应用程序中的数据均会被排序, 而不管逻辑上是否需要.

  • 默认排序就是按照字典顺序排序, 且实现该排序的方式是快速排序;

MapTask中的排序(待完善)

对于MapTask, 他会将处理的结果暂时放到环形缓冲区中,

  1. 环形缓冲区达到一定阈值时, 再对环形缓冲区中的数据进行一次快速排序(该排序结果是分区内部有序),
  2. 然后将这些有序数据溢写到磁盘上,
  3. 而当数据处理完毕后, 它会对磁盘上的所有文件进行归并排序(分区内有序, 不同分区之间也有序);

ReduceTask中的排序(存疑)

对于ReduceTask, 它从每个MapTask上远程拷贝相应的数据文件,

  1. 如果文件大小超过一定阈值, 则溢写到磁盘上, 否则存储在内存中.
  2. 如果磁盘上文件数目达到一定阈值, 则进行一次归并排序以生成一个更大文件;
  3. 如果内存中文件大小或者数目查过一定阈值, 则进行一次合并后将数据溢写到磁盘上.
  4. 当所有的数据拷贝完毕后, ReduceTask统一对内存和磁盘上的所有数据进行一次归并排序.

排序的分类

  1. 部分排序
    MapReduce根据输入记录的键,对数据集进行排序, 保证输出的每个文件内部有序.

  2. 全排序
    最终输出结果只有一个文件, 且文件内部有序. 实现方式是只设置一个ReduceTask. 但该方法处理大型文件时效率极低, 因为一台机器处理所有文件, 完全丧失了可MapReduce所提供的并行架构.

  3. 辅助排序(GroupingComparator分组)
    在Reduce端对key进行分组, 应用于: 在接收的key为bean对象时, 想让一个或几个字段相同(全部字段不相同)的key进入到同一个Reduce方法, 可以采用分组排序

  4. 二次排序
    自定义排序过程中, 如果compareTo中的判断条件为两个即为二次排序.

自定义排序 WritableComparable 与案例分析

因为Hadoop中传输的KEY必须是经过排序的, 所以在bean对象作为key传输时, 需要实现WritableComparable接口重写compreTo()方法, 就可以实现排序.

@Override
public int compareTo(FlowBean bean){
    
    
    int result;

    //按照流量大小, 倒序排列
    if(this.sumFlow > bean.getSumFlow()){
    
    
        result = -1;
    }else if(this.sumFlow < bean.getSumFlow()){
    
    
        result = 1;
    }else{
    
    
        result = 0;
    }

    return result;
}

3.3.2.2 WritableComparable 排序案例实操(全排序)

[需求]

请添加图片描述

[思路和代码实现]

  1. 首先对于FlowBean类, 我们改为继承WritableComparable<FlowBean>类, 并在FlowBean类中重写对应的compareTo()方法.

请添加图片描述
请添加图片描述

  1. 我们梳理一下各个类相比于序列化栗子中的改变:
  • 首先Mapper类, 输入类型<LongWritable, Text>对应于<每一行首字符距离文件首字符偏移量, 每一行的字符>, 这个不用改变,

  • 然后是输出类型需要改变, 为什么呢? 因为我们是需要自定义排序, 排序是对什么时候对什么数据进行的呢? -->mapper处理之后的输出数据, 而且上面的需求是对FlowBean类中的总流量sumFlow进行排序, 所以我们需要改变Mapper的输出类型为<FlowBean, Text>
    请添加图片描述

  • 再者是Reducer类, 我们的输入类型需要改成与Mapper的输出类型一致, 这是毋庸置疑的; Reducer类的输出仍旧为(Text, FlowBean)->(手机号, 流量信息封装类)
    请添加图片描述

  • 最后是Driver类, never忘记该Driver类的对应配置!!!
    请添加图片描述

  1. 最终的排序结果如下图所示:
    请添加图片描述

二次排序

二次排序就是说, 当用某一属性大小进行排序时, 如果这个属性相同的话, 就再按照别的一个属性的大小进行排序.

  • 实现方法:

    • 只需要在compareTo()的相等分支下继续扩充其他别的属性进行比较的逻辑就可以了.
  • 举个栗子:

上一个小节, 我们对输出的流量信息, 实行总流量SumFLow大小的倒序输出, 现在, 我们对总流量相同的行继续排序: 按照上行流量进行排序.
请添加图片描述
请添加图片描述

3.3.2.2 WritableComparable 排序案例实操(区内排序)

请添加图片描述
请添加图片描述

[实现思路]
[完整代码]

3.3.3 Combiner 合并

3.3.3.1 Combiner定义和使用

[定义](待完善)

  1. Combiner 是MR程序中Mapper和Reducer之外的一种可选组件.
  2. Combiner组件的父类就是Reducer.
  3. Combiner和Reducer的区别在于运行的位置:
    • Combiner是在每一个Maptask所在的节点运行;
    • Reducer是接收全局所有Mapper的输出结果;
  4. Combiner的意义就是对每一个MapTask 的输出进行局部汇总, 以减少网络传输量;
  5. Combiner能够应用的前提是不能影响最终的业务逻辑, 而且, Combiner 的输出K-V应该跟Reducer 的输入K-V类型要对应起来.(这么来说, Combiner特别适合加法的业务逻辑)

[自定义Combiner 的实现步骤]

  1. 自定义一个Combiner类 继承Reducer, 重写Reduce方法
    请添加图片描述

  2. 在Job驱动类中设置:
    请添加图片描述

3.3.3.1 Combiner 案例实操

请添加图片描述

[方法一]

请添加图片描述
请添加图片描述

运行结果
请添加图片描述

[方法二]

如果Combiner类跟Reducer类的实现逻辑大致相同的话, 就在job.setCombinerClass()参数中使用Reducer.class即可.

请添加图片描述

3.4 OutputFormat 数据输出

[概述]

在这里插入图片描述

Reducer处理完数据后, 并不会立马就把数据输出到文件, 而是会把数据交给OutputFormat类, 由这个类中的RecordWrite()方法去控制最终文件的输出格式和输出路径.(写到普通文件, 写到数据库, 还是怎么着)

[类结构]

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

  • 下面是几种常见的OutputFormat实现类.

在这里插入图片描述

[自定义输出输出的方法]

在这里插入图片描述

3.4.1 自定义实现OutputFormat案例实操

[需求分析和实现思路]

在这里插入图片描述

[完整代码]

  1. Mapper和Reducer代码:

因为需求主要是过滤输出(即对输入的源文件进行分类), 所以Mapper的输出仅仅是一行行的数据(key), 而value为nullWritable. Reducer也不做任何的计算,仅仅是把相同key值的value重复写出去(context.write(k,v));

/Mapper
public class LogMapper extends Mapper<LongWritable, Text, Text, NullWritable> {
    
    
    @Override
    protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
    
    
        context.write(value, NullWritable.get());
    }
}

Reducer
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());
        }
    }
}

  1. LogOutputFormat代码:

自定义类, 继承自OutputFormat, 能够对getRecordWrite进行重写, 返回自定义类的Recordwrite对象(确切地说是继承了RecordWrite并重写了他的write() 和 close()方法 的 自定义类的对象.)

自定义的LogRecordWrite类
public class LogRecordWriter extends RecordWriter<Text, NullWritable> {
    
    
    //全局变量
    private FSDataOutputStream baidu;
    private FSDataOutputStream other;

    public  LogRecordWriter(TaskAttemptContext job){
    
    
        //获取文件系统对象, 创建两只输出流
        try {
    
    
            FileSystem fs = FileSystem.get(job.getConfiguration());
            baidu = fs.create(new Path("D:\\user\\outputformat\\baidu.txt"));
            other = fs.create(new Path("D:\\user\\outputformat\\other.txt"));
        } catch (IOException e) {
    
    
            e.printStackTrace();
        }

    }



///自定义输出类, LogOutputFormat类
public class LogOutputFormat extends FileOutputFormat<Text, NullWritable>{
    
    

  @Override
    public RecordWriter<Text, NullWritable> getRecordWriter(TaskAttemptContext job) throws IOException, InterruptedException {
    
    
        LogRecordWriter lrw = new LogRecordWriter(job);
        return lrw;
    }
}
  1. Driver类

老老实实写出七步骤. 并加上这么一句

  ///outputFormat自定义的设置项
 job.setOutputFormatClass(LogOutputFormat.class); //设置自定义输出类

3.5 MapReduce 工作全部流程(重要!)

3.5.1 MapTask工作流程

在这里插入图片描述

阶段〇, Job提交阶段(逻辑上切片, 提交三文件到YARN RM)

  1. 前置准备工作—>(确认Job的状态, 新旧API的兼容性处理, 集群的连接,检查输出目录,生成JobId, 建立临时工作目录等等)
  2. 切片, 生成配置文件—> 在Job提交的末尾, 对输入的文件进行切片, 切片完成后生成XML配置文件;
  3. 客户端提交(jar包, 切片规划文件, xml配置文件)到Yarn RM
  4. Yarn RM根据切片计算出MapTask的数量

阶段一, Read阶段(定义读取数据方式)

  1. Yarn调用ResourceManager来创建Mr appmaster,而Mr appmaster则会根据切片的个数来创建几个Map Task。MapTask启动, 开始处理分到的切片文件(一个切片对应于一个MapTask). 使用InputFormat抽象类的子类(默认是使用子类TextInputFormat)处理文件, 调用RecorderReader()方法将切片中的数据格式化为相应的K-V值.

阶段二, Map阶段(用户自主实现, 过滤分发数据)

  1. Mapper根据用户自定义的业务逻辑去处理从MapTask中得到的K-V数据, 得到一组组新的K-V数据

阶段三, Collect阶段(对上一步得到的K-V进行分区后写入到缓冲区)

  1. Mapper处理后的K-V数据,会根据key(默认的分区方式是key的hash值 % ReduceTask数量)进行分区, 然后送往OutPutCollect(环形缓冲区, 字节数组实现的一块内存区域), 在这个内存区域中, 一半用来存储K-V数据, 另一半存储这些数据的元数据信息(索引, 位于哪个分区, keystart, valueStart).

阶段四, Spill(溢写) 阶段(分区内排序后写入到磁盘)

  1. 环形缓冲区默认100MB, 溢写的百分比是80%, 也就是说环形缓冲区中的数据大小达到80MB时开始往磁盘上写入, 与此同时, 采用反向写入的方法将K-V数据继续放入到剩余的环形缓冲区中.

  2. 需要注意的是,将数据写入本地磁盘之前, 同一分区中key会进行快速排序. 利用快速排序算法对缓存区内的数据进行排序,排序方式是,先按照分区编号Partition 进行排序,然后按照 key 进行排序。这样,经过排序后,数据以分区为单位聚集在一起,且同一分区内所有数据按照 key 有序. 此外, 如果定义了的话, 我们还会进行combine操作和压缩数据操作.

在这里插入图片描述

阶段五, Merge(合并)阶段

  1. 每次溢写会在磁盘上生成一个溢写文件,如果map的输出结果真的很大,有多次这样的溢写发生,磁盘上相应的就会有多个溢写临时文件存在。当所有数据处理完成后,MapTask 对所有溢写到磁盘的临时文件进行一次合并操作,以确保最终只会生成一个数据文件。

在这里插入图片描述

  • 扩展
    • Merge是怎样的?如前面的例子,“aaa”从某个map task读取过来时值是5,从另外一个map 读取时值是8,因为它们有相同的key,所以得merge成group。什么是group。对于“aaa”就是像这样的:{“aaa”, [5, 8, 2, …]},数组中的值就是从不同溢写文件中读取出来的,然后再把这些值加起来。请注意,因为merge是将多个溢写文件合并到一个文件,所以可能也有相同的key存在,在这个过程中如果client设置过Combiner,也会使用Combiner来合并相同的key。

3.5.2 ReduceTask工作流程

在这里插入图片描述

在这里插入图片描述

阶段六, Copy阶段

  • 在所有Map Task任务都完成之后,根据分区的数量来启动相应数量的Reduce Task,并告知ReduceTask处理数据范围(数据分区)(有几个分区就启动几个Reduce Task,每个Reduce Task专门处理同一个分区的数据,比如处理MapTask1中partition0和MapTask2中partition0的数据)

阶段七, Merge + Sort阶段

  • Copy过来的数据会先放入内存缓冲区中,如果内存缓冲区中能放得下这次数据的话就直接把数据写到内存中,即内存到内存merge

  • 当内存缓存区中的数据量达到一定阈值,开始把内存中的数据merge输出到磁盘上的一个文件中,即内存到磁盘merge

  • 当属于该reducer的map输出全部拷贝完成,会在reducer上生成多个文件(如果拖取的所有map数据总量都没有内存缓冲区大,则数据就只存在于内存中),这时开始执行合并操作,即磁盘到磁盘merge. 一般Reduce是一边copy一边sort,即copy和sort两个阶段是重叠而不是完全分开的。最终Reduce shuffle过程会输出一个整体有序的数据块。

阶段八, Reduce阶段

  • 对文件内数据按照key分组,并且一次读取一组数据到用户自定义的reduce方法中

阶段九, 输出阶段

  • 将reduce运算结果写出到本地文件中,默认是TextOutputFormat,reduce对文件中分组数据运算结束后,整个mr任务工作流程结束。

3.6 Shuffle机制

  • 从Map输出到Reduce输入的整个过程可以广义的称为Shuffle. Shuffle过程横跨Map端和Reduce端, 在Map端包括collect, Spill(溢写)和Merge过程, 在Reduce端包括copy 和sort过程.

[具体过程]

  • 参见上一小节从阶段三到阶段七.

3.7 ReduceTask 并行度设置

在这里插入图片描述
在这里插入图片描述

3.8 MapTask 和 ReduceTask源码分析(待补充)

3.9 MapReduce开发总结

在这里插入图片描述
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/nmsLLCSDN/article/details/118711272