Young's blog Young's blog
首页
Spring
  • 前端文章1

    • JavaScript
  • 学习笔记

    • 《JavaScript教程》
    • 《JavaScript高级程序设计》
    • 《ES6 教程》
    • 《Vue》
    • 《React》
    • 《TypeScript 从零实现 axios》
    • 《Git》
    • TypeScript
    • JS设计模式总结
  • HTML
  • CSS
  • 技术文档
  • GitHub技巧
  • Nodejs
  • 博客搭建
  • 学习
  • 面试
  • 心情杂货
  • 实用技巧
  • 友情链接
关于
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

Young

首页
Spring
  • 前端文章1

    • JavaScript
  • 学习笔记

    • 《JavaScript教程》
    • 《JavaScript高级程序设计》
    • 《ES6 教程》
    • 《Vue》
    • 《React》
    • 《TypeScript 从零实现 axios》
    • 《Git》
    • TypeScript
    • JS设计模式总结
  • HTML
  • CSS
  • 技术文档
  • GitHub技巧
  • Nodejs
  • 博客搭建
  • 学习
  • 面试
  • 心情杂货
  • 实用技巧
  • 友情链接
关于
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • Hadoop

    • 第一章 大数据技术之大数据概论
    • 第二章大数据技术之 Hadoop概念讲解
    • 第三章Hadoop 运行环境搭建
    • 第四章Hadoop之HDFS详解以及工作机制介绍
    • 第五章MapReduce编程框架
      • 1.1 MapReduce 定义
      • 1.2 MapReduce 优缺点
        • 1.2.1 优点
        • 1.2.2 缺点
      • 1.3 MapReduce 核心思想
      • 1.4 MapReduce 进程
      • 1.5 官方 WordCount 源码
      • 1.6 常用数据序列化类型
      • 1.7 MapReduce 编程规范
      • 1.8 WordCount 案例实操
        • 1.8.1 本地测试
        • 1.8.2 提交到集群测试
      • 2.1 序列化概述
      • 2.2 自定义 bean 对象实现序列化接口(Writable)
      • 3.1 InputFormat 数据输入
        • 3.1.1 切片与 MapTask 并行度决定机制
        • 3.1.3 FileInputFormat 切片机制
        • 1、切片机制
        • 2、案例分析
        • 3.1.4 TextInputFormat
        • 1)FileInputFormat 实现类
        • 2)TextInputFormat
        • 3.1.5 CombineTextInputFormat 切片机制
        • (1)虚拟存储过程:
        • (2)切片过程:
        • 3.1.6 CombineTextInputFormat 案例实操
      • 3.2 MapReduce 工作流程
      • 3.3 Shuffle 机制
        • 3.3.1 Shuffle 机制
        • 3.3.2 Partition 分区
        • 1、问题引出
        • 2、默认 Partitioner 分区
        • 3、自定义 Partitioner 步骤
        • 4、分区总结
        • 5、案例分析
        • 3.3.3 Partition 分区案例实操
        • 3.3.4 WritableComparable 排序
        • 排序分类
        • 3.3.5 WritableComparable 排序案例实操(全排序)
        • 3.3.6 WritableComparable 排序案例实操(区内排序)
        • 3.3.7 Combiner 合并
      • 3.4 OutputFormat 数据输出
        • 3.4.1 OutputFormat 接口实现类
        • 3.4.2 自定义 OutputFormat 案例实操
      • 3.5 MapReduce 内核源码解析
        • 3.5.1 MapTask 工作机制
      • 3.5.2 ReduceTask 工作机制
        • 3.5.3 ReduceTask 并行度决定机制
      • 3.6 Join 应用
        • 3.6.1 Reduce Join
        • 3.6.2 Reduce Join 案例实操
        • 3.6.3 Map Join
      • 3.6.4 Map Join 案例实操
      • 3.7 数据清洗(ETL)
        • 3.8 MapReduce 开发总结
    • 第六章Hadoop 数据压缩
    • 第七章大数据技术之 Hadoop(Yarn)
    • 第八章Hadoop(生产调优手册)
    • Hadoop HA 高可用
    • Hadoop单节点伪分布式安装
    • hadoop 3.x 在windows10下编译
    • hadoop 踩坑记 DataNode 启动失败(ClusterID不一致):Initialization failed for Block pool
  • kafka

  • Flume

  • hive

  • scala

  • spark

  • 大数据
  • Hadoop
andanyang
2022-06-05
目录

第五章MapReduce编程框架

# 1. MapReduce 概述

# 1.1 MapReduce 定义

MapReduce 是一个分布式运算程序的编程框架,是用户开发“基于 Hadoop 的数据分析应用”的核心框架。

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

# 1.2 MapReduce 优缺点

# 1.2.1 优点

1)MapReduce 易于编程

它简单的实现一些接口,就可以完成一个分布式程序,这个分布式程序可以分布到大量廉价的 PC 机器上运行。也就是说你写一个分布式程序,跟写一个简单的串行程序是一模一样的。就是因为这个特点使得 MapReduce 编程变得非常流行。

2)良好的扩展性

当你的计算资源不能得到满足的时候,你可以通过简单的增加机器来扩展它的计算能力。

3)高容错性

MapReduce 设计的初衷就是使程序能够部署在廉价的 PC 机器上,这就要求它具有很高的容错性。比如其中一台机器挂了,它可以把上面的计算任务转移到另外一个节点上运行, 不至于这个任务运行失败, 而且这个过程不需要人工参与,而完全是由 Hadoop 内部完成的。

4)适合 PB 级以上海量数据的离线处理

可以实现上千台服务器集群并发工作,提供数据处理能力。

# 1.2.2 缺点

1)不擅长实时计算

MapReduce 无法像 MySQL 一样,在毫秒或者秒级内返回结果。

