MapReduce

MapReduce

MapReduce概述

MapReduce 定义

MapReduce 分布式运算程序编程框架,将用户编写的业务逻辑代码自带的默认组件整合成一个完整的分布式运算程序,并发运行在一个 hadoop 集群上。

MapReduce 优缺点

优点

  1. MapReduce 易于编程:它简单的实现一些接口,就可以完成一个分布式程序
  2. 良好的扩展性:通过简单地增加机器数量来提高它的计算能力
  3. 高容错性:自动处理某些问题,如其中一台机器挂了,它可以把上面的计算任务转移到另一个节点上运行,不至于这个任务运行
  4. 适合 PB 级以上海量数据的离线处理

缺点

  1. 不擅长实时计算
  2. 不擅长流式计算
  3. 不擅长 DAG (有向无环图)计算

MapReduce 进程

  1. MrAppMaster: 负责整个程序的过程调度及状态调度
  2. MapTask: 负责 Map 阶段的整个数据处理流程
  3. ReduceTask: 负责 Reduce 阶段的整个数据处理流程

Word Count

  1. 按照 MapReduce 编程规范,分别编写 Mapper,Reducer, Driver。

  2. 准备环境

    在 pom.xml 文件中添加如下依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<dependencies>
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-client</artifactId>
<version>3.1.3</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.30</version>
</dependency>
</dependencies>

​ 在项目的 src/main/resources 目录下,新建一个文件,命名为“log4j.properties”,在
文件中填入。

1
2
3
4
5
6
7
8
log4j.rootLogger=INFO, stdout
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] - %m%n
log4j.appender.logfile=org.apache.log4j.FileAppender
log4j.appender.logfile.File=target/spring.log
log4j.appender.logfile.layout=org.apache.log4j.PatternLayout
log4j.appender.logfile.layout.ConversionPattern=%d %p [%c] - %m%n
  1. 编写程序

    编写 Mapper 类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    package com.test.mapreduce.wordcount;
    import java.io.IOException;
    import org.apache.hadoop.io.IntWritable;
    import org.apache.hadoop.io.LongWritable;
    import org.apache.hadoop.io.Text;
    import org.apache.hadoop.mapreduce.Mapper;
    public class WordCountMapper extends Mapper<LongWritable, Text, Text, IntWritable>{
    Text k = new Text();
    IntWritable v = new IntWritable(1);
    @Override
    protected void map(LongWritable key, Text value, Context context)
    throws IOException, InterruptedException {
    // 1 获取一行
    String line = value.toString();
    // 2 切割
    String[] words = line.split(" ");
    // 3 输出
    for (String word : words) {
    k.set(word);
    context.write(k, v);
    }
    }
    }

    ​ 编写 Reducer 类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    package com.test.mapreduce.wordcount;
    import java.io.IOException;
    import org.apache.hadoop.io.IntWritable;
    import org.apache.hadoop.io.Text;
    import org.apache.hadoop.mapreduce.Reducer;
    public class WordCountReducer extends Reducer<Text, IntWritable, Text, IntWritable>{
    int sum;
    IntWritable v = new IntWritable();
    @Override
    protected void reduce(Text key, Iterable<IntWritable> values,Context
    context) throws IOException, InterruptedException {
    // 1 累加求和
    sum = 0;
    for (IntWritable count : values) {
    sum += count.get();
    }
    // 2 输出
    v.set(sum);
    context.write(key,v);
    }
    }

    ​ 编写 Driver 驱动类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    package com.test.mapreduce.wordcount;
    import java.io.IOException;
    import org.apache.hadoop.conf.Configuration;
    import org.apache.hadoop.fs.Path;
    import org.apache.hadoop.io.IntWritable;
    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;
    public class WordCountDriver {
    public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
    // 1 获取配置信息以及获取 job 对象
    Configuration conf = new Configuration();
    Job job = Job.getInstance(conf);
    // 2 关联本 Driver 程序的 jar
    job.setJarByClass(WordCountDriver.class);
    // 3 关联 Mapper 和 Reducer 的 jar
    job.setMapperClass(WordCountMapper.class);
    job.setReducerClass(WordCountReducer.class);
    // 4 设置 Mapper 输出的 kv 类型
    job.setMapOutputKeyClass(Text.class);
    job.setMapOutputValueClass(IntWritable.class);
    // 5 设置最终输出 kv 类型
    job.setOutputKeyClass(Text.class);
    job.setOutputValueClass(IntWritable.class);
    // 6 设置输入和输出路径
    FileInputFormat.setInputPaths(job, new Path(args[0]));
    FileOutputFormat.setOutputPath(job, new Path(args[1]));
    // 7 提交 job
    boolean result = job.waitForCompletion(true);
    System.exit(result ? 0 : 1);
    }
    }

