MapReduce核心框架原理
3.1 InputFormat 数据输入
选择数据读取的方式(常用的):
1.默认按行读取<k,v>
k:偏移量 v:这一行内容
2.FileInputFormat
3.TextInputFormat
4.CombineTextInputFormat
5.自定义读取方式等
3.1.1 切片与 MapTask 并行度决定机制
1)问题引出
MapTask 的并行度决定 Map 阶段的任务处理并发度,进而影响到整个 Job 的处理速度。
思考:1G 的数据,启动 8 个 MapTask,可以提高集群的并发处理能力。那么 1K 的数据,也启动 8 个 MapTask,会提高集群性能吗?MapTask 并行任务是否越多越好呢?哪些因素影响了 MapTask 并行度?
2)MapTask 并行度决定机制
MapTask的个数决定了并行度。
数据块:Block 是 HDFS 物理上把数据分成一块一块。数据块是 HDFS 存储数据单位。
数据切片:每个文件单独切片,数据切片只是在逻辑上对输入进行分片,并不会在磁盘上将其切分成片进行存储。数据切片是MapReduce 程序计算输入数据的单位,一个切片会对应启动一个MapTask。
数据切片与MapTask 并行度决定机制图解
3.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());
2) FileInputFormat 切片源码解析(input.getSplits(job))
(1)程序先找到你数据存储的目录。
(2)开始遍历处理(规划切片)目录下的每一个文件
(3)遍历第一个文件ss.txt(每一个文件单独切片)
a)获取文件大小fs.sizeOf(ss.txt)
b)计算切片大小
computeSplitSize(Math.max(minSize,Math.min(maxSize,blocksize)))=blocksize=128M
c)默认情况下,切片大小=blocksize。(本地模式:32M,集群模式:128M,可修改)
d)开始切,形成第1个切片:ss.txt—0:128M 第2个切片ss.txt—128:256M 第3个切片ss.txt—256M:300M
(每次切片时,都要判断切完剩下的部分是否大于块的1.1倍,不大于1.1倍就划分一块切片)
e)将切片信息写到一个切片规划文件中
f)整个切片的核心过程在getSplit()方法中完成
g)InputSplit只记录了切片的元数据信息,比如起始位置、长度以及所在的节点列表等。
(4)提交切片规划文件到YARN上,YARN上的MrAppMaster就可以根据切片规划文件计算开启MapTask个数。
3.1.3 FileInputFormat 切片机制
1、切片机制
(1)简单地按照文件的内容长度进行切片
(2)切片大小,默认等于Block大小
(3)切片时不考虑数据集整体,而是逐个针对每一个文件单独切片
2、案例分析
3、FileInputFormat切片大小的参数配置
(1)源码中计算切片大小的公式
Math.max(minSize, Math.min(maxSize, blockSize));
mapreduce.input.fileinputformat.split.minsize=1 //默 认 值 为 1
mapreduce.input.fileinputformat.split.maxsize= Long.MAXValue// 默认值Long.MAXValue 因此,默认情况下,切片大小=blocksize。
(2)切片大小设置
maxsize(切片最大值):参数如果调得比blockSize小,则会让切片变小,而且就等于配置的这个参数的值。
minsize(切片最小值):参数调的比blockSize大,则可以让切片变得比blockSize还大。
(3)获取切片信息API
// 获取切片的文件名称
String name = inputSplit.getPath().getName();
// 根据文件类型获取切片信息
FileSplit inputSplit = (FileSplit) context.getInputSplit();
3.1.4 TextInputForma
1) FileInputFormat 实现类
思考:在运行 MapReduce 程序时,输入的文件格式包括:基于行的日志文件、二进制格式文件、数据库表等。那么,针对不同的数据类型,MapReduce 是如何读取这些数据的呢?
FileInputFormat 常见的接口实现类包括:
1.TextInputFormat、
2.KeyValueTextInputFormat
3.NLineInputFormat
4.CombineTextInputFormat(把多个文件放在一起,统一进行处理)
5.自定义 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
每条记录表示为以下键/值对:
(0,Rich learning form)
(20,Intelligent learning engine)
(49,Learning more convenient)
(74,From the real demand for more close to the enterprise)
3.1.5 CombineTextInputFormat 切片机制
框架默认的 TextInputFormat 切片机制是对任务按文件规划切片,不管文件多小,都会是一个单独的切片,都会交给一个 MapTask,这样如果有大量小文件,就会产生大量的MapTask,处理效率极其低下。
1)应用场景:
CombineTextInputFormat 用于小文件过多的场景,它可以将多个小文件从逻辑上规划到一个切片中,这样,多个小文件就可以交给一个 MapTask 处理。
2)虚拟存储切片最大值设置
CombineTextInputFormat.setMaxInputSplitSize(job, 4194304);// 4m
注意:虚拟存储切片最大值设置最好根据实际的小文件大小情况来设置具体的值。
3)切片机制
生成切片过程包括:虚拟存储过程和切片过程二部分。
(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)需求
2)实现过程
(1)不做任何处理,运行 1.8 节的 WordCount 案例程序(按行切割,单个文件处理),观察切片个数为 4,MapTask4个,ReduceTask1个(因为设置的默认值为1)。
number of splits:4
(2)在 WordcountDriver 中增加如下代码,运行程序,并观察运行的切片个数为 3。
a)驱动类中添加代码如下并导包:
// 如果不设置 InputFormat,它默认用的是 TextInputFormat.class job.setInputFormatClass(CombineTextInputFormat.class);
// 虚 拟 存 储 切 片 最 大 值 设 置 4m CombineTextInputFormat.setMaxInputSplitSize(job, 4194304);
(b)运行如果为 3 个切片,3个MapTask,1个ReduceTask。
number of splits:3
(3)在 WordcountDriver 中增加如下代码,运行程序,并观察运行的切片个数为 1。
(a)驱动中添加代码如下:
// 如果不设置 InputFormat,它默认用的是 TextInputFormat.class job.setInputFormatClass(CombineTextInputFormat.class);
// 虚 拟 存 储 切 片 最 大 值 设 置 20m CombineTextInputFormat.setMaxInputSplitSize(job, 20971520);
(b)运行结果为 1 个切片,1个MapTask,1个ReduceTask
number of splits:1
3.2 MapReduce 工作流程
2.
上面的流程是整个 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。
3.3.2 Partition 分区
1、问题引出
要求将统计结果按照条件输出到不同文件中(分区)。比如:将统计结果按照手机归属地不同省份输出到不同文件中(分区)
2、默认Partitioner分区
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;
}
}
默认分区是根据key的hashCode对ReduceTasks个数取模得到的。用户没法控制哪个key存储到哪个分区。
3、自定义Partitioner步骤
(1)自定义类继承Partitioner,重写getPartition()方法
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(默认为1,则走的是默认分区方法,不会使用自定义分区方法,使用必须重新设置ReduceTask数量)
job.setNumReduceTasks(5);
4、分区总结
(1)如果ReduceTask的数量> getPartition的结果数,则会多产生几个空的输出文件part-r-000xx;
(2)如果1<ReduceTask的数量<getPartition的结果数,则有一部分分区数据无处安放,会Exception;
(3)如果ReduceTask的数量=1,则不管MapTask端输出多少个分区文件,最终结果都交给这一个
ReduceTask,最终也就只会产生一个结果文件 part-r-00000;
(4)分区号必须从零开始,逐一累加。
5、案例分析
例如:假设自定义分区数为5,则
(1) job.setNumReduceTasks(1); 会正常运行,只不过会产生一个输出文件
(2) job.setNumReduceTasks(2); 会报错
(3) job.setNumReduceTasks(6); 大于5,程序会正常运行,会产生空文件
3.3.3 Partition 分区案例实操
1)需求:将统计结果按照手机归属地不同省份输出到不同文件中(分区)
(1)输入数据
(2)期望输出数据
手机号 136、137、138、139 开头都分别放到一个独立的 4 个文件中,其他开头的放到一个文件中。
2)需求分析
3)在案例 2.3 的基础上,增加一个分区类
package com.pcz.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
部分截图:
4)在驱动函数中增加自定义数据分区设置和 ReduceTask 设置
package com.pcz.mapreduce.partitioner;
import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.Text; import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
import java.io.IOException;
public class FlowDriver {
public static void main(String[] args) throws IOException,
ClassNotFoundException, InterruptedException {
//1 获取 job 对象
Configuration conf = new Configuration();
Job job = Job.getInstance(conf);
//2 关联本 Driver 类
job.setJarByClass(FlowDriver.class);
//3 关 联 Mapper 和 Reducer
job.setMapperClass(FlowMapper.class);
job.setReducerClass(FlowReducer.class);
//4 设置 Map 端输出数据的 KV 类型
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(FlowBean.class);
//5 设置程序最终输出的 KV 类型
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(FlowBean.class);
//8 指定自定义分区器
job.setPartitionerClass(ProvincePartitioner.class);
//9 同时指定相应数量的 ReduceTask
//设置个数要和自己在ProvincePartitioner设置的分区个数一致
//如果少了会报错,但是为1的话会以默认分区方式正常运行
//设置多了可正常运行,但是结果会出现空的分区
job.setNumReduceTasks(5);
//6 设置输入输出路径
FileInputFormat.setInputPaths(job, new Path("D:\\inputflow"));
FileOutputFormat.setOutputPath(job, new Path("D\\partitionout"));
//7 提交 Job
boolean b = job.waitForCompletion(true);
System.exit(b ? 0 : 1);
4)运行后验证:
五个文件:按号码分区
3.3.4 WritableComparable 排序
1.排序概述(面试重点:排序)
(1)、排序是MapReduce框架中最重要的操作之一。
MapTask 和ReduceTask 均会对数据按照key 进行排序。该操作属于Hadoop的默认行为。任何应用程序中的数据均会被排序,而不管逻辑上是否需要。
默认排序是按照字典顺序排序,且实现该排序的方法是快速排序。
(2)、对于MapTask,它会将处理的结果暂时放到环形缓冲区中,当环形缓冲区使用率达到一定阈值后,再对缓冲区中的数据进行一次快速排序,并将这些有序数据溢写到磁盘上,而当数据处理完毕后,它会对磁盘上所有文件进行归并排序。
(3)、对于ReduceTask,它从每个MapTask上远程拷贝相应的数据文件,如果文件大小超过一定阈值,则溢写磁盘上,否则存储在内存中。如果磁盘上文件数目达到 一定阈值,则进行一次归并排序以生成一个更大文件;如果内存中文件大小或者 数目超过一定阈值,则进行一次合并后将数据溢写到磁盘上。当所有数据拷贝完毕后,ReduceTask统一对内存和磁盘上的所有数据进行一次归并排序。
2.排序分类
(1)部分排序
MapReduce根据输入记录的键对数据集排序。保证输出的每个文件内部有序。
(2)全排序
最终输出结果只有一个文件,且文件内部有序。实现方式是只设置一个ReduceTask。但该方法在处理大型文件时效率极低,因为一台机器处理所有文件,完全丧失了MapReduce所提供的并行架构。
(3)辅助排序:(GroupingComparator分组)
在Reduce端对key进行分组。应用于:在接收的key为bean对象时,想让一个或几个字段相同(全部 字段比较不相同)的key进入到同一个reduce方法时,可以采用分组排序,该方式比较少用。
(4)二次排序
在自定义排序过程中,如果compareTo中的判断条件为两个即为二次排序。
3.自定义排序 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;
}
3.3.5 WritableComparable 排序案例实操(全排序)
1)需求
根据案例 2.3 序列化案例产生的结果再次对总流量进行倒序排序。
(1)输入数据(原始数据)
第一次处理后的数据
(2)期望输出数据(部分截图)
2)需求分析
3)代码实现
(1)FlowBean 对象在在需求 1 基础上增加了比较功能
1.修改接口
2.写入倒叙排序方法
完整代码:
package com.pcz.mapreduce.writableComparable;
import org.apache.hadoop.io.Writable;
import org.apache.hadoop.io.WritableComparable;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
/*
1.定义类实现Writable接口
2.重写序列化和反序列化方法
3.重写空参构造
4.toString方法
*/
public class FlowBean implements WritableComparable<FlowBean> {
//定义三个属性,流量数据量比较大,所以用long类型定义
private long upFlow; //上行流量(upFlow),
private long downFlow; //下行流量(downFlow),
private long sumFlow; //总流量(sunFlow)
//空参构造(空白处右键-->点击生成-->点击构造函数)
//用于后续反射使用
public FlowBean() {
}
//生成upFlow,downFlow,sumFlow的get和set方法
//空白处右键-->点击生成-->点击get和set-->选择这三个函数,添加进去
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;
}
//重载setSumFlow(即改成无参,并把up和down相加)
public void setSumFlow() {
this.sumFlow = this.upFlow+this.downFlow;
}
@Override
public void write(DataOutput out) throws IOException {
//序列化方法
out.writeLong(upFlow); //因为前面定义的是long类型,所以这里要用writelong
out.writeLong(downFlow);
out.writeLong(sumFlow);
}
@Override
public void readFields(DataInput in) throws IOException {
//反序列化方法,要和前面序列化的顺序相对应。
this.upFlow=in.readLong(); //long类型
this.downFlow=in.readLong();
this.sumFlow=in.readLong();
}
//重写toString方法(快捷:toString按enter,然后修改)
@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;
}
}
}
(2)编写 Mapper 类
package com.pcz.mapreduce.writableComparable;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;
import java.io.IOException;
public class FlowMapper extends Mapper<LongWritable, Text,FlowBean, Text> { //FlowBean是前面定义的类型,有上行流量,下行流量和总流量
private FlowBean outK=new FlowBean();
private Text outV=new Text();
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
//获取一行
String line = value.toString();
//切割
String[] split = line.split("\t");
//封装
outV.set(split[0]);
outK.setUpFlow(Long.parseLong(split[1]));//Long.parseLong()表示转换成Long类型
outK.setDownFlow(Long.parseLong(split[2]));
outK.setSumFlow();
//写出
context.write(outK,outV);
}
}
(3)编写 Reducer 类
package com.pcz.mapreduce.writableComparable;
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 {
for (Text value : values) {
context.write(value,key);
}
}
}
(4)编写 Driver 类
package com.pcz.mapreduce.writableComparable;
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 FlowDiver {
public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
//1.获取job
Configuration conf = new Configuration();
Job job = Job.getInstance(conf);
//2.设置jar
job.setJarByClass(FlowDiver.class);
//3.关联mapper和reducer
job.setMapperClass(FlowMapper.class);
job.setReducerClass(FlowReducer.class);
//4.设置mapper 输出的k,v类型
job.setMapOutputKeyClass(FlowBean.class);
job.setMapOutputValueClass(Text.class);
//5.设置最终数据输出的k,v类型
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(FlowBean.class);
//6.设置数据的输入路径和输出路径
FileInputFormat.setInputPaths(job,new Path("P:\\test\\hadoop test\\output\\wordoutput1\\output8"));
FileOutputFormat.setOutputPath(job,new Path("P:\\test\\hadoop test\\output\\writableComparableoutput\\output1"));
//7.提交job
boolean result = job.waitForCompletion(true);
System.exit(result ? 0: 1);
}
}
补充:
二次排序:在原有的基础上增加排序条件:第二次通过上行流量大小正序排序(如有需要还可以增加其他排序条件,例如下行流量等)
验证:
3.3.6 WritableComparable 排序案例实操(区内排序)
1)需求
要求每个省份手机号输出的文件中按照总流量内部排序。
2)需求分析
基于前一个需求,增加自定义分区类,分区按照省份手机号设置。
3)案例实操
(1)增加自定义分区类ProvincePartitioner2
package com.pcz.mapreduce.partitionAndComparable;
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) {
//获取phone文本的一行
String phone = text.toString();
String prephone =phone.substring(0,3);
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;
}
return partition;
}
}
(2)在驱动类中添加分区类Driver
// 设置自定义分区器
job.setPartitionerClass(ProvincePartitioner2.class);
// 设置对应的 ReduceTask 的个数
job.setNumReduceTasks(5);
3)验证
3.3.7 Combiner 合并
可减少Reduce阶段合并压力,提高工作效率
(1) Combiner是MR程序中Mapper和Reducer之外的一种组件。(可选流程)
(2) Combiner组件的父类就是Reducer。
(3) Combiner和Reducer的区别在于运行的位置
Combiner是在每一个MapTask所在的节点运行;
Reducer是接收全局所有Mapper的输出结果;
(4) Combiner的意义就是对每一个MapTask的输出进行局部汇总,以减小网络传输量。
(5) Combiner能够应用的前提是不能影响最终的业务逻辑,而且,Combiner的输出kv 应该跟Reducer的输入kv类型要对应起来。并不是所有场景适用,例如计算平均值不能用,求和可以:
Mapper Reducer
3 5 7 ->(3+5+7)/3=5 (3+5+7+2+6)/5=23/5 不等于 (5+4)/2=9/2
2 6 ->(2+6)/2=4
(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);
}
}
(b)在 Job 驱动类中设置:
job.setCombinerClass(WordCountCombiner.class);
3.3.8 Combiner 合并案例实操
1)需求
统计过程中对每一个 MapTask 的输出进行局部汇总(在Map阶段提前进行预聚合), 以减小网络传输量即采用Combiner 功能。
(1)数据输入
(2)期望输出数据
期望:Combine 输入数据多,输出时经过合并,输出数据降低。
2)需求分析
3)案例实操-方案一
(1)增加一个 WordCountCombiner 类继承 Reducer
package com.pcz.mapreduce.combiner;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;
import java.io.IOException;
public class WordCountCombiner extends Reducer<Text, IntWritable,Text,IntWritable> {
private IntWritable outV=new IntWritable();
@Override
protected void reduce(Text key, Iterable<IntWritable> values, Context context) throws IOException, InterruptedException {
int sum =0;
for (IntWritable value : values) {
sum+=value.get();
}
outV.set(sum);
context.write(key,outV);
}
}
(2)在 WordcountDriver 驱动类中指定 Combiner
// 指定需要使用 combiner,以及用哪个类作为 combiner 的逻辑
job.setCombinerClass(WordCountCombiner.class);
4)案例实操-方案二
(1)将 WordcountReducer 作为 Combiner 在 WordcountDriver 驱动类中指定
// 指定需要使用 Combiner,以及用哪个类作为 Combiner 的逻辑
job.setCombinerClass(WordCountReducer.class);
结果比对:
3.4 OutputFormat 数据输出
3.4.1 OutputFormat 接口实现类
OutputFormat是MapReduce输出的基类,所有实现MapReduce输出都实现了 OutputFormat接口。下面我们介绍几种常见的OutputFormat实现类。
1.OutputFormat实现类
2.默认输出格式TextOutputFormat,按行写出
3.自定义OutputFormat
3.1 应用场景:
例如:输出数据到MySQL/HBase/Elasticsearch等存储框架中。
3.2 自定义OutputFormat步骤
➢ 自定义一个类继承FileOutputFormat。
➢ 改写RecordWriter,具体改写输出数据的方法write()。
3.4.2 自定义 OutputFormat 案例实操
1)需求
2)案例实操
(1)编写 LogMapper 类
package com.pcz.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;
public class LogMapper extends Mapper<LongWritable, Text,Text, NullWritable> {
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
//http://www.baidu.com
//http://www.google.com
//(http://www.baidu.com,NullWritable)
//不做任何处理
context.write(value,NullWritable.get());
}
}
(2)编写 LogReducer 类
package com.pcz.mapreduce.outputFormat;
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) { //for循环防止相同数据丢数据
context.write(key,NullWritable.get());
}
}
}
(3)自定义一个 LogOutputFormat 类
package com.pcz.mapreduce.outputFormat;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.RecordWriter;
import org.apache.hadoop.mapreduce.TaskAttemptContext;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
import java.io.IOException;
//定义一个类LogOutputFormat继承FileOutputFormat
public class LogOutputFormat extends FileOutputFormat<Text, NullWritable> {
@Override
//重写getRecordWriter这个方法
public RecordWriter<Text, NullWritable> getRecordWriter(TaskAttemptContext job) throws IOException, InterruptedException {
LogRecordWriter lrw =new LogRecordWriter(job);
return lrw;
}
}
(4)编写 LogRecordWriter 类
package com.pcz.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.mapreduce.RecordWriter;
import org.apache.hadoop.mapreduce.TaskAttemptContext;
import java.io.IOException;
public class LogRecordWriter extends RecordWriter {
//将pczOut,otherOut定义为全局变量
private FSDataOutputStream pczOut;
private FSDataOutputStream otherOut;
public LogRecordWriter(TaskAttemptContext job) {
//创建两条流:pczOut otherOut
try {
FileSystem fs = FileSystem.get(job.getConfiguration());
pczOut = fs.create(new Path("P:\\test\\hadoop test\\output\\LogRecordWriter\\pcz.log"));
otherOut = fs.create(new Path("P:\\test\\hadoop test\\output\\LogRecordWriter\\other.log"));
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void write(Object key, Object value) throws IOException, InterruptedException {
//转换成string进行处理
String log =key.toString();
//具体写
if (log.contains("pcz")){ //判断是否包含pcz
pczOut.writeBytes(log+"\n"); //包含就写入pczOut
}else {
otherOut.writeBytes(log+"\n"); //不包含就写入otherOut
}
}
@Override
public void close(TaskAttemptContext context) throws IOException, InterruptedException {
//关流
IOUtils.closeStream(pczOut);
IOUtils.closeStream(otherOut);
}
}
(5)编写 LogDriver 类
package com.pcz.mapreduce.outputFormat;
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("P:\\test\\hadoop test\\input\\inputlog"));
// 虽然 我们 自定义 了 outputformat , 但 是 因为我 们的 outputformat 继承自fileoutputformat
//而 fileoutputformat 要输出一个_SUCCESS 文件,所以在这还得指定一个输出目录
FileOutputFormat.setOutputPath(job, new Path("P:\\test\\hadoop test\\output\\LogDriverOutput\\output2"));
boolean b = job.waitForCompletion(true); System.exit(b ? 0 : 1);
}
}
验证:
3.5 MapReduce 内核源码解析
3.5.1MapTask工作机制
(1)Read阶段:MapTask通过InputFormat(输入方法)获得的RecordReader,从InSplit中解析出一个个Key/value(Key:偏移量,value:这一行内容)
(2)Map阶段:该节点主要是将解析出的Key/value交给用户编写map()函数处理,并产生新的Key/value(Key:内容,value:1)
(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 工作机制
(1) Copy 阶段:ReduceTask 从各个 MapTask 上远程拷贝一片数据,并针对某一片数据,如果其大小超过一定阈值,则写到磁盘上,否则直接放到内存中。
(2) Sort 阶段:在远程拷贝数据的同时,ReduceTask 启动了两个后台线程对内存和磁盘上的文件进行合并,以防止内存使用过多或磁盘上文件过多。按照 MapReduce 语义,用户编写 reduce()函数输入数据是按 key 进行聚集的一组数据。为了将 key 相同的数据聚在一起,Hadoop 采用了基于排序的策略。由于各个 MapTask 已经实现对自己的处理结果进行了局部排序,因此,ReduceTask 只需对所有数据进行一次归并排序即可。
(3) Reduce 阶段:reduce()函数将计算结果写到 HDFS 上。
3.5.3 ReduceTask 并行度决定机制
回顾:MapTask 并行度由切片个数决定,切片个数由输入文件和切片规则决定。思考:ReduceTask 并行度由谁决定?
ReduceTask 的并行度同样影响整个 Job 的执行并发度和执行效率,但与 MapTask 的并发数由切片数决定不同,ReduceTask 数量的决定是可以直接手动设置:
// 默认值是 1,手动设置为 4
job.setNumReduceTasks(4);
实验:测试 ReduceTask 多少合适
(1)实验环境:1 个 Master 节点,16 个 Slave 节点:CPU:8GHZ,内存: 2G
(2)实验结论:
(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.5.4 MapTask & ReduceTask 源码解析
1) MapTask 源码解析流程
2) ReduceTask 源码解析流程
===================ReduceTask===================
if (isMapOrReduce()) //reduceTask324 行,提前打断点
initialize() // reduceTask333 行,进入
init(shuffleContext); // reduceTask375 行,走到这需要先给下面的打断点
totalMaps=job.getNumMapTasks();//ShuffleSchedulerImpl 第 120 行,提前打断点
merger = createMergeManager(context); //合并方法,Shuffle 第 80 行
// MergeManagerImpl 第 232 235 行,提前打断点
this.inMemoryMerger=createInMemoryMerger(); //内存合并
this.onDiskMerger = new OnDiskMerger(this); //磁盘合并
rIter = shuffleConsumerPlugin.run();
eventFetcher.start(); //开始抓取数据,Shuffle 第 107 行,提前打断点
eventFetcher.shutDown(); //抓取结束,Shuffle 第 141 行,提前打断点
copyPhase.complete(); //copy 阶段完成,Shuffle 第 151 行
taskStatus.setPhase(TaskStatus.Phase.SORT); //开始排序阶段,Shuffle 第 152 行
sortPhase.complete(); //排序阶段完成,即将进入 reduce 阶段 reduceTask382 行
reduce(); //reduce 阶段调用的就是我们自定义的 reduce 方法,会被调用多次
cleanup(context); //reduce 完成之前,
3.6 Join 应用
3.6.1 Reduce Join
Map 端的主要工作:为来自不同表或文件的 key/value 对,打标签以区别不同来源的记录。然后用连接字段作为 key,其余部分和新加的标志作为 value,最后进行输出。
Reduce 端的主要工作:在 Reduce 端以连接字段作为 key 的分组已经完成,我们只需要在每一个分组当中将那些来源于不同文件的记录(在 Map 阶段已经打标志)分开,最后进行合并就 ok 了。
3.6.2 Reduce Join 案例实操
1)需求
将商品信息表中数据根据商品 pid 合并到订单数据表中。
2)需求分析
通过将关联条件作为 Map 输出的 key,将两表满足 Join 条件的数据并携带数据所来源的文件信息,发往同一个 ReduceTask,在 Reduce 中进行数据的串联。
3)代码实现
(1)创建商品和订单合并后的 JoinBean 类
package com.pcz.mapreduce.reducejoin;
import org.apache.hadoop.io.Writable;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
public class JoinBean implements Writable {
//定义五个属性
private String id; //订单id
private String pid; //商品id
private int amount; //商品数量
private String pname; //商品名称
private String flag; //标记表名(order、pd)
//空参构造
public JoinBean() {
}
//生成get、set方法
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); //String类型用UTF
out.writeUTF(pid);
out.writeInt(amount); //Int类型用wirteInt
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();
}
//重写toString方法
@Override
public String toString() {
return id + '\t' +pname + "\t" + amount;
}
}
(2)编写 JoinMapper 类
package com.pcz.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;
public class JoinMapper extends Mapper<LongWritable, Text,Text,JoinBean> {
//定义属性
private String fileName;
private Text outK =new Text();
private JoinBean outV =new JoinBean();
@Override //setup()初始化方法
protected void setup(Context context) throws IOException, InterruptedException {
//初始化 order pd
FileSplit split = (FileSplit) context.getInputSplit(); //FileSpiit是一种文件切割形式
//默认切片规则是每个文件单独切,pd这个文件进来只会执行一次MapTask,有一个setup()方法
// 和对应的map方法,如果不这样,则按行读取,增加了工作量
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")){//如果文件名中有“order”,处理的是订单表
//“\t”分隔
String[] split = Line.split("\t");
//封装 k,v 订单order表
outK.set(split[1]);
outV.setId(split[0]);
outV.setPid(split[1]);
outV.setAmount(Integer.parseInt(split[2]));
outV.setPname("");
outV.setFlag("order");
}else { //处理的是商品表 pd
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);
}
}
(3)编写 JoinReducer 类
package com.pcz.mapreduce.reducejoin;
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 JoinReducer extends Reducer<Text,JoinBean,JoinBean, NullWritable> {
@Override
protected void reduce(Text key, Iterable<JoinBean> values, Context context) throws IOException, InterruptedException {
//1.创建并初始化order的集合
ArrayList<JoinBean> orderBeans = new ArrayList<>();
//创建并初始化pd的集合
JoinBean pdBean = new JoinBean();
//循环遍历
for (JoinBean value : values) {
if ("order".equals(value.getFlag())) {//订单表
//创建一个临时对象,将数据传入,如果不创建
// ,则后面的数据会覆盖上一个数据
JoinBean tmpJoinBean = new JoinBean();
//
try {
BeanUtils.copyProperties(tmpJoinBean,value); //括号中(,)第一个对象是目标地址,即给谁赋值,第二个是原数据
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
orderBeans.add(tmpJoinBean);
}else {//商品表
//因为进来的数据只有一行,所以不会覆盖,就不需要建临时文件
try {
BeanUtils.copyProperties(pdBean,value);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
}
//循环遍历orderBeans,赋值pdname
for (JoinBean orderBean : orderBeans) {
orderBean.setPname(pdBean.getPname());
context.write(orderBean,NullWritable.get());
}
}
}
(4)编写 JoinDriver 类
package com.pcz.mapreduce.reducejoin;
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 JoinDriver {
public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
//获取Job
Job job = Job.getInstance(new Configuration());
//2.设置jar
job.setJarByClass(JoinDriver.class);
//3.关联mapper和reducer
job.setMapperClass(JoinMapper.class);
job.setReducerClass(JoinReducer.class);
//4.设置mapper 输出的k,v类型
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(JoinBean.class);
//5.设置最终数据输出的k,v类型
job.setOutputKeyClass(JoinBean.class);
job.setOutputValueClass(NullWritable.class);
//6.设置数据的输入路径和输出路径
FileInputFormat.setInputPaths(job,new Path("P:\\test\\hadoop test\\input\\inputjoin"));
FileOutputFormat.setOutputPath(job,new Path("P:\\test\\hadoop test\\output\\outputJoin\\output4"));
//7.提交job
boolean result =job.waitForCompletion(true);
System.exit(result ? 0: 1);
}
}
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"));
3.6.4 Map Join 案例实操
1)需求
将商品信息表中数据根据商品 pid 合并到订单数据表中。
2)需求分析
MapJoin 适用于关联表中有小表的情形。
3)实现代码
(1)编写MapJoinDriver类
package com.pcz.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;
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 (因为不需要执行reducer就可以得到我们需要的数据,需要就没有添加reducer)
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:///P:/inputjoincache/pd.txt"));
//如果是集群运行,需要设置 HDFS 路径
//job.addCacheFile(new URI("hdfs://hadoop102:8020/cache/pd.txt"));
//map端Join的逻辑不需要reduce阶段,所以设置reduceTask的数量为0
job.setNumReduceTasks(0);
//6.设置输入输出路径
FileInputFormat.setInputPaths(job,new Path("P:\\test\\hadoop test\\input\\inputreducejoin"));
FileOutputFormat.setOutputPath(job,new Path("P:\\test\\hadoop test\\output\\outputreducejoin\\output01"));
//7.提交job
boolean b =job.waitForCompletion(true);
System.exit(b ? 0 : 1);
}
}
(2)编写MapJoinMapper类
package com.pcz.mapreduce.mapjoin;
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> {
//准备一个集合pdMap,存储pid,pname 。放在全局中,map方法也可以直接调用
HashMap<String, String> pdMap = new HashMap<>();
private Text outK =new Text();
//重写setup方法
@Override
protected void setup(Context context) throws IOException, InterruptedException {
//获取缓存文件,并把文件内容封装到集合 pd.txt
URI[] cacheFiles = context.getCacheFiles();
//获取一个fs文件系统
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())){//从reader里面获取一行数据,如果这一行数据不为空,则执行如下操作
//切割
String[] fields = line.split("\t");
//赋值给准备好的集合pdmap
pdMap.put(fields[0],fields[1]);
}
//关流
IOUtils.closeStream(reader);
}
//重写map方法
@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和订单amount
//封装
outK.set(fields[0]+"\t"+pname+"\t"+fields[2]);
//写出
context.write(outK,NullWritable.get());
}
}
3.7 数据清洗(ETL)
“ETL,是英文 Extract-Transform-Load 的缩写,用来描述将数据从来源端经过抽取(Extract)、转换(Transform)、加载(Load)至目的端的过程。ETL 一词较常用在数据仓库,但其对象并不限于数据仓库。
在运行核心业务 MapReduce 程序之前,往往要先对数据进行清洗,清理掉不符合用户要求的数据。清理的过程往往只需要运行 Mapper 程序,不需要运行 Reduce 程序。
1)需求
(2)去除日志中字段个数小于等于 11 的日志。
(2)期望输出数据,每行字段长度都大于 11。
2)需求分析
需要在 Map 阶段对输入的数据根据规则进行过滤清洗。
3)实现代码
(1)编写 WebETLMapper 类
package com.pcz.mapreduce.ETL;
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 WebETLMapper 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.ELT
boolean result=parseLog(line,context);
if (!result){
return;
}
//4.写出
context.write(value,NullWritable.get());
}
private boolean parseLog(String line, Context context) {
//切割
String[] fields = line.split(" ");
//判断日志长度是否大于11
if (fields.length>25){
return true;
}else {
return false;
}
}
}
(2)编写WebETLDriver类
package com.pcz.mapreduce.ETL;
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 WebETLDriver {
public static void main(String[] args) throws Exception{
//将输入路径和输出路径存到args中
args=new String[]{"P:\\test\\hadoop test\\input\\inputETL","P:\\test\\hadoop test\\output\\WebETLoutput\\output2"};
//获取job信息
Configuration conf = new Configuration();
Job job = Job.getInstance();
//加载jar包
job.setJarByClass(WebETLDriver.class);
//连接Mapper类
job.setMapperClass(WebETLMapper.class);
//设置输出的k,v类型
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(NullWritable.class);
//设置reduceTask数量为0,跳过reduce阶段
job.setNumReduceTasks(0);
//设置文件输入和输出路径
FileInputFormat.setInputPaths(job,new Path(args[0]));
FileOutputFormat.setOutputPath(job,new Path(args[1]));
//提交job
boolean b=job.waitForCompletion(true);
System.exit(b ? 0 : 1);
}
}
3.8 MapReduce 开发总结
1)输入数据接口:InputFormat
(1)默认使用的实现类是:TextInputFormat
(2) TextInputFormat 的功能逻辑是:一次读一行文本,然后将该行的起始偏移量作为
(3) CombineTextInputFormat 可以把多个小文件合并成一个切片处理,提高处理效率。
2)逻辑处理接口:Mapper
用户根据业务需求实现其中三个方法:
setup() :初始化
map():根据业务需求编写代码
cleanup ():关闭资源
3)Partitioner 分区
(1)有默认实现 HashPartitioner,逻辑是根据 key 的哈希值和 numReduces 来返回一个分区号;key.hashCode()&Integer.MAXVALUE % numReduces
(2)如果业务上有特别的需求,可以自定义分区。
4) Comparable 排序
(1)当我们用自定义的对象作为 key 来输出时,就必须要实现 WritableComparable 接口,重写其中的 compareTo()方法。
(2)部分排序:对最终输出的每一个文件进行内部排序。
(3)全排序:对所有数据进行排序,通常只有一个 Reduce。
(4)二次排序:排序的条件有两个。自定义排序范畴,实现WritableCompare接口,重写compareTo方法
5) Combiner 合并
前提:不影响最终的业务逻辑(例如:求和可用,求平均值不可用)
提前在map()阶段聚合==》解决数据倾斜的一个方法
Combiner 合并可以提高程序执行效率,减少 IO 传输。但是使用时必须不能影响原有的业务处理结果。
6)逻辑处理接口:Reducer
用户根据业务需求实现其中三个方法:
setup()
reduce()
cleanup ()
7)输出数据接口:OutputFormat
(1)默认实现类是 TextOutputFormat,(按行输出到文件)功能逻辑是:将每一个 KV 对,向目标文本文件输出一行。
(2)用户还可以自定义 OutputFormat。