1 MapReduce概述
1.1 MapReduce定义
MapReduce是一个分布式运算程序的编程框架,是用户开发“基于Hadoop的数据分析应用”的核心框架。
MapReduce核心功能是将用户编写的业务逻辑代码和自带默认组件整合成一个完整的分布式运算程序,并发运行在一个Hadoop集群上。
1.2 MapReduce优缺点
1.2.1 优点
易于编程。用户只关心业务逻辑,实现框架的接口
良好的扩展性。当你的计算资源不能得到满足的时候,你可以通过简单的增加机器来扩展它的计算能力。
高容错性。其中一台机器挂了,它可以把上面的计算任务转移到另外一个节点上运行,不至于这个任务运行失败
适合海量数据计算(TB/PB),几千台服务器共同计算
1.2.2 缺点
不擅长实时计算。MapReduce无法像MySQL一样,在毫秒或者秒级内返回结果。
不擅长流式计算。流式计算的输入数据是动态的,而MapReduce的输入数据集是静态的,不能动态变化。这是因为MapReduce自身的设计特点决定了数据源必须是静态的。(使用Spark、Flink)。
不擅长DAG(有向无环图)计算。多个应用程序存在依赖关系,后一个应用程序的输入为前一个的输出。在这种情况下,MapReduce并不是不能做,而是使用后,每个MapReduce作业的输出结果都会写入到磁盘,会造成大量的磁盘IO,导致性能非常的低下。(使用Spark)
1.3 MapReduce核心思想
分布式的运算程序往往需要分成至少2 个阶段。
第一个阶段的MapTask并发实例,完全并行运行,互不相干。
第二个阶段的ReduceTask并发实例互不相干,但是他们的数据依赖于上一个阶段的所有MapTask并发实例的输出。
MapReduce编程模型只能包含一个Map阶段和一个Reduce阶段,如果用户的业务逻辑非常复杂,那就只能多个MapReduce程序,串行运行。
1.4 MapReduce进程
个完整的MapReduce程序在分布式运行时有三类实例进程:
MrAppMaster:负责整个程序的过程调度及状态协调。
MapTask:负责Map阶段的整个数据处理流程。
ReduceTask:负责Reduce阶段的整个数据处理流程。
1.5 用数据序列化类型
采用反编译工具反编译源码,发现WordCount案例有Map类、Reduce类和驱动类。且数据的类型是Hadoop自身封装的序列化类型。
1.6 MapReduce编程规范
用户编写的程序分成三个部分:Mapper、Reducer和Driver。
1.6.1 Mapper阶段
用户自定义的Mapper要继承自己的父类
Mapper的输入数据是KV对的形式(KV的类型可自定义)
Mapper中的业务逻辑写在map()方法中
Mapper的输出数据是KV对的形式(KV的类型可自定义)
map()方法(MapTask进程)对每一个<K,V>调用一次
1.6.2 Reducer阶段
用户自定义的Reducer要继承自己的父类
Reducer阶段
Reducer的输入数据类型对应Mapper的输出数据类型,也是KV
Reducer的业务逻辑写在reduce()方法中
ReduceTask进程对每一组相同k的<k,v>组调用一次reduce()方法
1.6.3 Driver阶段
相当于YARN集群的客户端,用于提交我们整个程序到YARN集群,提交的是 封装了MapReduce程序相关运行参数的job对象
1.7 WordCount案例实操
1.7.1 环境准备
创建maven工程, 在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>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.24</version>
</dependency>
</dependencies>
创建三个类
1.7.2 编写Mapper
package com.adrainty.mapreduce.wordcount;
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
* @author AdRainty
* @version V1.0.0
* @date 2023/3/11 10:52
*/
public class WordCountMapper extends Mapper<LongWritable, Text, Text, IntWritable> {
private final Text text = new Text();
private final IntWritable writable = new IntWritable(1);
@Override
protected void map(LongWritable key, Text value, Mapper<LongWritable, Text, Text, IntWritable>.Context context) throws IOException, InterruptedException {
// 获取一行
String line = value.toString();
// 分割
String[] words = line.split(" ");
// 循环写出
for (String word : words) {
text.set(word);
// 写出
context.write(text, writable);
}
}
}
1.7.3 编写Reduce
package com.adrainty.mapreduce.wordcount;
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
* @author AdRainty
* @version V1.0.0
* @date 2023/3/11 10:52
*/
public class WordCountReducer extends Reducer<Text, IntWritable, Text, IntWritable> {
private final IntWritable intWritable = new IntWritable();
@Override
protected void reduce(Text key, Iterable<IntWritable> values, Reducer<Text, IntWritable, Text, IntWritable>.Context context) throws IOException, InterruptedException {
int sum = 0;
// 累加
for (IntWritable value : values) {
sum += value.get();
}
intWritable.set(sum);
// 写出
context.write(key, intWritable);
}
}
1.7.4 编写Driver
package com.adrainty.mapreduce.wordcount;
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 AdRainty
* @version V1.0.0
* @date 2023/3/11 10:52
*/
public class WordCountDriver {
public static void main(String[] args) throws IOException, InterruptedException, ClassNotFoundException {
// 获取job
Configuration configuration = new Configuration();
Job job = Job.getInstance(configuration);
// 设置jar包路径
job.setJarByClass(WordCountDriver.class);
// 关联mapper和reducer
job.setMapperClass(WordCountMapper.class);
job.setReducerClass(WordCountReducer.class);
// 设置map输出的kv类型
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(IntWritable.class);
// 设置最终的kv路径
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(IntWritable.class);
// 设置输入路径和输出路径
FileInputFormat.setInputPaths(job, new Path("E:\\hadoop_test\\input\\inputword"));
FileOutputFormat.setOutputPath(job, new Path("E:\\hadoop_test\\output\\outputword"));
// 提交job
boolean result = job.waitForCompletion(true);
System.exit(result? 0: 1);
}
}
1.7.5 运行调试
1.7.6 打包并集群运行
<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>
2 Hadoop序列化
2.1 序列化概述
序列化就是把内存中的对象,转换成字节序列(或其他数据传输协议)以便于存储到磁盘(持久化)和网络传输。
反序列化就是将收到字节序列(或其他数据传输协议)或者是磁盘的持久化数据,转换成内存中的对象。
Hadoop序列化特点:
紧凑 :高效使用存储空间。
快速:读写数据的额外开销小。
互操作:支持多语言的交互
2.2 自定义bean对象实现序列化接口
在企业开发中往往常用的基本序列化类型不能满足所有需求,比如在Hadoop框架内部传递一个bean对象,那么该对象就需要实现序列化接口。
具体实现bean对象序列化步骤如下7步。
必须实现Writable接口
反序列化时,需要反射调用空参构造函数,所以必须有空参构造
重写序列化方法
重写反序列化方法
注意反序列化的顺序和序列化的顺序完全一致
要想把结果显示在文件中,需要重写toString(),可用"\t"分开,方便后续用。
如果需要将自定义的bean放在key中传输,则还需要实现Comparable接口,因为MapReduce框中的Shuffle过程要求对key必须能排序。详见后面排序案例。
2.3 序列化案例实操
2.3.1 编写Bean
package com.adrainty.mapreduce.writable.moudle;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.apache.hadoop.io.Writable;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
/**
* @author AdRainty
* @version V1.0.0
* @date 2023/3/11 22:44
*/
@NoArgsConstructor
@Data
public class FlowBean implements Writable {
/**
* 上行流量
*/
private long upFlow;
/**
* 下行流量
*/
private long downFlow;
/**
* 总流量
*/
private long sumFlow;
public void setSumFlow() {
this.sumFlow = this.upFlow + this.downFlow;
}
@Override
public void write(DataOutput out) throws IOException {
out.writeLong(upFlow);
out.writeLong(downFlow);
out.writeLong(sumFlow);
}
@Override
public void readFields(DataInput in) throws IOException {
this.upFlow = in.readLong();
this.downFlow = in.readLong();
this.sumFlow = in.readLong();
}
@Override
public String toString() {
return upFlow + "\t" + downFlow + "\t" + sumFlow;
}
}
2.3.2 编写Mapper
package com.adrainty.mapreduce.writable;
import com.adrainty.mapreduce.writable.moudle.FlowBean;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;
import java.io.IOException;
/**
* @author AdRainty
* @version V1.0.0
* @date 2023/3/11 22:51
*/
public class FlowMapper extends Mapper<LongWritable, Text, Text, FlowBean> {
private final Text outKey = new Text();
private final FlowBean outValue = new FlowBean();
@Override
protected void map(LongWritable key, Text value, Mapper<LongWritable, Text, Text, FlowBean>.Context context) throws IOException, InterruptedException {
String line = value.toString();
String[] split = line.split("\t");
String phone = split[1];
String up = split[split.length - 3];
String down = split[split.length - 2];
outKey.set(phone);
outValue.setUpFlow(Long.parseLong(up));
outValue.setDownFlow(Long.parseLong(down));
outValue.setSumFlow();
context.write(outKey, outValue);
}
}
2.3.3 编写Reducer
package com.adrainty.mapreduce.writable;
import com.adrainty.mapreduce.writable.moudle.FlowBean;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;
import java.io.IOException;
/**
* @author AdRainty
* @version V1.0.0
* @date 2023/3/11 22:59
*/
public class FlowReducer extends Reducer<Text, FlowBean, Text, FlowBean> {
private final FlowBean outValue = new FlowBean();
@Override
protected void reduce(Text key, Iterable<FlowBean> values, Reducer<Text, FlowBean, Text, FlowBean>.Context context) throws IOException, InterruptedException {
long totalUp = 0;
long totalDown = 0;
for (FlowBean value : values) {
totalUp += value.getUpFlow();
totalDown += value.getDownFlow();
}
outValue.setUpFlow(totalUp);
outValue.setDownFlow(totalDown);
outValue.setSumFlow();
context.write(key, outValue);
}
}
2.3.4 编写Driver
package com.adrainty.mapreduce.writable;
import com.adrainty.mapreduce.wordcount.WordCountDriver;
import com.adrainty.mapreduce.wordcount.WordCountMapper;
import com.adrainty.mapreduce.wordcount.WordCountReducer;
import com.adrainty.mapreduce.writable.moudle.FlowBean;
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 AdRainty
* @version V1.0.0
* @date 2023/3/11 23:04
*/
public class FlowDriver {
public static void main(String[] args) throws IOException, InterruptedException, ClassNotFoundException {
// 获取job
Configuration configuration = new Configuration();
Job job = Job.getInstance(configuration);
// 设置jar包路径
job.setJarByClass(FlowDriver.class);
// 关联mapper和reducer
job.setMapperClass(FlowMapper.class);
job.setReducerClass(FlowReducer.class);
// 设置map输出的kv类型
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(FlowBean.class);
// 设置最终的kv路径
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(FlowBean.class);
// 设置输入路径和输出路径
FileInputFormat.setInputPaths(job, new Path("E:\\hadoop_test\\input\\inputflow"));
FileOutputFormat.setOutputPath(job, new Path("E:\\hadoop_test\\output\\outputflow"));
// 提交job
boolean result = job.waitForCompletion(true);
System.exit(result? 0: 1);
}
}
2.3.5 运行调试
3 MapReduce框架原理
3.1 InputFormat数据输入
3.1.1 切片与MapTask并行度决定机制
MapTask的并行度决定Map阶段的任务处理并发度,进而影响到整个Job的处理速度。
数据块:Block是HDFS物理上把数据分成一块一块。数据块是HDFS存储数据单位。
数据切片:数据切片只是在逻辑上对输入进行分片,并不会在磁盘上将其切分成片进行存储。数据切片是MapReduce程序计算输入数据的单位,一个切片会对应启动一个MapTask。
3.1.2 Job提交流程源码解析
3.1.3 FileInputFormat切片机制
maxsize(切片最大值):参数如果调得比blockSize小,则会让切片变小,而且就等于配置的这个参数的值。
minsize(切片最小值):参数调的比blockSize大,则可以让切片变得比blockSize还大。
3.1.4 TextInputFormat
在运行MapReduce程序时,输入的文件格式包括:基于行的日志文件、二进制格式文件、数据库表等。那么,针对不同的数据类型,MapReduce是如何读取这些数据的呢
FileInputFormat常见的接口实现类包括:TextInputFormat、KeyValueTextInputFormat、NLineInputFormat、CombineTextInputFormat和自定义InputFormat等。
TextInputFormat是默认的FileInputFormat实现类。按行读取每条记录。键是存储该行在整个文件中的起始字节偏移量, LongWritable类型。值是这行的内容,不包括任何行终止符(换行符和回车符),Text类型。
3.1.5 CombineTextInputFormat切片机制
框架默认的TextInputFormat切片机制是对任务按文件规划切片,不管文件多小,都会是一个单独的切片,都会交给一个MapTask,这样如果有大量小文件,就会产生大量的MapTask,处理效率极其低下。
CombineTextInputFormat用于小文件过多的场景,它可以将多个小文件从逻辑上规划到一个切片中,这样,多个小文件就可以交给一个MapTask处理。
生成切片过程包括:虚拟存储过程和切片过程二部分。
测试:
// 如果不设置InputFormat,它默认用的是TextInputFormat.class
job.setInputFormatClass(CombineTextInputFormat.class);
//虚拟存储切片最大值设置4m
CombineTextInputFormat.setMaxInputSplitSize(job, 4194304);
3.2 MapReduce工作流程
具体Shuffle过程详解,如下:
MapTask收集我们的map()方法输出的kv对,放到内存缓冲区中
从内存缓冲区不断溢出本地磁盘文件,可能会溢出多个文件
多个溢出文件会被合并成大的溢出文件
在溢出过程及合并的过程中,都要调用Partitioner进行分区和针对key进行排序
ReduceTask根据自己的分区号,去各个MapTask机器上取相应的结果分区数据
ReduceTask会抓取到同一个分区的来自不同MapTask的结果文件,ReduceTask会将这些文件再进行合并(归并排序)
合并成大文件后,Shuffle的过程也就结束了,后面进入ReduceTask的逻辑运算过程(从文件中取出一个一个的键值对Group,调用用户自定义的reduce()方法)
Shuffle中的缓冲区大小会影响到MapReduce程序的执行效率,原则上说,缓冲区越大,磁盘io的次数越少,执行速度就越快。
缓冲区的大小可以通过参数调整,参数:mapreduce.task.io.sort.mb默认100M。
3.3 Shuffle机制
3.3.1 Shuffle机制
Map方法之后,Reduce方法之前的数据处理过程称之为Shuffle。
3.3.2 Partition分区
默认分区:
public class HashPartitioner<K, V> extends Partitioner<K, V> {
/** Use {@link Object#hashCode()} to partition. */
public int getPartition(K key, V value,
int numReduceTasks) {
return (key.hashCode() & Integer.MAX_VALUE) % numReduceTasks;
}
}
默认分区是根据key的hashCode对ReduceTasks个数取模得到的。用户没法控制哪个key存储到哪个分区。
自定义Partitioner步骤:
自定义类继承Partitioner,重写getPartition()方法
在Job驱动中,设置自定义Partitioner
自定义Partition后,要根据自定义Partitioner的逻辑设置相应数量的ReduceTask
例如将统计结果按照手机归属地不同省份输出到不同文件中
自定义类继承Partitioner,重写getPartition()方法
package com.adrainty.mapreduce.partitioner;
import com.adrainty.mapreduce.partitioner.moudle.FlowBean;
import org.apache.commons.lang3.StringUtils;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Partitioner;
/**
* @author AdRainty
* @version V1.0.0
* @date 2023/3/14 23:50
*/
public class ProvincePartitioner extends Partitioner<Text, FlowBean> {
@Override
public int getPartition(Text text, FlowBean flowBean, int numPartitions) {
String phone = text.toString();
String prePhone = phone.substring(0, 3);
int partition;
switch (prePhone) {
case "136":
partition = 0;
break;
case "137":
partition = 1;
break;
case "138":
partition = 2;
break;
case "139":
partition = 3;
break;
default:
partition =4;
}
return numPartitions;
}
}
在Job驱动中,设置自定义Partitioner,同时指定相应数量的ReduceTask
job.setPartitionerClass(ProvincePartitioner.class);
job.setNumReduceTasks(5);
如果ReduceTask的数量> getPartition的结果数,则会多产生几个空的输出文件part-r-000xx;
如果1<ReduceTask的数量<getPartition的结果数,则有一部分分区数据无处安放,会Exception;
如果ReduceTask的数量=1,则不管MapTask端输出多少个分区文件,最终结果都交给这一个ReduceTask,最终也就只会产生一个结果文件part-r-00000;
分区号必须从零开始,逐一累加。
3.3.3 WritableComparable排序
MapTask 和ReduceTask 均会对数据按照key进行排序。该操作属于Hadoop的默认行为。任何应用程序中的数据均会被排序,而不管逻辑上是否需要。默认排序是按照字典顺序排序,且实现该排序的方法是快速排序。
对于MapTask,它会将处理的结果暂时放到环形缓冲区中,当环形缓冲区使用率达到一定阈值后,再对缓冲区中的数据进行一次快速排序,并将这些有序数据溢写到磁盘上,而当数据处理完毕后,它会对磁盘上所有文件进行归并排序。
对于ReduceTask,它从每个MapTask上远程拷贝相应的数据文件,如果文件大小超过一定阈值,则溢写磁盘上,否则存储在内存中。如果磁盘上文件数目达到一定阈值,则进行一次归并排序以生成一个更大文件;如果内存中文件大小或者数目超过一定阈值,则进行一次合并后将数据溢写到磁盘上。当所有数据拷贝完毕后,ReduceTask统一对内存和磁盘上的所有数据进行一次归并排序。
排序分类
部分排序:MapReduce根据输入记录的键对数据集排序。保证输出的每个文件内部有序。
全排序:最终输出结果只有一个文件,且文件内部有序。实现方式是只设置一个ReduceTask。但该方法在处理大型文件时效率极低,因为一台机器处理所有文件,完全丧失了MapReduce所提供的并行架构。
辅助排序:(GroupingComparator分组):在Reduce端对key进行分组。应用于:在接收的key为bean对象时,想让一个或几个字段相同(全部字段比较不相同)的key进入到同一个reduce方法时,可以采用分组排序。
二次排序:在自定义排序过程中,如果compareTo中的判断条件为两个即为二次排序。
自定义排序
bean 对象做为key 传输,需要实现WritableComparable 接口重写compareTo 方法,就可以实现排序。
package com.adrainty.mapreduce.partitioner.moudle;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.apache.hadoop.io.Writable;
import org.apache.hadoop.io.WritableComparable;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
/**
* @author AdRainty
* @version V1.0.0
* @date 2023/3/11 22:44
*/
@NoArgsConstructor
@Data
public class FlowBean implements WritableComparable<FlowBean> {
/**
* 上行流量
*/
private long upFlow;
/**
* 下行流量
*/
private long downFlow;
/**
* 总流量
*/
private long sumFlow;
public void setSumFlow() {
this.sumFlow = this.upFlow + this.downFlow;
}
@Override
public void write(DataOutput out) throws IOException {
out.writeLong(upFlow);
out.writeLong(downFlow);
out.writeLong(sumFlow);
}
@Override
public void readFields(DataInput in) throws IOException {
this.upFlow = in.readLong();
this.downFlow = in.readLong();
this.sumFlow = in.readLong();
}
@Override
public String toString() {
return upFlow + "\t" + downFlow + "\t" + sumFlow;
}
@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.4 Combiner合并
Combiner是MR程序中Mapper和Reducer之外的一种组件。
Combiner组件的父类就是Reducer。Combiner和Reducer的区别在于运行的位置,Combiner是在每一个MapTask所在的节点运行;Reducer是接收全局所有Mapper的输出结果;
Combiner的意义就是对每一个MapTask的输出进行局部汇总,以减小网络传输量。
Combiner能够应用的前提是不能影响最终的业务逻辑,而且,Combiner的输出kv应该跟Reducer的输入kv类型要对应起来。
自定义实现:
自定义一个Combiner继承Reducer,重写Reduce方法
package com.adrainty.mapreduce.combiner;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;
import java.io.IOException;
/**
* @author AdRainty
* @version V1.0.0
* @date 2023/3/18 22:42
*/
public class WordCountCombiner extends Reducer<Text, IntWritable, Text, IntWritable> {
private IntWritable outValue = new IntWritable();
@Override
protected void reduce(Text key, Iterable<IntWritable> values, Reducer<Text, IntWritable, Text, IntWritable>.Context context) throws IOException, InterruptedException {
int sum = 0;
for (IntWritable value : values) {
sum += value.get();
}
outValue.set(sum);
context.write(key, outValue);
}
}
在Job驱动类中设置
job.setCombinerClass(WordCountCombiner.class);
实际上我们可以发现WordCountCombiner
和WordCountReducer
逻辑是一样的,因此我们其实也可以使用
job.setCombinerClass(WordCountReducer.class);
3.4 OutputFormat数据输出
OutputFormat是MapReduce输出的基类,所有实现MapReduce输出都实现了OutputFormat接口。
自定义OutputFormat步骤
自定义一个类继承FileOutputFormat。
改写RecordWriter,具体改写输出数据的方法write()
例如:过滤输入的log 日志,包含atguigu 的网站输出到e:/atguigu.log,不包含atguigu的网站
Mapper:
package com.adrainty.mapreduce.outputformat;
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;
/**
* @author AdRainty
* @version V1.0.0
* @date 2023/3/18 23:17
*/
public class LogMapper extends Mapper<LongWritable, Text, Text, NullWritable> {
@Override
protected void map(LongWritable key, Text value, Mapper<LongWritable, Text, Text, NullWritable>.Context context) throws IOException, InterruptedException {
context.write(value, NullWritable.get());
}
}
Reducer:
package com.adrainty.mapreduce.outputformat;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;
import java.io.IOException;
/**
* @author AdRainty
* @version V1.0.0
* @date 2023/3/18 23:19
*/
public class LogReducer extends Reducer<Text, NullWritable, Text, NullWritable> {
@Override
protected void reduce(Text key, Iterable<NullWritable> values, Reducer<Text, NullWritable, Text, NullWritable>.Context context) throws IOException, InterruptedException {
for (NullWritable value : values) {
context.write(key, NullWritable.get());
}
}
}
OutputFormat:
package com.adrainty.mapreduce.outputformat;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.RecordWriter;
import org.apache.hadoop.mapreduce.TaskAttemptContext;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
import java.io.IOException;
/**
* @author AdRainty
* @version V1.0.0
* @date 2023/3/18 23:22
*/
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;
}
}
实际操作:
package com.adrainty.mapreduce.outputformat;
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;
/**
* @author AdRainty
* @version V1.0.0
* @date 2023/3/18 23:23
*/
public class LogRecordWriter extends RecordWriter<Text, NullWritable> {
private FSDataOutputStream atguiguOut;
private FSDataOutputStream otherOut;
public LogRecordWriter(TaskAttemptContext job) {
// 创建两条流
try {
FileSystem fileSystem = FileSystem.get(job.getConfiguration());
atguiguOut = fileSystem.create(new Path("E:\\hadoop_test\\output\\output_atguigu.log"));
otherOut = fileSystem.create(new Path("E:\\hadoop_test\\output\\output_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);
} else {
otherOut.writeBytes(log);
}
}
@Override
public void close(TaskAttemptContext context) throws IOException, InterruptedException {
IOUtils.closeStream(atguiguOut);
IOUtils.closeStream(otherOut);
}
}
3.5 MapReduce内核源码解析
3.5.1 MapTask工作机制
Read 阶段:MapTask 通过InputFormat 获得的RecordReader,从输入InputSplit 中解析出一个个key/value
Map 阶段:该节点主要是将解析出的key/value 交给用户编写map()函数处理,并产生一系列新的key/value。
Collect 收集阶段:在用户编写map()函数中,当数据处理完成后,一般会调用OutputCollector.collect()输出结果。在该函数内部,它会将生成的key/value 分区(调用Partitioner),并写入一个环形内存缓冲区中。
Spill 阶段:即“溢写”,当环形缓冲区满后,MapReduce 会将数据写到本地磁盘上,生成一个临时文件。需要注意的是,将数据写入本地磁盘之前,先要对数据进行一次本地排序,并在必要时对数据进行合并 、压缩等操作 。
溢写流程:
利用快速排序算法对缓存区内的数据进行排序,排序方式是,先按照分区编号Partition进行排序,然后 按照 key进行排序。这样 经过排序后,数据以分区为单位聚集在一起,且同一分区内所有数据按照 key有序。
按照分区编号由小到大依次将每个分区中的数据写入任务工作目录下的临时文件 output/spillN.out N表示当前溢写次数 )中 。如果 用户 设置了 Combiner,则写入文之 前,对每个分区中 的 数据进行一次聚集操作。
将分区数据的元 信息 写到内存索引数据结构 SpillRecord中 ,其中每个分区的元信息包括在临时文件中的偏移量、压缩前数据大小和压缩后数据大小。如果 当前 内存索引大小超过 1MB,则将内存索引写到文件 output/spillN.out.index中 。
Merge阶段 :当所有数据处理完成后 MapTask对 所有临时文件进行一次合并,以确保最终只会生成一个数据文件。
当 所有数据处理完后, MapTask会 将所有临时文件合并成一个大文件 并保存到文件output/file.out中 ,同时生成相应的索引文件 output/file.out.index。在 进行文件合并过程中, MapTask以 分区为单位进行合并。对于 某个 分区, 它 将采用多轮递归合并的方式 。 每轮合并 mapreduce.task.io.sort.factor(默认 10 个 文件,并将产生的文件重新加入待合并 列表 中,对文件排序后,重复以上过程,直到最终得到一个大文件。让每个 MapTask最终只生成一个数据文件,可避免同时打开大量文件和同时读取大量小文件产生的随机读取 带来 的开销。
3.5.2 ReduceTask工作机制
Copy 阶段:ReduceTask 从各个MapTask 上远程拷贝一片数据,并针对某一片数据,如果其大小超过一定阈值,则写到磁盘上,否则直接放到内存中。
Sort 阶段:在远程拷贝数据的同时,ReduceTask 启动了两个后台线程对内存和磁盘上的文件进行合并,以防止内存使用过多或磁盘上文件过多。按照MapReduce 语义,用户编写reduce()函数输入数据是按key 进行聚集的一组数据。为了将key 相同的数据聚在一起,Hadoop 采用了基于排序的策略。由于各个MapTask 已经实现对自己的处理结果进行了局部排序,因此,ReduceTask 只需对所有数据进行一次归并排序即可。
Reduce 阶段:reduce()函数将计算结果写到HDFS 上。
3.5.3 ReduceTask并行度决定机制
MapTask 并行度由切片个数决定,切片个数由输入文件和切片规则决定。那ReduceTask 并行度由谁决定?
ReduceTask 的并行度同样影响整个Job 的执行并发度和执行效率,但与MapTask 的并发数由切片数决定不同,ReduceTask 数量的决定是可以直接手动设置:
// 默认值是1,手动设置为4
job.setNumReduceTasks(4);
ReduceTask=0,表示没有Reduce阶段,输出文件个数和Map个数一致。
ReduceTask默认值就是1,所以输出文件个数为一个。
如果数据分布不均匀,就有可能在Reduce阶段产生数据倾斜
ReduceTask数量并不是任意设置,还要考虑业务逻辑需求,有些情况下,需要计算全局汇总结果,就只能有1个ReduceTask。
具体多少个ReduceTask,需要根据集群性能而定。
如果分区数不是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案例实操
订单数据表t_order
商品信息表t_product
将商品信息表中数据根据商品pid合并到订单数据表中。
bean:
package com.adrainty.mapreduce.reducejoin;
import lombok.Data;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.apache.hadoop.io.Writable;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
/**
* @author AdRainty
* @version V1.0.0
* @date 2023/3/19 0:24
*/
@NoArgsConstructor
@Getter
@Setter
public class TableBean implements Writable {
/**
* 订单id
*/
private String id;
/**
* 商品id
*/
private String pid;
/**
* 商品数量
*/
private int amount;
/**
* 商品名称
*/
private String pname;
/**
* 标记是哪张表
*/
private String 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() {
return id + '\t' + pname + '\t' + amount;
}
}
Mapper:
package com.adrainty.mapreduce.reducejoin;
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;
/**
* @author AdRainty
* @version V1.0.0
* @date 2023/3/19 0:31
*/
public class TableMapper extends Mapper<LongWritable, Text, Text, TableBean> {
private String fileName;
private Text outKey = new Text();
private TableBean outVal = new TableBean();
@Override
protected void setup(Mapper<LongWritable, Text, Text, TableBean>.Context context) throws IOException, InterruptedException {
// 初始化
FileSplit split = (FileSplit) context.getInputSplit();
fileName = split.getPath().getName();
}
@Override
protected void map(LongWritable key, Text value, Mapper<LongWritable, Text, Text, TableBean>.Context context) throws IOException, InterruptedException {
String line = value.toString();
String[] split = line.split("\t");
if (fileName.contains("order")) {
outKey.set(split[1]);
outVal.setId(split[0]);
outVal.setPid(split[1]);
outVal.setAmount(Integer.parseInt(split[2]));
outVal.setPname("");
outVal.setFlag("order");
} else {
outKey.set(split[0]);
outVal.setId("");
outVal.setPid(split[0]);
outVal.setAmount(0);
outVal.setPname(split[1]);
outVal.setFlag("pd");
}
context.write(outKey, outVal);
}
}
Reducer:
package com.adrainty.mapreduce.reducejoin;
import org.apache.commons.beanutils.BeanUtils;
import org.apache.commons.lang3.SerializationUtils;
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;
import java.util.List;
/**
* @author AdRainty
* @version V1.0.0
* @date 2023/3/19 0:40
*/
public class TableReducer extends Reducer<Text, TableBean, TableBean, NullWritable> {
@Override
protected void reduce(Text key, Iterable<TableBean> values, Reducer<Text, TableBean, TableBean, NullWritable>.Context context) throws IOException, InterruptedException {
List<TableBean> orderBeans = new ArrayList<>();
TableBean pdBean = new TableBean();
for (TableBean value : values) {
if ("order".equals(value.getFlag())) {
TableBean temp = new TableBean();
try {
BeanUtils.copyProperties(temp ,value);
} catch (IllegalAccessException | InvocationTargetException e) {
e.printStackTrace();
}
orderBeans.add(temp);
} else {
try {
BeanUtils.copyProperties(pdBean, value);
} catch (IllegalAccessException | InvocationTargetException e) {
e.printStackTrace();
}
}
}
for (TableBean orderBean : orderBeans) {
orderBean.setPname(pdBean.getPname());
context.write(orderBean, NullWritable.get());
}
}
}
缺点:这种方式中,合并的操作是在Reduce阶段完成, Reduce端的处理压力太大, Map节点的运算负载则很低,资源利用率不高,且在 Reduce阶段极易产生数据倾斜。
因此,我们在Map端实现数据合并 。
3.6.3 Map Join
Map Join 适用于一张表十分小、一张表很大的场景。
在Map 端缓存多张表,提前处理业务逻辑,这样增加Map 端业务,减少Reduce 端数据的压力,尽可能的减少数据倾斜。
采用DistributedCache
在Mapper的setup阶段,将文件读取到缓存集合中。
在Driver驱动类中加载缓存
3.6.4 Map Join案例实操
Driver:
package com.adrainty.mapreduce.mapjoin;
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;
/**
* @author AdRainty
* @version V1.0.0
* @date 2023/3/19 9:50
*/
public class MapJoinDriver {
public static void main(String[] args) throws IOException, URISyntaxException, InterruptedException, ClassNotFoundException {
// 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"));
FileOutputFormat.setOutputPath(job, new Path("D:\\output"));
// 7 提交
boolean b = job.waitForCompletion(true);
System.exit(b ? 0 : 1);
}
}
Mapper:
package com.adrainty.mapreduce.mapjoin;
import org.apache.commons.lang3.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.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
/**
* @author AdRainty
* @version V1.0.0
* @date 2023/3/19 9:55
*/
public class MapJoinMapper extends Mapper<LongWritable, Text, Text, NullWritable> {
private Map<String, String> pdMap = new HashMap<>();
private Text outKey = new Text();
@Override
protected void setup(Mapper<LongWritable, Text, Text, NullWritable>.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, StandardCharsets.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, Mapper<LongWritable, Text, Text, NullWritable>.Context context) throws IOException, InterruptedException {
String line = value.toString();
String[] fields = line.split("\t");
outKey.set(fields[0] + "\t" + pdMap.get(fields[1]) + " \t" + fields[2]);
context.write(outKey, NullWritable.get());
}
}
3.7 数据清洗(ETL)
“ETL,是英文Extract-Transform-Load的缩写,用来描述将数据从来源端经过抽取(Extract)、转换(Transform)、加载(Load)至目的端的过程。ETL一词较常用在数据仓库,但其对象并不限于数据仓库
在运行核心业务MapReduce程序之前,往往要先对数据进行清洗,清理掉不符合用户要求的数据。清理的过程往往只需要运行Mapper程序,不需要运行Reduce程序。
跟之前类似,这里就不上代码了
4 Hadoop数据压缩
4.1 概述
压缩的好处和坏处
压缩的优点:以减少磁盘IO、减少磁盘存储空间。
压缩的缺点:增加CPU开销。
压缩原则
运算密集型的Job,少用压缩
IO密集型的Job,多用压缩
4.2 MR支持的压缩编码
4.3 压缩方式选择
压缩方式选择时重点考虑:压缩/解压缩速度、压缩率(压缩后存储大小)、压缩后是否可以支持切片。
4.3.1 Gzip压缩
优点:压缩率比较高;
缺点:不支持Split;压缩/解压速度一般;
4.3.2 Bzip2压缩
优点:压缩率高;支持Split;
缺点:压缩/解压速度慢。
4.3.3 Lzo压缩
优点:压缩/解压速度比较快;支持Split;
缺点:压缩率一般;想支持切片需要额外创建索引
4.3.4 Snappy压缩
优点:压缩和解压缩速度快;
缺点:不支持Split;压缩率一般;
4.4 压缩参数配置
为了支持多种压缩/解压缩算法,Hadoop引入了编码/解码器
要在Hadoop中启用压缩,可以配置如下参数
4.5 压缩实操案例
4.5.1 Map输出端采用压缩
即使你的MapReduce的输入输出文件都是未压缩的文件,你仍然可以对Map任务的中间结果输出做压缩,因为它要写在硬盘并且通过网络传输到Reduce节点,对其压缩可以提高很多性能,这些工作只要设置两个属性即可
Configuration conf = new Configuration();
// 开启map端输出压缩
conf.setBoolean("mapreduce.map.output.compress", true);
// 设置map端输出压缩方式
conf.setClass("mapreduce.map.output.compress.codec", BZip2Codec.class, CompressionCodec.class);
Job job = Job.getInstance(conf);
4.5.2 Reduce输出端采用压缩
// 设置reduce端输出压缩开启
FileOutputFormat.setCompressOutput(job, true);
// 设置压缩的方式
FileOutputFormat.setOutputCompressorClass(job, BZip2Codec.class);