Hadoop 序列化

  1. 什么是序列化

序列化:把内存中的对象,转换成字节序列(或其他数据传输协议)以便于存储到磁盘(持久化)和网络传输。

反序列化:将收到的字节序列(或其他数据传输协议)或者是磁盘的持久化数据,转为内存中的对象。

  1. 内存中的对象只能在本地进程中使用而且关机就没了,序列化可以存储对象,在需要的时候调用,也可以将其发送到远程计算机使用。

  2. Java 的序列化是一个重量级序列化框架(Serializable),一个对象被序列化后,会附带很多额外的信息(各种校验信息, Header,继承体系等),不便于在网络中高效传输。所以,Hadoop 自己开发了一套序列化机制(Writable)。

  3. Hadoop 序列化特点:

紧凑 : 高效使用存储空间。

快速: 读写数据的额外开销小。

互操作: 支持多语言的交互 。

自定义 bean 对象实现序列化接口(Writable)

  1. 必须实现 Writable 接口
  2. 反序列化时,需要反射调用空参构造函数,所以必须有空参构造
  3. 重写序列化方法
  4. 重写反序列化方法
  5. 注意反序列化的顺序和序列化的顺序完全一致
  6. 要想把结果显示在文件中,需要重写 toString(),可用”\t”分开,方便后续用。
  7. 如果需要将自定义的 bean 放在 key 中传输,则还需要实现 Comparable 接口,因为MapReduce 框中的 Shuffle 过程要求对 key 必须能排序。

例如:

统计每一个手机号耗费的总上行流量、 总下行流量、总流量

输入数据格式:

id 手机号码 网络ip 上行流量 下行流量 网络状态码
7 13560436666 120.196.100.99 1116 954 200

期望输出数据格式:

手机号码 上行流量 下行流量 总流量
13560436666 1116 954 2070

编写流量统计的 Bean 对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
package com.test.mapreduce.writable;
import org.apache.hadoop.io.Writable;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
//1 继承 Writable 接口
public class FlowBean implements Writable {
private long upFlow; //上行流量
private long downFlow; //下行流量
private long sumFlow; //总流量
//2 提供无参构造
public FlowBean() {
}
//3 提供三个参数的 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;
}
//4 实现序列化和反序列化方法,注意顺序一定要保持一致
@Override
public void write(DataOutput dataOutput) throws IOException {
dataOutput.writeLong(upFlow);
dataOutput.writeLong(downFlow);
dataOutput.writeLong(sumFlow);
}
@Override
public void readFields(DataInput dataInput) throws IOException {
this.upFlow = dataInput.readLong();
this.downFlow = dataInput.readLong();
this.sumFlow = dataInput.readLong();
}
//5 重写 ToString
@Override
public String toString() {
return upFlow + "\t" + downFlow + "\t" + sumFlow;
}
}