2)不擅长流式计算

流式计算的输入数据是动态的,而 MapReduce 的输入数据集是静态的,不能动态变化。 这是因为 MapReduce 自身的设计特点决定了数据源必须是静态的。

3)不擅长 DAG(有向无环图)计算

多个应用程序存在依赖关系,后一个应用程序的输入为前一个的输出。在这种情况下, MapReduce 并不是不能做,而是使用后,每个 MapReduce 作业的输出结果都会写入到磁盘, 会造成大量的磁盘 IO,导致性能非常的低下。

# 1.3 MapReduce 核心思想

image-20230608222632302

(1)分布式的运算程序往往需要分成至少 2 个阶段。

(2)第一个阶段的 MapTask 并发实例,完全并行运行,互不相干。

(3)第二个阶段的 ReduceTask 并发实例互不相干,但是他们的数据依赖于上一个阶段的所有 MapTask 并发实例的输出。

(4)MapReduce 编程模型只能包含一个 Map 阶段和一个 Reduce 阶段,如果用户的业务逻辑非常复杂,那就只能多个 MapReduce 程序,串行运行。

总结:分析 WordCount 数据流走向深入理解 MapReduce 核心思想。

# 1.4 MapReduce 进程

一个完整的 MapReduce 程序在分布式运行时有三类实例进程:

(1)MrAppMaster:负责整个程序的过程调度及状态协调。

(2)MapTask:负责 Map 阶段的整个数据处理流程。

(3)ReduceTask:负责 Reduce 阶段的整个数据处理流程。

# 1.5 官方 WordCount 源码

采用反编译工具反编译源码,发现 WordCount 案例有 Map 类、Reduce 类和驱动类。且 数据的类型是 Hadoop 自身封装的序列化类型。

# 1.6 常用数据序列化类型

Java 基本类型 Hadoop Writable 类型
boolean BooleanWritable
byte ByteWritable
int IntWritable
float FloatWritable
long LongWritable
double DoubleWritable
String Text
map MapWritable
array ArrayWritable

# 1.7 MapReduce 编程规范

用户编写的程序分成三个部分:Mapper、Reducer 和 Driver。

1.Mapper 阶段

(1)用户自定义的 Mapper 要继承自己的父类

(2)Mapper 的输入数据是 KV 对的形式(KV 的类型可自定义)

(3)Mapper 中的业务逻辑写在 map()方法中

(4)Mapper 的输出数据是 KV 对的形式(KV 的类型可自定义)

(5)map()方法(MapTask 进程)对每一个<K,V>调用一次

2.Reducer 阶段

(1)用户自定义的 Reducer 要继承自己的父类

(2)Reducer 的输入数据类型对应 Mapper 的输出数据类型,也是 KV

(3)Reducer 的业务逻辑写在 reduce()方法中

(4)ReduceTask 进程对每一组相同 k 的组调用一次 reduce()方法

3.Driver 阶段

相当于 YARN 集群的客户端,用于提交我们整个程序到 YARN 集群,提交的是封装了 MapReduce 程序相关运行参数的 job 对象

# 1.8 WordCount 案例实操

# 1.8.1 本地测试

1)需求

在给定的文本文件中统计输出每一个单词出现的总次数

2)需求分析

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

3)环境准备

(1)创建 maven 工程,MapReduceDemo

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

  <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>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

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

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
2
3
4
5
6
7
8

4)编写程序

(1)编写 Mapper 类

import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;

import java.io.IOException;
/**
 * KEYIN, map阶段输入的key的类型:LongWritable
 * VALUEIN,map阶段输入value类型:Text
 * KEYOUT,map阶段输出的Key类型:Text
 * VALUEOUT,map阶段输出的value类型:IntWritable
 */
public class WordCountMapper extends Mapper<LongWritable, Text, Text, IntWritable> {
    private Text outK = new Text();
    private IntWritable outV = new IntWritable(1);

    @Override
    protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {

        // 1 获取一行
        // atguigu atguigu
        String line = value.toString();

        // 2 切割
        // atguigu
        // atguigu
        String[] words = line.split(" ");

        // 3 循环写出
        for (String word : words) {
            // 封装outk
            outK.set(word);

            // 写出
            context.write(outK, outV);
        }
    }
}
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

(2)编写 Reducer 类

import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;

import java.io.IOException;
/**
 * KEYIN, reduce阶段输入的key的类型:Text
 * VALUEIN,reduce阶段输入value类型:IntWritable
 * KEYOUT,reduce阶段输出的Key类型:Text
 * VALUEOUT,reduce阶段输出的value类型:IntWritable
 */
public class WordCountReducer 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;
        // atguigu, (1,1)
        // 累加
        for (IntWritable value : values) {
            sum += value.get();
        }

        outV.set(sum);

        // 写出
        context.write(key, outV);
    }
}
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

(3)编写 Driver 驱动类

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;

import java.io.IOException;

/**
 * @author andanyoung
 * @version 1.0
 * @date 2023/6/8 22:55
 */

public class WordCountDriver {

