目录
- 前言
- 一、概述
- 1. MapReduce定义、优缺点
- 2. MapReduce核心思想、进程
- 3. MapReduce编程——WordCount
- 二、Hadoop序列化
- 1. 序列化概述
- 2. 自定义bean对象实现序列化接口(Writable)
- 三、MapReduce框架原理
- 1. InputFormat 数据输入
- 2. TextInputFormat
- 3. CombineTextInputFormat切片机制
- 4. MapReduce工作流程
- 5. Shuffer机制
- 6. OutputFormat数据输出
- 7. MapReduce内核源码解析
- 8. Join多种应用
- 9. 数据清洗(ETL)
- 四、Hadoop数据压缩
- 其他
MapReduce是hadoop的核心组件之一,hadoop要分布式包括两部分,一是分布式文件系统hdfs,一是分布式计算框,就是mapreduce,二者缺一不可,也就是说,可以通过mapreduce很容易在hadoop平台上进行分布式的计算编程
sftp命令:Windows下登录Hadoop102xftp root@hadoop102
,lcd
切换Windows路径,cd
切换Linux路径,get
从Linux下载到Windows,put
从Windows上传到Linux
MapReduce是一个分布式运算程序的编程框架,是用户开发“基于Hadoop 的数据分析应用”的核心框架。MapReduce核心功能是将用户编写的业务逻辑代码和自带默认组件整合成一个完整的分布式运算程序,并发运行在一个Hadoop集群上
优点:
缺点:
“分而治之”是MapReduce的核心思想,它表示把一个大规模的数据集切分成很多小的单独的数据集,然后放在多个机器上同时处理。MapReduce把整个并行运算过程高度抽象到两个函数上,一个是map另一个是reduce。Map函数就是分而治之中的“分”,reduce函数就是分而治之中的“治”
MapReduce进程:一个完整的MapReduce程序在分布式运行时有三类实例进程
官方WordCount源码:采用IDEA反编译源码,发现WordCount案例有Map类、Reduce类和驱动类。且数据的类型是 Hadoop自身封装的序列化类型
# 打开一个cmd,下载含有WordCount案例的jar包
sftp root@hadoop102
lcd F:\
cd /opt/module/hadoop/share/hadoop/mapreduce
get hadoop-mapreduce-examples-3.1.3.jar
IDEA轻松反编译jar包点此前往,不再赘述…
常用数据序列化类型:
java 类型 | Hadoop Writable 类型 |
---|---|
Boolean | BooleanWritable |
Byte | ByteWritable |
Int | IntWritable |
Float | FloatWritable |
Long | LongWritable |
Double | DoubleWritable |
String | Text |
Map | MapWritable |
Array | ArrayWritable |
Null | NullWritable |
MapReduce 编程规范:用户编写的程序分成三个部分: Mapper、Reducer 和 Driver
用户自定义的Mapper要继承自己的父类
Mapper的输入数据是K-V对的形式(K-V的类型可自定义)
Mapper中的业务逻辑写在map()方法中
Mapper的输出数据是K-V对的形式(K-V的类型可自定义)
map()方法(MapTask进程)对每一个调用一次
用户自定义的Reducer要继承自己的父类
Reducer的输入数据类型对应Mapper的输出数据类型,也是KV
Reducer的业务逻辑写在reduce()方法中
ReduceTask进程对每一组相同k的组调用一次reduce()方法
相当于YARN集群的客户端,用于提交我们整个程序到YARN集群,提交的是封装了MapReduce程序相关运行参数的job对象
继上次完成maven配置后,IDEA进入maven_workspace,在java文件夹下创建
com.ygy.mapreduce.wordcount
包,创建如下三个java类
WordCountMapper类的代码如下(注意导包):
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;
/**
* public class Mapper
*
* KEYIN,map阶段输入key的类型:LongWritable
* VAULEIN,map阶段输入value类型:Text
* KEYOUT,map阶段输出的key类型:Text
* VALUEOUT,map阶段输出的value类型:IntWritable
*/
public class WordCountMapper extends Mapper <LongWritable, Text,Text, IntWritable>{
// 固定格式(outKey,1)
private Text outKey = new Text();
private IntWritable outValue = new IntWritable(1);
// 重写 map方法
@Override
protected void map(LongWritable key, Text value, Mapper<LongWritable, Text, Text, IntWritable>.Context context) throws IOException, InterruptedException {
// 1.读取一行输入的数据
// ygy ygy
String line = value.toString();
// 2.以空格为分隔符切割数据
// ygy
// ygy
String[] words = line.split(" ");
// 3.循环写出
for(String word : words){
// 封装outKey
outKey.set(word);
// 写出
context.write(outKey,outValue);
}
}
}
context.write()
方法是 Apache Hadoop 中的一个重要方法,主要用于输出结果。它是org.apache.hadoop.mapreduce.Reducer
类的一个成员方法,通常在 Reducer 的 reduce() 方法中调用。它的作用是将计算得到的键/值对写入到输出上下文中,在使用context.write()
方法之前将键和值序列化为字节数组,因为 Hadoop 中的 MapReduce 框架会将这些字节数组传输到输出端
WordCountReducer类的编写(注意导包):
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;
import java.io.IOException;
/**
* public class Reducer
*
* KEYIN,reduce阶段输入key的类型:Text
* VAULEIN,reduce阶段输入value类型:LongWritable
* KEYOUT,reduce阶段输出的key类型:Text
* VALUEOUT,reduce阶段输出的value类型:IntWritable
*/
public class WordCountReducer extends Reducer<Text, IntWritable, Text, IntWritable> {
// 固定格式(outKey,1)
private IntWritable outValue = new IntWritable();
// 重写 reduce方法
@Override
protected void reduce(Text key, Iterable<IntWritable> values, Reducer<Text, IntWritable, Text, IntWritable>.Context context) throws IOException, InterruptedException {
// 1.定义一个计数器
int sum = 0;
// ygy,(1,1,1,1,1,1,1,1,1,1,1,1,1,1,1)
// 2.循环values,累加求和
for(IntWritable value : values){
sum += value.get();
}
// 3.封装outValue
outValue.set(sum);
// 4.写出
context.write(key,outValue);
}
}
WordCountDriver类的编写(注意导包,最后执行main方法即可):
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 configuration = new Configuration();
Job job = Job.getInstance(configuration);
// 2.设置jar存储位置
job.setJarByClass(WordCountDriver.class);
// 3.关联Map和Reduce类
job.setMapperClass(WordCountMapper.class);
job.setReducerClass(WordCountReducer.class);
// 4.设置Mapper阶段输出数据的key和value类型
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(IntWritable.class);
// 5.设置最终数据输出的key和value类型
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(IntWritable.class);
// 6.设置输入路径和输出路径
// input.txt在F:\\input\\inputword路径下
// 输入到F:\\output下,不能有outputword这个文件夹
FileInputFormat.setInputPaths(job, new Path("F:\\input\\inputword"));
FileOutputFormat.setOutputPath(job, new Path("F:\\output\\outputword"));
// 7.提交job
boolean result = job.waitForCompletion(true);
System.exit(result ? 0 : 1);
}
}
在Hadoop中,一个Job代表一个计算任务,它是Hadoop分布式计算框架中的基本单位。一个Job可以包含多个MapReduce任务,MapReduce是一种用于大规模数据处理的编程模型。当一个Job被提交给Hadoop集群时,Hadoop会将其分解成多个任务(Tasks)并在集群中的各个节点上执行。这些任务包括Map任务和Reduce任务,它们分别执行Map函数和Reduce函数,并通过网络进行数据交换和传输
提交到HDFS集群上测试:
上面通过Window依赖在Windows环境中运行,但是一般是在Linux环境下运行,故我们需要用maven打包成jar包
# 在pom.xml中加入插件,注意本地和虚拟机的jdk版本要统一
<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>
修改输入路径和输出路径,使其接受传参
// 6.设置输入路径和输出路径
FileInputFormat.setInputPaths(job, new Path(args[0]));
FileOutputFormat.setOutputPath(job, new Path(args[1]));
上传重新生成的jar包(任一皆可,重命名为wc.jar移动到F盘)
# 上传
sftp root@hadoop102
lcd F:\
cd /root/
put wc.jar
# Linux下执行,Driver要敲Driver类的全类名
hadoop jar wc.jar com.ygy.mapreduce.wordcount2.WordCountDriver /input /output
打开Hadoop102web端看文件输出
打开Hadoop103web端看任务运行状态
假如报错:has been compiled by a more recent version of the Java Runtime (class file version 61.0), this version of the Java Runtime only recognizes class file versions up to 52.0
翻译:这个文件编译的JDK版本号应该是52(8),但是实际是61(17)
原因:windows上jdk在17及以上,SpringApplication该类的major版本是61,所以Linux上为jdk1.8用52的版本编译打包就会报错了
方法:
1、升级JDK版本到17及以上
2、降低依赖,不用spring6了
3、这么麻烦,懒得弄了
序列化: 序列化就是把内存中的对象,转换成字节序列(或其他数据传输协议)以便于存储到磁盘(持久化)和网络传输
反序列化: 反序列化就是将收到字节序列(或其他数据传输协议)或者是磁盘的持久化数据,转换 成内存中的对象
为什么要用序列化?为什么不用Java的序列化?
Hadoop 序列化特点:
在企业开发中往往常用的基本序列化类型不能满足所有需求,比如在 Hadoop 框架内部传递一个 bean 对象,那么该对象就需要实现序列化接口
具体实现bean对象序列化步骤:
public FlowBean() {
}
@Override
public void write(DataOutput dataOutput) throws IOException {
dataOutput.writeLong(upFlow);
dataOutput.writeLong(downFlow);
dataOutput.writeLong(sumFlow);
}
@Override
public void readFields(DataInput dataInput) throws IOException {
this.upFlow = dataInput.readLong();
this.downFlow = dataInput.readLong();
this.sumFlow = dataInput.readLong();
}
- 注意反序列化的顺序和序列化的顺序完全一致
- 要想把结果显示在文件中,需要重写toString(),可用
\t
分开,方便后续用- 如果需要将自定义的 bean 放在 key 中传输,则还需要实现 Comparable 接口,因为 MapReduce 框中的 Shuffle 过程要求对 key 必须能排序
FlowBean.java:
import org.apache.hadoop.io.Writable;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
/*
* 1.定义类实现Writable接口
* 2.重写序列化和反序列化方法
* 3.重写空参构造
* 4.重写toString方法
* */
public class FlowBean implements Writable {
private long upFlow;
private long downFlow;
private long sumFlow;
// 空参构造
public FlowBean() {
}
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() {
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 ;
}
}
FlowMapper.java:
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;
import java.io.IOException;
public class FlowMapper extends Mapper<LongWritable, Text, Text, FlowBean> {
private Text k = new Text();
private FlowBean v = new FlowBean();
@Override
protected void map(LongWritable key, Text value, Mapper<LongWritable, Text, Text, FlowBean>.Context context) throws IOException, InterruptedException {
// 获取一行
// 13480253104 192.196.100.1 www.atguigu.com 2481 24681 200
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];
// 封装k和v
k.set(phone);
v.setUpFlow(Long.parseLong(up));
v.setDownFlow(Long.parseLong(down));
v.setSumFlow();
// 写出
context.write(k, v);
}
}
FlowReducer.java:
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;
import java.io.IOException;
public class FlowReducer extends Reducer<Text, FlowBean, Text, FlowBean> {
private FlowBean value = new FlowBean();
@Override
protected void reduce(Text key, Iterable<FlowBean> values, Reducer<Text, FlowBean, Text, FlowBean>.Context context) throws IOException, InterruptedException {
// 遍历集合累加值
long totalUpFlow = 0;
long totalDownFlow = 0;
for (FlowBean value : values) {
totalUpFlow += value.getUpFlow();
totalDownFlow += value.getDownFlow();
}
// 封装对象
value.setUpFlow(totalUpFlow);
value.setDownFlow(totalDownFlow);
value.setSumFlow();
context.write(key, value);
}
}
FlowDriver.java:
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, InterruptedException, ClassNotFoundException {
// 1. 获取job对象
Configuration conf = new Configuration();
Job job = Job.getInstance(conf);
// 2. 关联Driver类
job.setJarByClass(FlowDriver.class);
// 3. 关联Mapper和Reducer
job.setMapperClass(FlowMapper.class);
job.setReducerClass(FlowReducer.class);
// 4. 设置Map端输出kv类型
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(FlowBean.class);
// 5. 设置程序最终输出的kv类型
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(FlowBean.class);
// 6. 设置程序的输入输出路径
FileInputFormat.setInputPaths(job, new Path("F:\\hadoopTest\\input\\phone_data.txt"));
FileOutputFormat.setOutputPath(job, new Path("F:\\hadoopTest\\output2"));
// 7. 提交job
boolean result = job.waitForCompletion(true);
System.exit(result ? 0 : 1);
}
}
MapTask 的并行度决定Map阶段的任务处理并发度,进而影响到整个Job的处理速度
数据块:Block是HDFS物理上把数据分成一块一块,数据块是HDFS存储数据单位
数据切片:数据切片只是在逻辑上对输入进行分片,并不会在磁盘上将其切分成片进行存储。数据切片是MapReduce程序计算输入数据的单位,一个切片会对应启动一个MapTask
MapTask并行度决定机制:
FileInputFormat切片源码解析:
- 获取文件大小fs.sizeOf(ss.txt)
- 计算切片大小
computeSplitSize(Math.max(minSize,Math.min(maxSize,blocksize)))=blocksize=128M- 默认情况下,切片大小=blocksize
- 开始切,形成第1个切片: ss.txt–0:128M 第2个切片ss.txt–128:256M第3个切片ss.txt—256M:300M(每次切片时,都要判断切完剩下的部分是否大于块的1.1倍,不大于1.1倍就划分一块切片)
- 将切片信息写到一个切片规划文件中
- 整个切片的核心过程在getSplit()方法中完成
- ImputSplit只记录了切片的元数据信息,比如起始位置、长度以及所在的节点列表等
FileInputFormat切片机制:
思考:在运行MapReduce程序时,输入的文件格式包括:基于行的日志文件、二进制格式文件、数据库表等。那么,针对不同的数据类型,MapReduce是如何读取这些数据的呢?
FileInputFormat常见的接口实现类包括:TextInputFormat、KeyValueTextInputFormat、NLineInputFormat、CombineTextInputFormat和自定义InputFormat等
TextInputFormat:是默认的FileInputFormat实现类。按行读取每条记录。键是存储该行在整个文件中的起始字节偏移量, LongWritable类型。值是这行的内容,不包括任何行终止符(换行符和回车符),Text类型
框架默认的TextInputFormat 切片机制是对任务按文件规划切片,不管文件多小,都会是一个单独的切片,都会交给一个MapTask,这样如果有大量小文件,就会产生大量的MapTask,处理效率极其低下
需求:将输入的大量小文件合并成一个切片统一处理,准备4个小文件a.txt、b.txt、c.txt、d.txt,期望一个切片处理4个文件
# 将wordcount包下三个java类一起复制到新建包combineTextInputFormat里,并且修改一下输出输入路径
FileInputFormat.setInputPaths(job, new Path("F:\\hadoopTest\\input\\inputword"));
FileOutputFormat.setOutputPath(job, new Path("F:\\hadoopTest\\outputCombine1"));
实现过程:
number of splits:4
// 如果不设置工InputFormat,它默认用的是 TextInputFormat.class
job.setInputFormatClass(CombineTextInputFormat.class);
// 虚拟存储切片最大值设置4m
CombineTextInputFormat.setMaxInputSplitSize(job,4194304);
排序是MapReduce框架中最重要的操作之一(提高效率)。MapTask和ReduceTask均会对数据按照key进行排序。该操作属于Hadoop的默认行为。任何应用程序中的数据均会被排序,而不管逻辑上是否需要。默认排序是按照字典顺序排序,且实现该排序的方法是快速排序。
- 对于MapTask:它会将处理的结果暂时放到环形缓冲区中,当环形缓冲区使用率达到一定阈值后,再对缓冲区中的数据进行一次快速排序,并将这些有序数据溢写到磁盘上,而当数据处理完毕后,它会对磁盘上所有文件进行归并排序。
- 对于ReduceTask:它从每个MapTask上远程拷贝相应的数据文件,如果文件大小超过一定阈值,则溢写磁盘上,否则存储在内存中。如果磁盘上文件数目达到一定阈值,则进行一次归并排序以生成一个更大文件;如果内存中文件大小或者数目超过一定阈值,则进行一次合并后将数据溢写到磁盘上。当所有数据拷贝完毕后,ReduceTask统一对内存和磁盘上的所有数据进行一次归并排序。
排序分类:
二次排序:在自定义排序过程中,如果compareTo中的判断条件为两个即为二次排序
Map方法之后,Reduce方法之前的数据处理过程称之为Shuffle
Partition分区
默认分区是根据key的hashCode对ReduceTasks个数取模得到的,用户没法控制哪个key存储到哪个分区
自定义Partitioner步骤:
job.setPartitioncrClass(CustomPartitioner.class);
job.setNumReduceTasks(5);
Partition分区案例分析:
实现:由于是在序列化案例的结果后将手机号不同划分到不同分区,复制Writable包下所有java类过来并且增加一个ProvincePartitioner分区类:
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) {
// 1.获取手机号前三位
String preNum = text.toString().substring(0, 3);
// 2.分区
int partition = 4;
if ("136".equals(preNum)){
partition = 0;
}else if ("137".equals(preNum)){
partition = 1;
}else if ("138".equals(preNum)){
partition = 2;
}else if ("139".equals(preNum)){
partition = 3;
}
return partition;
}
}
在FlowDriver.java中加上下面代码后,运行即可:
job.setPartitionerClass(ProvincePartitioner.class);
job.setNumReduceTasks(5);
自定义排序WritableComparable:bean对象做为key传输,必须要有排序功能,需要实现WritableComparable接口重写compareTo方法,就可以实现排序
WritableComparable排序案例实操(全排序)
由于是在序列化案例的结果后对总流量进行倒序排序,复制Writable包下所有java类过来,修改FlowBean.java的接口为WritableComparable
,并实现compareTo方法:
@Override
public int compareTo(FlowBean o) {
// 按照总流量大小,倒序排列
return this.sumFlow > o.getSumFlow() ? -1 : 1;
}
输入数据都边了,处理方式自然不同,修改FlowMapper.java
public class FlowMapper extends Mapper<LongWritable, Text, FlowBean, Text> {
private FlowBean outK = new FlowBean();
private Text outV = new Text();
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
// 1.获取一行
String line = value.toString();
// 2.切割
String[] split = line.split("\t");
// 3.封装对象
outV.set(split[0]);
outK.setUpFlow(Long.parseLong(split[1]));
outK.setDownFlow(Long.parseLong(split[2]));
outK.setSumFlow();
// 4.写出
context.write(outK, outV);
}
修改FlowReducer.java
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);
}
}
}
修改FlowDriver.java,执行即可
// 互换kv类型
job.setMapOutputKeyClass(FlowBean.class);
job.setMapOutputValueClass(Text.class);
若想在总流量相同时按照上行流量倒序排序,只需修改FlowBean.java中修改compareTo方法即可
WritableComparable排序案例实操(区内排序)
实现:复制writableComarable包下所有java类过来,在FlowDriver中加上:
job.setPartitionerClass(ProvincePartitioner.class);
job.setNumReduceTasks(5);
Combiner合并
需求:统计过程中对每一个MapTask的输出进行局部汇总,以减小网络传输量即采用Combiner功能,希望在Map阶段就能看到统计结果
实现:复制wordcount包中所有类到新建包combiner中,并新建WordCountCombiner类:
public class WordCountCombiner extends Reducer<Text, IntWritable, Text, IntWritable> {
private IntWritable outV = 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();
}
outV.set(sum);
context.write(key, outV);
}
}
在FlowDriver中加入
// 设置Combiner类
job.setCombinerClass(WordCountCombiner.class);
// 发现combiner类和WordCountReducer类一样,故可以直接用reducer不用编写Combiner类(常用,直接提前在Map阶段搞好)
job.setCombinerClass(WordCountReducer.class);
OutputFormat是MapReduce输出的基类,所有实现MapReduce输出都实现了OutputFormat接口。下面我们介绍几种常见的OutputFormat实现类。
自定义OutputFormat案例:
实现:创建outputFormat包,创建LogDriver、LogMapper、LogOutputFormat、LogRecordWriter、LogReducer五个Java类,代码如下:
// Mapper 类
import org.apache.hadoop.io.NullWritable;
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 {
// http://www.baidu.com
// http://www.google.cos
//(http://www.gaogle.com,NullWritable)
// 不做任何处理,直接输出
context.write(value,NullWritable.get());
}
}
// Reducer 类
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 {
// http://www.baidu.com
// http://www.baidu.com
// 防止有重复的数据,丢数据
for (NullWritable value : values) {
context.write(key, NullWritable.get());
}
}
}
自定义outputformat类:
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;
}
}
继承了 RecordWriter 的 LogRecordWriter 类:
public class LogRecordWriter extends RecordWriter<Text, NullWritable>{
private FSDataOutputStream out;
private FSDataOutputStream out2;
public LogRecordWriter(TaskAttemptContext job) throws IOException {
// 创建两条流
FileSystem fs =FileSystem.get(job.getConfiguration());
out = fs.create(new Path("F:\\hadoopTest\\log.txt"));
out2 = fs.create(new Path("F:\\hadoopTest\\log2.txt"));
}
@Override
public void write(Text key, NullWritable value) throws IOException, InterruptedException {
// 判断key中是否有baidu
if (key.toString().contains("baidu")){
out.write(key.toString().getBytes());
}else {
out2.write(key.toString().getBytes());
}
}
@Override
public void close(TaskAttemptContext context) throws IOException, InterruptedException {
// 关闭流
if (out != null){
out.close();
}
if (out2 != null){
out2.close();
}
}
}
最后是Driver类:
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("F:\\hadoopTest\\log.txt"));
// 虽然我们自定义了outputformat,但是因为我们的outputformat继承自fileoutputformat
// 而fileoutputformat要输出一个_SUCCESS文件,所以,在这还得指定一个输出目录
LogOutputFormat.setOutputPath(job, new Path("F:\\hadoopTest\\output"));
boolean b = job.waitForCompletion(true);
System.exit(b ? 0 : 1);
}
}
MapTask五大阶段:
OutputCollector.collect()
输出结果。在该函数内部,它会将生成的key/value分区(调用Partitioner),并写入一个环形内存缓冲区中溢写阶段详情:
- 利用快速排序算法对缓存区内的数据进行排序,排序方式是,先按照分区编号Partition进行排序,然后按照key进行排序。这样,经过排序后,数据以分区为单位聚集在一起,且同一分区内所有数据按照key有序
- 按照分区编号由小到大依次将每个分区中的数据写入任务工作目录下的临时文件
output/spillN.out
(N表示当前溢写次数)中。如果用户设置了Combiner,则写入文件之前,对每个分区中的数据进行一次聚集操作- 将分区数据的元信息写到内存索引数据结构SpillRecord中,其中每个分区的元信息包括在临时文件中的偏移量、压缩前数据大小和压缩后数据大小。如果当前内存索引大小超过1MB,则将内存索引写到文件
output/spillN.out.index
中
当所有数据处理完后,MapTask会将所有临时文件合并成一个大文件,并保存到文件
output/file.out
中,同时生成相应的索引文件output/file.out.index
。在进行文件合并过程中,MapTask以分区为单位进行合并。对于某个分区,它将采用多轮递归合并的方式。每轮合并mapreduce.task.io.sort.factor
(默认10)个文件,并将产生的文件重新加入待合并列表中,对文件排序后,重复以上过程,直到最终得到一个大文件
ReduceTask工作机制图示:
ReduceTask三大阶段:
ReduceTask并行度决定机制
MapTask并行度由切片个数决定,切片个数由输入文件和切片规则决定
ReduceTask 的并行度同样影响整个Job 的执行并发度和执行效率,但与 MapTask 的并发数由切片数决定不同,ReduceTask数量的决定是可以直接手动设置job.setNumReduceTasks(4);
注意事项
- ReduceTask=0,表示没有Reduce阶段,输出文件个数和Map个数一致。
- ReduceTask默认值就是1,所以输出文件个数为一个。
- 如果数据分布不均匀,就有可能在Reduce阶段产生数据倾斜
- ReduceTask数量并不是任意设置,还要考虑业务逻辑需求,有些情况下,需要计算全局汇总结果,就只能有1个ReduceTask。
- 具体多少个ReduceTask,需要根据集群性能而定。
- 如果分区数不是1,但是ReduceTask为1,不执行分区过程。因为在MapTask的源码中,执行分区的前提是先判断ReduceNum个数是否大于1。不大于1肯定不执行。
Reduce Join:
案例:将两张表通过相同字段将两张表连接起来合并为一张表
创建reduceJoin包编写java类,TableBean:
public class TableBean implements Writable { // 序列化
// 两张表的字段
// id pid amount
// pid pname
private String id; // 订单id
private String pid;// 产品id
private int amount; // 产品数量
private String pname;// 产品名称
private String flag; // 表的标记
// 无参构造
public TableBean() {
}
// 省去getter/setter函数
// 序列化
@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 {
id = in.readUTF();
pid = in.readUTF();
amount = in.readInt();
pname = in.readUTF();
flag = in.readUTF();
}
@Override
public String toString() {
// id pid amount
return id + "\t" + pname + "\t" + amount;
}
}
TableMapper:
public class TableMapper extends Mapper<LongWritable, Text, Text, TableBean> {
private String name;
private Text outK = new Text();
private TableBean outV = new TableBean();
@Override
protected void setup(Context context) throws IOException, InterruptedException {
// 初始化方法,只执行一次
// 初始化 order 和 pd 两张表的数据
FileSplit split = (FileSplit) context.getInputSplit();
// 通过切片对象获取文件名
name = 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();
// 判断是哪张表的数据
if (name.contains("order")){
String[] fields = line.split("\t");
// 封装 key 和 value
outK.set(fields[1]);
outV.setId(fields[0]);
outV.setPid(fields[1]);
outV.setAmount(Integer.parseInt(fields[2]));
outV.setPname("");
outV.setFlag("order");
}else{
//处理商品表
String[] fields = line.split("\t");
// 封装 key 和 value
outK.set(fields[0]);
outV.setId("");
outV.setPid(fields[0]);
outV.setAmount(0);
outV.setPname(fields[1]);
outV.setFlag("pd");
}
// 写出
context.write(outK,outV);
}
}
TableReducer:
public class TableReducer extends Reducer<Text, TableBean, TableBean, NullWritable> {
@Override
protected void reduce(Text key, Iterable<TableBean> values, Context context) throws java.io.IOException, InterruptedException {
// 准备初始化集合
TableBean pdBean = new TableBean();
ArrayList<TableBean> orderBeans = new ArrayList<>();
// 遍历 values,区分两张表
values.forEach(tableBean -> {
if ("order".equals(tableBean.getFlag())){
// 订单表
TableBean orderBean = new TableBean();
try {
org.apache.commons.beanutils.BeanUtils.copyProperties(orderBean,tableBean);
} catch (Exception e) {
e.printStackTrace();
}
orderBeans.add(orderBean);
}else{
// 产品表
try {
org.apache.commons.beanutils.BeanUtils.copyProperties(pdBean,tableBean);
} catch (Exception e) {
e.printStackTrace();
}
}
});
// 表的拼接
orderBeans.forEach(orderBean -> {
orderBean.setPname(pdBean.getPname());
try {
context.write(orderBean,NullWritable.get());
} catch (Exception e) {
e.printStackTrace();
}
});
}
}
TableDriver:
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.setMapOutputValueClass(Text.class);
job.setMapOutputKeyClass(TableBean.class);
job.setOutputKeyClass(TableBean.class);
job.setOutputValueClass(NullWritable.class);
FileInputFormat.setInputPaths(job, new Path("F:\\hadoopTest\\input\\inputtable"));
FileOutputFormat.setOutputPath(job, new Path("F:\\hadoopTest\\output888"));
boolean b = job.waitForCompletion(true);
System.exit(b ? 0 : 1);
}
}
缺点:
这种方式中,合并的操作是在Reduce阶段完成,Reduce端的处理压力太大,Map节点的运算负载则很低,资源利用率不高,且在 Reduce阶段极易产生数据倾斜
解决方案:
Map端实现数据合并==>Map Join
Map Join
// 缓存普通文件到Task运行节点
job.addCacheFile(new URI("file://..."));
//如果是集群运行,需要设置HDFS路径
ob.addCacheFile(new URI("hdfs://..."));
同上一个案例,不同的是要求在Map阶段合并表,就不用reduce了
创建一个mapJoin包,代码如下,MapJoinMapper:
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 {
// 获取缓存的文件,并把文件内容封装到集合
URI[] cacheFiles = context.getCacheFiles();
FileSystem fs = FileSystem.get(context.getConfiguration());
FSDataInputStream fis = fs.open(new Path(cacheFiles[0]));
// 从流中读取数据
BufferedReader reader = new BufferedReader(new InputStreamReader(fis, "UTF-8"));
String line;
while ((line = reader.readLine()) != null) {
// 切割
String[] split = line.split("\t");
// 缓存到集合
pdMap.put(split[0], split[1]);
}
// 关流
reader.close();
}
@Override
protected void map(LongWritable key, Text value, Mapper<LongWritable, Text, Text, NullWritable>.Context context) throws IOException, InterruptedException {
// 处理 order.txt 文件
String[] split = value.toString().split("\t");
// 获取 pid
String pid = pdMap.get(split[1]);
// 获取订单id和订单数量
// 封装
outK.set(split[0] + "\t" + pid + "\t" + split[2]);
// 写出
context.write(outK, NullWritable.get());
}
}
MapJoinDriver:
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. 关联 map
job.setMapperClass(MapJoinMapper.class);
// 4. 设置Map输出KV类型
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(NullWritable.class);
// 5. 设置最终输出数据类型
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(NullWritable.class);
// 加载缓存数据
job.addCacheFile(new URI("file:///D:/input/mapjoin/pd.txt"));
// 设置 reduceTask 个数为 0
job.setNumReduceTasks(0);
// 6. 设置输入路径和输出路径
FileOutputFormat.setOutputPath(job, new Path("D:/output"));
FileInputFormat.setInputPaths(job, new Path("D:/input/mapjoin/order.txt"));
// 7. 提交 job
boolean result = job.waitForCompletion(true);
System.exit(result ? 0 : 1);
}
}
ETL,是英文Extract-Transform-Load 的缩写,用来描述将数据从来源端经过抽取(Extract)、转换(Transform)、加载(Load)至目的端的过程。
ETL一词较常用在数据仓库,但其对象并不限于数据仓库。在运行核心业务MapReduce程序之前,往往要先对数据进行清洗,清理掉不符合用户要求的数据。清理的过程往往只需要运行Mapper程序,不需要运行Reduce程序
需求:去除日志字段个数小于等于11的日志(map阶段),代码实现如下(WebLogMapper):
public class WebLogMapper extends Mapper<LongWritable, Text,Text, NullWritable> {
@Override
protected void map(LongWritable key, Text value, Mapper<LongWritable, Text, Text, NullWritable>.Context context) throws IOException, InterruptedException {
// 1.获取一行
String line = value.toString();
// 2.Etl
boolean result = parseLog(line, context);
if (!result) {
return;
}
// 3.写出
context.write(value, NullWritable.get());
}
private boolean parseLog(String line, Mapper<LongWritable, Text, Text, NullWritable>.Context context) {
// 1.切割
String[] fields = line.split(" ");
// 2.过滤
if (fields.length > 11) {
// 计数器
context.getCounter("map", "true").increment(1);
return true;
} else {
context.getCounter("map", "false").increment(1);
return false;
}
}
}
WebLogDriver:
public class WebLogDriver {
public static void main(String[] args) throws IOException {
args = new String[]{"D:\\Code\\Java\\big-data\\Hello\\src\\main\\java\\com\\ygy\\mapreduce\\etl\\input", "D:\\Code\\Java\\big-data\\Hello\\src\\main\\java\\com\\ygy\\mapreduce\\etl\\output"};
// 1.获取 job 对象
Configuration conf = new Configuration();
Job job = Job.getInstance(new Configuration());
// 2.设置 jar 存储位置
job.setJarByClass(LogDriver.class);
// 3.关联 mapper
job.setMapperClass(WebLogMapper.class);
// 4.设置最终输出类型
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(NullWritable.class);
// 5.设置输入输出路径
FileInputFormat.setInputPaths(job, new Path(args[0]));
FileOutputFormat.setOutputPath(job, new Path(args[1]));
// 6.提交 job
try {
boolean result = job.waitForCompletion(true);
System.exit(result ? 0 : 1);
} catch (InterruptedException | ClassNotFoundException e) {
e.printStackTrace();
}
}
}
可以利用正则匹配汇总来修改parseLog方法,实现更精确的过滤
优缺点:减少磁盘IO、减少磁盘存储空间,但是会增加CPU开销
原则:运算密集型的Job,少用压缩;IO密集型的Job,多用压缩
MapReduce支持的压缩编码:
压缩格式 | 是否需要安装 | 算法 | 文件拓展名 | 是否可切片 | 压缩后原程序是否需要修改 |
---|---|---|---|---|---|
DEFLATE | 否 | DEFLATE | .deflate | 否 | 和文本处理一样,不需要修改 |
Gzip | 否 | DEFLATE | .gz | 否 | 和文本处理一样,不需要修改 |
Bzip2 | 否 | Bzip2 | .bz2 | 是 | 和文本处理一样,不需要修改 |
LZO | 要 | LZO | .lzo | 是 | 需要建立索引,还需要指定输入格式 |
Snappy | 否 | Snappy | .snappy | 否 | 和文本处理一样,不需要修改 |
压缩方式选择时重点考虑:
压缩/解压缩速度、压缩率(压缩后存储大小)、压缩后是否可以支持切片
几种压缩格式优缺点比较:
压缩格式 | 优点 | 缺点 |
---|---|---|
Gzip | 压缩率比较高 | 不支持 Split;压缩/解压速度一般 |
Bzip2 | 压缩率高;支持Split | 压缩/解压速度慢 |
LZO | 压缩/解压速度比较快;支持 Split | 压缩率一般;想支持切片需要额外创建索引 |
Snappy | 压缩和解压缩速度快 | 不支持 Split;压缩率一般 |
压缩参数配置:为了支持多种压缩/解压缩算法,Hadoop引入了编码/解码器
# 查看是否安装,以及安装目录
Hadoop checknative
✍WordCount是最常见、最基本的一个需求,例如进行词频统计、用户访问记录统计。如果数据量非常小的情况下,使用单机、批处理的方式就可以很快得到结果。但是如果数据量非常大,数据量可能是10G、100G等等。这种情况下使用单机、批处理的方式就非常低效率。所以这个时候就需要借助于分布式的思想进行处理——使用集群进行处理。
✍JAR(Java ARchive,Java 归档)是一种与平台无关的文件格式,可将多个文件合成一个文件。用户可将多个 Java applet 及其所需组件(.class 文件、图像和声音)绑定到 JAR 文件中,而后作为单个的简单 HTTP(Hypertext Tranfer Protocal,超文本传输协议)事务下载到浏览器中,从而大大提高下载速度。JAR 格式也支持压缩,从而减小了文件的大小,进一步缩短下载时间。
在实际开发中,maven等项目管理工具为我们自动地管理jar包以及相关的依赖,让jar包的调用看起来如黑盒一般"密不透风"。因为jar包主要是对class文件进行打包,而java编译生成的class文件是平台无关的,这就意味着jar包是跨平台的。当我们开发了一个程序以后,程序中有很多的类,如果需要提供给别人使用,发给对方一大堆源文件是非常不好的,因此通常需要把这些类以及相关的资源文件打包成一个 jar 包,把这个 jar 包提供给别人使用,同时提供给使用者清晰的文档。这样他人在拿到我们提供的jar之后,就能方便地进行调用。在平时写代码搬砖的时候,注意把自己代码的通用部分抽离出来。积累一些通用的util类,将其逐渐模块化,最后打成jar包供自己在别的项目或者模块中使用,同时不断打磨jar里面的内容,将其做得越来越容易理解和通用,这样的好处是除了会对你的代码重构能力以及模块抽象能力有很好的帮助之外,更是一种从长期解放你的重复工作量,让你有更多的精力去做其他事情的方式,甚至当你抽象出业内足够通用的jar之后,jar包还能为你带来意想不到的利润(当然公司里该保密的东西还是得保密的)。这也是java发展得如此之好的原因,无论出于盈利或者非盈利的目的,将自己的通用工具或者框架抽取出来,打成jar包供他人调用,使得整个java生态圈变得越来越强大–几乎很多业务场景都能找到对应的jar包。
✍Java Bean
并非所有的类都是 Java Bean,其是一种特殊的类,具有以下特征:
✍下一篇:Yarn!