​ 编写 Mapper 类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package com.test.mapreduce.writable;
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, Text, FlowBean>
{
private Text outK = new Text();
private FlowBean outV = new FlowBean();
@Override
protected void map(LongWritable key, Text value, Context context)
throws IOException, InterruptedException {
//1 获取一行数据,转成字符串
String line = value.toString();
//2 切割数据
String[] split = line.split("\t");
//3 抓取我们需要的数据:手机号,上行流量,下行流量
String phone = split[1];
String up = split[split.length - 3];
String down = split[split.length - 2];
//4 封装 outK outV
outK.set(phone);
outV.setUpFlow(Long.parseLong(up));
outV.setDownFlow(Long.parseLong(down));
outV.setSumFlow();
//5 写出 outK outV
context.write(outK, outV);
}
}

​ 编写 Reducer 类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package com.test.mapreduce.writable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;
import java.io.IOException;
public class FlowReducer extends Reducer<Text, FlowBean, Text, FlowBean>
{
private FlowBean outV = new FlowBean();
@Override
protected void reduce(Text key, Iterable<FlowBean> values, Context
context) throws IOException, InterruptedException {
long totalUp = 0;
long totalDown = 0;
//1 遍历 values,将其中的上行流量,下行流量分别累加
for (FlowBean flowBean : values) {
totalUp += flowBean.getUpFlow();
totalDown += flowBean.getDownFlow();
}
//2 封装 outKV
outV.setUpFlow(totalUp);
outV.setDownFlow(totalDown);
outV.setSumFlow();
//3 写出 outK outV
context.write(key,outV);
}
}

​ 编写 Driver 驱动类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package com.test.mapreduce.writable;
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);
//6 设置程序的输入输出路径
FileInputFormat.setInputPaths(job, new Path("D:\\inputflow"));
FileOutputFormat.setOutputPath(job, new Path("D:\\flowoutput"));
//7 提交 Job
boolean b = job.waitForCompletion(true);
System.exit(b ? 0 : 1);
}
}

MapReduce 框架原理

InputFormat 数据输入

MapTask 的并行度决定 Map 阶段的任务处理并发度,进而影响到整个 Job 的处理速度

对于 1G 数据启动 8 个 MapTask 可以提高并发能力,但是对于 1 k 的数据启动 8 个是否合适呢?启动 8 MapTask 的时间远大于处理数据的时间。

MapTask 并行度决定机制

数据块:Block 是 HDFS 物理上把数据分成一块一块。数据块是 HDFS 的存储数据单位

数据切片:数据切片是逻辑上对输入进行切片,并不是在磁盘上切片存储。数据切片是 MapReduce 计算输入数据的单位,一个切片会对应启动一个 MapTask。

  1. 一个 job 的 Map 阶段并行度由客户端在提交 job 时的切片数决定
  2. 每一个 Split 切片分配一个 MapTask 并行实例处理
  3. 默认情况下,切片大小 = BlockSize
  4. 切片时不考虑数据集整体(总收入的数据涉及每日收入存储文件),而是逐个针对每一个文件单独切片(当日交易数据)。

Job 提交流程源码解析

JobSubmit

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
waitForCompletion()
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 路径
JobID jobId = 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());

FileInputFormat 切片机制

切片机制

简单地按照文件的内容长度进行切片

切片大小, 默认等于Block大小

切片时不考虑数据集整体, 而是逐个针对每一个文件单独切片

例如

源文件 切片后
file1.txt 320M file1.txt.split1 0-128M
file1.txt.split2 128-256M
file1.txt.split3 256-320M
file2.txt 10m file2.txt.split1 0-10M

源码解析

  1. 程序先找到你数据存储的目录。

  2. 开始遍历处理(规划切片)目录下的每一个文件

  3. 遍历第一个文件xx.txt

    1. 获取文件大小fs.sizeOf(xx.txt)
    2. 计算切片大小 computeSplitSize(Math.max(minSize,Math.min(maxSize,blocksize)))=blocksize=128M
    3. 默认情况下,切片大小=blocksize
    4. 开始切,形成第1个切片: xx.txt —0:128M 第2个切片 xx.txt —128:256M 第3个切片 xx.txt—256M:300M ( 每次切片时,都要判断切完剩下的部分是否大于块的1.1倍,不大于1.1倍就划分一块切片)
    5. 将切片信息写到一个切片规划文件中
    6. 整个切片的核心过程在getSplit()方法中完成
    7. InputSplit只记录了切片的元数据信息,比如起始位置、长度以及所在的节点列表等
  4. 提交切片规划文件到YARN上, YARN上的MrAppMaster就可以根据切片规划文件计算开启MapTask个数