    public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {

        // 1 获取job
        Configuration conf = new Configuration();
        //conf.set("fs.defaultFS", "hdfs://node1:8020");
        Job job = Job.getInstance(conf);

        // 2 设置jar包路径
        job.setJarByClass(WordCountDriver.class);

        // 3 关联mapper和reducer
        job.setMapperClass(WordCountMapper.class);
        job.setReducerClass(WordCountReducer.class);

        // 4 设置map输出的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("F:\\hadoop-test\\wc.txt"));
        FileOutputFormat.setOutputPath(job, new Path("F:\\hadoop-out11"));

        // 7 提交job
        boolean result = job.waitForCompletion(true);

        System.exit(result ? 0 : 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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50

5)本地测试

(1)需要首先配置好 HADOOP_HOME 变量以及 Windows 运行依赖

(2)在 IDEA/Eclipse 上运行程序

# 1.8.2 提交到集群测试

集群上测试

(1)用 maven 打 jar 包,需要添加的打包插件依赖

<build>
        <plugins>
            <plugin>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.6.1</version>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                </configuration>
            </plugin>
            <plugin>
                <artifactId>maven-assembly-plugin</artifactId>
                <configuration>
                    <descriptorRefs>
                        <descriptorRef>jar-with-dependencies</descriptorRef>
                    </descriptorRefs>
                </configuration>
                <executions>
                    <execution>
                        <id>make-assembly</id>
                        <phase>package</phase>
                        <goals>
                            <goal>single</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
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

(2)将程序打成 jar 包

mvn clean package
1

(3)修改不带依赖的 jar 包名称为 wc.jar,并拷贝该 jar 包到 Hadoop 集群的 /opt/module/hadoop-3.1.3 路径

(4)启动 Hadoop 集群

(5)执行 WordCount 程序

 hadoop jar wc.jar
com.atguigu.mapreduce.wordcount.WordCountDriver /user/atguigu/input
/user/atguigu/output
1
2
3

# 2. Hadoop 序列化

# 2.1 序列化概述

1)什么是序列化

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

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

2)为什么要序列化

一般来说,“活的”对象只生存在内存里,关机断电就没有了。而且“活的”对象只能由本地的进程使用,不能被发送到网络上的另外一台计算机。 然而序列化可以存储“活的” 对象,可以将“活的”对象发送到远程计算机。

3)为什么不用 Java 的序列化

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

4)Hadoop 序列化特点:

  • (1)紧凑 :高效使用存储空间。
  • (2)快速:读写数据的额外开销小。
  • (3)互操作:支持多语言的交互

Java 基本类型与 Hadoop 常用序列化类型

Java 基本类型 Hadoop Writable 类型
boolean BooleanWritable
byte ByteWritable
int IntWritable
float FloatWritable
long LongWritable
double DoubleWritable
String Text
map MapWritable
array ArrayWritable

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

在企业开发中往往常用的基本序列化类型不能满足所有需求,比如在 Hadoop 框架内部 传递一个 bean 对象,那么该对象就需要实现序列化接口。

具体实现 bean 对象序列化步骤如下 7 步。

(1)必须实现 Writable 接口

(2)反序列化时,需要反射调用空参构造函数,所以必须有空参构造

public FlowBean() {
	super();
}
1
2
3

(3)重写序列化方法

@Override
public void write(DataOutput out) throws IOException {
	out.writeLong(upFlow);
    out.writeLong(downFlow);
    out.writeLong(sumFlow);
}
1
2
3
4
5
6

(4)重写反序列化方法

@Override
public void readFields(DataInput in) throws IOException {
    upFlow = in.readLong();
    downFlow = in.readLong();
    sumFlow = in.readLong();
}
1
2
3
4
5
6

(5)注意反序列化的顺序和序列化的顺序完全一致

(6)要想把结果显示在文件中,需要重写 toString(),可用"\t"分开,方便后续用。

(7)如果需要将自定义的 bean 放在 key 中传输,则还需要实现 Comparable 接口,因为 MapReduce 框中的 Shuffle 过程要求对 key 必须能排序。详见后面排序案例。

@Override
public int compareTo(FlowBean o) {
    // 倒序排列,从大到小
    return this.sumFlow > o.getSumFlow() ? -1 : 1;
}
1
2
3
4
5

# 3. MapReduce 框架原理

image-20230614000658557

# 3.1 InputFormat 数据输入

# 3.1.1 切片与 MapTask 并行度决定机制

1)问题引出

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

思考:1G 的数据,启动 8 个 MapTask,可以提高集群的并发处理能力。那么 1K 的数据,也启动 8 个 MapTask,会提高集群性能吗?MapTask 并行任务是否越多越好呢?哪些因素影响了 MapTask 并行度?

2)MapTask 并行度决定机制

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

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

image-202306142039322733.1.2 Job 提交流程源码和切片源码详解

1)Job 提交流程源码详解

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

image-20230614205242036

2)FileInputFormat 切片源码解析(input.getSplits(job))

image-20230614212624794

# 3.1.3 FileInputFormat 切片机制

# 1、切片机制

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

(2)切片大小,默认等于 Block 大小

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

# 2、案例分析

image-20230614212747456

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

Math.max(minSize, Math.min(maxSize, blockSize));
mapreduce.input.fileinputformat.split.minsize=1 默认值为1
mapreduce.input.fileinputformat.split.maxsize= Long.MAXValue 默认值Long.MAXValue
1
2
3

因此,默认情况下,切片大小=blocksize。

(2)切片大小设置

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

(3)获取切片信息 API

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

# 3.1.4 TextInputFormat

# 1)FileInputFormat 实现类

思考:在运行 MapReduce 程序时,输入的文件格式包括:基于行的日志文件、二进制格式文件、数据库表等。 那么,针对不同的数据类型,MapReduce 是如何读取这些数据的呢?

FileInputFormat 常见的接口实现类包括:TextInputFormat、KeyValueTextInputFormat、NLineInputFormat、CombineTextInputFormat 和自定义 InputFormat 等

# 2)TextInputFormat

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

以下是一个示例,比如,一个分片包含了如下 4 条文本记录。

Rich learning form
Intelligent learning engine
Learning more convenient
From the real demand for more close to the enterprise
1
2
3
4

每条记录表示为以下键/值对:

(0,Rich learning form)
(20,Intelligent learning engine)
(49,Learning more convenient)
(74,From the real demand for more close to the enterprise)
1
2
3
4

# 3.1.5 CombineTextInputFormat 切片机制

