1.概述
MapReduce是一个分布式运算程序的编程框架,是用户开发“基于Hadoop的数据分析应用”的核心框架 MapReduce核心功能是将用户编写的业务逻辑代码和自带默认组件整合成一个完整的分布式运算程序,并发运行在一个Hadoop集群上
优点 1)MapReduce易于编程 它简单的实现一些接口,就可以完成一个分布式程序,这个分布式程序可以分布到大量廉价的PC机器上运行
2)良好的扩展性 当计算资源不能得到满足时,通过简单的增加机器来扩展它的计算能力
3)高容错性 其中一台机器挂了,它可以把上面的计算任务转移到另外一个节点上运行,不至于这个任务运行失败,完全是由Hadoop内部完成的
4)适合PB级以上海量数据的离线处理 可以实现上千台服务器集群并发工作,提供数据处理能力
缺点 1)不擅长实时计算 MapReduce无法像MySQL一样,在毫秒或者秒级内返回结果
2)不擅长流式计算 流式计算的输入数据是动态的,而MapReduce的输入数据集是静态的,不能动态变化这是因为MapReduce自身的设计特点决定了数据源必须是静态的
3)不擅长DAG(有向无环图)计算 多个应用程序存在依赖关系,后一个应用程序的输入为前一个的输出。在这种情况下,MapReduce并不是不能做,而是使用后,每个MapReduce作业的输出结果都会写入到磁盘,会造成大量的磁盘IO,导致性能非常的低下
2.核心思想
(1)分布式的运算程序往往需要分成至少2个阶段 (2)第一个阶段的MapTask并发实例,完全并行运行,互不相干 (3)第二个阶段的ReduceTask并发实例互不相干,但是他们的数据依赖于上一个阶段的所有MapTask并发实例的输出 (4)MapReduce编程模型只能包含一个Map阶段和一个Reduce阶段,如果用户的业务逻辑非常复杂,那就只能多个MapReduce程序,串行运行
3.MapReduce编程
编程规范
用户编写的程序分成三个部分:Mapper、Reducer和Driver。
Mapper阶段
1.用户自定义的Mapper要继承自己的父类 2.Mapper的输入数据是K-V对的形式 3.Mapper中的业务逻辑要写在map()方法中 4.Mapper的输出数据是K-V对的形式 5.map()方法对每一行数据都会调用一次
package com.gzhu.mapreduce.worldcount;
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 WorldCountMapper extends Mapper<LongWritable, Text,Text, IntWritable> {
private Text text = new Text();
private IntWritable intWritable = new IntWritable(1);
@Override
protected void map(LongWritable key, Text value, Mapper<LongWritable, Text, Text, IntWritable>.Context context) throws IOException, InterruptedException {
String string = value.toString();
String[] words = string.split(" ");
for (String word : words) {
text.set(word);
context.write(text,intWritable);
}
}
}
Reduce阶段 1.用户自定义的reduce要继承自己的父类 2.Reduce的输入数据类型对应Mapper的输出数据类型,也是K-V 3.Reduce的业务逻辑写在reduce()方法中 4.ReduceTask进程对每一组相同K 的K-V组调用一次reduce()方法
public class WorldCountReduce extends Reducer<Text, IntWritable,Text,IntWritable> {
private IntWritable intWritable = new IntWritable();
@Override
protected void reduce(Text key, Iterable<IntWritable> values, Reducer<Text, IntWritable, Text, IntWritable>.Context context) throws IOException, InterruptedException {
int sum = 0;
for (IntWritable value : values) {
sum += value.get();
}
intWritable.set(sum);
context.write(key,intWritable);
}
}
Driver驱动
package com.gzhu.mapreduce.worldcount;
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 WorldCountDriver {
public static void main(String[] args) throws IOException, InterruptedException, ClassNotFoundException {
Configuration conf = new Configuration();
Job job = Job.getInstance(conf);
job.setJarByClass(WorldCountDriver.class);
job.setMapperClass(WorldCountMapper.class);
job.setReducerClass(WorldCountReduce.class);
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(IntWritable.class);
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(IntWritable.class);
FileInputFormat.setInputPaths(job, new Path("D:\\song"));
FileOutputFormat.setOutputPath(job, new Path("D:\\output"));
boolean b = job.waitForCompletion(true);
System.exit(b ? 0 : 1);
}
}
Yarn阶段 相当于Yarn的客户端,用于提交我们整个程序到YARN集群
4.Hadoop集群测试WordCount案例
打包
<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>
修改路径 打包上传到hadoop文件目录下
文件准备
测试
[gzhu@hadoop102 hadoop-3.1.3]$ hadoop jar wc.jar com.gzhu.mapreduce.worldcount2.WorldCountDriver /input /output
5.序列化
概念 序列化: 序列化就是把内存中的对象,转换成字节序列(或其他数据传输协议)以便于存储到磁盘(持久化)和网络传输
反序列化: 将收到字节序列(或其他数据传输协议)或者是磁盘的持久化数据,转换成内存中的对象
为什么要序列化 一般来说,“活的”对象只生存在内存里,关机断电就没有了。而且“活的”对象只能由本地的进程使用,不能被发送到网络上的另外一台计算机。 然而序列化可以存储“活的”对象,可以将“活的”对象发送到远程计算
关于Hadoop序列化 Java的序列化是一个重量级序列化框架(Serializable),一个对象被序列化后,会附带很多额外的信息(各种校验信息,Header,继承体系等),不便于在网络中高效传输。所以,Hadoop自己开发了一套序列化机制(Writable),附带的信息仅需一部分的校验信息
Hadoop序列化特点 (1)紧凑 :高效使用存储空间 (2)快速:读写数据的额外开销小 (3)互操作:支持多语言的交互
自定义bean对象实现序列化接口(Writable)
Java类型 -Hadoop Writable类型
Boolean -BooleanWritable Byte -ByteWritable Int -IntWritable Float -FloatWritable Long -LongWritable Double -DoubleWritable String -Text Map -MapWritable Array -ArrayWritable Null -NullWritable
企业开发中往往常用的基本序列化类型不能满足所有需求,比如在Hadoop框架内部传递一个bean对象,那么该对象就需要实现序列化接口,我们需要自定义序列化
自定义序列化案例分析
对于如下的数据,我们希望可以根据用户的id,统计其上流量和下流量以及总和
自定义bean序列化的类
public class FlowBean implements Writable {
private long upFlow;
private long downFlow;
private long sumFlow;
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();
}
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 = upFlow + downFlow;
}
@Override
public String toString() {
return
"upFlow=" + upFlow +
", downFlow=" + downFlow +
", sumFlow=" + sumFlow ;
}
}
Mapper
public class FlowMapper extends Mapper<LongWritable, Text,Text,FlowBean> {
Text text = new Text();
FlowBean flowBean = new FlowBean();
@Override
protected void map(LongWritable key, Text value, Mapper<LongWritable, Text, Text, FlowBean>.Context context) throws IOException, InterruptedException {
String string = value.toString();
String []words = string.split("\t");
String keyNumber = words[1];
text.set(keyNumber);
flowBean.setDownFlow(Long.parseLong(words[words.length - 3]));
flowBean.setUpFlow(Long.parseLong(words[words.length - 2]));
flowBean.setSumFlow();
context.write(text,flowBean);
}
}
Reduce
public class FlowReduce extends Reducer<Text,FlowBean,Text,FlowBean> {
FlowBean flowBean = new FlowBean();
@Override
protected void reduce(Text key, Iterable<FlowBean> values, Reducer<Text, FlowBean, Text, FlowBean>.Context context) throws IOException, InterruptedException {
long down = 0;
long up = 0;
for (FlowBean value : values) {
down += value.getDownFlow();
up += value.getUpFlow();
}
flowBean.setDownFlow(down);
flowBean.setUpFlow(up);
flowBean.setSumFlow();
context.write(key,flowBean);
}
}
Driver
public class FlowDriver {
public static void main(String[] args) throws IOException, InterruptedException, ClassNotFoundException {
Configuration conf = new Configuration();
Job job = Job.getInstance(conf);
job.setJarByClass(FlowDriver.class);
job.setMapperClass(FlowMapper.class);
job.setReducerClass(FlowReduce.class);
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(FlowBean.class);
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(FlowBean.class);
FileInputFormat.setInputPaths(job, new Path("F:\\phone_data.txt"));
FileOutputFormat.setOutputPath(job, new Path("F:\\output"));
boolean b = job.waitForCompletion(true);
System.exit(b ? 0 : 1);
}
}
6.MapReduce框架原理
6.1 InputFormat数据输入
切片与MapTask并行度决定机制
数据块 :Block是HDFS物理上把数据分成一块一块。数据块是HDFS存储数据单位
数据切片 :数据切片只是在逻辑上对输入进行分片,并不会在磁盘上将其切分成片进行存储。数据切片是MapReduce程序计算输入数据的单位,一个切片会对应启动一个MapTask
默认情况下切片大小 = BlockSize,一个切片分配一个MapTask进程,数据切片针对的对象是单个文件
假如切片大小为100M,则对第一个128M的block文件处理时,要跨服务器,必然会降低性能
FileInputFormat切片过程
1.程序先找到数据存储的目录 2.开始遍历目录下的每一个文件(切片的单位是一个文件) 3.对于每一个文件(getSplit()方法):
- 获取文件的大小 fs.sizeOf(a.txt)
- 计算切片大小,默认是128M = blocksize
- 形成切片,每次切片时,都要判断剩下的部分是否大于块的1.1倍,不大于就划分为一个切片
- 将切片信息写到一个切片规划文件中
- 提交切片文件到Yarn上,MrAppMaster根据计算的切片开启MapTask个数
FileInputFormat实现类
FileInputFormat常见的接口实现类包括:TextInputFormat、KeyValueTextInputFormat、NLineInputFormat、CombineTextInputFormat和自定义InputFormat等
①TextInputFormat TextInputFormat是默认的FileInputFormat实现类。按行读取每条记录。键是存储该行在整个文件中的起始字节偏移量, LongWritable类型。值是这行的内容,不包括任何行终止符(换行符和回车符),为Text类型
②CombineTextInputFormat
框架默认的TextInputFormat切片机制是对任务按文件规划切片,不管文件多小,都会是一个单独的切片,都会交给一个MapTask,这样如果有大量小文件,就会产生大量的MapTask,处理效率极其低下
CombineTextInputFormat用于小文件过多的场景,它可以将多个小文件从逻辑上规划到一个切片中,这样,多个小文件就可以交给一个MapTask处理
CombineTextInputFormat切片机制 - 分为虚拟存储过程和切片过程
虚拟存储过程:将输入目录下所有文件大小,依次和设置的setMaxInputSplitSize值比较,如果不大于设置的最大值,逻辑上划分一个块。如果输入文件大于设置的最大值两倍,那么以最大值切割一块;当剩余数据大小超过设置的最大值且不大于最大值2倍,此时将文件均分成2个虚拟存储块(防止出现太小切片)
例如setMaxInputSplitSize值为4M,输入文件大小为8.02M,则先逻辑上分成一个4M。剩余的大小为4.02M,如果按照4M逻辑划分,就会出现0.02M的小的虚拟存储文件,所以将剩余的4.02M文件切分成(2.01M和2.01M)两个文件
CombineTextInputFormat案例
有4个小文件,如果使用默认的TextInputFormat,则会启动4个MapTask,浪费资源 使用上面的wordcount,修改文件路径,可以看到有4个切片
在Driver里面设置CombineTextInputFormat并且设置虚拟存储的大小
job.setInputFormatClass(CombineTextInputFormat.class);
CombineTextInputFormat.setMaxInputSplitSize(job, 4194304);
可以看到使用了3个切片
6.2 MapReduce工作流程
Map机制 :Read阶段 - Map阶段 - Collect阶段 - 溢写阶段 - Merge阶段
map方法之后,reduce方法之前的数据处理过程称之为Shuffle
Reduce机制:Copy阶段 - Sort阶段 - Reduce阶段
1.文件在HDFS,客户端提交前,首先会获取处理数据的信息,然后根据切片机制将数据切片
2.客户端将信息job.split、jar包、job.xml提交到Yarn
3.Yarn读取相关的信息,根据切片信息开启MapTask
4.根据InputFormat读取数据信息,默认TextInputFormat,按行为单位进行读取数据,形成K-V
5.将K-V给Mapper程序处理,按照用户的业务逻辑处理后,context.write(K,V)输出数据
6.map 方法之后,数据首先进入到分区方法,把数据标记好分区,然后把数据发送到环形缓冲区(收集器);环形缓冲区默认大小 100m,环形缓冲区达到 80%时,开始反向写入,并进行溢写 。溢写前对数据进行快速排序 ,排序按照对 key 的索引进行字典顺序排序 。溢写产生大量溢写文件,需要对溢写文件进行归并排序。对溢写的文件也可以进行 Combiner 操作,前提是汇总操作 ,求平均值不行。最后将文件按照分区存储到磁盘 ,等待 Reduce 端拉取
7.每个 Reduce 拉取 Map 端对应分区的数据。拉取数据后先存储到内存中,内存不够了,再存储到磁盘。拉取完所有数据后,采用归并排序将内存和磁盘中的数据都进行排序,按照Key分组进入reduce方法,根据OutputFormat输出
6.3 分区
默认分区是根据key的hashcode对ReduceTask个数取模得到的,用户没法控制某一个key存储到哪个分区,但是我们可以自定义分区方法
案例:自定义分区规则
1.增加一个分区类
由于分区是处理map阶段后的数据,所以泛型应该是map的输出类型
public class ProvincePartitioner extends Partitioner<Text, FlowBean> {
@Override
public int getPartition(Text text, FlowBean flowBean, int i) {
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(ProvincePartitioner.class);
job.setNumReduceTasks(5);
可以看到生成了5个文件
- ReduceTask个数为0,表示没有reduce阶段,输出文件个数和Map个数一致
- ReduceTask = 1,则不管几个分区,根据源码,不会进行分区,所以只会生成一个文件(ReduceTask默认为1,所以输出文件个数为1)
- ReduceTask < 分区数,报错
- ReduceTask > 分区数,则多余的文件会是空文件
所以,ReduceTask的个数一定要与分区数保持一致,否则分区将不具有任何意义 !
6.4 排序
在MapReduce整个流程中,Shuffle阶段会进行两次排序
- 从缓冲区
快速排序 后溢写到文件 - 将多个溢写的文件
归并排序 后写入磁盘
Reduce阶段会将磁盘和内存属于同一个分区的数据进行归并排序
例如<a,1> <a,2> <b,2>
排好序后,我只检测当前的key和前面的key是否一样,不一样直接统一进入Reduce,提高了效率
全排序案例
全排序定义:输出一个文件,排序
自定义序列话的类实现WritableComparable 接口 ,E为要比较的对象,重写compareTo方法,由于Map输出的K为对象,Reduce阶段会对每一个相同的key进行处理,所以在Reduce阶段,根据谁排序,谁就是Key
比如我根据总流量进行排序,手机号123的总流量为200,手机号为234的总流量也为200,这时Reduce会对所有总流量为200的对象进行一次reduce方法
key为所有总流量为200的对象,values为所有总流量为200的手机号的集合
FlowBean key, Iterable<Text> values
自定义类
public class FlowBean implements Writable, WritableComparable<FlowBean> {
private long upFlow;
private long downFlow;
private long sumFlow;
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();
}
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 = upFlow + downFlow;
}
@Override
public String toString() {
return
"upFlow=" + upFlow +
", downFlow=" + downFlow +
", sumFlow=" + 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;
}
}
}
Mapper阶段
public class FlowMapper extends Mapper<LongWritable, Text,FlowBean, Text> {
Text text = new Text();
FlowBean flowBean = new FlowBean();
@Override
protected void map(LongWritable key, Text value, Mapper<LongWritable, Text, FlowBean, Text>.Context context) throws IOException, InterruptedException {
String string = value.toString();
String []words = string.split("\t");
String keyNumber = words[1];
text.set(keyNumber);
flowBean.setDownFlow(Long.parseLong(words[words.length - 3]));
flowBean.setUpFlow(Long.parseLong(words[words.length - 2]));
flowBean.setSumFlow();
context.write(flowBean,text);
}
}
Reduce阶段
public class FlowReduce extends Reducer<FlowBean, Text,Text, FlowBean> {
@Override
protected void reduce(FlowBean key, Iterable<Text> values, Reducer<FlowBean, Text, Text, FlowBean>.Context context) throws IOException, InterruptedException {
for (Text value : values) {
context.write(value,key);
}
}
}
Driver阶段
public class FlowDriver {
public static void main(String[] args) throws IOException, InterruptedException, ClassNotFoundException {
Configuration conf = new Configuration();
Job job = Job.getInstance(conf);
job.setJarByClass(FlowDriver.class);
job.setMapperClass(FlowMapper.class);
job.setReducerClass(FlowReduce.class);
job.setMapOutputKeyClass(FlowBean.class);
job.setMapOutputValueClass(Text.class);
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(FlowBean.class);
FileInputFormat.setInputPaths(job, new Path("F:\\input\\phone"));
FileOutputFormat.setOutputPath(job, new Path("F:\\output3"));
boolean b = job.waitForCompletion(true);
System.exit(b ? 0 : 1);
}
}
二次排序就是在compareTo方法里再定义一个排序
区内排序案例
区内排序就是输出的每一个文件,文件内部有序
在全排序的基础上加一个分区即可
public class PrPartitioner extends Partitioner<FlowBean, Text> {
@Override
public int getPartition(FlowBean flowBean, Text text, int i) {
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指定分区器
job.setPartitionerClass(PrPartitioner.class);
job.setNumReduceTasks(5);
6.5 Combiner合并
Combiner是处理经过MapTask处理后的数据,进行求和聚集,比如有100个<a,1>,进行Combiner合并后变成<a,100>,减少网络传输量
1.创建一个类,实现Reduce,重写reduce方法 2.Driver指明驱动类
或者
将WordcountReducer作为Combiner在WordcountDriver驱动类中指定
6.6 OutputFormat数据输出
Reduce处理完成后,以何种方式写,怎么写,写到哪里,都是由OutputFormat决定的
默认是TextOutputFormat ,按行去写,写到一个文件里面
自定义OutputFormat案例
需求:有如下网址,我们希望域名长度<= 4的输出到文件web1,长度大于4的输出到web2 Mapper
package com.gzhu.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 WebMapper 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());
}
}
Reduce
public class WebReduce extends Reducer<Text, NullWritable,Text,NullWritable> {
@Override
protected void reduce(Text key, Iterable<NullWritable> values, Reducer<Text, NullWritable, Text, NullWritable>.Context context) throws IOException, InterruptedException {
for (NullWritable value : values) {
context.write(key,NullWritable.get());
}
}
}
自定义OutPutFormat
public class WebOutPutFormat extends FileOutputFormat<Text, NullWritable> {
@Override
public RecordWriter<Text, NullWritable> getRecordWriter(TaskAttemptContext job) throws IOException, InterruptedException {
WebRecordWriter webRecordWriter = new WebRecordWriter(job);
return webRecordWriter;
}
}
RecordWriter
public class WebRecordWriter extends RecordWriter<Text, NullWritable> {
private FSDataOutputStream web1;
private FSDataOutputStream web2;
public WebRecordWriter(TaskAttemptContext job) {
try {
FileSystem fs = FileSystem.get(job.getConfiguration());
web1 = fs.create(new Path("F:\\output\\outputformat\\web1"));
web2 = fs.create(new Path("F:\\output\\outputformat\\web2"));
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void write(Text text, NullWritable nullWritable) throws IOException, InterruptedException {
String str = text.toString();
String []arr = str.split("\\.");
if(arr[1].length() <= 4){
web1.writeBytes(str + "\n");
}else{
web2.writeBytes(str + "\n");
}
}
@Override
public void close(TaskAttemptContext taskAttemptContext) throws IOException, InterruptedException {
IOUtils.closeStream(web1);
IOUtils.closeStream(web2);
}
}
Driver
public class WebDriver {
public static void main(String[] args) throws IOException, InterruptedException, ClassNotFoundException {
Configuration conf = new Configuration();
Job job = Job.getInstance(conf);
job.setJarByClass(WebDriver.class);
job.setMapperClass(WebMapper.class);
job.setReducerClass(WebReduce.class);
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(NullWritable.class);
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(NullWritable.class);
job.setOutputFormatClass(WebOutPutFormat.class);
FileInputFormat.setInputPaths(job, new Path("F:\\input\\web"));
FileOutputFormat.setOutputPath(job, new Path("F:\\output\\outputformat"));
boolean b = job.waitForCompletion(true);
System.exit(b ? 0 : 1);
}
}
链接: Hadoop — 从MySQL数据库读取数据,经MapReduce处理后,写入MySQL数据库
6.7 Join应用
ReduceJoin案例
基本思想
我们知道,Join两张表需要有一个公共字段,我们可以把这个公共字段设置为Map的输出key,输出value设置为序列化的类(这个类包含两张表的所有字段),但我们还需要一个额外的标记信息 ,以此来标明每一条数据来源与哪个表。Reduce端以连接字段作为key的分组已经完成,我们只需要在每一个分组当中将那些来源于不同文件的记录(在Map阶段已经打标志)分开,最后进行合并就ok了
- Map输出key:公共字段
- Map输出value:序列化类(包括两张表所有字段和一个额外的标记信息)
- Redcue输出key:序列化类(按照要求输出所需要的字段)
- Reduce输出value:NullWritable
需求:如图有如下的两张表
我们最终想得到id,pname,amount三列 TableBean 包括两张表的全部属性并且有一个额外的标记信息
public class TableBean implements Writable {
private String id;
private String pid;
private int amount;
private String pname;
private String flag;
public TableBean() {
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getPid() {
return pid;
}
public void setPid(String pid) {
this.pid = pid;
}
public int getAmount() {
return amount;
}
public void setAmount(int amount) {
this.amount = amount;
}
public String getPname() {
return pname;
}
public void setPname(String pname) {
this.pname = pname;
}
public String getFlag() {
return flag;
}
public void setFlag(String flag) {
this.flag = flag;
}
@Override
public void write(DataOutput 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();
}
@Override
public String toString() {
return id + "\t" + pname + "\t" + amount;
}
}
Mapper
public class TableMapper extends Mapper<LongWritable, Text,Text,TableBean> {
String fileName;
Text text = new Text();
TableBean tableBean = new TableBean();
@Override
protected void setup(Mapper<LongWritable, Text, Text, TableBean>.Context context) throws IOException, InterruptedException {
FileSplit split = (FileSplit) context.getInputSplit();
fileName = split.getPath().getName();
}
@Override
protected void map(LongWritable key, Text value, Mapper<LongWritable, Text, Text, TableBean>.Context context) throws IOException, InterruptedException {
String string = value.toString();
String[] split = string.split("\t");
if(fileName.equals("order")){
text.set(split[1]);
tableBean.setId(split[0]);
tableBean.setPid(split[1]);
tableBean.setAmount(Integer.parseInt(split[2]));
tableBean.setPname("");
tableBean.setFlag("order");
}else{
text.set(split[0]);
tableBean.setId("");
tableBean.setPid(split[0]);
tableBean.setAmount(0);
tableBean.setPname(split[1]);
tableBean.setFlag("pd");
}
context.write(text,tableBean);
}
}
Map阶段输出的数据应为
Reduce
在Hadoop中,Iterable values中所有的对象都是用的同一块内存地址!!!如果我们往集合中直接添加value,由于都是同一块地址,所以集合中只会添加一个元素!!!所以我们每次都创建一个新的对象,将目前这块地址的对象赋值给这个对象,就可以将所有的对象拿到添加到集合!!!
public class TableReduce 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 {
ArrayList<TableBean> tableBeans = new ArrayList<>();
TableBean pdBean = new TableBean();
for (TableBean value : values) {
if("order".equals(value.getFlag())){
TableBean tableBean = new TableBean();
try {
BeanUtils.copyProperties(tableBean,value);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
tableBeans.add(tableBean);
}else{
try {
BeanUtils.copyProperties(pdBean,value);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
}
for (TableBean bean : tableBeans) {
bean.setPname(pdBean.getPname());
context.write(bean,NullWritable.get());
}
}
}
Driver
public class TableDriver {
public static void main(String[] args) throws IOException, InterruptedException, ClassNotFoundException {
Configuration conf = new Configuration();
Job job = Job.getInstance(conf);
job.setJarByClass(TableDriver.class);
job.setMapperClass(TableMapper.class);
job.setReducerClass(TableReduce.class);
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(TableBean.class);
job.setOutputKeyClass(TableBean.class);
job.setOutputValueClass(NullWritable.class);
FileInputFormat.setInputPaths(job, new Path("F:\\input\\inputtable"));
FileOutputFormat.setOutputPath(job, new Path("F:\\output\\outputtablen"));
boolean b = job.waitForCompletion(true);
System.exit(b ? 0 : 1);
}
}
通过控制toString()方法来控制输出的属性
MapJoin案例
合并的操作是在Reduce阶段完成,Reduce端的处理压力太大,Map节点的运算负载则很低,资源利用率不高,且在Reduce阶段极易产生数据倾斜
使用场景
Map Join适用于一张表十分小、一张表很大的场景,小表缓存到内存中
Mapper
package com.gzhu.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> map = new HashMap<>();
private Text text = new Text();
@Override
protected void setup(Mapper<LongWritable, Text, Text, NullWritable>.Context context) throws IOException, InterruptedException {
URI[] files = context.getCacheFiles();
FileSystem fs = FileSystem.get(context.getConfiguration());
FSDataInputStream fis = fs.open(new Path(files[0]));
BufferedReader reader = new BufferedReader(new InputStreamReader(fis, "UTF-8"));
String line;
while(StringUtils.isNotEmpty(line = reader.readLine())){
String[] split = line.split("\t");
map.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 {
String[] fields = value.toString().split("\t");
String pname = map.get(fields[1]);
text.set(fields[0] + "\t" + pname + "\t" + fields[2]);
context.write(text,NullWritable.get());
}
}
Driver
package com.gzhu.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, InterruptedException, ClassNotFoundException, URISyntaxException {
Configuration conf = new Configuration();
Job job = Job.getInstance(conf);
job.setJarByClass(MapJoinDriver.class);
job.setMapperClass(MapJoinMapper.class);
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(NullWritable.class);
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(NullWritable.class);
job.addCacheFile(new URI("file:///F:/input/pdcache/pd.txt"));
job.setNumReduceTasks(0);
FileInputFormat.setInputPaths(job, new Path("F:\\input\\inputtable"));
FileOutputFormat.setOutputPath(job, new Path("F:\\output\\mapjoin"));
boolean b = job.waitForCompletion(true);
System.exit(b ? 0 : 1);
}
}
6.8 总结
1.输入数据接口:InputFormat (1)默认使用的实现类是:TextInputFormat
(2)TextInputFormat的功能逻辑是:一次读一行文本,然后将该行的起始偏移量作为key,行内容作为value返回
(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)二次排序:排序的条件有两个
5.Combiner合并
Combiner合并可以提高程序执行效率,减少IO传输。但是使用时必须不能影响原有的业务处理结果,提前聚合map,在Map阶段实现聚合,减轻Reduce端压力
6.逻辑处理接口:Reducer 用户根据业务需求实现其中三个方法:
- setup()
- reduce()
- cleanup ()
7.输出数据接口:OutputFormat
(1)默认实现类是TextOutputFormat,功能逻辑是:将每一个KV对,向目标文本文件输出一行 (2)可以自定义OutputFormat
7.Hadoop数据压缩
1.压缩的好处和坏处
压缩的优点:以减少磁盘IO、减少磁盘存储空间
压缩的缺点:增加CPU开销
2.压缩原则
- 运算密集型的Job,少用压缩
- IO密集型的Job,多用压缩
3.压缩位置选择 Map输出端压缩
即使你的MapReduce的输入输出文件都是未压缩的文件,你仍然可以对Map任务的中间结果输出做压缩,因为它要写在硬盘并且通过网络传输到Reduce节点,对其压缩可以提高很多性能,这些工作只要设置两个属性即可
conf.setBoolean("mapreduce.map.output.compress", true);
conf.setClass("mapreduce.map.output.compress.codec", BZip2Codec.class, CompressionCodec.class);
Reduce输出端压缩
FileOutputFormat.setCompressOutput(job, true);
FileOutputFormat.setOutputCompressorClass(job, BZip2Codec.class);
|