参数配置

源码中计算切片大小的公式

Math.max(minSize, Math.min(maxSize, blockSize));
mapreduce.input.fileinputformat.split.minsize=1 默认值为1
mapreduce.input.fileinputformat.split.maxsize= Long.MAXValue 默认值Long.MAXValue 因此, 默认情况下, 切片大小=blocksize。

切片大小设置

maxsize( 切片最大值) :参数如果调得比blockSize小, 则会让切片变小, 而且就等于配置的这个参数的值。
minsize( 切片最小值) :参数调的比blockSize大, 则可以让切片变得比blockSize还大。

获取切片信息API

// 获取切片的文件名称
String name = inputSplit.getPath().getName();
// 根据文件类型获取切片信息
FileSplit inputSplit = (FileSplit) context.getInputSplit();

TextInputFormat

FileInputFormat 实现类

在运行 MapReduce 程序时,输入的文件格式包括:基于行的日志文件、二进制格式文件、数据库表等。 针对不同的数据类型,FileInputFormat 提供了常见的接口实现类来解决包括: TextInputFormat、 KeyValueTextInputFormat、NLineInputFormat、 CombineTextInputFormat 和自定义 InputFormat 等。

TextInputFormat

TextInputFormat 是默认的 FileInputFormat 实现类。按行读取每条记录。 键是存储该行在整个文件中的起始字节偏移量, LongWritable 类型。值是这行的内容,不包括任何行终止符(换行符和回车符), Text 类型。

源文件 Key/Value
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)

CombineTextInputFormat 切片机制

按文件规划切片,不管文件多小,都会单独一个切片(如果小文件很多,最后会合并产生好几个符合最大虚拟存储值的切片,MapTask 的数量是由切片数决定的),只交给一个 MapTask,用于解决大量小文件问题。

虚拟存储切片最大值设置

CombineTextInputFormat.setMaxInputSplitSize(job, 4194304);// 4m
注意: 虚拟存储切片最大值设置最好根据实际的小文件大小情况来设置具体的值。

切片机制

  1. 虚拟存储过程

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

    1. 判断虚拟存储的文件大小是否大于 setMaxInputSplitSize 值,大于等于则单独
      形成一个切片。 (主要是用于合并之后做判断,合并之后发现大于就不合了,小于就继续合)

    2. 如果不大于则跟下一个虚拟存储文件进行合并,共同形成一个切片。

    3. 测试举例:有 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

MapReduce 工作流程

MapReduce Working Procedure