框架默认的 TextInputFormat 切片机制是对任务按文件规划切片,不管文件多小,都会是一个单独的切片, 都会交给一个 MapTask,这样如果有大量小文件,就会产生大量的 MapTask(一个 MapTask 消耗一个 CPU ,1G 内存),处理效率极其低下。

1)应用场景:

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

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

CombineTextInputFormat.setMaxInputSplitSize(job, 4194304);// 4m

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

3)切片机制

生成切片过程包括:虚拟存储过程和切片过程二部分

setMaxInputSplitSize 值为 4M

image-20230614224847034

# (1)虚拟存储过程:

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

例如 setMaxInputSplitSize 值为 4M,输入文件大小为 8.02M,则先逻辑上分成一个 4M。剩余的大小为 4.02M,如果按照 4M 逻辑划分,就会出现 0.02M 的小的虚拟存储 文件,所以将剩余的 4.02M 文件切分成(2.01M 和 2.01M)两个文件。

# (2)切片过程:

  • (a)判断虚拟存储的文件大小是否大于 setMaxInputSplitSize 值,大于等于则单独形成一个切片。

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

  • (c)测试举例:有 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.1.6 CombineTextInputFormat 案例实操

1)需求

将输入的大量小文件合并成一个切片统一处理。

  • (1)输入数据 准备 4 个小文件

    image-20230614225641606

  • (2)期望

    期望一个切片处理 4 个文件

2)实现过程

  • (1)不做任何处理,运行 1.8 节的 WordCount 案例程序,观察切片个数为 4。

    number of splits:4
    
    1
  • (2)在 WordcountDriver 中增加如下代码,运行程序,并观察运行的切片个数为 3。

    (a)驱动类中添加代码如下:

    // 如果不设置 InputFormat,它默认用的是 TextInputFormat.class
    job.setInputFormatClass(CombineTextInputFormat.class);
    //虚拟存储切片最大值设置 4m
    CombineTextInputFormat.setMaxInputSplitSize(job, 4194304);
    
    1
    2
    3
    4

    (b)运行如果为 3 个切片。

    number of splits:3
    
    1
  • (3)在 WordcountDriver 中增加如下代码,运行程序,并观察运行的切片个数为 1。

    (a)驱动中添加代码如下:

    // 如果不设置 InputFormat,它默认用的是 TextInputFormat.class
    job.setInputFormatClass(CombineTextInputFormat.class);
    //虚拟存储切片最大值设置 20m
    CombineTextInputFormat.setMaxInputSplitSize(job, 20971520);
    
    1
    2
    3
    4

    (b)运行如果为 1 个切片

    number of splits:1
    
    1

    # 3.2 MapReduce 工作流程

    image-20230614230134735

image-20230614230202750

上面的流程是整个 MapReduce 最全工作流程,但是 Shuffle 过程只是从第 7 步开始到第 16 步结束,具体 Shuffle 过程详解,如下:

  • (1)MapTask 收集我们的 map()方法输出的 kv 对,放到内存缓冲区中
  • (2)从内存缓冲区不断溢出本地磁盘文件,可能会溢出多个文件
  • (3)多个溢出文件会被合并成大的溢出文件
  • (4)在溢出过程及合并的过程中,都要调用 Partitioner 进行分区和针对 key 进行排序
  • (5)ReduceTask 根据自己的分区号,去各个 MapTask 机器上取相应的结果分区数据
  • (6)ReduceTask 会抓取到同一个分区的来自不同 MapTask 的结果文件,ReduceTask 会将这些文件再进行合并(归并排序)
  • (7)合并成大文件后,Shuffle 的过程也就结束了,后面进入 ReduceTask 的逻辑运算过程(从文件中取出一个一个的键值对 Group,调用用户自定义的 reduce()方法)

注意:

(1)Shuffle 中的缓冲区大小会影响到 MapReduce 程序的执行效率,原则上说,缓冲区越大,磁盘 io 的次数越少,执行速度就越快。

(2)缓冲区的大小可以通过参数调整,参数:mapreduce.task.io.sort.mb 默认 100M。

# 3.3 Shuffle 机制

# 3.3.1 Shuffle 机制

Map 方法之后,Reduce 方法之前的数据处理过程称之为 Shuffle。

image-20230614232316005

# 3.3.2 Partition 分区

# 1、问题引出

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

# 2、默认 Partitioner 分区

image-20230614232752640

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

# 3、自定义 Partitioner 步骤

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

image-20230614232903693(2)在 Job 驱动中,设置自定义 Partitioner

job.setPartitionerClass(CustomPartitioner.class);
1

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

job.setNumReduceTasks(5);
1

# 4、分区总结

image-20230614233420614

# 5、案例分析

例如:假设自定义分区数为 5,则

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

(2)job.setNumReduceTasks(2); 会报错

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

# 3.3.3 Partition 分区案例实操

package com.atguigu.mapreduce.partitioner;
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;
 	}
}
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

4)在驱动函数中增加自定义数据分区设置和 ReduceTask 设置

//8 指定自定义分区器
 job.setPartitionerClass(ProvincePartitioner.class);
 //9 同时指定相应数量的 ReduceTask
 job.setNumReduceTasks(5);
1
2
3
4

# 3.3.4 WritableComparable 排序

排序是 MapReduce 框架中最重要的操作之一。

MapTask 和 ReduceTask 均会对数据按 照 key 进行排序。该操作属于 Hadoop 的默认行为。任何应用程序中的数据均会被排序,而不管逻辑上是否需要。默认排序是按照字典顺序排序,且实现该排序的方法是快速排序。

对于 MapTask,它会将处理的结果暂时放到环形缓冲区中,当环形缓冲区使用率**达到一定阈值后,再对缓冲区中的数据进行一次快速排序,**并将这些有序数据溢写到磁盘上,而当数据处理完毕后,它会对磁盘上所有文件进行归并排序。

