MapReduce
1.概述
1)定义:MapReduce是一个分布式运算程序的编程框架
核心功能:将用户编写的业务逻辑代码和自带默认组件整合成一个完整的分布式运算程序,并发运行在一个Hadoop集群上。
2)优缺点:
优点:
- 易于编程,它简单的实现一些接口,就可以完成一个分布式程序
- 扩展性,增加机器计算
- 高容错,比如其中一台机器挂了,它可以把上面的计算任务转移到另外一个节点上运行,不至于这个任务运行失败
缺点:
- 不擅长实时计算
- 不擅长流式计算
- 不擅长DAG计算
3)核心思想
4)常用的序列化类型
5)编程规范
6)WordCount实操:
本地测试:
1)给定hello.txt文本和预期结果
统计后预期:
2)环境准备,创建maven工程
-
调整maven的setting文件(可能存在版本不兼容问题 我的idea-2021 ,maven-3.54是可以的)
-
在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>
-
在项目的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
-
在main/java下创建com.wts.mapreduce.wordcount
-
写代码(特别注意导包,导入关于hadoop、mapreduce包;)
(1)Mapper类
package com.wts.mapreduce.wordcount;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.io.LongWritable;
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, Mapper<LongWritable, Text, Text, IntWritable>.Context context) throws IOException, InterruptedException {
//1. 获取一行 wts wts
String line = value.toString();
//2. 切割
//wts
//wts
String[] words = line.split(" ");
//3. 输出(封装)
for (String word : words) {
outK.set(word);
context.write(outK, outV);
}
}
}
(2)Reducer类
package com.wts.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
* */
public class WordCountReducer extends Reducer<Text, IntWritable, Text, IntWritable> {
private IntWritable outV = new IntWritable();
int sum;
@Override
protected void reduce(Text key, Iterable<IntWritable> values, Context context) throws IOException, InterruptedException {
sum = 0;
//wts(1,1)
//累加
for (IntWritable value : values) {
sum += value.get();
}
outV.set(sum);
//写出
context.write(key, outV);
}
}
(3)Driver类
package com.wts.mapreduce.wordcount;
//是0的有以下几种情况:1.mapper阶段传new intwaierable的时候没写1 2.reducer 把sum+=coun。get写到了循环内 3.最后v.set()没有传sum
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;
public class WordCountDriver {
public static void main(String[] args) throws IOException, InterruptedException, ClassNotFoundException {
//1.获取job
Configuration conf = new Configuration();
Job job = Job.getInstance(conf);
//2、关联driver当前的jar(设置当前jar包路径)
job.setJarByClass(WordCountDriver.class);
//3、关联mapper和reducer
job.setMapperClass(WordCountMapper.class);
job.setReducerClass(WordCountReducer.class);
//4、设置map输出k、v类型
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(IntWritable.class);
//5、设置最终输出k、v类型
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(IntWritable.class);
//6、设置输入输出路径
FileInputFormat.setInputPaths(job, new Path("G:\\1_Program\\hadoop_learning\\hadoop-input"));
FileOutputFormat.setOutputPath(job, new Path("G:\\1_Program\\hadoop_learning\\hadoop-output\\output02"));//output1不用自己创建?
//7、提交job
boolean result = job.waitForCompletion(true);
System.exit(result ? 0 : 1);
}
}
本地测试:
- 配置了HADOOP_HOME变量和windows运行依赖
- 运行程序 查看
上传集群
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>
报错解决方案:工程上出现红色报错,是因为插件没有自动加载出来,需要加入依赖
<dependency><!-- 解决的方法:加载插件! -->
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>3.2.0</version>
</dependency>
2)把程序打包成jar包
(3)修改不带依赖的jar包名称为wc.jar,并拷贝该jar包到Hadoop集群的/opt/module/hadoop-3.1.3路径。
(4)启动Hadoop集群,执行WordCount程序
[atguigu@hadoop102 hadoop-3.1.3]$ hadoop jar wc.jar
com.atguigu.mapreduce.wordcount2.WordCountDriver /input /output
很成功啊 ^ _ ^ !
2.Hadoop序列化
1.概述
hadoop自己开发的一套序列化(Writable)的特点:
(1)紧凑 :高效使用存储空间。
(2)快速:读写数据的额外开销小。
(3)互操作:支持多语言的交互
2.自定义bean对象实现序列化接口(Writable)
在企业开发中往往常用的基本序列化类型不能满足所有需求,比如在Hadoop框架内部传递一个bean对象,那么该对象就需要实现序列化接口。
具体实现bean对象序列化步骤如下7步。
(1)必须实现Writable接口
(2)反序列化时,需要反射调用空参构造函数,所以必须有空参构造
public FlowBean() {
super();
}
(3)重写序列化方法
@Override
public void write(DataOutput out) throws IOException {
out.writeLong(upFlow);
out.writeLong(downFlow);
out.writeLong(sumFlow);
}
(4)重写反序列化方法
@Override
public void readFields(DataInput in) throws IOException {
upFlow = in.readLong();
downFlow = in.readLong();
sumFlow = in.readLong();
}
(5)注意反序列化的顺序和序列化的顺序完全一致
(6)要想把结果显示在文件中,需要重写toString(),可用"\t"分开,方便后续用。
(7)如果需要将自定义的bean放在key中传输,则还需要实现Comparable接口,因为MapReduce框中的Shuffle过程要求对key必须能排序。详见后面排序案例。
@Override
public int compareTo(FlowBean o) {
// 倒序排列,从大到小
return this.sumFlow > o.getSumFlow() ? -1 : 1;
}
3.实操演示
1)需求:
内容:
要求输出结果:
2)分析过程
3)写代码:导包很重要,很容易出错
FlowBean:
package com.wts.mapreduce.writable;
import org.apache.hadoop.io.Writable;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
//(1)实现Writable接口
public class FlowBean implements Writable {
private long upFlow;
private long downFlow;
private long sumFlow;
//2.反序列化,需要反射调用空参构造方法,所以必须有空参构造
public FlowBean() {
}
//3.提供三个getter、setter方法
public long getUpFlow() {
return upFlow;
}
public void setUpFlow(long upFlow) {
this.upFlow = upFlow;
}
public long getDownFlow() {
return downFlow;
}
public void setDownFlow(long downFlow) {
this.downFlow = downFlow;
}
public long getSumFlow() {
return sumFlow;
}
public void setSumFlow(long sumFlow) {
this.sumFlow = sumFlow;
}
//这里再加来一个重载
public void setSumFlow() {
this.sumFlow = this.upFlow + this.downFlow;
}
//4.重写序列化、反序列化方法 ,注意序列化的顺序要一直
@Override
public void write(DataOutput dataOutput) throws IOException {
dataOutput.writeLong(upFlow);
dataOutput.writeLong(downFlow);
dataOutput.writeLong(sumFlow);
}
@Override
public void readFields(DataInput dataInput) throws IOException {
upFlow = dataInput.readLong();
downFlow = dataInput.readLong();
sumFlow = dataInput.readLong();
}
//5.重写toStirng方法输出结果
@Override
public String toString() {
return upFlow + "\t" + downFlow + "\t" + sumFlow;
}
}
Mapper类:
package com.wts.mapreduce.writable;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;
import java.io.IOException;
public class FlowBeanMapper extends Mapper<LongWritable, Text, Text, FlowBean> {
private Text OutK = new Text();
private FlowBean OutV = new FlowBean();
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
//1.获取一行数据
//1 13736230513 192.196.100.1 www.atguigu.com 2481 24681 200
String line = value.toString();
//2.切割\t
//1 13736230513 192.196.100.1 www.atguigu.com 2481 24681 200
String[] split = line.split("\t");
//3.获取需要的数据
String phone = split[1];
String up = split[split.length - 3];
String down = split[split.length - 2];
//4.封装
OutK.set(phone);
OutV.setUpFlow(Long.parseLong(up));
OutV.setDownFlow(Long.parseLong(down));
OutV.setSumFlow();
//5.写出
context.write(OutK, OutV);
}
}
Reudcer类:
package com.wts.mapreduce.writable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;
import java.io.IOException;
public class FlowBeanReducer extends Reducer<Text, FlowBean, Text, FlowBean> {
private FlowBean OutV = new FlowBean();
@Override
protected void reduce(Text key, Iterable<FlowBean> values, Reducer<Text, FlowBean, Text, FlowBean>.Context context) throws IOException, InterruptedException {
//1.遍历结合 累加值
long totalUp = 0;
long totalDown = 0;
for (FlowBean value : values) {
totalUp += value.getUpFlow();
totalDown += value.getDownFlow();
}
//2.封装OutK,OutV
OutV.setUpFlow(totalUp);
OutV.setDownFlow(totalDown);
OutV.setSumFlow();
//3.写出
context.write(key, OutV);
}
}
Driver类:
package com.wts.mapreduce.writable;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
import java.io.IOException;
public class FlowBeanDriver {
public static void main(String[] args) throws IOException, InterruptedException, ClassNotFoundException {
//1.获取job
Configuration conf = new Configuration();
Job job = Job.getInstance(conf);
//2.设置jar包
job.setJarByClass(FlowBeanDriver.class);
//3.关联mapper和reducer
job.setMapperClass(FlowBeanMapper.class);
job.setReducerClass(FlowBeanReducer.class);
//4.设置mapper 输出key和value的类型
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(FlowBean.class);
//5.设置最后输出的key和value类型
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(FlowBean.class);
//6.设置输入、输出路径
FileInputFormat.setInputPaths(job, new Path("G:\\1_Program\\hadoop_learning\\hadoop-input\\phone_date"));
FileOutputFormat.setOutputPath(job, new Path("G:\\1_Program\\hadoop_learning\\hadoop-output\\flow-output01"));
//7.提交job
boolean result = job.waitForCompletion(true);
System.exit(result ? 0 : 1);
}
}
运行结果:
3.MapReduce框架原理
3.1 InputFormat数据输入
1)MapTask并行度决定机制
- 数据块:Block是HDFS物理上把数据分成一块一块。数据块是HDFS存储数据单位。
- 数据切片:数据切片只是在逻辑上对输入进行分片,并不会在磁盘上将其切分成片进行存储。数据切片是MapReduce程序计算输入数据的单位,一个切片会对应启动一个MapTask。
2)job提交流程源码分析
3) FileInputFormat 切片机制:
- 简单的按照文件的内容长度切片
- 切片大小默认等于block的值
- 切片是逐个针对每个文件单独切片的
切片大小调整:默认等于block
调大:使minSize(默认为1)比block还大
调小:使maxSiz(默认为L的最大值)比block还小
4)TextInputFormat
实现类: FileInputFormat常见的接口实现类包括:TextInputFormat、KeyValueTextInputFormat、NLineInputFormat、CombineTextInputFormat和自定义InputFormat等。
TextInputFormat是默认的FileInputFormat实现类。按行读取每条记录。
5 ) CombineTextInputFormat
如果使用,TextInputFormat切片机制是对任务按文件规划切片,不管文件多小,都会是一个单独的切片,都会交给一个MapTask,这样如果有大量小文件,就会产生大量的MapTask,处理效率极其低下。
应用场景: 所以,引入 CombineTextInputFormat切片机制:用于小文件过多的场景,它可以将多个小文件从逻辑上规划到一个切片中,这样,多个小文件就可以交给一个MapTask处理。
虚拟存储切片最大值: CombineTextInputFormat.setMaxInputSplitSize(job, 4194304);// 4m
注意: 虚拟存储切片最大值设置最好根据实际的小文件大小情况来设置具体的值。
生成过程:
(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
6) CombineTextInputFormat案例实操
为什么每次我的切片数量都是1???算了不学了没必要!
找到问题了:
-
首先一定要有四个小文件:
不做任何处理,默认执行的wordcount 切片是4个
-
其次在WordcountDriver中增加如下代码,运行程序,并观察运行的切片个数为3。
(a)驱动类中添加代码如下:
// 如果不设置InputFormat,它默认用的是TextInputFormat.class
job.setInputFormatClass(CombineTextInputFormat.class);
//虚拟存储切片最大值设置4m
CombineTextInputFormat.setMaxInputSplitSize(job, 4194304);
//number of splits:3
(b)虚拟存储切片最大值设置20m
// 如果不设置InputFormat,它默认用的是TextInputFormat.class
job.setInputFormatClass(CombineTextInputFormat.class);
//虚拟存储切片最大值设置20m
CombineTextInputFormat.setMaxInputSplitSize(job, 20971520);
//number of splits:1
看我极端案例:
3.2 MapReduce工作流程
3.3. Shuffle机制
3.3.1 概述:
Shuffle是Map方法之后,Reduce方法之前的数据处理过程称为Shuffle
3.3.2 Partition分区
问题的引出,统计结果按照条件输出到不同文件中去
默认的分区:
自定义分区:
分区实操
手机号案例:
手机号136、137、138、139开头都分别放到一个独立的4个文件中,其他开头的放到一个文件中。(5个文件)
1)在Writable自定义bean对象实现序列化接口中:增加一个分区类
代码如下:
package com.wts.mapreduce.partitioner;
import org.apache.hadoop.io.Text;
public class Partitionerself extends org.apache.hadoop.mapreduce.Partitioner<Text, FlowBean> {
@Override
public int getPartition(Text text, FlowBean flowBean, int numPartitions) {
//获取手机号前三位
String phone = text.toString();
String pre = phone.substring(0, 3);
//设置分区
int partiton;
if ("136".equals(pre)) {
partiton = 0;
} else if ("137".equals(pre)) {
partiton = 1;
} else if ("138".equals(pre)) {
partiton = 2;
} else if ("139".equals(pre)) {
partiton = 3;
} else {
partiton = 4;
}
//返回分区号
return partiton;
}
}
2)在Driver主函数中:指定自定义分区,同时指定相应的数量ReduceTask
//指定自定义分区器
job.setPartitionerClass(Partitionerself.class);
//同时指定相应的数量
job.setNumReduceTasks(5);
3)效果:
分区总结
案例
3.3.3WritableComparable排序
1)概述:
排序是MapReduce框架最重要的操作之一
MapTask和ReduceTask均会对数据key进行排序(默认行为),按照字典顺序排序(实现该排序的方法是快速排序)
排序分类:
-
部分排序
保证输出的每个文件内部有序 -
全排序
最终输出只有一个文件,其内部文件有序 -
辅助排序
-
二次排序
判断条件有两个
2)原理分析:bean对象做为key传输,需要实现WritableComparable接口重写compareTo方法,就可以实现排序。
3)实操
排序实操
在Writable序列化基础上实现 手机号倒叙排列:
代码实现步骤:
(1) 在FlowBean基础上实现WritableComparable接口,重写方法
添加一下重写方法的代码:
@Override
public int compareTo(FlowBean o) {
//降序排列
if (o.sumFlow < this.sumFlow) {
return -1;
} else if (o.sumFlow > this.sumFlow) {
return 1;
} else {
return 0;
}
}
(2) 重新编写Mapper类
package com.wts.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 FlowBeanMapper extends Mapper<LongWritable, Text, FlowBean, Text> {
//
private FlowBean outK = new FlowBean();
private Text outV = new Text();
@Override
protected void map(LongWritable key, Text value, Mapper<LongWritable, Text, FlowBean, Text>.Context context) throws IOException, InterruptedException {
//获取一行
//手机号 上 下 总
String line = value.toString();
String[] splits = line.split("\t");
//只对key排序 换句话就是:把要排序的作为key
outK.setUpFlow(Long.parseLong(splits[1]));//Flowbean作为Key
outK.setDownFlow(Long.parseLong(splits[2]));
outK.setSumFlow();
outV.set(splits[0]);//手机号作为value
//写出
context.write(outK, outV);
}
}
(3) 重新写Reducer类
package com.wts.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 FlowBeanMapper extends Mapper<LongWritable, Text, FlowBean, Text> {
//
private FlowBean outK = new FlowBean();
private Text outV = new Text();
@Override
protected void map(LongWritable key, Text value, Mapper<LongWritable, Text, FlowBean, Text>.Context context) throws IOException, InterruptedException {
//获取一行
//手机号 上 下 总
String line = value.toString();
String[] splits = line.split("\t");
//只对key排序 换句话就是:把要排序的作为key
outK.setUpFlow(Long.parseLong(splits[1]));//Flowbean作为Key
outK.setDownFlow(Long.parseLong(splits[2]));
outK.setSumFlow();
outV.set(splits[0]);//手机号作为value
//写出
context.write(outK, outV);
}
}
(4) 修改Driver类:(Map的输出数据类型)(文件读取文件夹只保留一个那啥)
package com.wts.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 FlowBeanDriver {
public static void main(String[] args) throws IOException, InterruptedException, ClassNotFoundException {
//1.获取job
Configuration conf = new Configuration();
Job job = Job.getInstance(conf);
//2.设置jar包
job.setJarByClass(FlowBeanDriver.class);
//3.关联mapper和reducer
job.setMapperClass(FlowBeanMapper.class);
job.setReducerClass(FlowBeanReducer.class);
//4.设置mapper 输出key和value的类型
job.setMapOutputKeyClass(FlowBean.class);
job.setMapOutputValueClass(Text.class);
//5.设置最后输出的key和value类型
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(FlowBean.class);
//6.设置输入、输出路径
FileInputFormat.setInputPaths(job, new Path("G:\\1_Program\\hadoop_learning\\hadoop-output\\flow-output\\output000"));
FileOutputFormat.setOutputPath(job, new Path("G:\\1_Program\\hadoop_learning\\hadoop-output\\flow-output\\fbcomparable-output"));
//7.提交job
boolean result = job.waitForCompletion(true);
System.exit(result ? 0 : 1);
}
}
效果:
进一步改进:(次要条件比较)
在FlowBean类重写compareTo方法里添加修改:
@Override
public int compareTo(FlowBean o) {
//降序排列
if (o.sumFlow < this.sumFlow) {
return -1;
} else if (o.sumFlow > this.sumFlow) {
return 1;
} else {
//按照上 流 降序排序
if (o.upFlow > this.upFlow) {
return -1;
} else if (o.upFlow < this.upFlow) {
return 1;
} else {
return 0;
}
}
}
效果:
前
后:
排序和分区结合实操
在上述代码的基础上,增加Partition类
package com.wts.mapreduce.writablecomparandpartitioner;
import org.apache.hadoop.io.Text;
public class Partitioner extends org.apache.hadoop.mapreduce.Partitioner<FlowBean, Text> {
@Override
public int getPartition(FlowBean flowBean, Text text, int numPartitions) {
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;
}
}
然后再在Driver主函数类中,设置分区
//设置partition分区器
job.setPartitionerClass(Partitioner.class);
job.setNumReduceTasks(5);
效果:五个reducer结果,内容降序排列
3.3.4 Combiner 合并
1)概述:
Combiner是在每一个MapTask所在的节点运行
Reducer是接收所有Mapper的数据结果
意义:减少网络传输量
(A,1)、(A,1)、(A,1)、… --> (A,1000))
应用前提,不能影响最终业务逻辑:比如求平均值的时候
自定义Combiner实操:wordcount
- 创建一个WordCountCombiner类:
package com.wts.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();
int sum;
@Override
protected void reduce(Text key, Iterable<IntWritable> values, Context context)
throws IOException, InterruptedException {
sum = 0;
//wts(1,1)
//累加
for (IntWritable value : values) {
sum += value.get();
}
outV.set(sum);
//写出
context.write(key, outV);
}
}
- 在Driver主函数里面,设置Combiner
//Combiner
job.setCombinerClass(WordCountCombiner.class);
效果:
3.4 OutputFormat数据输出
3.4.1 接口实现类
概述:OutputFormat是MapReduce输出的基类
默认是TextOutputFormat(按行输出)
还可以自定义OutputFormat:(自定义输出到mysql、hbase等存储框架中)
3.4.2 自定义案例
要求:log.txt文件 ,期望输出到 wts.log和other.log文件中
分析:
1)编写Mapper
package com.wts.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, Mapper<LongWritable, Text, Text, NullWritable>.Context context) throws IOException, InterruptedException {
//默认一行
context.write(value, NullWritable.get());
}
}
2)编写Reducer
package com.wts.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, Reducer<Text, NullWritable, Text, NullWritable>.Context context) throws IOException, InterruptedException {
//防止相同的key
for (NullWritable value : values) {
context.write(key, NullWritable.get());
}
}
}
3)自定义一个LogOutputFormat类
package com.wts.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;
public class LogOutputFormat extends FileOutputFormat<Text, NullWritable> {
@Override
public RecordWriter<Text, NullWritable> getRecordWriter(TaskAttemptContext job) throws IOException, InterruptedException {
LogRecordWritable lrw = new LogRecordWritable(job);
return lrw;
}
}
4)编写LogRecordWriter类
package com.wts.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;
public class LogRecordWritable extends RecordWriter<Text, NullWritable> {
private FSDataOutputStream wtsOut;
private FSDataOutputStream otherOut;
public LogRecordWritable(TaskAttemptContext job) {
//创建两条流
try {
FileSystem fs = FileSystem.get(job.getConfiguration());
wtsOut = fs.create(new Path("G:\\1_Program\\hadoop_learning\\hadoop-output\\outputformat\\1\\wts.log"));
otherOut = fs.create(new Path("G:\\1_Program\\hadoop_learning\\hadoop-output\\outputformat\\other\\others.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")) {
wtsOut.writeBytes(log + "\n");
} else {
otherOut.writeBytes(log + "\n");
}
}
@Override
public void close(TaskAttemptContext context) throws IOException, InterruptedException {
//关流
IOUtils.closeStream(wtsOut);
IOUtils.closeStream(otherOut);
}
}
5)编写Driver
package com.wts.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, InterruptedException, ClassNotFoundException {
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("G:\\1_Program\\hadoop_learning\\hadoop-input\\outputformat"));
//虽然我们自定义了outputformat,但是因为我们的outputformat继承自fileoutputformat
//而fileoutputformat要输出一个_SUCCESS文件,所以在这还得指定一个输出目录
FileOutputFormat.setOutputPath(job, new Path("G:\\1_Program\\hadoop_learning\\hadoop-output\\outputformat\\logout"));
boolean result = job.waitForCompletion(true);
System.out.println(result ? 0 : 1);
}
}
效果展示:
3.5 Join应用(表关联)
3.5.1 Reduce Join
多个表的关联
需求:
两个表关联:
最后形成新的表:
分析过程:
上代码:
1)编写WritableBean
package com.wts.mapreduce.reducerjoin;
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;
private String pid;
private int amount;
private String pname;
private String flag; //判断order表还是pd表
public TableBean() {
}
public TableBean(String id, String pid, int amount, String pname, String flag) {
this.id = id;
this.pid = pid;
this.amount = amount;
this.pname = pname;
this.flag = flag;
}
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;
}
//重写toString方法
@Override
public String toString() {
return id + '\t' + pname + '\t' + amount;
}
@Override
public void write(DataOutput dataOutput) throws IOException {
dataOutput.writeUTF(id);
dataOutput.writeUTF(pid);
dataOutput.writeInt(amount);
dataOutput.writeUTF(pname);
dataOutput.writeUTF(flag);
}
@Override
public void readFields(DataInput dataInput) throws IOException {
this.id = dataInput.readUTF();
this.pid = dataInput.readUTF();
this.amount = dataInput.readInt();
this.pname = dataInput.readUTF();
this.flag = dataInput.readUTF();
}
}
2)Mapper (setup的代码不是很理解)
package com.wts.mapreduce.reducerjoin;
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(Mapper<LongWritable, Text, Text, TableBean>.Context context) throws IOException, InterruptedException {
//获取对应的文件名称
InputSplit Split = context.getInputSplit();
FileSplit fileSplit = (FileSplit) Split;
filename = fileSplit.getPath().getName();
}
@Override
protected void map(LongWritable key, Text value, Mapper<LongWritable, Text, Text, TableBean>.Context context) throws IOException, InterruptedException {
String line = value.toString();
if (filename.contains("order")) {
String[] split = line.split("\t");
//封装
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);
}
}
3)Reducer
package com.wts.mapreduce.reducerjoin;
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, Reducer<Text, TableBean, TableBean, NullWritable>.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 (value.getFlag().equals("order")) {
//order表
//创建零时存储value
TableBean tmpOrderBean = new TableBean();
try {
//暂时存储到tmp中
BeanUtils.copyProperties(tmpOrderBean, value);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
//将零时tmp对象添加到集合中
orderBeans.add(tmpOrderBean);
} else {
//pd表格
try {
BeanUtils.copyProperties(pdBean, value);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
}
//遍历集合 替换掉order中的pid -->name 之后写出
for (TableBean orderBean : orderBeans) {
orderBean.setPname(pdBean.getPname());
//写出
context.write(orderBean, NullWritable.get());
}
}
}
4)Driver
package com.wts.mapreduce.reducerjoin;
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, InterruptedException, ClassNotFoundException {
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("G:\\1_Program\\hadoop_learning\\hadoop-input\\joinbean"));
FileOutputFormat.setOutputPath(job, new Path("G:\\1_Program\\hadoop_learning\\hadoop-output\\reducejoin\\output01"));
boolean b = job.waitForCompletion(true);
System.exit(b ? 0 : 1);
}
}
小结:表格关联的时候的应用,里面代码思路不是很容易吃透
3.5.2 Mapjoin
使用环境:适用于一张表十分小、一张表很大的场景。
思考:在Reduce端处理过多的表,非常容易产生数据倾斜。怎么办?
答案:在Map端缓存多张表,提前处理业务逻辑,这样增加Map端业务,减少Reduce端数据的压力,尽可能的减少数据倾斜。
具体办法:采用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"));
实操:(和reducejoin一样的案例)
1)Mapper
package com.wts.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> {
private HashMap<String, String> pdMap = new HashMap();
private Text outK = new Text();
@Override
protected void setup(Mapper<LongWritable, Text, Text, NullWritable>.Context context) throws IOException, InterruptedException {
//获取缓存文件,并把文件内容封装到集合pd.txt
URI[] cacheFiles = context.getCacheFiles();
Path path = new Path(cacheFiles[0]);
FileSystem fs = FileSystem.get(context.getConfiguration());
FSDataInputStream fis = fs.open(new Path(cacheFiles[0]));
//通过包装流转换为reader,方便按行读取
BufferedReader reader = new BufferedReader(new InputStreamReader(fis, "UTF-8"));
//逐行读取,按行处理
String line;
while (StringUtils.isNotEmpty(line = reader.readLine())) {
//切割
//01 小米
String[] split = line.split("\t");
pdMap.put(split[0], split[1]);
}
//关流
IOUtils.closeStream(reader);
}
@Override
protected void map(LongWritable key, Text value, Mapper<LongWritable, Text, Text, NullWritable>.Context context) throws IOException, InterruptedException {
//读取order
//1001 01 1
String[] split = value.toString().split("\t");
//通过每行数据的pid 对应取出pdMap里面的pname
String pname = pdMap.get(split[1]);
//封装: id pname amount
outK.set(split[0] + "\t" + pname + "\t" + split[2]);
//写出
context.write(outK, NullWritable.get());
}
}
2)Driver
package com.wts.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 ClassNotFoundException, InterruptedException, IOException, URISyntaxException {
// 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:///G:/1_Program/hadoop_learning/hadoop-input/mapjoin/pd/pd.txt"));
// Map端Join的逻辑不需要Reduce阶段,设置reduceTask数量为0
job.setNumReduceTasks(0);
// 6 设置输入输出路径
FileInputFormat.setInputPaths(job, new Path("G:\\1_Program\\hadoop_learning\\hadoop-input\\mapjoin\\order"));
FileOutputFormat.setOutputPath(job, new Path("G:\\1_Program\\hadoop_learning\\hadoop-output\\mapjoin\\output00"));
// 7 提交
boolean b = job.waitForCompletion(true);
System.exit(b ? 0 : 1);
}
}
效果:
3.6 数据清洗(ETL)
清理的过程往往只需要运行Mapper程序,不需要运行Reduce程序。
案例:去除日志中字段个数小于等于11的日志(只用Map阶段清洗)
上代码:
1)Mapper
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 ETL_Mapper extends Mapper<LongWritable, Text, Text, NullWritable> {
@Override
protected void map(LongWritable key, Text value, Mapper<LongWritable, Text, Text, NullWritable>.Context context) throws IOException, InterruptedException {
String line = value.toString();
//解析
boolean result = parseLog(line, context);
//不合法退出
if (!result) {
return;
}
//输出
context.write(value, NullWritable.get());
}
private boolean parseLog(String line, Context context) {
String[] split = line.split(" ");
if (split.length > 11) {
return true;
} else {
return false;
}
}
}
2)Driver
package com.wts.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;
import java.io.IOException;
public class ETL_Driver {
public static void main(String[] args) throws IOException, InterruptedException, ClassNotFoundException {
// 输入输出路径需要根据自己电脑上实际的输入输出路径设置
args = new String[]{"G:\\1_Program\\hadoop_learning\\hadoop-input\\etl", "G:\\1_Program\\hadoop_learning\\hadoop-output\\ETL\\output00"};
// 1 获取job信息
Configuration conf = new Configuration();
Job job = Job.getInstance(conf);
// 2 加载jar包
job.setJarByClass(ETL_Driver.class);
// 3 关联map
job.setMapperClass(ETL_Mapper.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);
}
}
效果:
3.7 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。
4.Hadoop数据压缩
直接上实操:
1)map端压缩
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);
2)reduce端压缩
FileInputFormat.setInputPaths(job, new Path(args[0]));
FileOutputFormat.setOutputPath(job, new Path(args[1]));
// 设置reduce端输出压缩开启
FileOutputFormat.setCompressOutput(job, true);
// 设置压缩的方式
FileOutputFormat.setOutputCompressorClass(job, BZip2Codec.class);
// FileOutputFormat.setOutputCompressorClass(job, GzipCodec.class);
// FileOutputFormat.setOutputCompressorClass(job, DefaultCodec.class);
map和reduce压缩可以不对应