MapReduce 工作步骤

  1. 首先有一个待处理的文本ss.txt 假设为200M大小

  2. 在客户端submit()之前,获取待处理的数据的信息,然后根据参数配置,形成一个任务分配的规划 。(默认128m一个数据块)

    ss.txt 0-128 任务1

    ss.txt 128-200 任务2

  3. 提交信息

    Job.split(任务切片信息)

    wc.jar(需要提交的jar包)

    Job.xml(xml配置文件)

    将这三个文件从MapReduce客户端提交到Yarn上的ResourceManager上进行处理。

  4. 4.Yarn上提交时,会将每个任务封装成一个job,提交给yarn处理,ResourceManager会计算出MapTask数量(和切片数量一致)然后RM把任务分配给NodeMamager,在MR appmaster允许后,NodeManager就会来处理相应的任务(Maptask1&Maptask2),每个任务会并行执行。

  5. MapTask会执行Mapper中的map方法,此方法需要传入k,v值,所以我们需要先从数据中获取k,v值,以作为输入的参数具体做法是:首先调用InputFormat方法,默认为TextInputFormat方法,在此方法中调用createRecordReader方法,将每个块封装(k,v)键值对,然后传递给map方法。

  6. 数据进入MapTask中以后会进行Map端的逻辑运算,运算完后,会进行写操作。

  7. map端产生的数据如果直接进行写操作,写入到reduce中,会直接操作磁盘,这样就会进行大量的io操作,效率太低,所以map端reduce端之间会进行一个shuffle操作。

  8. 所以map端产生数据后会通过outputCollector向环形缓冲区写入数据,环形缓冲区分为两部分,一部分写入文件的元数据信息,另一部分写入文件的真实内容。环形缓冲区默认大小为100M,环形缓冲区写入80%数据以后,会反向溢写。

  9. 在溢写之前会对环形缓冲区中数据会按照指定的分区排序规则进行分区和排序,之所以反向溢写是为了可以边接收数据边向磁盘中溢写数据。

  10. 在分区和排序过后会把文件溢写到磁盘当中,可能发生多次溢写,可能溢写到多个文件。

  11. 对所有溢写到磁盘中的文件进行Merge归并排序操作。

  12. 在溢写到磁盘后和对磁盘中文件归并排序之前可能进行combine合并操作,它的意义是对每个MapTask输出的数据进行局部汇总,以减少网络传输量。

    第一:在Map阶段,由于map的进程数量是多于reduce的,所以map阶段处理的效率更高
    第二:在Map阶段进行合并,这样传递给reduce的数据
    会少很多。
    第三:combine操作能够应用的前提是不能够影响最终的业务逻辑,combine的输出的kv要和reduce输入的kv对应起来。

Map 阶段汇总

宏观上看,MapTask阶段分为 Read阶段,Map阶段,Collect阶段,和溢写(spill)阶段, Merge 阶段

Read阶段:MapTask运用用户编写的RecordReader方法,从输入的inputsplit中解析出key/value

Map阶段:将key/value值放入用户编写的map()方法中,产生一系列新的key/value值。

Collect阶段,将用户在map()阶段处理完成的每个key/value数据,调用OutputCollector.collect()方法,输出结果。在函数内部,它将会生成key/value的分区(调用partioner),并写入到一个环形缓冲区当中。

spill阶段,即溢写,当环形缓冲区数据满时,MapReduce会将数据写入到本地磁盘上,此时会生成一个临时文件,需要注意的是,在数据写入到磁盘前,会对数据进行一次本地排序,有必要时,还会对数据进行合并,压缩等操作。

Merge 阶段:当所有数据处理完成后, MapTask 对所有临时文件进行一次合并,以确保最终只会生成一个数据文件。

当所有数据处理完后, MapTask 会将所有临时文件合并成一个大文件, 并保存到文件output/file.out 中,同时生成相应的索引文件 output/file.out.index。
在进行文件合并过程中, MapTask 以分区为单位进行合并。对于某个分区, 它将采用多轮递归合并的方式。 每轮合并 mapreduce.task.io.sort.factor(默认 10) 个文件,并将产生的文件重新加入待合并列表中,对文件排序后,重复以上过程,直到最终得到一个大文件。让每个 MapTask 最终只生成一个数据文件,可避免同时打开大量文件和同时读取大量小文件产生的随机读取带来的开销。

MRAppmaster职能:启动MapTask任务

  1. 所有MapTask数据处理完成后,启动相印数量的ReduceTask,并告知ReduceTask需要处理数据的范围(数据分区)
  2. ReduceTask将MapTask中的数据下载到ReduceTask本地磁盘,然后合并不同的文件,进行归并排序。
  3. 最后将数据交给Reduce处理,一次读区一组数据。
  4. 最后通过OutputFormat的RecordWriter方法将数据写入到本地磁盘的文件当中。

Reduce 阶段汇总

1.Copy 2.Merge 3.Sort 4.Reduce

copy: ReduceTask将远程从MapTask上复制过来要处理的数据,针对某一片数据,如果数据的大小超过一个阈值,则直接存储在磁

盘中,否则直接放到内存中。