对于 ReduceTask,它从每个 MapTask 上远程拷贝相应的数据文件,如果文件大小超过一定阈值,则溢写磁盘上,否则存储在内存中。如果磁盘上文件数目达到一定阈值,则进行一次归并排序以生成一个更大文件;如果内存中文件大小或者数目超过一定阈值,则进行一次合并后将数据溢写到磁盘上。当所有数据拷贝完毕后,ReduceTask 统一对内存和磁盘上的所有数据进行一次归并排序。

# 排序分类

(1)部分排序

MapReduce 根据输入记录的键对数据集排序。保证输出的每个文件内部有序。

(2)全排序

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

(3)辅助排序:

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

(4)二次排序

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

自定义排序 WritableComparable 原理分析

bean 对象做为 key 传输,需要实现 WritableComparable 接口重写 compareTo 方法,就可以实现排序。

@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;
}
1
2
3
4
5
6
7
8
9
10
11
12
13

# 3.3.5 WritableComparable 排序案例实操(全排序)

1)需求

根据案例 2.3 序列化案例产生的结果再次对总流量进行倒序排序。

image-20230615213538542

3)代码实现

(1)FlowBean 对象在在需求 1 基础上增加了比较功能

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;
         }
     }
 }
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
55
56
57
58
59
60
61
62
63

(2)编写 Mapper 类

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 封装 outK outV
         outK.setUpFlow(Long.parseLong(split[1]));
         outK.setDownFlow(Long.parseLong(split[2]));
         outK.setSumFlow();
         outV.set(split[0]);
         //4 写出 outK outV
         context.write(outK,outV);
     }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

(3)编写 Reducer 类

package com.atguigu.mapreduce.writablecompable;
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);
         }
     }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

(4)编写 Driver 类

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

# 3.3.6 WritableComparable 排序案例实操(区内排序)

1)需求

要求每个省份手机号输出的文件中按照总流量内部排序。

2)需求分析

基于前一个需求,增加自定义分区类,分区按照省份手机号设置。

image-20230615215911259

3)案例实操

  • (1)增加自定义分区类
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;
     }
}
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

(2)在驱动类中添加分区类

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

# 3.3.7 Combiner 合并

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

(2)Combiner 组件的父类就是 Reducer。

(3)Combiner 和 Reducer 的区别在于运行的位置 Combiner 是在每一个 MapTask 所在的节点运行;

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

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

image-20230615221257999

(6)自定义 Combiner 实现步骤

  • (a)自定义一个 Combiner 继承 Reducer,重写 Reduce 方法

    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);
     }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
  • (b)在 Job 驱动类中设置:

    job.setCombinerClass(WordCountCombiner.class);
    
    1

# 3.4 OutputFormat 数据输出

# 3.4.1 OutputFormat 接口实现类

OutputFormat 是 MapReduce 输出的基类,所有实现 MapReduce 输出都实现了 OutputFormat 接口。下面我们介绍几种常见的 OutputFormat 实现类。

1.OutputFormat 实现类

image-20230615222357367

2.默认输出格式 TextOutputFormat

3.自定义 OutputFormat

​ 3.1 应用场景:

​ 例如:输出数据到 MySQL/HBase/Elasticsearch 等存储框架中。

​ 3.2 自定义 OutputFormat 步骤

​ ➢ 自定义一个类继承 FileOutputFormat。

​ ➢ 改写 RecordWriter,具体改写输出数据的方法 write()。

# 3.4.2 自定义 OutputFormat 案例实操

1)需求

​ 过滤输入的 log 日志,包含 atguigu 的网站输出到 e:/atguigu.log,不包含 atguigu 的网站输出到 e:/other.log。

image-20230615222556151

3)案例实操

  • (1)编写 LogMapper 类
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());
     }
}
1
2
3
4
5
6
7
8
9
10
11
12
  • (2)编写 LogReducer 类

    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());
             }
         }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
  • (3)自定义一个 LogOutputFormat 类

    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;
         }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
  • (4)编写 LogRecordWriter 类

    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 atguiguOut;
        private  FSDataOutputStream otherOut;
    
        public LogRecordWriter(TaskAttemptContext job) {
            // 创建两条流
            try {
                FileSystem fs = FileSystem.get(job.getConfiguration());
    
                atguiguOut = fs.create(new Path("D:\\hadoop\\atguigu.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();
    
            // 具体写
            if (log.contains("atguigu")){
                atguiguOut.writeBytes(log+"\n");
            }else {
                otherOut.writeBytes(log+"\n");
            }
        }
    
        @Override
        public void close(TaskAttemptContext context) throws IOException, InterruptedException {
            // 关流
            IOUtils.closeStream(atguiguOut);
            IOUtils.closeStream(otherOut);
        }
    }
    
    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

    (5)编写 LogDriver 类

    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\\inputoutputformat"));
            //虽然我们自定义了outputformat,但是因为我们的outputformat继承自fileoutputformat
            //而fileoutputformat要输出一个_SUCCESS文件,所以在这还得指定一个输出目录
            FileOutputFormat.setOutputPath(job, new Path("D:\\hadoop\\output1111"));
    
            boolean b = job.waitForCompletion(true);
            System.exit(b ? 0 : 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
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40

    # 3.5 MapReduce 内核源码解析

    # 3.5.1 MapTask 工作机制

    image-20230615224158805

  • (1)Read 阶段:MapTask 通过 InputFormat 获得的 RecordReader,从输入 InputSplit 中 解析出一个个 key/value。

  • (2)Map 阶段:该节点主要是将解析出的 key/value 交给用户编写 map()函数处理,并产生一系列新的 key/value。

  • (3)Collect 收集阶段:在用户编写 map()函数中,当数据处理完成后,一般会调 OutputCollector.collect()输出结果。在该函数内部,它会将生成的 key/value 分区(调用 Partitioner),并写入一个环形内存缓冲区中。

  • (4)Spill 阶段:即“溢写”,当环形缓冲区满后,MapReduce 会将数据写到本地磁盘上, 生成一个临时文件。需要注意的是,将数据写入本地磁盘之前,先要对数据进行一次本地排序,并在必要时对数据进行合并、压缩等操作。

    溢写阶段详情:

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

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

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

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

    当所有数据处理完后,MapTask 会将所有临时文件合并成一个大文件,并保存到文件 output/file.out 中,同时生成相应的索引文件 output/file.out.index。

    在进行文件合并过程中,MapTask 以分区为单位进行合并。对于某个分区,它将采用多轮递归合并的方式。每轮合并 mapreduce.task.io.sort.factor(默认 10)个文件,并将产生的文件重新加入待合并列表中,对文件排序后,重复以上过程,直到最终得到一个大文件。

    让每个 MapTask 最终只生成一个数据文件,可避免同时打开大量文件和同时读取大量小文件产生的随机读取带来的开销。

    # 3.5.2 ReduceTask 工作机制

    image-20230615230302680

  • (1)Copy 阶段:ReduceTask 从各个 MapTask 上远程拷贝一片数据,并针对某一片数据,如果其大小超过一定阈值,则写到磁盘上,否则直接放到内存中。

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

  • (3)Reduce 阶段:reduce()函数将计算结果写到 HDFS 上。

# 3.5.3 ReduceTask 并行度决定机制

回顾: MapTask 并行度由切片个数决定,切片个数由输入文件和切片规则决定。

思考: ReduceTask 并行度由谁决定?

1)设置 ReduceTask 并行度(个数)

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

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

2)实验:测试 ReduceTask 多少合适

(1)实验环境:1 个 Master 节点,16 个 Slave 节点:CPU:8GHZ,内存: 2

(2)实验结论

image-20230615230550050

3)注意事项

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

# 3.6 Join 应用

# 3.6.1 Reduce Join

Map 端的主要工作:为来自不同表或文件的 key/value 对,打标签以区别不同来源的记录。然后用连接字段作为 key,其余部分和新加的标志作为 value,最后进行输出。

Reduce 端的主要工作:在 Reduce 端以连接字段作为 key 的分组已经完成,我们只需要在每一个分组当中将那些来源于不同文件的记录(在 Map 阶段已经打标志)分开,最后进行合并就 ok 了。

# 3.6.2 Reduce Join 案例实操

1)需求

表 4-4 订单数据表 t_order

id pid amount
1001 01 1
1002 02 2
1003 03 3
1004 01 4
1005 02 5
1006 03 6

表 4-5 商品信息表 t_product

pid pname
01 小米
02 华为
03 格力

将商品信息表中数据根据商品 pid 合并到订单数据表中。

表 4-6 最终数据形式

id pname amount
1001 小米 1
1004 小米 4
1002 华为 2
1005 华为 5
1003 格力 3
1006 格力 6

2)需求分析

通过将关联条件作为 Map 输出的 key,将两表满足 Join 条件的数据并携带数据所来源的文件信息,发往同一个 ReduceTask,在 Reduce 中进行数据的串联。

image-20230615232128226

3)代码实现

​ (1)创建商品和订单合并后的 TableBean 类

import org.apache.hadoop.io.Writable;

import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;

public class TableBean implements Writable {

    private String id; // 订单id
    private String pid; // 商品id
    private int amount; // 商品数量
    private String pname;// 商品名称
    private String flag; // 标记是什么表 order pd

    // 空参构造
    public TableBean() {
    }

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public String getPid() {
        return pid;
    }

    public void setPid(String pid) {
        this.pid = pid;
    }

    public int getAmount() {
        return amount;
    }

    public void setAmount(int amount) {
        this.amount = amount;
    }

    public String getPname() {
        return pname;
    }

    public void setPname(String pname) {
        this.pname = pname;
    }

    public String getFlag() {
        return flag;
    }

    public void setFlag(String flag) {
        this.flag = flag;
    }

    @Override
    public void write(DataOutput out) throws IOException {
        out.writeUTF(id);
        out.writeUTF(pid);
        out.writeInt(amount);
        out.writeUTF(pname);
        out.writeUTF(flag);
    }

    @Override
    public void readFields(DataInput in) throws IOException {

        this.id = in.readUTF();
        this.pid = in.readUTF();
        this.amount = in.readInt();
        this.pname = in.readUTF();
        this.flag = in.readUTF();
    }

    @Override
    public String toString() {
        // id	pname	amount
        return  id + "\t" +  pname + "\t" + amount ;
    }
}
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
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83

(2)编写 TableMapper 类

import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.InputSplit;
import org.apache.hadoop.mapreduce.Mapper;
import org.apache.hadoop.mapreduce.lib.input.FileSplit;

import java.io.IOException;

public class TableMapper extends Mapper<LongWritable, Text, Text, TableBean> {

    private String fileName;
    private Text outK  = new Text();
    private TableBean outV = new TableBean();

    @Override
    protected void setup(Context context) throws IOException, InterruptedException {
        // 初始化  order  pd
        FileSplit split = (FileSplit) context.getInputSplit();

        fileName = split.getPath().getName();
    }

    @Override
    protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
        // 1 获取一行
        String line = value.toString();

        // 2 判断是哪个文件的
        if (fileName.contains("order")){// 处理的是订单表

            String[] split = line.split("\t");

            // 封装k  v
            outK.set(split[1]);
            outV.setId(split[0]);
            outV.setPid(split[1]);
            outV.setAmount(Integer.parseInt(split[2]));
            outV.setPname("");
            outV.setFlag("order");

        }else {// 处理的是商品表
            String[] split = line.split("\t");

            outK.set(split[0]);
            outV.setId("");
            outV.setPid(split[0]);
            outV.setAmount(0);
            outV.setPname(split[1]);
            outV.setFlag("pd");
        }