Merge:ReduceTask在远程复制的同时,后台启动了两个线程,将硬盘和内存中的数据进行合并,样可以避免内存占用过多,或者磁盘文件过多。

sort:按照MapReduce的语义,Reduce()的输入值是按照key进行聚集的一组数据,为了将key相同的数据放在一起,hadoop采用了基于排序的策略,由于MapTask阶段已经进行了局部排序,所以ReduceTask阶段只需要对所有数据进行一次归并排序即可

Reduce:Reducer会调用reduce()方法将处理好的数据写入到HDFS当中。

设置 ReduceTask 并行度 (个数)

ReduceTask 的并行度同样影响整个 Job 的执行并发度和执行效率,但与 MapTask 的并发数由切片数决定不同, ReduceTask 数量的决定是可以直接手动设置:

// 默认值是 1,手动设置为 4
job.setNumReduceTasks(4);

注:

  1. ReduceTask=0, 表示没有Reduce阶段, 输出文件个数和Map个数一致。
  2. ReduceTask默认值就是1, 所以输出文件个数为一个。
  3. 如果数据分布不均匀, 就有可能在Reduce阶段产生数据倾斜
  4. ReduceTask数量并不是任意设置, 还要考虑业务逻辑需求, 有些情况下, 需要计算全局汇总结果, 就只能有1个ReduceTask。
  5. 具体多少个ReduceTask, 需要根据集群性能而定。
  6. 如果分区数不是1, 但是ReduceTask为1, 是否执行分区过程。 答案是:不执行分区过程。 因为在MapTask的源码中, 执行分区的前提是先判断ReduceNum个数是否大于1。 不大于1肯定不执行

shuffle

工作流程

Shuffle Working Procedure

Shuffle是指在map()方法之后,reduce()方法之前所进行的数据处理过程,shuffle的流程详解如下:

  1. 首先MapTask的Map方法将输出的(k,v)数据放入到环形缓冲区中。
  2. 当环形缓冲区数据达到80%时,会将数据不断溢写到磁盘当中,有可能溢出多个文件,多个溢出的文件会合并成为一个大的文件。
  3. 在溢出的过程以及合并的过程中,会调用partioner对磁盘中的数据会进行分区,进行按照key排序。
  4. reduceTask会根据自身的分区号结合map端数据,取出相应的MapTask中的分区数据。
  5. ReduceTask会储存来自不同MapTask的结果文件进行归并,排序。
  6. 当合并成大文件之后,shuffle过程就结束了,此时会进入reduce()方法。

spill

spill线程为这次spill过程创建一个磁盘文件(从所有本地目录中轮询查找拥有足够大空间的目录,找到之后在该目录下创建一个类似“spillxx.out”的文件)。spill线程根据排过序的kvmeta逐个将partition的数据写入该文件,直到所有的partition都写完。一个partition在文件中对应的数据叫“段(segment)”。

partition在文件中的索引信息是由一个三元组记录的,该三元组包括:起始位置、原始数据长度、压缩之后的数据长度,一个partition对应一个三元组。所有的索引信息是存放在内存中的,若果内存空间不足了就会把后续的索引信息写到磁盘中。

从所有的本地目录总轮询查找拥有足够大空间的目录,在该目录下创建一个类似“spillxx.out.index”的文件,文件中不光存储了索引数据还存储了crc32的校验数据(spillxx.out.index文件和spillxx.out文件不一定是在同一个目录下)。

每一次spill过程会生成至少一个out文件,有时还会生成index文件,spill的次数也会在文件名中显现出来。

spill

溢写阶段详情:
步骤 1: 利用快速排序算法对缓存区内的数据进行排序,排序方式是,先按照分区编号Partition 进行排序然后按照 key 进行排序。这样, 经过排序后,数据以分区为单位聚集在一起,且同一分区内所有数据按照 key 有序。

步骤 2: 按照分区编号由小到大依次将每个分区中的数据写入任务工作目录下的临时文件 output/spillN.out(N 表示当前溢写次数)中。如果用户设置了 Combiner,则写入文件之前,对每个分区中的数据进行一次聚集操作。