        // 写出
        context.write(outK, outV);
    }
}
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
55

(3)编写 TableReducer 类

import org.apache.commons.beanutils.BeanUtils;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;

import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;

public class TableReducer extends Reducer<Text, TableBean,TableBean, NullWritable> {

    @Override
    protected void reduce(Text key, Iterable<TableBean> values, Context context) throws IOException, InterruptedException {
//        01 	1001	1   order
//        01 	1004	4   order
//        01	小米   	     pd
        // 准备初始化集合
        ArrayList<TableBean> orderBeans = new ArrayList<>();
        TableBean pdBean = new TableBean();

        // 循环遍历
        for (TableBean value : values) {

            if ("order".equals(value.getFlag())){// 订单表

                TableBean tmptableBean = new TableBean();

                try {
                    BeanUtils.copyProperties(tmptableBean,value);
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                } catch (InvocationTargetException e) {
                    e.printStackTrace();
                }

                orderBeans.add(tmptableBean);
            }else {// 商品表

                try {
                    BeanUtils.copyProperties(pdBean,value);
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                } catch (InvocationTargetException e) {
                    e.printStackTrace();
                }
            }
        }

        // 循环遍历orderBeans,赋值 pdname
        for (TableBean orderBean : orderBeans) {

            orderBean.setPname(pdBean.getPname());

            context.write(orderBean,NullWritable.get());
        }
    }
}
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
55
56
57

(4)编写 TableDriver 类

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 TableDriver {

    public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
        Job job = Job.getInstance(new Configuration());

        job.setJarByClass(TableDriver.class);
        job.setMapperClass(TableMapper.class);
        job.setReducerClass(TableReducer.class);

        job.setMapOutputKeyClass(Text.class);
        job.setMapOutputValueClass(TableBean.class);

        job.setOutputKeyClass(TableBean.class);
        job.setOutputValueClass(NullWritable.class);

        FileInputFormat.setInputPaths(job, new Path("D:\\input\\inputtable"));
        FileOutputFormat.setOutputPath(job, new Path("D:\\hadoop\\output2"));

        boolean b = job.waitForCompletion(true);
        System.exit(b ? 0 : 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
28
29
30
31
32

4)测试运行程序查看结果

1004 小米 4
1001 小米 1
1005 华为 5
1002 华为 2
1006 格力 6
1003 格力 3
1
2
3
4
5
6

5)总结

缺点:这种方式中,合并的操作是在 Reduce 阶段完成,Reduce 端的处理压力太大,Map 节点的运算负载则很低,资源利用率不高,且在 Reduce 阶段极易产生数据倾斜。

解决方案:Map 端实现数据合并。

# 3.6.3 Map Join

1)使用场景

Map Join 适用于一张表十分小、一张表很大的场景。

2)优点

思考:在 Reduce 端处理过多的表,非常容易产生数据倾斜。怎么办? 在 Map 端缓存多张表,提前处理业务逻辑,这样增加 Map 端业务,减少 Reduce 端数据的压力,尽可能的减少数据倾斜。

3)具体办法:采用 DistributedCache

(1)在 Mapper 的 setup 阶段,将文件读取到缓存集合中。

(2)在 Driver 驱动类中加载缓存。

//缓存普通文件到 Task 运行节点。
job.addCacheFile(new URI("file:///e:/cache/pd.txt"));
//如果是集群运行,需要设置 HDFS 路径
job.addCacheFile(new URI("hdfs://hadoop102:8020/cache/pd.txt"));
1
2
3
4

# 3.6.4 Map Join 案例实操

image-20230615234248374

3)实现代码

(1)先在 MapJoinDriver 驱动类中添加缓存文件

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;
import java.net.URI;
import java.net.URISyntaxException;

public class MapJoinDriver {
    public static void main(String[] args) throws IOException, URISyntaxException, ClassNotFoundException, InterruptedException {

        // 1 获取job信息
        Configuration conf = new Configuration();
        Job job = Job.getInstance(conf);
        // 2 设置加载jar包路径
        job.setJarByClass(MapJoinDriver.class);
        // 3 关联mapper
        job.setMapperClass(MapJoinMapper.class);
        // 4 设置Map输出KV类型
        job.setMapOutputKeyClass(Text.class);
        job.setMapOutputValueClass(NullWritable.class);
        // 5 设置最终输出KV类型
        job.setOutputKeyClass(Text.class);
        job.setOutputValueClass(NullWritable.class);

        // 加载缓存数据
        job.addCacheFile(new URI("file:///D:/input/tablecache/pd.txt"));
        // Map端Join的逻辑不需要Reduce阶段,设置reduceTask数量为0
        job.setNumReduceTasks(0);

        // 6 设置输入输出路径
        FileInputFormat.setInputPaths(job, new Path("D:\\input\\inputtable2"));
        FileOutputFormat.setOutputPath(job, new Path("D:\\hadoop\\output8888"));
        // 7 提交
        boolean b = job.waitForCompletion(true);
        System.exit(b ? 0 : 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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

(2)在 MapJoinMapper 类中的 setup 方法中读取缓存文件

import org.apache.commons.lang.StringUtils;
import org.apache.hadoop.fs.FSDataInputStream;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.IOUtils;
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.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URI;
import java.util.HashMap;

public class MapJoinMapper extends Mapper<LongWritable, Text, Text, NullWritable> {
    private HashMap<String, String> pdMap = new HashMap<>();
    private Text outK = new Text();

    @Override
    protected void setup(Context context) throws IOException, InterruptedException {
        // 获取缓存的文件,并把文件内容封装到集合 pd.txt
        URI[] cacheFiles = context.getCacheFiles();

        FileSystem fs = FileSystem.get(context.getConfiguration());
        FSDataInputStream fis = fs.open(new Path(cacheFiles[0]));

        // 从流中读取数据
        BufferedReader reader = new BufferedReader(new InputStreamReader(fis, "UTF-8"));

        String line;
        while (StringUtils.isNotEmpty(line = reader.readLine())) {
            // 切割
            String[] fields = line.split("\t");

            // 赋值
            pdMap.put(fields[0], fields[1]);
        }

        // 关流
        IOUtils.closeStream(reader);
    }

    @Override
    protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {

        // 处理 order.txt
        String line = value.toString();

        String[] fields = line.split("\t");

        // 获取pid
        String pname = pdMap.get(fields[1]);

        // 获取订单id 和订单数量
        // 封装
        outK.set(fields[0] + "\t" + pname + "\t" + fields[2]);

        context.write(outK, NullWritable.get());
    }
}
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
55
56
57
58
59
60
61
62

# 3.7 数据清洗(ETL)

“ETL,是英文 Extract-Transform-Load 的缩写,用来描述将数据从来源端经过抽取 (Extract)、转换(Transform)、加载(Load)至目的端的过程。ETL 一词较常用在数据仓库,但其对象并不限于数据仓库在运行核心业务 MapReduce 程序之前,往往要先对数据进行清洗,清理掉不符合用户 要求的数据。清理的过程往往只需要运行 Mapper 程序,不需要运行 Reduce 程序。

1)需求

去除日志中字段个数小于等于 11 的日志。

2)需求分析

需要在 Map 阶段对输入的数据根据规则进行过滤清洗。

3)实现代码

(1)编写 WebLogMapper 类

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 WebLogMapper extends Mapper<LongWritable, Text, Text, NullWritable> {

    @Override
    protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {

        // 1 获取一行
        String line = value.toString();

        // 2 ETL
        boolean result = parseLog(line, context);

        if (!result){
            return;
        }

        // 3 写出
        context.write(value, NullWritable.get());
    }

    private boolean parseLog(String line, Context context) {
        // 切割
        // 1.206.126.5 - - [19/Sep/2013:05:41:41 +0000] "-" 400 0 "-" "-"
        String[] fields = line.split(" ");

        // 2 判断一下日志的长度是否大于11
        if (fields.length > 11){
            return true;
        }else {
            return false;
        }
    }
}
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

(2)编写 WebLogDriver 类

import com.atguigu.mapreduce.outputformat.LogDriver;
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;

public class WebLogDriver {

    public static void main(String[] args) throws Exception {

        // 输入输出路径需要根据自己电脑上实际的输入输出路径设置
        args = new String[]{"D:/input/inputlog", "D:/hadoop/output11111"};

        // 1 获取job信息
        Configuration conf = new Configuration();
        Job job = Job.getInstance(conf);

        // 2 加载jar包
        job.setJarByClass(LogDriver.class);

        // 3 关联map
        job.setMapperClass(WebLogMapper.class);

        // 4 设置最终输出类型
        job.setOutputKeyClass(Text.class);
        job.setOutputValueClass(NullWritable.class);

        // 设置reducetask个数为0
        job.setNumReduceTasks(0);

        // 5 设置输入和输出路径
        FileInputFormat.setInputPaths(job, new Path(args[0]));
        FileOutputFormat.setOutputPath(job, new Path(args[1]));

        // 6 提交
        boolean b = job.waitForCompletion(true);
        System.exit(b ? 0 : 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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

# 3.8 MapReduce 开发总结

1)输入数据接口:InputFormat

  • (1)默认使用的实现类是:TextInputFormat
  • (2)TextInputFormat 的功能逻辑是:一次读一行文本,然后将该行的起始偏移量作为 key,行内容作为 value 返回。
  • (3)CombineTextInputFormat 可以把多个小文件合并成一个切片处理,提高处理效率。

2)逻辑处理接口:Mapper

用户根据业务需求实现其中三个方法:map() setup() cleanup ()

3)Partitioner 分区

  • (1)有默认实现 HashPartitioner,逻辑是根据 key 的哈希值和 numReduces 来返回一个分区号;key.hashCode()&Integer.MAXVALUE % numReduces
  • (2)如果业务上有特别的需求,可以自定义分区。

4)Comparable 排序

  • (1)当我们用自定义的对象作为 key 来输出时,就必须要实现 WritableComparable 接 口,重写其中的 compareTo()方法。
  • (2)部分排序:对最终输出的每一个文件进行内部排序。
  • (3)全排序:对所有数据进行排序,通常只有一个 Reduce。
  • (4)二次排序:排序的条件有两个。

5)Combiner

合并 Combiner 合并可以提高程序执行效率,减少 IO 传输。但是使用时必须不能影响原有的业务处理结果。

6)逻辑处理接口:

Reducer 用户根据业务需求实现其中三个方法:reduce() setup() cleanup ()

7)输出数据接口:OutputFormat

  • (1)默认实现类是 TextOutputFormat,功能逻辑是:将每一个 KV 对,向目标文本文件输出一行。
  • (2)用户还可以自定义 OutputFormat。
编辑 (opens new window)
#MapReduce
上次更新: 2024/04/19, 08:52:45
第四章Hadoop之HDFS详解以及工作机制介绍
第六章Hadoop 数据压缩

← 第四章Hadoop之HDFS详解以及工作机制介绍 第六章Hadoop 数据压缩→

最近更新
01
idea 热部署插件 JRebel 安装及破解,不生效问题解决
04-10
02
spark中代码的执行位置(Driver or Executer)
12-12
03
大数据技术之 SparkStreaming
12-12
更多文章>
Theme by Vdoing | Copyright © 2019-2024 Young | MIT License
浙ICP备20002744号
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式