步骤 3: 将分区数据的元信息写到内存索引数据结构 SpillRecord 中,其中每个分区的元信息包括在临时文件中的偏移量、压缩前数据大小和压缩后数据大小。如果当前内存索引大小超过 1MB,则将内存索引写到文件 output/spillN.out.index 中。

MapReduce 优化

Map 阶段

  1. 减少溢写的次数

    mapreduce.task.io.sort.mb

    Shuffle的环形缓冲区大小,默认100m,可以提高到200m

    mapreduce.map.sort.spill.percent

    环形缓冲区溢出的阈值,默认80% ,可以提高的90%

  2. 增加每次Merge合并次数

    mapreduce.task.io.sort.factor默认10,可以提高到20

  3. 在不影响业务结果的前提条件下可以提前采用Combiner

    job.setCombinerClass(xxxReducer.class);

  4. 异常重试

    mapreduce.map.maxattempts每个Map Task最大重试次数,一旦重试次数超过该值,则认为Map Task运行失败,默认值:4。根据机器性能适当提高。

Reduce阶段

  1. 合理设置Map和Reduce数

    两个都不能设置太少,也不能设置太多。太少,会导致Task等待,延长处理时间;太多,会导致 Map、Reduce任务间竞争资源,造成处理超时等错误。

  2. 设置Map、Reduce共存

    调整mapreduce.job.reduce.slowstart.completedmaps当MapTask完成的比例达到该值后才会为ReduceTask申请资源。默认是0.05。

  3. 规避使用Reduce,因为Reduce在用于连接数据集的时候将会产生大量的网络消耗。

  4. 增加每个Reduce去Map中拿数据的并行数

    mapreduce.reduce.shuffle.parallelcopies每个Reduce去Map中拉取数据的并行数,默认值是5。可以提高到10。

  5. 集群性能可以的前提下,增大Reduce端存储数据内存的大小

    mapreduce.reduce.memory.mb 默认ReduceTask内存上限1024MB,根据128m数据对应1G内存原则,适当提高内存到4-6G

  6. 异常重试

    mapreduce.reduce.maxattempts每个Reduce Task最大重试次数,一旦重试次数超过该值,则认为Reduce Task运行失败,默认值:4。

  7. mapreduce.reduce.shuffle.input.buffer.percent

    Buffer大小占Reduce可用内存的比例,默认值0.7。可以提高到0.8

  8. mapreduce.reduce.shuffle.merge.percent

    Buffer中的数据达到多少比例开始写入磁盘,默认值0.66。可以提高到0.75

IO传输

采用数据压缩的方式,减少网络IO的的时间。安装Snappy和LZOP压缩编码器。

为了减少磁盘和传输IO,可以采用Snappy或者LZO压缩

conf.setBoolean(“mapreduce.map.output.compress”, true);

conf.setClass(“mapreduce.map.output.compress.codec”, SnappyCodec.class,CompressionCodec.class);

  1. map输入端主要考虑数据量大小和切片,支持切片的有Bzip2、LZO。注意:LZO要想支持切片必须创建索引;
  2. map输出端主要考虑速度,速度快的snappy、LZO;
  3. reduce输出端主要看具体需求,例如作为下一个mr输入需要考虑切片,永久保存考虑压缩率比较大的gzip。

整体

  1. 自定义分区,减少数据倾斜

    定义类,继承Partitioner接口,重写getPartition方法

  2. NodeManager默认内存8G,需要根据服务器实际配置灵活调整

    例如128G内存,配置为100G内存左右,yarn.nodemanager.resource.memory-mb

  3. 单容器默认内存8G,需要根据该任务的数据量灵活调整

    例如128m数据,配置1G内存,yarn.scheduler.maximum-allocation-mb。

  4. mapreduce.map.memory.mb

    控制分配给MapTask内存上限,如果超过会kill掉进程(报:Container is running beyond physical memory limits. Current usage:565MB of512MB physical memory used;Killing Container)。默认内存大小为1G,如果数据量是128m,正常不需要调整内存;如果数据量大于128m,可以增加MapTask内存,最大可以增加到4-5g。

  5. mapreduce.reduce.memory.mb

    控制分配给ReduceTask内存上限。默认内存大小为1G,如果数据量是128m,正常不需要调整内存;如果数据量大于128m,可以增加ReduceTask内存大小为4-5g。

  6. mapreduce.map.java.opts

    控制MapTask堆内存大小。(如果内存不够,报:java.lang.OutOfMemoryError)

  7. mapreduce.reduce.java.opts

    控制ReduceTask堆内存大小。(如果内存不够,报:java.lang.OutOfMemoryError)

  8. mapreduce.task.timeout

    如果一个Task在一定时间内没有任何进入,即不会读取新的数据,也没有输出数据,则认为该Task处于Block状态,可能是卡住了,也许永远会卡住,为了防止因为用户程序永远Block住不退出,则强制设置了一个该超时时间(单位毫秒),默认是600000(10分钟)。如果你的程序对每条输入数据的处理时间过长,建议将该参数调大。

  9. 可以增加MapTask的CPU核数,增加ReduceTask的CPU核数

  10. 增加每个Container的CPU核数和内存大小

  11. 在hdfs-site.xml文件中配置多目录(多磁盘)

Partition 分区

要求将统计结果按照条件输出到不同文件中( 分区) 。

默认Partitioner分区

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

1
2
3
4
5
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步骤

  1. 自定义类继承Partitioner,重写getPartition()方法

    1
    2
    3
    4
    5
    6
    7
    8
    public class CustomPartitioner extends Partitioner<Text, FlowBean> {
    @Override
    public int getPartition(Text key, FlowBean value, int numPartitions) {
    // 控制分区代码逻辑
    … …
    return partition;
    }
    }
  2. 在Job驱动中, 设置自定义Partitioner

    job.setPartitionerClass(CustomPartitioner.class);

  3. 自定义Partition后,要根据自定义Partitioner的逻辑设置相应数量的ReduceTask

    job.setNumReduceTasks(5);

分区总结

  1. 如果ReduceTask的数量> getPartition的结果数, 则会多产生几个空的输出文件part-r-000xx

    job.setNumReduceTasks(6); 大于5, 程序会正常运行, 会产生空文件

  2. 如果1<ReduceTask的数量<getPartition的结果数, 则有一部分分区数据无处安放, 会Exception

  3. 如果ReduceTask的数量=1, 则不管MapTask端输出多少个分区文件, 最终结果都交给这一个ReduceTask, 最终也就只会产生一个结果文件 part-r-00000;

    job.setNumReduceTasks(1); 会正常运行,只不过会产生一个输出文件

  4. 分区号必须从零开始, 逐一累加。

案例实操

  1. 增加自定义分区类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    package com.test.mapreduce.partitionercompable;
    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;
    }
    }
  2. 在驱动类中添加分区类

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

Combiner 合并

  1. Combiner是MR程序中Mapper和Reducer之外的一种组件。

  2. Combiner组件的父类就是Reducer。

  3. Combiner和Reducer的区别在于运行的位置

    Combiner是在每一个MapTask所在的节点运行;

    Reducer是接收全局所有Mapper的输出结果;

  4. Combiner的意义就是对每一个MapTask的输出进行局部汇总, 以减小网络传输量。

  5. Combiner能够应用的前提是不能影响最终的业务逻辑, 而且, Combiner的输出kv
    应该跟Reducer的输入kv类型要对应起来。

自定义 Combiner 实现步骤

  1. 自定义一个 Combiner 继承 Reducer,重写 Reduce 方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    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();
    }
    outV.set(sum);
    context.write(key,outV);
    }
    }
  2. 在 Job 驱动类中设置:

    job.setCombinerClass(WordCountCombiner.class);


MapReduce
http://example.com/2022/07/17/Hadoop-MapReduce/
作者
Zhao Zhuoyue
发布于
2022年7月17日
许可协议