3.1.2 HADOOP框架
文章目录
- 3.1.2 HADOOP框架
- 六、MapReduce编程框架
六、MapReduce编程框架
6.1 MapReduce思想
MapReduce思想在⽣活中处处可⻅。我们或多或少都曾接触过这种思想。MapReduce的思想核⼼是分⽽治之,充分利⽤了并⾏处理的优势。
即使是发布过论⽂实现分布式计算的⾕歌也只是实现了这种思想,⽽不是⾃⼰原创。
MapReduce任务过程是分为两个处理阶段:
Map阶段:Map阶段的主要作⽤是“分”,即把复杂的任务分解为若⼲个“简单的任务”来并⾏处理。
Map阶段的这些任务可以并⾏计算,彼此间没有依赖关系。
Reduce阶段:Reduce阶段的主要作⽤是“合”,即对map阶段的结果进⾏全局汇总。
再次理解MapReduce的思想
6.2 官⽅WordCount案例源码解析
经过查看分析官⽅WordCount案例源码我们发现⼀个统计单词数量的MapReduce程序的代码由三个部分组成,
- Mapper类
- Reducer类
- 运⾏作业的代码(Driver)
Mapper类继承了org.apache.hadoop.mapreduce.Mapper类重写了其中的map⽅法,Reducer类继承了org.apache.hadoop.mapreduce.Reducer类重写了其中的reduce⽅法。
重写的Map⽅法作⽤:map⽅法其中的逻辑就是⽤户希望mr程序map阶段如何处理的逻辑;
重写的Reduce⽅法作⽤:reduce⽅法其中的逻辑是⽤户希望mr程序reduce阶段如何处理的逻辑;
- Hadoop序列化
为什么进⾏序列化?
序列化主要是我们通过⽹络通信传输数据时或者把对象持久化到⽂件,需要把对象序列化成⼆进制的结构。
观察源码时发现⾃定义Mapper类与⾃定义Reducer类都有泛型类型约束,⽐如⾃定义Mapper有四个形参类型,但是形参类型并不是常⻅的java基本类型。
为什么Hadoop要选择建⽴⾃⼰的序列化格式⽽不使⽤java⾃带serializable?
- 序列化在分布式程序中⾮常重要,在Hadoop中,集群中多个节点的进程间的通信是通过RPC(远程过程调⽤:Remote Procedure Call)实现;RPC将消息序列化成⼆进制流发送到远程节点,远程节点再将接收到的⼆进制数据反序列化为原始的消息,因此RPC往往追求如下特点:
& 紧凑:数据更紧凑,能充分利⽤⽹络带宽资源
& 快速:序列化和反序列化的性能开销更低
- Hadoop使⽤的是⾃⼰的序列化格式Writable,它⽐java的序列化serialization更紧凑速度更快。⼀个对象使⽤Serializable序列化后,会携带很多额外信息⽐如校验信息,Header,继承体系等。
Java基本类型与Hadoop常⽤序列化类型
6.3 MapReduce编程规范及示例编写
6.3.1 Mapper类
- ⽤户⾃定义⼀个Mapper类继承Hadoop的Mapper类
- Mapper的输⼊数据是KV对的形式(类型可以⾃定义)
- Map阶段的业务逻辑定义在map()⽅法中
- Mapper的输出数据是KV对的形式(类型可以⾃定义)
注意:map()⽅法是对输⼊的⼀个KV对调⽤⼀次!!
6.3.2 Reducer类
- ⽤户⾃定义Reducer类要继承Hadoop的Reducer类
- Reducer的输⼊数据类型对应Mapper的输出数据类型(KV对)
- Reducer的业务逻辑写在reduce()⽅法中
- Reduce()⽅法是对相同K的⼀组KV对调⽤执⾏⼀次
6.3.3 Driver阶段
创建提交YARN集群运⾏的Job对象,其中封装了MapReduce程序运⾏所需要的相关参数⼊输⼊数据路径,输出数据路径等,也相当于是⼀个YARN集群的客户端,主要作⽤就是提交我们MapReduce程序运⾏。
6.3.4 WordCount代码实现
6.3.4.1 需求
在给定的⽂本⽂件中统计输出每⼀个单词出现的总次数
输⼊数据:wc.txt;
输出:
6.3.4.2 具体步骤
按照MapReduce编程规范,分别编写Mapper,Reducer,Driver。
- 新建maven⼯程
a. 导⼊hadoop依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.lagou.hadoop</groupId>
<artifactId>wordcount</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.8.2</version>
</dependency>
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-common</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-client</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-hdfs</artifactId>
<version>2.9.2</version>
</dependency>
</dependencies>
<!--maven打包插件 -->
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>2.3.2</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>
</project>
注意:以上依赖第⼀次需要联⽹下载!!
b. 添加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
- 整体思路梳理(仿照源码)
Map阶段:
I. map()⽅法中把传⼊的数据转为String类型
II. 根据空格切分出单词
III. 输出<单词,1>
Reduce阶段:
I. 汇总各个key(单词)的个数,遍历value数据进⾏累加
II. 输出key的总数
Driver
I. 获取配置⽂件对象,获取job对象实例
II. 指定程序jar的本地路径
III. 指定Mapper/Reducer类
IV. 指定Mapper输出的kv数据类型
Ⅴ. 指定最终输出的kv数据类型
VI. 指定job处理的原始数据路径
VII. 指定job输出结果路径
VIII. 提交作业 - 编写Mapper类
package com.lagou.mr.wc;
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;
//需求:单词计数
//1 继承Mapper类
//2 Mapper类的泛型参数:共4个,2对kv
// 第一对kv: map输入参数类型
// LongWritable, Text-->文本偏移量(后面不会用到),一行文本内容
// 第二队kv:map输出参数类型
// Text, IntWritable-->单词,1
public class WordCountMapper extends Mapper<LongWritable, Text, Text, IntWritable> {
//3 重写Mapper类的map方法
/*
1 接收到文本内容,转为String类型
2 按照空格进行切分
3 输出<单词,1>
*/
//提升为全局变量,避免每次执行map方法都执行此操作
final Text word = new Text();
final IntWritable one = new IntWritable(1);
// LongWritable, Text-->文本偏移量,一行文本内容,map方法的输入参数,一行文本就调用一次map方法
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
// 1 接收到文本内容,转为String类型
final String str = value.toString();
// 2 按照空格进行切分
final String[] words = str.split(" ");
// 3 输出<单词,1>
//遍历数据
for (String s : words) {
word.set(s);
context.write(word, one);
}
}
}
- 编写Reducer类
package com.lagou.mr.wc;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;
import java.io.IOException;
//继承的Reducer类有四个泛型参数,2对kv
//第一对kv:类型要与Mapper输出类型一致:Text, IntWritable
//第二对kv:自己设计决定输出的结果数据是什么类型:Text, IntWritable
public class WordCountReducer extends Reducer<Text, IntWritable, Text, IntWritable> {
//1 重写reduce方法
// Text key:map方法输出的key,本案例中就是单词,
// Iterable<IntWritable> values:一组key相同的kv的value组成的集合
/*
假设map方法:hello 1;hello 1;hello 1
reduce的key和value是什么?
key:hello,
values:<1,1,1>
假设map方法输出:hello 1;hello 1;hello 1,hadoop 1, mapreduce 1,hadoop 1
reduce的key和value是什么?
reduce方法何时调用:一组key相同的kv中的value组成集合然后调用一次reduce方法
第一次:key:hello ,values:<1,1,1>
第二次:key:hadoop ,values<1,1>
第三次:key:mapreduce ,values<1>
*/
IntWritable total = new IntWritable();
@Override
protected void reduce(Text key, Iterable<IntWritable> values, Context context) throws IOException, InterruptedException {
//2 遍历key对应的values,然后累加结果
int sum = 0;
for (IntWritable value : values) {
int i = value.get();
sum += 1;
}
// 3 直接输出当前key对应的sum值,结果就是单词出现的总次数
total.set(sum);
context.write(key, total);
}
}
- 编写Driver驱动类
package com.lagou.mr.wc;
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, ClassNotFoundException, InterruptedException {
/*
1. 获取配置文件对象,获取job对象实例
2. 指定程序jar的本地路径
3. 指定Mapper/Reducer类
4. 指定Mapper输出的kv数据类型
5. 指定最终输出的kv数据类型
6. 指定job处理的原始数据路径
7. 指定job输出结果路径
8. 提交作业
*/
// 1. 获取配置文件对象,获取job对象实例
final Configuration conf = new Configuration();
final Job job = Job.getInstance(conf, "WordCountDriver");
// 2. 指定程序jar的本地路径
job.setJarByClass(WordCountDriver.class);
// 3. 指定Mapper/Reducer类
job.setMapperClass(WordCountMapper.class);
job.setReducerClass(WordCountReducer.class);
// 4. 指定Mapper输出的kv数据类型
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(IntWritable.class);
// 5. 指定最终输出的kv数据类型
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(IntWritable.class);
// 6. 指定job处理的原始数据路径
FileInputFormat.setInputPaths(job, new Path(args[0])); //指定读取数据的原始路径
// 7. 指定job输出结果路径
FileOutputFormat.setOutputPath(job, new Path(args[1])); //指定结果数据输出路径
// 8. 提交作业
final boolean flag = job.waitForCompletion(true);
//jvm退出:正常退出0,非0值则是错误退出
System.exit(flag ? 0 : 1);
}
}
- 运⾏任务
I.本地模式
直接Idea中运⾏驱动类即可
idea运⾏需要传⼊参数:
注意本地idea运⾏mr任务与集群没有任何关系,没有提交任务到yarn集群,是在本地使⽤多线程⽅式模拟的mr的运⾏。
II. Yarn集群模式
- 把程序打成jar包,改名为wc.jar;上传到Hadoop集群选择合适的Jar包
准备原始数据⽂件,上传到HDFS的路径,不能是本地路径,因为跨节点运⾏⽆法获取数据!!
- 启动Hadoop集群(Hdfs,Yarn)
- 使⽤Hadoop 命令提交任务运⾏
hadoop jar wc.jar com.lagou.mr.wc.WordCountDriver /wcinput /wcoutput
Yarn集群任务运⾏成功展示图
6.4 序列化Writable接口
基本序列化类型往往不能满足所有需求,比如在Hadoop框架内部传递一个自定义bean对象,那么该对象就需要实现Writable序列化接口。
6.4.1 实现Writable序列化步骤如下
- 必须实现Writable接口
- 反序列化时,需要反射调用空参构造函数,所以必须有空参构造
public CustomBean() {
super();
}
- 重写序列化方法
@Override
public void write(DataOutput out) throws IOException {
....
}
- 重写反序列化方法
@Override
public void readFields(DataInput in) throws IOException {
....
}
- 反序列化的字段顺序和序列化字段的顺序必须完全一致
- 方便展示结果数据,需要重写bean对象的toString()方法,可以自定义分隔符
- 如果自定义Bean对象需要放在Mapper输出KV中的K,则该对象还需实现Comparable接口,因为MapReduce框中的Shuffle过程要求对key必须能排序!!
排序内容专门案例讲解!!
@Override
public int compareTo(CustomBean o) {
// 自定义排序规则
return this.num > o.getNum() ? -1 : 1;
}
6.4.2 Writable接口案例
- 需求
统计每台智能音箱设备内容播放时长
- 编写MapReduce程序
a.创建SpeakBean对象
package com.lagou.mr.speak;
import org.apache.hadoop.io.Writable;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
// 1 实现writable接口
public class SpeakBean implements Writable {
//定义属性
private Long selfDuration;//自有内容时长
private Long thirdPartDuration;//第三方内容时长
private String deviceId;//设备id
private Long sumDuration;//总时长
//2 反序列化时,需要反射调用空参构造函数,所以必须有
public SpeakBean() {
}
public SpeakBean(long selfDuration, long thirdPartDuration ,String deviceId) {
this.selfDuration = selfDuration;
this.thirdPartDuration = thirdPartDuration;
this.deviceId = deviceId;
this.sumDuration = this.selfDuration + this.thirdPartDuration;
}
//3 写序列化方法
@Override
public void write(DataOutput out) throws IOException {
out.writeLong(selfDuration);
out.writeLong(thirdPartDuration);
out.writeUTF(deviceId);
out.writeLong(sumDuration);
}
//4 反序列化方法
// 5 反序列化方法读顺序必须和写序列化方法的写顺序必须一致
@Override
public void readFields(DataInput in) throws IOException {
this.selfDuration = in.readLong();
this.thirdPartDuration = in.readLong();
this.deviceId = in.readUTF();//设备id
this.sumDuration = in.readLong();
}
// 6 编写toString方法,方便后续打印到文本
@Override
public String toString() {
return selfDuration + "\t" + thirdPartDuration + "\t" + deviceId + "\t" + sumDuration;
}
public String getDeviceId() {
return deviceId;
}
public void setDeviceId(String deviceId) {
this.deviceId = deviceId;
}
public long getSelfDuration() {
return selfDuration;
}
public void setSelfDuration(long selfDuration) {
this.selfDuration = selfDuration;
}
public long getThirdPartDuration() {
return thirdPartDuration;
}
public void setThirdPartDuration(long thirdPartDuration) {
this.thirdPartDuration = thirdPartDuration;
}
public long getSumDuration() {
return sumDuration;
}
public void setSumDuration(long sumDuration) {
this.sumDuration = sumDuration;
}
//总时长相加
public void set(long selfDuration, long thirdPartDuration) {
this.selfDuration = selfDuration;
this.thirdPartDuration = thirdPartDuration;
this.sumDuration = this.selfDuration + this.thirdPartDuration;
}
}
b.编写Mapper类
package com.lagou.mr.speak;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;
import java.io.IOException;
//四个参数:分为两对kv
//第一对kv:map输入参数的kv类型;k-->一行文本偏移量,v-->一行文本内容
//第二对kv:map输出参数kv类型;k-->map输出的key类型,v:map输出的value类型
public class SpeakMapper extends Mapper<LongWritable, Text, Text, SpeakBean> {
/*
1 转换接收到的text数据为String
2 按照制表符进行切分;得到自有内容时长,第三方内容时长,设备id,封装为SpeakBean
3 直接输出:k-->设备id,value-->speakbean
*/
Text device_id = new Text();
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
// 1 转换接收到的text数据为String
final String line = value.toString();
// 2 按照制表符进行切分;得到自有内容时长,第三方内容时长,设备id,封装为SpeakBean
final String[] fields = line.split("\t");
//自有内容时长
String selfDuration = fields[fields.length - 3];
//第三方内容时长
String thirdPartDuration = fields[fields.length - 2];
//设备id
String deviceId = fields[1];
final SpeakBean bean = new SpeakBean(Long.parseLong(selfDuration), Long.parseLong(thirdPartDuration), deviceId);
// 3 直接输出:k-->设备id,value-->speakbean
device_id.set(deviceId);
context.write(device_id, bean);
}
}
c.编写Reducer
package com.lagou.mr.speak;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;
import java.io.IOException;
public class SpeakReducer extends Reducer<Text, SpeakBean, Text, SpeakBean> {
@Override
protected void reduce(Text key, Iterable<SpeakBean> values, Context context) throws IOException, InterruptedException {
//定义时长累加的初始值
Long self_duration = 0L;
Long third_part_duration = 0L;
//reduce方法的key:map输出的某一个key
//reduce方法的value:map输出的kv对中相同key的value组成的一个集合
//reduce 逻辑:遍历迭代器累加时长即可
for (SpeakBean bean : values) {
final Long selfDuration = bean.getSelfDuration();
final Long thirdPartDuration = bean.getThirdPartDuration();
self_duration += selfDuration;
third_part_duration += thirdPartDuration;
}
//输出,封装成一个bean对象输出
final SpeakBean bean = new SpeakBean(self_duration, third_part_duration, key.toString());
context.write(key, bean);
}
}
d.编写驱动
package com.lagou.mr.speak;
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 SpeakDriver {
public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
// 输入输出路径需要根据自己电脑上实际的输入输出路径设置
args = new String[] { "E:/text/input", "E:/text/output" };
// 1 获取配置信息,或者job对象实例
final Configuration conf = new Configuration();
final Job job = Job.getInstance(conf, "speakDriver");
//设置jar包本地路径
job.setJarByClass(SpeakDriver.class);
//使用的mapper和reducer
job.setMapperClass(SpeakMapper.class);
job.setReducerClass(SpeakReducer.class);
//map的输出kv类型
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(SpeakBean.class);
//设置reduce输出
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(SpeakBean.class);
//读取的数据路径
FileInputFormat.setInputPaths(job, new Path(args[0]));
FileOutputFormat.setOutputPath(job, new Path(args[1]));
//提交任务
final boolean flag = job.waitForCompletion(true);
System.exit(flag ? 0 : 1);
}
}
mr编程技巧总结
- 结合业务设计Map输出的key和v,利用key相同则去往同一个reduce的特点!!
- map()方法中获取到只是一行文本数据尽量不做聚合运算
- reduce()方法的参数要清楚含义
6.5 MapReduce原理分析
6.5.1 MapTask运行机制详解
MapTask流程
详细步骤:
- 首先,读取数据组件InputFormat(默认TextInputFormat)会通过getSplits方法对输入目录中文件进行逻辑切片规划得到splits,有多少个split就对应启动多少个MapTask。split与block的对应关系默认是一对一。
- 将输入文件切分为splits之后,由RecordReader对象(默认LineRecordReader)进行读取,以\n作为分隔符,读取一行数据,返回<key,value>。Key表示每行首字符偏移值,value表示这一行文本内容。
- 读取split返回<key,value>,进入用户自己继承的Mapper类中,执行用户重写的map函数。RecordReader读取一行这里调用一次。
- map逻辑完之后,将map的每条结果通过context.write进行collect数据收集。在collect中,会先对其进行分区处理,默认使用HashPartitioner。
MapReduce提供Partitioner接口,它的作用就是根据key或value及reduce的数量来决定当前的这对输出数据最终应该交由哪个reduce task处理。默认对key hash后再以reduce task数量取模。默认的取模方式只是为了平均reduce的处理能力,如果用户自己对Partitioner有需求,可以订制并设置到job上。 - 接下来,会将数据写入内存,内存中这片区域叫做环形缓冲区,缓冲区的作用是批量收集map结果,减少磁盘IO的影响。我们的key/value对以及Partition的结果都会被写入缓冲区。当然写入之前,key与value值都会被序列化成字节数组。
- 环形缓冲区其实是一个数组,数组中存放着key、value的序列化数据和key、value的元数据信息,包括partition、key的起始位置、value的起始位置以及value的长度。环形结构是一个抽象概念。
- 缓冲区是有大小限制,默认是100MB。当map task的输出结果很多时,就可能会撑爆内存,所以需要在一定条件下将缓冲区中的数据临时写入磁盘,然后重新利用这块缓冲区。这个从内存往磁盘写数据的过程被称为Spill,中文可译为溢写。这个溢写是由单独线程来完成,不影响往缓冲区写map结果的线程。溢写线程启动时不应该阻止map的结果输出,所以整个缓冲区有个溢写的比例spill.percent。这个比例默认是0.8,也就是当缓冲区的数据已经达到阈值(buffer size * spill percent = 100MB * 0.8 = 80MB),溢写线程启动,锁定这80MB的内存,执行溢写过程。Maptask的输出结果还可以往剩下的20MB内存中写,互不影响。
- 当溢写线程启动后,需要对这80MB空间内的key做排序(Sort)。排序是MapReduce模型默认的行为!
- 如果job设置过Combiner,那么现在就是使用Combiner的时候了。将有相同key的key/value对的value加起来,减少溢写到磁盘的数据量。Combiner会优化MapReduce的中间结果,所以它在整个模型中会多次使用。
- 那哪些场景才能使用Combiner呢?从这里分析,Combiner的输出是Reducer的输入,Combiner
绝不能改变最终的计算结果。Combiner只应该用于那种Reduce的输入key/value与输出key/value类型完全一致,且不影响最终结果的场景。比如累加,最大值等。Combiner的使用一定得慎重,如果用好,它对job执行效率有帮助,反之会影响reduce的最终结果。
- 合并溢写文件:每次溢写会在磁盘上生成一个临时文件(写之前判断是否有combiner),如果map的输出结果真的很大,有多次这样的溢写发生,磁盘上相应的就会有多个临时文件存在。当整个数据处理结束之后开始对磁盘中的临时文件进行merge合并,因为最终的文件只有一个,写入磁盘,并且为这个文件提供了一个索引文件,以记录每个reduce对应数据的偏移量。
至此map整个阶段结束!!
MapTask的一些配置
官方参考地址
https://hadoop.apache.org/docs/r2.9.2/hadoop-mapreduce-client/hadoop-mapreduce-client-core/mapred-default.xml
6.5.2 MapTask的并行度
- MapTask并行度思考
MapTask的并行度决定Map阶段的任务处理并发度,从而影响到整个Job的处理速度。
思考:MapTask并行任务是否越多越好呢?哪些因素影响了MapTask并行度?
答:并不是越多越好,见下面解析 - MapTask并行度决定机制
数据块:Block是HDFS物理上把数据分成一块一块。
切片:数据切片只是在逻辑上对输入进行分片,并不会在磁盘上将其切分成片进行存储。
6.5.2.1 切片机制源码阅读
默认就是128M;
MapTask并行度是不是越多越好呢?
答案不是,如果一个文件仅仅比128M大一点点也被当成一个split来对待,而不是多个split.
MR框架在并行运算的同时也会消耗更多资源,并行度越高资源消耗也越高,假设129M文件分为两个分
片,一个是128M,一个是1M;
对于1M的切片的Maptask来说,太浪费资源。
129M的文件在Hdfs存储的时候会不会切成两块?
6.5.3 ReduceTask 工作机制
Reduce大致分为copy、sort、reduce三个阶段,重点在前两个阶段。copy阶段包含一个eventFetcher来获取已完成的map列表,由Fetcher线程去copy数据,在此过程中会启动两个merge线程,分别为inMemoryMerger和onDiskMerger,分别将内存中的数据merge到磁盘和将磁盘中的数据进行merge。待数据copy完成之后,copy阶段就完成了,开始进行sort阶段,sort阶段主要是执行finalMerge操作,纯粹的sort阶段,完成之后就是reduce阶段,调用用户定义的reduce函数进行处理。
详细步骤
- Copy阶段,简单地拉取数据。Reduce进程启动一些数据copy线程(Fetcher),通过HTTP方式请求maptask获取属于自己的文件。
- Merge阶段。这里的merge如map端的merge动作,只是数组中存放的是不同map端copy来的数值。Copy过来的数据会先放入内存缓冲区中,这里的缓冲区大小要比map端的更为灵活。merge有三种形式:内存到内存;内存到磁盘;磁盘到磁盘。默认情况下第一种形式不启用。当内存中的数据量到达一定阈值,就启动内存到磁盘的merge。与map 端类似,这也是溢写的过程,这个过程中如果你设置有Combiner,也是会启用的,然后在磁盘中生成了众多的溢写文件。第二种merge方式一直在运行,直到没有map端的数据时才结束,然后启动第三种磁盘到磁盘的merge方式生成最终的文件。
- 合并排序。把分散的数据合并成一个大的数据后,还会再对合并后的数据排序。
- 对排序后的键值对调用reduce方法,键相等的键值对调用一次reduce方法,每次调用会产生零个或者多个键值对,最后把这些输出的键值对写入到HDFS文件中。
6.5.4 ReduceTask并行度
ReduceTask的并行度同样影响整个Job的执行并发度和执行效率,但与MapTask的并发数由切片数决定不同,ReduceTask数量的决定是可以直接手动设置:
// 默认值是1,手动设置为4
job.setNumReduceTasks(4);
注意事项
- ReduceTask=0,表示没有Reduce阶段,输出文件数和MapTask数量保持一致;
- ReduceTask数量不设置默认就是一个,输出文件数量为1个;
- 如果数据分布不均匀,可能在Reduce阶段产生倾斜;
6.5.5 Shuffle机制
map阶段处理的数据如何传递给reduce阶段,是MapReduce框架中最关键的一个流程,这个流程就叫shuffle。
shuffle: 洗牌、发牌——(核心机制:数据分区,排序,分组,combine,合并等过程)
6.5.5.1 MapReduce的分区与reduceTask的数量
在MapReduce中,通过我们指定分区,会将同一个分区的数据发送到同一个reduce当中进行处理(默认是key相同去往同个分区),例如我们为了数据的统计,我们可以把一批类似的数据发送到同一个reduce当中去,在同一个reduce当中统计相同类型的数据,
如何才能保证相同key的数据去往同个reduce呢?只需要保证相同key的数据分发到同个分区即可。结合以上原理分析我们知道MR程序shuffle机制默认就是这种规则!!
- 分区源码
翻阅源码验证以上规则,MR程序默认使用HashPartitioner,保证了相同的key去往同个分区!!
- 自定义分区
实际生产中需求变化多端,默认分区规则往往不能满足需求,需要结合业务逻辑来灵活控制分区规则以及分区数量!!
如何制定自己需要的分区规则?
具体步骤
a. 自定义类继承Partitioner,重写getPartition()方法
b. 在Driver驱动中,指定使用自定义Partitioner
c. 在Driver驱动中,要根据自定义Partitioner的逻辑设置相应数量的ReduceTask数量。
需求 按照不同的appkey把记录输出到不同的分区中
需求分析
面对业务需求,结合mr的特点,来设计map输出的kv,以及reduce输出的kv数据。
一个ReduceTask对应一个输出文件,因为在shuffle机制中每个reduceTask拉取的都是某一个分区的数据,一个分区对应一个输出文件。
结合appkey的前缀相同的特点,同时不能使用默认分区规则,而是使用自定义分区器,只要appkey前缀相同则数据进入同个分区。
整体思路
Mapper
- 读取一行文本,按照制表符切分
- 解析出appkey字段,其余数据封装为PartitionBean对象(实现序列化Writable接口)
- 设计map()输出的kv,key–>appkey(依靠该字段完成分区),PartitionBean对象作为Value输出
Partition
自定义分区器,实现按照appkey字段的前缀来区分所属分区
Reduce
reduce()正常输出即可,无需进行聚合操作
Driver
- 在原先设置job属性的同时增加设置使用自定义分区器
- 注意设置ReduceTask的数量(与分区数量保持一致)
代码实现
Mapper
package com.lagou.mr.partition;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;
import java.io.IOException;
/*
1. 读取一行文本,按照制表符切分
2. 解析出appkey字段,其余数据封装为PartitionBean对象(实现序列化Writable接口)
3. 设计map()输出的kv,key-->appkey(依靠该字段完成分区),PartitionBean对象作为Value输出
*/
public class PartitionMapper extends Mapper<LongWritable, Text, Text, PartitionBean> {
final PartitionBean bean = new PartitionBean();
final Text k = new Text();
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
final String[] fields = value.toString().split("\t");
String appkey = fields[2];
bean.setId(fields[0]);
bean.setDeviceId(fields[1]);
bean.setAppkey(fields[2]);
bean.setIp(fields[3]);
bean.setSelfDuration(Long.parseLong(fields[4]));
bean.setThirdPartDuration(Long.parseLong(fields[5]));
bean.setStatus(fields[6]);
k.set(appkey);
context.write(k, bean); //shuffle开始时会根据k的hashcode值进行分区,但是结合我们自己的业务,默认hash分区方式不能满足需求
}
}
PartitionBean
package com.lagou.mr.partition;
import org.apache.hadoop.io.Writable;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
public class PartitionBean implements Writable {
//准备一个空参构造
public PartitionBean() {
}
public PartitionBean(String id, String deviceId, String appkey, String ip, Long selfDuration, Long thirdPartDuration, String status) {
this.id = id;
this.deviceId = deviceId;
this.appkey = appkey;
this.ip = ip;
this.selfDuration = selfDuration;
this.thirdPartDuration = thirdPartDuration;
this.status = status;
}
//序列化方法
@Override
public void write(DataOutput out) throws IOException {
out.writeUTF(id);
out.writeUTF(deviceId);
out.writeUTF(appkey);
out.writeUTF(ip);
out.writeLong(selfDuration);
out.writeLong(thirdPartDuration);
out.writeUTF(status);
}
//反序列化方法 要求序列化与反序列化字段顺序要保持一致
@Override
public void readFields(DataInput in) throws IOException {
this.id = in.readUTF();
this.deviceId = in.readUTF();
this.appkey = in.readUTF();
this.ip = in.readUTF();
this.selfDuration = in.readLong();
this.thirdPartDuration = in.readLong();
this.status = in.readUTF();
}
//定义属性
private String id;//日志id
private String deviceId;//设备id
private String appkey;//appkey厂商id
private String ip;//ip地址
private Long selfDuration;//自有内容播放时长
private Long thirdPartDuration;//第三方内容时长
private String status;//状态码
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getDeviceId() {
return deviceId;
}
public void setDeviceId(String deviceId) {
this.deviceId = deviceId;
}
public String getAppkey() {
return appkey;
}
public void setAppkey(String appkey) {
this.appkey = appkey;
}
public String getIp() {
return ip;
}
public void setIp(String ip) {
this.ip = ip;
}
public Long getSelfDuration() {
return selfDuration;
}
public void setSelfDuration(Long selfDuration) {
this.selfDuration = selfDuration;
}
public Long getThirdPartDuration() {
return thirdPartDuration;
}
public void setThirdPartDuration(Long thirdPartDuration) {
this.thirdPartDuration = thirdPartDuration;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
//方便文本中的数据易于观察,重写toString()方法
@Override
public String toString() {
return id + '\t' +
"\t" + deviceId + '\t' + appkey + '\t' +
ip + '\t' +
selfDuration +
"\t" + thirdPartDuration +
"\t" + status;
}
}
CustomPartitioner
package com.lagou.mr.partition;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Partitioner;
//Partitioner分区器的泛型是map输出的kv类型
public class CustomPartitioner extends Partitioner<Text, PartitionBean> {
@Override
public int getPartition(Text text, PartitionBean partitionBean, int numPartitions) {
int partition = 0;
if (text.toString().equals("kar")) {
//只需要保证满足此if条件的数据获得同个分区编号集合
partition = 0;
} else if (text.toString().equals("pandora")) {
partition = 1;
} else {
partition = 2;
}
return partition;
}
}
PartitionReducer
package com.lagou.mr.partition;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;
import java.io.IOException;
//reduce输入类型:Text,PartitionBean,输出:Text,PartitionBean
public class PartitionReducer extends Reducer<Text, PartitionBean, Text, PartitionBean> {
@Override
protected void reduce(Text key, Iterable<PartitionBean> values, Context context) throws IOException, InterruptedException {
//无需聚合运算,只需要进行输出即可
for (PartitionBean bean : values) {
context.write(key, bean);
}
}
}
PartitionDriver
package com.lagou.mr.partition;
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 PartitionDriver {
public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
//1 获取配置文件
final Configuration conf = new Configuration();
//2 获取job实例
final Job job = Job.getInstance(conf);
//3 设置任务相关参数
job.setJarByClass(PartitionDriver.class);
job.setMapperClass(PartitionMapper.class);
job.setReducerClass(PartitionReducer.class);
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(PartitionBean.class);
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(PartitionBean.class);
// 4 设置使用自定义分区器
job.setPartitionerClass(CustomPartitioner.class);
//5 指定reducetask的数量与分区数量保持一致,分区数量是3
job.setNumReduceTasks(3); //reducetask不设置默认是1个
// job.setNumReduceTasks(5);
// job.setNumReduceTasks(2);
// 6 指定输入和输出数据路径
FileInputFormat.setInputPaths(job, new Path("e:/text/parition/speak.data"));
FileOutputFormat.setOutputPath(job, new Path("e:/text/parition/out"));
// 7 提交任务
final boolean flag = job.waitForCompletion(true);
System.exit(flag ? 0 : 1);
}
}
总结
- 自定义分区器时最好保证分区数量与reduceTask数量保持一致;
- 如果分区数量不止1个,但是reduceTask数量1个,此时只会输出一个文件。
- 如果reduceTask数量大于分区数量,但是输出多个空文件
- 如果reduceTask数量小于分区数量,有可能会报错。
6.5.5.2 MapReduce中的Combiner
- combiner运行机制:
- Combiner是MR程序中Mapper和Reducer之外的一种组件
- Combiner组件的父类就是Reducer
- Combiner和reducer的区别在于运行的位置
- Combiner是在每一个maptask所在的节点运行;
- Combiner的意义就是对每一个maptask的输出进行局部汇总,以减小网络传输量。
- Combiner能够应用的前提是不能影响最终的业务逻辑,此外,Combiner的输出kv应该跟reducer的输入kv类型要对应起来。
举例说明
假设一个计算平均值的MR任务
Map阶段
2个MapTask
MapTask1输出数据:10,5,15 如果使用Combiner:(10+5+15)/3=10
MapTask2输出数据:2,6 如果使用Combiner:(2+6)/2=4
Reduce阶段汇总
(10+4)/2=7
而正确结果应该是:
(10+5+15+2+6)/5=7.6
- 自定义Combiner实现步骤
- 自定义一个Combiner继承Reducer,重写Reduce方法
- 在驱动(Driver)设置使用Combiner(默认是不适用Combiner组件)
a. 改造WordCount程序
新建WordCountCombiner类
package com.lagou.mr.wcCombiner;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;
import java.io.IOException;
//combiner组件的输入和输出类型与map()方法保持一致
public class WordCountCombiner extends Reducer<Text, IntWritable, Text, IntWritable> {
final IntWritable total = new IntWritable();
@Override
protected void reduce(Text key, Iterable<IntWritable> values, Context context) throws IOException, InterruptedException {
int num = 0;
//进行局部汇总,逻辑是与reduce方法保持一致
for (IntWritable value : values) {
final int i = value.get();
num += i;
}
total.set(num);
//输出单词,累加结果
context.write(key, total);
}
}
在驱动(WordCountDriver)设置使用Combiner
//5.1 设置使用combiner组件
job.setCombinerClass(WordCountReducer.class); //直接使用Reducer作为Combiner组件来使用是可以的!!
验证结果
观察程序运行日志
如果直接使用WordCountReducer作为Combiner使用是否可以?
直接使用Reducer作为Combiner组件来使用是可以的!!
6.5.6 MapReduce中的排序
6.5.6.1 WritableComparable
Bean对象如果作为Map输出的key时,需要实现WritableComparable接口并重写compareTo方法指定排序规则
1 全排序
基于统计的播放时长案例的输出结果对总时长进行排序
实现全局排序只能设置一个ReduceTask!!
Mapper代码
package com.lagou.mr.sort;
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 SortMapper extends Mapper<LongWritable, Text, SpeakBean, NullWritable> {
final SpeakBean bean = new SpeakBean();
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
//1 读取一行文本,转为字符串,切分
final String[] fields = value.toString().split("\t");
//2 解析出各个字段封装成SpeakBean对象
bean.setDeviceId(fields[0]);
bean.setSelfDrutation(Long.parseLong(fields[1]));
bean.setThirdPartDuration(Long.parseLong(fields[2]));
bean.setSumDuration(Long.parseLong(fields[4]));
//3 SpeakBean作为key输出
context.write(bean, NullWritable.get());
}
}
Reducer
package com.lagou.mr.sort;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.mapreduce.Reducer;
import java.io.IOException;
public class SortReducer extends Reducer<SpeakBean, NullWritable, SpeakBean, NullWritable> {
//reduce方法的调用是相同key的value组成一个集合调用一次
/*
java中如何判断两个对象是否相等?
根据equals方法,比较还是地址值
*/
@Override
protected void reduce(SpeakBean key, Iterable<NullWritable> values, Context context) throws IOException, InterruptedException {
//讨论按照总流量排序这件事情,还需要在reduce端处理吗?因为之前已经利用mr的shuffle对数据进行了排序
//为了避免前面compareTo方法导致总流量相等被当成对象相等,而合并了key,所以遍历values获取每个key(bean对象)
for (NullWritable value : values) { //遍历value同时,key也会随着遍历。
context.write(key, value);
}
}
}
Bean对象实现WritableComparable接口
package com.lagou.mr.sort;
import org.apache.hadoop.io.WritableComparable;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
import java.util.Objects;
//因为这个类的实例对象要作为map输出的key,所以要实现writablecomparalbe接口
public class SpeakBean implements WritableComparable<SpeakBean> {
//定义属性
private Long selfDrutation;//自有内容播放时长
private Long thirdPartDuration;//第三方内容播放时长
private String deviceId;//设备id
private Long sumDuration;//总时长
//准备构造方法
public SpeakBean() {
}
public SpeakBean(Long selfDrutation, Long thirdPartDuration, String deviceId, Long sumDuration) {
this.selfDrutation = selfDrutation;
this.thirdPartDuration = thirdPartDuration;
this.deviceId = deviceId;
this.sumDuration = sumDuration;
}
public Long getSelfDrutation() {
return selfDrutation;
}
public void setSelfDrutation(Long selfDrutation) {
this.selfDrutation = selfDrutation;
}
public Long getThirdPartDuration() {
return thirdPartDuration;
}
public void setThirdPartDuration(Long thirdPartDuration) {
this.thirdPartDuration = thirdPartDuration;
}
public String getDeviceId() {
return deviceId;
}
public void setDeviceId(String deviceId) {
this.deviceId = deviceId;
}
public Long getSumDuration() {
return sumDuration;
}
public void setSumDuration(Long sumDuration) {
this.sumDuration = sumDuration;
}
//序列化方法
@Override
public void write(DataOutput out) throws IOException {
out.writeLong(selfDrutation);
out.writeLong(thirdPartDuration);
out.writeUTF(deviceId);
out.writeLong(sumDuration);
}
//反序列化方法
@Override
public void readFields(DataInput in) throws IOException {
this.selfDrutation = in.readLong();
this.thirdPartDuration = in.readLong();
this.deviceId = in.readUTF();
this.sumDuration = in.readLong();
}
//指定排序规则,我们希望按照总时长进行排序
@Override
public int compareTo(SpeakBean o) { //返回值三种:0:相等 1:小于 -1:大于
System.out.println("compareTo 方法执行了。。。");
//指定按照bean对象的总时长字段的值进行比较
if (this.sumDuration > o.sumDuration) {
return -1;
} else if (this.sumDuration < o.sumDuration) {
return 1;
} else {
return 0; //加入第二个判断条件,二次排序
}
}
@Override
public boolean equals(Object o) {
System.out.println("equals方法执行了。。。");
return super.equals(o);
}
@Override
public int hashCode() {
return Objects.hash(getSelfDrutation(), getThirdPartDuration(), getDeviceId(), getSumDuration());
}
@Override
public String toString() {
return selfDrutation +
"\t" + thirdPartDuration +
"\t" + deviceId + '\t' +
sumDuration
;
}
}
Driver
package com.lagou.mr.sort;
import com.lagou.mr.wc.WordCountDriver;
import com.lagou.mr.wc.WordCountMapper;
import com.lagou.mr.wc.WordCountReducer;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.IntWritable;
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 SortDriver {
public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
/*
1. 获取配置文件对象,获取job对象实例
2. 指定程序jar的本地路径
3. 指定Mapper/Reducer类
4. 指定Mapper输出的kv数据类型
5. 指定最终输出的kv数据类型
6. 指定job处理的原始数据路径
7. 指定job输出结果路径
8. 提交作业
*/
// 1. 获取配置文件对象,获取job对象实例
final Configuration conf = new Configuration();
final Job job = Job.getInstance(conf, "SortDriver");
// 2. 指定程序jar的本地路径
job.setJarByClass(SortDriver.class);
// 3. 指定Mapper/Reducer类
job.setMapperClass(SortMapper.class);
job.setReducerClass(SortReducer.class);
// 4. 指定Mapper输出的kv数据类型
job.setMapOutputKeyClass(SpeakBean.class);
job.setMapOutputValueClass(NullWritable.class);
// 5. 指定最终输出的kv数据类型
job.setOutputKeyClass(SpeakBean.class);
job.setOutputValueClass(NullWritable.class);
//指定reduceTask的数量,默认是1个
job.setNumReduceTasks(1);
// 6. 指定job处理的原始数据路径
//import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
//import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
FileInputFormat.setInputPaths(job, new Path("E:\\speak\\out")); //指定读取数据的原始路径
// 7. 指定job输出结果路径
FileOutputFormat.setOutputPath(job, new Path("e:\\speak\\sortout")); //指定结果数据输出路径
// 8. 提交作业
final boolean flag = job.waitForCompletion(true);
//jvm退出:正常退出0,非0值则是错误退出
System.exit(flag ? 0 : 1);
}
}
总结
- 自定义对象作为Map的key输出时,需要实现WritableComparable接口,排序:重写compareTo()方法,序列以及反序列化方法
- 再次理解reduce()方法的参数;reduce()方法是map输出的kv中key相同的kv中的v组成一个集合调用一次reduce()方法,选择遍历values得到所有的key.
- 默认reduceTask数量是1个;
- 对于全局排序需要保证只有一个reduceTask!!
2 分区排序(默认的分区规则,区内有序)
6.5.6.2 GroupingComparator
GroupingComparator是mapreduce当中reduce端的一个功能组件,主要的作用是决定哪些数据作为一组,调用一次reduce的逻辑,默认是每个不同的key,作为多个不同的组,每个组调用一次reduce逻辑,我们可以自定义GroupingComparator实现不同的key作为同一个组,调用一次reduce逻辑
参考代码
- OrderBean
OrderBean定义两个字段,一个字段是orderId,第二个字段是金额(注意金额一定要使用Double或者DoubleWritable类型,否则没法按照金额顺序排序)
排序规则指定为先按照订单Id排序,订单Id相等再按照金额降序排!!
package com.lagou.mr.group;
import org.apache.hadoop.io.WritableComparable;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
public class OrderBean implements WritableComparable<OrderBean> {
private String orderId;//订单id
private Double price;//金额
public OrderBean(String orderId, Double price) {
this.orderId = orderId;
this.price = price;
}
public OrderBean() {
}
public String getOrderId() {
return orderId;
}
public void setOrderId(String orderId) {
this.orderId = orderId;
}
public Double getPrice() {
return price;
}
public void setPrice(Double price) {
this.price = price;
}
//指定排序规则,先按照订单id比较再按照金额比较,按照金额降序排
@Override
public int compareTo(OrderBean o) {
int res = this.orderId.compareTo(o.getOrderId()); //0 1 -1
if (res == 0) {
//订单id相同,比较金额
res = - this.price.compareTo(o.getPrice());
}
return res;
}
//序列化
@Override
public void write(DataOutput out) throws IOException {
out.writeUTF(orderId);
out.writeDouble(price);
}
//反序列化
@Override
public void readFields(DataInput in) throws IOException {
this.orderId = in.readUTF();
this.price = in.readDouble();
}
//重写toString()
@Override
public String toString() {
return orderId + '\t' +
price
;
}
}
- 自定义分区器
保证ID相同的订单去往同个分区最终去往同一个Reduce中
package com.lagou.mr.group;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.mapreduce.Partitioner;
public class CustomPartitioner extends Partitioner<OrderBean, NullWritable> {
@Override
public int getPartition(OrderBean orderBean, NullWritable nullWritable, int numPartitions) {
//希望订单id相同的数据进入同个分区
return (orderBean.getOrderId().hashCode() & Integer.MAX_VALUE) % numPartitions;
}
}
- 自定义GroupingComparator
保证id相同的订单进入一个分组中,进入分组的数据已经是按照金额降序排序。reduce()方法取出第一个即是金额最高的交易
package com.lagou.mr.group;
import org.apache.hadoop.io.WritableComparable;
import org.apache.hadoop.io.WritableComparator;
public class CustomGroupingComparator extends WritableComparator {
//将我们自定义的OrderBean注册到我们自定义的CustomGroupIngCompactor当中来
// 表示我们的分组器在分组的时候,对OrderBean这一种类型的数据进行分组
// 传入作为key的bean的class类型,以及制定需要让框架做反射获取实例对象
public CustomGroupingComparator() {
super(OrderBean.class, true); //注册自定义的GroupingComparator接受OrderBean对象
}
//重写其中的compare方法,通过这个方法来让mr接受orderid相等则两个对象相等的规则,key相等
@Override
public int compare(WritableComparable a, WritableComparable b) { //a 和b是orderbean的对象
//比较两个对象的orderid
final OrderBean o1 = (OrderBean) a;
final OrderBean o2 = (OrderBean) b;
return o1.getOrderId().compareTo(o2.getOrderId()); // 0 1 -1
}
}
- Mapper
package com.lagou.mr.group;
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 GroupMapper extends Mapper<LongWritable, Text, OrderBean, NullWritable> {
OrderBean bean = new OrderBean();
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
final String[] fields = value.toString().split("\t");
//订单id与jine封装为一个orderBean
bean.setOrderId(fields[0]);
bean.setPrice(Double.parseDouble(fields[2]));
context.write(bean, NullWritable.get());
}
}
- Reducer
package com.lagou.mr.group;
import org.apache.avro.Schema;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.mapreduce.Reducer;
import java.io.IOException;
public class GroupReducer extends Reducer<OrderBean, NullWritable, OrderBean, NullWritable> {
//key:reduce方法的key注意是一组相同key的kv的第一个key作为传入reduce方法的key,因为我们已经指定了排序的规则
//按照金额降序排列,则第一个key就是金额最大的交易数据
//value:一组相同key的kv对中v的集合
//对于如何判断key是否相同,自定义对象是需要我们指定一个规则,这个规则通过Groupingcomaprator来指定
@Override
protected void reduce(OrderBean key, Iterable<NullWritable> values, Context context) throws IOException, InterruptedException {
//直接输出key就是金额最大的交易
context.write(key, NullWritable.get());
}
}
- Driver
package com.lagou.mr.group;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.NullWritable;
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 GroupDriver {
public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
/*
1. 获取配置文件对象,获取job对象实例
2. 指定程序jar的本地路径
3. 指定Mapper/Reducer类
4. 指定Mapper输出的kv数据类型
5. 指定最终输出的kv数据类型
6. 指定job处理的原始数据路径
7. 指定job输出结果路径
8. 提交作业
*/
// 1. 获取配置文件对象,获取job对象实例
final Configuration conf = new Configuration();
final Job job = Job.getInstance(conf, "GroupDriver");
// 2. 指定程序jar的本地路径
job.setJarByClass(GroupDriver.class);
// 3. 指定Mapper/Reducer类
job.setMapperClass(GroupMapper.class);
job.setReducerClass(GroupReducer.class);
// 4. 指定Mapper输出的kv数据类型
job.setMapOutputKeyClass(OrderBean.class);
job.setMapOutputValueClass(NullWritable.class);
// 5. 指定最终输出的kv数据类型
job.setOutputKeyClass(OrderBean.class);
job.setOutputValueClass(NullWritable.class);
//指定分区器
job.setPartitionerClass(CustomPartitioner.class);
//指定使用groupingcomparator
job.setGroupingComparatorClass(CustomGroupingComparator.class);
FileInputFormat.setInputPaths(job, new Path("E:\\text\\group")); //指定读取数据的原始路径
// 7. 指定job输出结果路径
FileOutputFormat.setOutputPath(job, new Path("E:\\text\\group\\out")); //指定结果数据输出路径
//指定reducetask的数量,不要使用默认的一个,分区效果不明显
job.setNumReduceTasks(2);
// 8. 提交作业
final boolean flag = job.waitForCompletion(true);
//jvm退出:正常退出0,非0值则是错误退出
System.exit(flag ? 0 : 1);
}
}
结果
6.5.7 MapReduce Join实战
6.5.7.1 MR reduce端join
1.1 需求分析
1.2 代码实现
通过将关联的条件作为map输出的key,将两表满足join条件的数据并携带数据所来源的文件信息,发往同一个reduce task,在reduce中进行数据的串联
Driver
package com.lagou.mr.join.reduce_join;
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 ReduceJoinDriver {
public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
// 输入输出路径需要根据自己电脑上实际的输入输出路径设置
args = new String[] { "E:\\text\\reduce_join\\input", "E:\\text\\reduce_join\\output" };
// 1. 获取配置文件对象,获取job对象实例
final Configuration conf = new Configuration();
final Job job = Job.getInstance(conf, "ReduceJoinDriver");
// 2. 指定程序jar的本地路径
job.setJarByClass(ReduceJoinDriver.class);
// 3. 指定Mapper/Reducer类
job.setMapperClass(ReduceJoinMapper.class);
job.setReducerClass(ReduceJoinReducer.class);
// 4. 指定Mapper输出的kv数据类型
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(DeliverBean.class);
// 5. 指定最终输出的kv数据类型
job.setOutputKeyClass(DeliverBean.class);
job.setOutputValueClass(NullWritable.class);
// 6. 指定job输出结果路径
FileInputFormat.setInputPaths(job, new Path(args[0]));
//指定读取数据的原始 路径
// 7. 指定job输出结果路径
FileOutputFormat.setOutputPath(job, new Path(args[1]));
//指定结果数据输出 路径
// 8. 提交作业
final boolean flag = job.waitForCompletion(true);
//jvm退出:正常退出0,非0值则是错误退出
System.exit(flag ? 0 : 1);
}
}
Mapper
package com.lagou.mr.join.reduce_join;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;
import org.apache.hadoop.mapreduce.lib.input.FileSplit;
import java.io.IOException;
public class ReduceJoinMapper extends Mapper<LongWritable, Text, Text, DeliverBean> {
String name;
DeliverBean bean = new DeliverBean();
Text k = new Text();
@Override
protected void setup(Context context) throws IOException, InterruptedException {
// 1 获取输入文件切片
FileSplit split = (FileSplit) context.getInputSplit();
// 2 获取输入文件名称
name = split.getPath().getName();
}
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
// 1 获取输入数据
String line = value.toString();
// 2 不同文件分别处理
if (name.startsWith("deliver_info")) {
// 2.1 切割
String[] fields = line.split("\t");
// 2.2 封装bean对象
bean.setUserId(fields[0]);
bean.setPositionId(fields[1]);
bean.setDate(fields[2]);
bean.setPositionName("");
bean.setFlag("deliver");
k.set(fields[1]);
} else {
// 2.3 切割
String[] fields = line.split("\t");
// 2.4 封装bean对象
bean.setPositionId(fields[0]);
bean.setPositionName(fields[1]);
bean.setUserId("");
bean.setDate("");
bean.setFlag("position");
k.set(fields[0]);
}
// 3 写出
context.write(k, bean);
}
}
Reducer
package com.lagou.mr.join.reduce_join;
import org.apache.commons.beanutils.BeanUtils;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;
import java.io.IOException;
import java.util.ArrayList;
public class ReduceJoinReducer extends Reducer<Text, DeliverBean, DeliverBean, NullWritable> {
@Override
protected void reduce(Text key, Iterable<DeliverBean> values, Context context) throws IOException, InterruptedException {
// 1准备投递行为数据的集合
ArrayList<DeliverBean> deBeans = new ArrayList<>();
// 2 准备bean对象
DeliverBean pBean = new DeliverBean();
for (DeliverBean bean : values) {
if ("deliver".equals(bean.getFlag())) {
DeliverBean dBean = new DeliverBean();
try {
BeanUtils.copyProperties(dBean, bean);
} catch (Exception e) {
e.printStackTrace();
}
deBeans.add(dBean);
} else {
try {
BeanUtils.copyProperties(pBean, bean);
} catch (Exception e) {
e.printStackTrace();
}
}
}
// 3 表的拼接
for (DeliverBean bean : deBeans) {
bean.setPositionName(pBean.getPositionName());
// 4 数据写出去
context.write(bean, NullWritable.get());
}
}
}
Bean
package com.lagou.mr.join.reduce_join;
import org.apache.hadoop.io.Writable;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
public class DeliverBean implements Writable {
private String userId;
private String positionId;
private String date;
private String positionName;
private String flag;
public DeliverBean() {
}
public DeliverBean(String userId, String positionId, String date, String positionName, String flag) {
this.userId = userId;
this.positionId = positionId;
this.date = date;
this.positionName = positionName;
this.flag = flag;
}
public String getUserId() {
return userId;
}
public void setUserId(String userId) {
this.userId = userId;
}
public String getPositionId() {
return positionId;
}
public void setPositionId(String positionId) {
this.positionId = positionId;
}
public String getDate() {
return date;
}
public void setDate(String date) {
this.date = date;
}
public String getPositionName() {
return positionName;
}
public void setPositionName(String positionName) {
this.positionName = positionName;
}
public String getFlag() {
return flag;
}
public void setFlag(String flag) {
this.flag = flag;
}
@Override
public void write(DataOutput out) throws IOException {
out.writeUTF(userId);
out.writeUTF(positionId);
out.writeUTF(date);
out.writeUTF(positionName);
out.writeUTF(flag);
}
@Override
public void readFields(DataInput in) throws IOException {
this.userId = in.readUTF();
this.positionId = in.readUTF();
this.date = in.readUTF();
this.positionName = in.readUTF();
this.flag = in.readUTF();
}
@Override
public String toString() {
return "DeliverBean{" + "userId='" + userId + '\'' + ", positionId='" + positionId + '\'' + ", date='" + date + '\'' + ", positionName='" + positionName;
}
}
缺点:这种方式中,join的操作是在reduce阶段完成,reduce端的处理压力太大,map节点的运算负载则很低,资源利用率不高,且在reduce阶段极易产生数据倾斜
解决方案: map端join实现方式
6.5.7.2 MR map端join
2.1 需求分析
适用于关联表中有小表的情形;
可以将小表分发到所有的map节点,这样,map节点就可以在本地对自己所读到的大表数据进行join并输出最终结果,可以大大提高join操作的并发度,加快处理速度
2.2 代码实现
- 在Mapper的setup阶段,将文件读取到缓存集合中
- 在驱动函数中加载缓存。
// 缓存普通文件到Task运行节点。
job.addCacheFile(new URI(“file:///e:/cache/position.txt”));
Driver
package com.lagou.mr.join.map_join;
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, ClassNotFoundException, InterruptedException, URISyntaxException {
// 输入输出路径需要根据自己电脑上实际的输入输出路径设置
args = new String[] { "E:\\text\\map_join\\input", "E:\\text\\map_join\\output" };
// 1. 获取配置文件对象,获取job对象实例
final Configuration conf = new Configuration();
final Job job = Job.getInstance(conf, "ReduceJoinDriver");
// 2. 指定程序jar的本地路径
job.setJarByClass(MapJoinDriver.class);
// 3. 指定Mapper类
job.setMapperClass(MapJoinMapper.class);
// 4. 指定最终输出的kv数据类型
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(NullWritable.class);
//5.指定job读取数据路径
FileInputFormat.setInputPaths(job, new Path(args[0]));
//指定读取数据的原始 路径
// 6. 指定job输出结果路径
FileOutputFormat.setOutputPath(job, new Path(args[1]));
//指定结果数据输出 路径
// 7.加载缓存文件
job.addCacheFile(new URI("file:///E:/text/map_join/cache/position.txt"));
job.setNumReduceTasks(0);
// 8. 提交作业
final boolean flag = job.waitForCompletion(true);
//jvm退出:正常退出0,非0值则是错误退出
System.exit(flag ? 0 : 1);
}
}
Mapper
package com.lagou.mr.join.map_join;
import org.apache.commons.lang3.StringUtils;
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 org.apache.hadoop.mapreduce.lib.input.FileSplit;
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.HashMap;
import java.util.Map;
public class MapJoinMapper extends Mapper<LongWritable, Text, Text, NullWritable> {
String name;
DeliverBean bean = new DeliverBean();
Text k = new Text();
Map<String, String> pMap = new HashMap<>();
//读取文件
@Override
protected void setup(Context context) throws IOException, InterruptedException {
// 1 获取缓存的文件
BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream("E:/text/map_join/cache/position.txt"), "UTF-8"));
String line;
while (StringUtils.isNotEmpty(line = reader.readLine())) {
// 2 切割
String[] fields = line.split("\t");
// 3 缓存数据到集合
pMap.put(fields[0], fields[1]);
}
// 4 关流
reader.close();
}
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
// 1 获取一行
String line = value.toString();
// 2 截取
String[] fields = line.split("\t");
// 3 获取职位id
String pId = fields[1];
// 4 获取职位名称
String pName = pMap.get(pId);
// 5 拼接
k.set(line + "\t" + pName);
// 写出
context.write(k, NullWritable.get());
}
}
DeliverBean
package com.lagou.mr.join.map_join;
import org.apache.hadoop.io.Writable;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
public class DeliverBean implements Writable {
private String userId;
private String positionId;
private String date;
private String positionName;
private String flag;
public DeliverBean() {
}
public DeliverBean(String userId, String positionId, String date, String positionName, String flag) {
this.userId = userId;
this.positionId = positionId;
this.date = date;
this.positionName = positionName;
this.flag = flag;
}
public String getUserId() {
return userId;
}
public void setUserId(String userId) {
this.userId = userId;
}
public String getPositionId() {
return positionId;
}
public void setPositionId(String positionId) {
this.positionId = positionId;
}
public String getDate() {
return date;
}
public void setDate(String date) {
this.date = date;
}
public String getPositionName() {
return positionName;
}
public void setPositionName(String positionName) {
this.positionName = positionName;
}
public String getFlag() {
return flag;
}
public void setFlag(String flag) {
this.flag = flag;
}
@Override
public void write(DataOutput out) throws IOException {
out.writeUTF(userId);
out.writeUTF(positionId);
out.writeUTF(date);
out.writeUTF(positionName);
out.writeUTF(flag);
}
@Override
public void readFields(DataInput in) throws IOException {
this.userId = in.readUTF();
this.positionId = in.readUTF();
this.date = in.readUTF();
this.positionName = in.readUTF();
this.flag = in.readUTF();
}
@Override
public String toString() {
return "DeliverBean{" + "userId='" + userId + '\'' + ", positionId='" + positionId + '\'' + ", date='" + date + '\'' + ", positionName='" + positionName + '\'' + '}';
}
}
6.5.7.3 数据倾斜解决方案
什么是数据倾斜?
- 数据倾斜无非就是大量的相同key被partition分配到一个分区里,
现象 - 绝大多数task执行得都非常快,但个别task执行的极慢。甚至失败!
通用解决方案:
对key增加随机数。
6.5.8 MapReduce读取和输出数据
6.5.8.1 InputFormat
运行MapReduce程序时,输入的文件格式包括:基于行的日志文件、二进制格式文件、数据库表等。那么,针对不同的数据类型,MapReduce是如何读取这些数据的呢?
InputFormat是MapReduce框架用来读取数据的类。
InputFormat常见子类包括:
- TextInputFormat (普通文本文件,MR框架默认的读取实现类型)
- KeyValueTextInputFormat(读取一行文本数据按照指定分隔符,把数据封装为kv类型)
- NLineInputF ormat(读取数据按照行数进行划分分片)
- CombineTextInputFormat(合并小文件,避免启动过多MapTask任务)
- 自定义InputFormat
具体使用方式
Driver里添加
// 如果不设置InputFormat,它默认用的是TextInputFormat.class
job.setInputFormatClass(CombineTextInputFormat.class); //虚拟存储切片最大值设置4m
CombineTextInputFormat.setMaxInputSplitSize(job, 4194304);
代码参考
自定义InputFormat
package com.lagou.mr.sequence;
//自定义inputformat读取多个小文件合并为一个SequenceFile文件
//SequenceFile文件中以kv形式存储文件,key--》文件路径+文件名称,value-->文件的整个内容
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.BytesWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.InputSplit;
import org.apache.hadoop.mapreduce.JobContext;
import org.apache.hadoop.mapreduce.RecordReader;
import org.apache.hadoop.mapreduce.TaskAttemptContext;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import java.io.IOException;
//TextInputFormat中泛型是LongWritable:文本的偏移量, Text:一行文本内容;指明当前inputformat的输出数据类型
//自定义inputformat:key-->文件路径+名称,value-->整个文件内容
public class CustomInputFormat extends FileInputFormat<Text, BytesWritable> {
//重写是否可切分
@Override
protected boolean isSplitable(JobContext context, Path filename) {
//对于当前需求,不需要把文件切分,保证一个切片就是一个文件
return false;
}
//recordReader就是用来读取数据的对象
@Override
public RecordReader<Text, BytesWritable> createRecordReader(InputSplit split, TaskAttemptContext context) throws IOException, InterruptedException {
CustomRecordReader recordReader = new CustomRecordReader();
//调用recordReader的初始化方法
recordReader.initialize(split, context);
return recordReader;
}
}
自定义RecordReader
package com.lagou.mr.sequence;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FSDataInputStream;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.BytesWritable;
import org.apache.hadoop.io.IOUtils;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.InputSplit;
import org.apache.hadoop.mapreduce.RecordReader;
import org.apache.hadoop.mapreduce.TaskAttemptContext;
import org.apache.hadoop.mapreduce.lib.input.FileSplit;
import java.io.IOException;
//负责读取数据,一次读取整个文件内容,封装成kv输出
public class CustomRecordReader extends RecordReader<Text, BytesWritable> {
private FileSplit split;
//hadoop配置文件对象
private Configuration conf;
//定义key,value的成员变量
private Text key = new Text();
private BytesWritable value = new BytesWritable();
//初始化方法,把切片以及上下文提升为全局
@Override
public void initialize(InputSplit split, TaskAttemptContext context) throws IOException, InterruptedException {
this.split = (FileSplit) split;
conf = context.getConfiguration();
}
private Boolean flag = true;
//用来读取数据的方法
@Override
public boolean nextKeyValue() throws IOException, InterruptedException {
//对于当前split来说只需要读取一次即可,因为一次就把整个文件全部读取了。
if (flag) {
//准备一个数组存放读取到的数据,数据大小是多少?
byte[] content = new byte[(int) split.getLength()];
final Path path = split.getPath();//获取切片的path信息
final FileSystem fs = path.getFileSystem(conf);//获取到文件系统对象
final FSDataInputStream fis = fs.open(path); //获取到输入流
IOUtils.readFully(fis, content, 0, content.length); //读取数据并把数据放入byte[]
//封装key和value
key.set(path.toString());
value.set(content, 0, content.length);
IOUtils.closeStream(fis);
//把再次读取的开关置为false
flag = false;
return true;
}
return false;
}
//获取到key
@Override
public Text getCurrentKey() throws IOException, InterruptedException {
return key;
}
//获取到value
@Override
public BytesWritable getCurrentValue() throws IOException, InterruptedException {
return value;
}
//获取进度
@Override
public float getProgress() throws IOException, InterruptedException {
return 0;
}
//关闭资源
@Override
public void close() throws IOException {
}
}
Mapper
package com.lagou.mr.sequence;
import org.apache.hadoop.io.BytesWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;
import java.io.IOException;
//text:代表的是一个文件的path+名称,BytesWritable:一个文件的内容
public class SequcenMapper extends Mapper<Text, BytesWritable, Text, BytesWritable> {
@Override
protected void map(Text key, BytesWritable value, Context context) throws IOException, InterruptedException {
context.write(key, value);
}
}
Reducer
package com.lagou.mr.sequence;
import org.apache.hadoop.io.BytesWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;
import java.io.IOException;
public class SequcenReducer extends Reducer<Text, BytesWritable, Text, BytesWritable> {
@Override
protected void reduce(Text key, Iterable<BytesWritable> values, Context context) throws IOException, InterruptedException {
//输出value值(文件内容),只获取其中第一个即可(只有一个)
context.write(key, values.iterator().next());
}
}
Driver
package com.lagou.mr.sequence;
import com.lagou.mr.wc.WordCountDriver;
import com.lagou.mr.wc.WordCountMapper;
import com.lagou.mr.wc.WordCountReducer;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.BytesWritable;
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.CombineTextInputFormat;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
import java.io.IOException;
public class SequenceDriver {
public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
/*
1. 获取配置文件对象,获取job对象实例
2. 指定程序jar的本地路径
3. 指定Mapper/Reducer类
4. 指定Mapper输出的kv数据类型
5. 指定最终输出的kv数据类型
6. 指定job处理的原始数据路径
7. 指定job输出结果路径
8. 提交作业
*/
// 1. 获取配置文件对象,获取job对象实例
final Configuration conf = new Configuration();
final Job job = Job.getInstance(conf, "SequenceDriver");
// 2. 指定程序jar的本地路径
job.setJarByClass(SequenceDriver.class);
// 3. 指定Mapper/Reducer类
job.setMapperClass(SequcenMapper.class);
job.setReducerClass(SequcenReducer.class);
// 4. 指定Mapper输出的kv数据类型
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(BytesWritable.class);
// 5. 指定最终输出的kv数据类型
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(BytesWritable.class);
//设置使用自定义InputFormat读取数据
job.setInputFormatClass(CustomInputFormat.class);
FileInputFormat.setInputPaths(job, new Path("E:\\text\\sequence\\sequencefile小文件")); //指定读取数据的原始路径
// 7. 指定job输出结果路径
FileOutputFormat.setOutputPath(job, new Path("E:\\text\\sequence\\out")); //指定结果数据输出路径
// 8. 提交作业
final boolean flag = job.waitForCompletion(true);
//jvm退出:正常退出0,非0值则是错误退出
System.exit(flag ? 0 : 1);
}
}
验证输出结果
6.5.8.2 OutputFormat
参考代码
Mapper
package com.lagou.mr.output;
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 OutputMapper extends Mapper<LongWritable, Text, Text, NullWritable> {
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
context.write(value, NullWritable.get());
}
}
Reducer
package com.lagou.mr.output;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;
import java.io.IOException;
public class OutputReducer extends Reducer<Text, NullWritable, Text, NullWritable> {
@Override
protected void reduce(Text key, Iterable<NullWritable> values, Context context) throws IOException, InterruptedException {
context.write(key, NullWritable.get());
}
}
OutputFormat
package com.lagou.mr.output;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FSDataOutputStream;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
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 CustomOutputFormat extends FileOutputFormat<Text, NullWritable> {
//写出数据的对象
@Override
public RecordWriter<Text, NullWritable> getRecordWriter(TaskAttemptContext context) throws IOException, InterruptedException {
//定义写出数据的路径信息,并获取到输出流传入writer对象中
final Configuration conf = context.getConfiguration();
final FileSystem fs = FileSystem.get(conf);
//定义输出的路径
final FSDataOutputStream lagouOut = fs.create(new Path("e:/text/lagou.log"));
final FSDataOutputStream otherOut = fs.create(new Path("e:/text/other.log"));
CustomWriter customWriter = new CustomWriter(lagouOut, otherOut);
return customWriter;
}
}
RecordWriter
package com.lagou.mr.output;
import org.apache.hadoop.fs.FSDataOutputStream;
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 CustomWriter extends RecordWriter<Text, NullWritable> {
//定义成员变量
private FSDataOutputStream lagouOut;
private FSDataOutputStream otherOut;
//定义构造方法接收两个输出流
public CustomWriter(FSDataOutputStream lagouOut, FSDataOutputStream otherOut) {
this.lagouOut = lagouOut;
this.otherOut = otherOut;
}
//写出数据的逻辑,控制写出的路径
@Override
public void write(Text key, NullWritable value) throws IOException, InterruptedException {
//写出数据需要输出流
final String line = key.toString();
if (line.contains("lagou")) {
lagouOut.write(line.getBytes());
lagouOut.write("\r\n".getBytes());
} else {
otherOut.write(line.getBytes());
otherOut.write("\r\n".getBytes());
}
}
//关闭,释放资源
@Override
public void close(TaskAttemptContext context) throws IOException, InterruptedException {
IOUtils.closeStream(lagouOut);
IOUtils.closeStream(otherOut);
}
}
Driver
package com.lagou.mr.output;
import com.lagou.mr.wc.WordCountDriver;
import com.lagou.mr.wc.WordCountMapper;
import com.lagou.mr.wc.WordCountReducer;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.IntWritable;
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.CombineTextInputFormat;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
import java.io.IOException;
public class OutputDriver {
public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
/*
1. 获取配置文件对象,获取job对象实例
2. 指定程序jar的本地路径
3. 指定Mapper/Reducer类
4. 指定Mapper输出的kv数据类型
5. 指定最终输出的kv数据类型
6. 指定job处理的原始数据路径
7. 指定job输出结果路径
8. 提交作业
*/
// 1. 获取配置文件对象,获取job对象实例
final Configuration conf = new Configuration();
final Job job = Job.getInstance(conf, "OutputDriver");
// 2. 指定程序jar的本地路径
job.setJarByClass(OutputDriver.class);
// 3. 指定Mapper/Reducer类
job.setMapperClass(OutputMapper.class);
job.setReducerClass(OutputReducer.class);
// 4. 指定Mapper输出的kv数据类型
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(NullWritable.class);
// 5. 指定最终输出的kv数据类型
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(NullWritable.class);
//指定使用自定义outputformat
job.setOutputFormatClass(CustomOutputFormat.class);
FileInputFormat.setInputPaths(job, new Path("E:\\text\\click_log")); //指定读取数据的原始路径
// 7. 指定job输出结果路径,因为mr默认要输出一个success等标识文件
FileOutputFormat.setOutputPath(job, new Path("E:\\clicklog\\out")); //指定结果数据输出路径
// 8. 提交作业
final boolean flag = job.waitForCompletion(true);
//jvm退出:正常退出0,非0值则是错误退出
System.exit(flag ? 0 : 1);
}
}
验证结果是否已把数据分别输出到不同的目录中!!
6.5.9 shuffle阶段数据的压缩机制
6.5.9.1 hadoop当中支持的压缩算法
数据压缩有两大好处,节约磁盘空间,加速数据在网络和磁盘上的传输!!
我们可以使用bin/hadoop checknative 来查看我们编译之后的hadoop支持的各种压缩,如果出现openssl为false,那么就在线安装一下依赖包!!
安装openssl
yum install -y openssl-devel
6.5.9.2 压缩位置
6.5.9.3 压缩配置方式
- 在驱动代码中通过Configuration直接设置使用的压缩方式,可以开启Map输出和Reduce输出压缩
//设置map阶段压缩
Configuration configuration = new Configuration(); configuration.set(“mapreduce.map.output.compress”,“true”); configuration.set(“mapreduce.map.output.compress.codec”,“org.apache.hadoop.i o.compress.SnappyCodec”);
//设置reduce阶段的压缩 configuration.set(“mapreduce.output.fileoutputformat.compress”,“true”); configuration.set(“mapreduce.output.fileoutputformat.compress.type”,“RECORD” );configuration.set(“mapreduce.output.fileoutputformat.compress.codec”,“org.apache.hadoop.io.compress.SnappyCodec”);
- 配置mapred-site.xml(修改后分发到集群其它节点,重启Hadoop集群),此种方式对运行在集群的所有MR任务都会执行压缩。(建议使用第一种方式)
<property>
<name>mapreduce.output.fileoutputformat.compress</name>
<value>true</value>
</property>
<property>
<name>mapreduce.output.fileoutputformat.compress.type</name>
<value>RECORD</value>
</property>
<property>
<name>mapreduce.output.fileoutputformat.compress.codec</name>
<value>org.apache.hadoop.io.compress.SnappyCodec</value>
</property>
6.5.9.4 压缩案例
需求
使用snappy压缩方式压缩WordCount案例的输出结果数据
具体实现
在驱动代码中添加压缩配置
configuration.set("mapreduce.output.fileoutputformat.compress","true");
configuration.set("mapreduce.output.fileoutputformat.compress.type","RECORD");
configuration.set("mapreduce.output.fileoutputformat.compress.codec","org.apache .hadoop.io.compress.SnappyCodec");
重新打成jar包,提交集群运行,验证输出结果是否已进行了snappy压缩!!
6.6 MR综合案例
6.6.1 需求
6.6.2 分析
- 自定义InputFormat合并小文件
- 自定义分区根据评论等级把数据分区
- 自定义OutputFormat把数据输出到多个目录
6.6.3 开发步骤
1、合并小文件
Mapper
package com.lagou.mr.comment.step1;
import org.apache.hadoop.io.BytesWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;
import java.io.IOException;
//text:代表的是一个文件的path+名称,BytesWritable:一个文件的内容
public class MergeMapper extends Mapper<Text, BytesWritable, Text, BytesWritable> {
@Override
protected void map(Text key, BytesWritable value, Context context) throws IOException, InterruptedException {
context.write(key, value);
}
}
自定义InputFormat
MergeInputFormat
package com.lagou.mr.comment.step1;
//自定义inputformat读取多个小文件合并为一个SequenceFile文件
//SequenceFile文件中以kv形式存储文件,key--》文件路径+文件名称,value-->文件的整个内容
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.BytesWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.InputSplit;
import org.apache.hadoop.mapreduce.JobContext;
import org.apache.hadoop.mapreduce.RecordReader;
import org.apache.hadoop.mapreduce.TaskAttemptContext;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import java.io.IOException;
//TextInputFormat中泛型是LongWritable:文本的偏移量, Text:一行文本内容;指明当前inputformat的输出数据类型
//自定义inputformat:key-->文件路径+名称,value-->整个文件内容
public class MergeInputFormat extends FileInputFormat<Text, BytesWritable> {
//重写是否可切分
@Override
protected boolean isSplitable(JobContext context, Path filename) {
//对于当前需求,不需要把文件切分,保证一个切片就是一个文件
return false;
}
//recordReader就是用来读取数据的对象
@Override
public RecordReader<Text, BytesWritable> createRecordReader(InputSplit split, TaskAttemptContext context) throws IOException, InterruptedException {
MergeRecordReader recordReader = new MergeRecordReader();
//调用recordReader的初始化方法
recordReader.initialize(split, context);
return recordReader;
}
}
MergeRecordReader
package com.lagou.mr.comment.step1;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FSDataInputStream;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.BytesWritable;
import org.apache.hadoop.io.IOUtils;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.InputSplit;
import org.apache.hadoop.mapreduce.RecordReader;
import org.apache.hadoop.mapreduce.TaskAttemptContext;
import org.apache.hadoop.mapreduce.lib.input.FileSplit;
import java.io.IOException;
//负责读取数据,一次读取整个文件内容,封装成kv输出
public class MergeRecordReader extends RecordReader<Text, BytesWritable> {
private FileSplit split;
//hadoop配置文件对象
private Configuration conf;
//定义key,value的成员变量
private Text key = new Text();
private BytesWritable value = new BytesWritable();
//初始化方法,把切片以及上下文提升为全局
@Override
public void initialize(InputSplit split, TaskAttemptContext context) throws IOException, InterruptedException {
this.split = (FileSplit) split;
conf = context.getConfiguration();
}
private Boolean flag = true;
//用来读取数据的方法
@Override
public boolean nextKeyValue() throws IOException, InterruptedException {
//对于当前split来说只需要读取一次即可,因为一次就把整个文件全部读取了。
if (flag) {
//准备一个数组存放读取到的数据,数据大小是多少?
byte[] content = new byte[(int) split.getLength()];
final Path path = split.getPath();//获取切片的path信息
final FileSystem fs = path.getFileSystem(conf);//获取到文件系统对象
final FSDataInputStream fis = fs.open(path); //获取到输入流
IOUtils.readFully(fis, content, 0, content.length); //读取数据并把数据放入byte[]
//封装key和value
key.set(path.toString());
value.set(content, 0, content.length);
IOUtils.closeStream(fis);
//把再次读取的开关置为false
flag = false;
return true;
}
return false;
}
//获取到key
@Override
public Text getCurrentKey() throws IOException, InterruptedException {
return key;
}
//获取到value
@Override
public BytesWritable getCurrentValue() throws IOException, InterruptedException {
return value;
}
//获取进度
@Override
public float getProgress() throws IOException, InterruptedException {
return 0;
}
//关闭资源
@Override
public void close() throws IOException {
}
}
Reducer
package com.lagou.mr.comment.step1;
import org.apache.hadoop.io.BytesWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;
import java.io.IOException;
public class MergeReducer extends Reducer<Text, BytesWritable, Text, BytesWritable> {
@Override
protected void reduce(Text key, Iterable<BytesWritable> values, Context context) throws IOException, InterruptedException {
//输出value值(文件内容),只获取其中第一个即可(只有一个)
context.write(key, values.iterator().next());
}
}
Driver
package com.lagou.mr.comment.step1;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.BytesWritable;
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 MergeDriver {
public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
/*
1. 获取配置文件对象,获取job对象实例
2. 指定程序jar的本地路径
3. 指定Mapper/Reducer类
4. 指定Mapper输出的kv数据类型
5. 指定最终输出的kv数据类型
6. 指定job处理的原始数据路径
7. 指定job输出结果路径
8. 提交作业
*/
// 1. 获取配置文件对象,获取job对象实例
final Configuration conf = new Configuration();
final Job job = Job.getInstance(conf, "MergeDriver");
// 2. 指定程序jar的本地路径
job.setJarByClass(MergeDriver.class);
// 3. 指定Mapper/Reducer类
job.setMapperClass(MergeMapper.class);
job.setReducerClass(MergeReducer.class);
// 4. 指定Mapper输出的kv数据类型
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(BytesWritable.class);
// 5. 指定最终输出的kv数据类型
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(BytesWritable.class);
//设置使用自定义InputFormat读取数据
job.setInputFormatClass(MergeInputFormat.class);
FileInputFormat.setInputPaths(job, new Path("E:\\text\\MR综合案例\\input")); //指定读取数据的原始路径
// 7. 指定job输出结果路径
FileOutputFormat.setOutputPath(job, new Path("E:\\text\\MR综合案例\\out")); //指定结果数据输出路径
// 8. 提交作业
final boolean flag = job.waitForCompletion(true);
//jvm退出:正常退出0,非0值则是错误退出
System.exit(flag ? 0 : 1);
}
}
2、分区排序多目录输出
Mapper
package com.lagou.mr.comment.step2;
import org.apache.commons.lang3.StringUtils;
import org.apache.hadoop.io.BytesWritable;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;
import java.io.IOException;
//第一对kv:使用SequenceFileinputformat读取,所以key:Text,Value:BytesWritable(原因是生 成sequencefile文件指定就是这种类型)
public class CommentMapper extends Mapper<Text, BytesWritable,CommentBean, NullWritable> {
//key就是文件名
// value:一个文件的完整内容
@Override
protected void map(Text key, BytesWritable value, Context context) throws IOException, InterruptedException {
//分区每一行
String str = new String(value.getBytes());
String[] lines = str.split("\n");
for (String line:lines){
CommentBean commentBean=parseStrToCommentBean(line);
if(null !=commentBean){
context.write(commentBean,NullWritable.get());
}
}
super.map(key, value, context);
}
//切分字符串封装成commentbean对象
private CommentBean parseStrToCommentBean(String line) {
if(StringUtils.isNotBlank(line)){
//每一行进行切分
String[] fields=line.split("\t");
if(fields.length>=9){
return new CommentBean(fields[0], fields[1], fields[2], Integer.parseInt(fields[3]), fields[4], fields[5], fields[6], Integer.parseInt(fields[7]), fields[8]);
}
{
return null;
}
}
return null;
}
}
CommentBean
package com.lagou.mr.comment.step2;
import org.apache.hadoop.io.WritableComparable;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
public class CommentBean implements WritableComparable<CommentBean> {
private String orderId;
private String comment;
private String commentExt;
private int goodsNum;
private String phoneNum;
private String userName;
private String address;
private int commentStatus;
private String commentTime;
@Override
public String toString() {
return orderId + "\t" + comment + "\t" + commentExt + "\t" + goodsNum + "\t" + phoneNum + "\t" + userName + " \t" + address + "\t" + commentStatus + "\t" + commentTime;
}
public CommentBean() {
}
public CommentBean(String orderId, String comment, String commentExt, int goodsNum, String phoneNum, String userName, String address, int commentStatus, String commentTime) {
this.orderId = orderId;
this.comment = comment;
this.commentExt = commentExt;
this.goodsNum = goodsNum;
this.phoneNum = phoneNum;
this.userName = userName;
this.address = address;
this.commentStatus = commentStatus;
this.commentTime = commentTime;
}
public String getOrderId() {
return orderId;
}
public void setOrderId(String orderId) {
this.orderId = orderId;
}
public String getComment() {
return comment;
}
public void setComment(String comment) {
this.comment = comment;
}
public String getCommentExt() {
return commentExt;
}
public void setCommentExt(String commentExt) {
this.commentExt = commentExt;
}
public int getGoodsNum() {
return goodsNum;
}
public void setGoodsNum(int goodsNum) {
this.goodsNum = goodsNum;
}
public String getPhoneNum() {
return phoneNum;
}
public void setPhoneNum(String phoneNum) {
this.phoneNum = phoneNum;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
public int getCommentStatus() {
return commentStatus;
}
public void setCommentStatus(int commentStatus) {
this.commentStatus = commentStatus;
}
public String getCommentTime() {
return commentTime;
}
public void setCommentTime(String commentTime) {
this.commentTime = commentTime;
}
//定义排序规则,按照时间降序;0,1,-1
@Override
public int compareTo(CommentBean o) {
return o.getCommentTime().compareTo(this.commentTime);
}
//序列化
@Override
public void write(DataOutput out) throws IOException {
out.writeUTF(orderId);
out.writeUTF(comment);
out.writeUTF(commentExt);
out.writeInt(goodsNum);
out.writeUTF(phoneNum);
out.writeUTF(userName);
out.writeUTF(address);
out.writeInt(commentStatus);
out.writeUTF(commentTime);
}
//反序列化
@Override
public void readFields(DataInput in) throws IOException {
this.orderId = in.readUTF();
this.comment = in.readUTF();
this.commentExt = in.readUTF();
this.goodsNum = in.readInt();
this.phoneNum = in.readUTF();
this.userName = in.readUTF();
this.address = in.readUTF();
this.commentStatus = in.readInt();
this.commentTime = in.readUTF();
}
}
自定义分区器
package com.lagou.mr.comment.step2;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.mapreduce.Partitioner;
public class CommentPartitioner extends Partitioner<CommentBean, NullWritable> {
@Override
public int getPartition(CommentBean commentBean, NullWritable nullWritable, int numPartitions) {
// return (commentBean.getCommentStatus() & Integer.MAX_VALUE) % numPartitions;
return commentBean.getCommentStatus();//0,1,2 -->对应分区编号
}
}
自定义OutputFormat
CommentOutputFormat
package com.lagou.mr.comment.step2;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FSDataOutputStream;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.mapreduce.RecordWriter;
import org.apache.hadoop.mapreduce.TaskAttemptContext;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
import java.io.IOException;
//最终输出的kv类型
public class CommentOutputFormat extends FileOutputFormat<CommentBean, NullWritable> {
//负责写出数据的对象
@Override
public RecordWriter<CommentBean, NullWritable> getRecordWriter(TaskAttemptContext job) throws IOException, InterruptedException {
Configuration conf=job.getConfiguration();
FileSystem fs=FileSystem.get(conf);
//当前reducetask处理的分区编号来创建文件获取输出流
// 获取到在Driver指定的输出路径;0是好评,1是中评,2是差评
String outputDir=conf.get("mapreduce.output.fileoutputformat.outputdir");
FSDataOutputStream goodOut=null;
FSDataOutputStream commonOut=null;
FSDataOutputStream badOut=null;
int id = job.getTaskAttemptID().getTaskID().getId();//当前reducetask 处理的分区编号
if(id==0){
//好评数据
goodOut=fs.create(new Path(outputDir+"\\good\\good.log"));
}else if(id==1){
//中评数据
commonOut=fs.create(new Path(outputDir+"\\common\\common.log"));
}else {
badOut = fs.create(new Path(outputDir + "\\bad\\bad.log"));
}
return new CommentRecorderWrtier(goodOut,commonOut,badOut);
}
}
RecordWriter
package com.lagou.mr.comment.step2;
import org.apache.hadoop.fs.FSDataOutputStream;
import org.apache.hadoop.io.IOUtils;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.mapreduce.RecordWriter;
import org.apache.hadoop.mapreduce.TaskAttemptContext;
import java.io.IOException;
public class CommentRecorderWrtier extends RecordWriter<CommentBean, NullWritable> {
//定义写出数据的流
private FSDataOutputStream goodOut;
private FSDataOutputStream commonOut;
private FSDataOutputStream badOut;
public CommentRecorderWrtier(FSDataOutputStream goodOut, FSDataOutputStream commonOut, FSDataOutputStream badOut) {
this.goodOut = goodOut;
this.commonOut = commonOut;
this.badOut = badOut;
}
//实现把数据根据不同的评论类型输出到不同的目录下
// 写出数据的逻辑
@Override
public void write(CommentBean key, NullWritable value) throws IOException, InterruptedException {
int commentStatus = key.getCommentStatus();
String beanStr = key.toString();
if (commentStatus == 0) {
goodOut.write(beanStr.getBytes());
goodOut.write("\n".getBytes());
goodOut.flush();
} else if (commentStatus == 1) {
commonOut.write(beanStr.getBytes());
commonOut.write("\n".getBytes());
commonOut.flush();
} else {
badOut.write(beanStr.getBytes());
badOut.write("\n".getBytes());
badOut.flush();
}
}
//释放资源
@Override
public void close(TaskAttemptContext Context) throws IOException, InterruptedException {
IOUtils.closeStream(goodOut);
IOUtils.closeStream(commonOut);
IOUtils.closeStream(badOut);
}
}
Reducer
package com.lagou.mr.comment.step2;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.mapreduce.Reducer;
import java.io.IOException;
public class CommentReducer extends Reducer<CommentBean, NullWritable, CommentBean, NullWritable> {
@Override
protected void reduce(CommentBean key, Iterable<NullWritable> values, Context context) throws IOException, InterruptedException {
//遍历values,输出的是key;key:是一个引用地址,底层获取value同时,key的值也发生了 变化
for (NullWritable value : values) {
context.write(key, value);
}
}
}
Driver
package com.lagou.mr.comment.step2;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.input.SequenceFileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
import java.io.IOException;
public class CommentDriver {
public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
Configuration conf=new Configuration();
Job job = Job.getInstance(conf, "CommentDriver");
job.setJarByClass(CommentDriver.class);
job.setMapperClass(CommentMapper.class);
job.setReducerClass(CommentReducer.class);
job.setMapOutputKeyClass(CommentBean.class);
job.setMapOutputValueClass(NullWritable.class);
job.setOutputKeyClass(CommentBean.class);
job.setOutputValueClass(NullWritable.class);
job.setPartitionerClass(CommentPartitioner.class);
//指定inputformat类型
job.setInputFormatClass(SequenceFileInputFormat.class);
//指定输出outputformat类型
job.setOutputFormatClass(CommentOutputFormat.class);
//指定输入,输出路径
FileInputFormat.setInputPaths(job,new Path("E:\\text\\MR综合案例\\out"));
FileOutputFormat.setOutputPath(job,new Path("E:\\text\\MR综合案例\\multi-out"));
//指定reducetask的数量
job.setNumReduceTasks(3);
boolean b = job.waitForCompletion(true);
if (b){
System.exit(0);
}
}
}
如下报错是由于输出文件没有指定sequenceFileFormat类型,MergeDriver添加 job.setOutputFormatClass(SequenceFileOutputFormat.class);
问题解决
6.6.4 程序调优
- 预合并
CombineMapper
package com.lagou.mr.comment.step3;
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 CombineMapper extends Mapper<LongWritable, Text, NullWritable, Text> {
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
context.write(NullWritable.get(), value);
}
}
CombineDriver
package com.lagou.mr.comment.step3;
import com.lagou.mr.comment.step2.*;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.LongWritable;
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.CombineTextInputFormat;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.input.SequenceFileInputFormat;
import org.apache.hadoop.mapreduce.lib.input.TextInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
import java.io.IOException;
public class CombineDriver {
public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
Configuration conf = new Configuration();
Job job = Job.getInstance(conf, "CombineDriver");
job.setJarByClass(CombineDriver.class);
job.setMapperClass(CombineMapper.class);
job.setMapOutputKeyClass(NullWritable.class);
job.setMapOutputValueClass(Text.class);
job.setOutputKeyClass(NullWritable.class);
job.setOutputValueClass(Text.class);
//指定inputformat
job.setInputFormatClass(CombineTextInputFormat.class);
CombineTextInputFormat.setMaxInputSplitSize(job, 1024 * 1024 * 4);
//指定输入,输出路径
FileInputFormat.setInputPaths(job, new Path("E:\\text\\MR综合案例\\input"));
FileOutputFormat.setOutputPath(job, new Path("E:\\text\\MR综合案例\\merge-out"));
//指定reducetask的数量
job.setNumReduceTasks(3);
boolean b = job.waitForCompletion(true);
if (b) {
System.exit(0);
}
}
}
- 输出压缩
MergeDriver
package com.lagou.mr.comment.step1;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.BytesWritable;
import org.apache.hadoop.io.SequenceFile;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.io.compress.CompressionCodec;
import org.apache.hadoop.io.compress.DefaultCodec;
import org.apache.hadoop.io.compress.SnappyCodec;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
import org.apache.hadoop.mapreduce.lib.output.SequenceFileOutputFormat;
import java.io.IOException;
public class MergeDriver {
public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
// 1. 获取配置文件对象,获取job对象实例
final Configuration conf = new Configuration();
final Job job = Job.getInstance(conf, "MergeDriver");
// 2. 指定程序jar的本地路径
job.setJarByClass(MergeDriver.class);
// 3. 指定Mapper/Reducer类
job.setMapperClass(MergeMapper.class);
// job.setReducerClass(MergeReducer.class);
// 4. 指定Mapper输出的kv数据类型
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(BytesWritable.class);
// 5. 指定最终输出的kv数据类型
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(BytesWritable.class);
//设置使用自定义InputFormat读取数据
job.setInputFormatClass(MergeInputFormat.class);
FileInputFormat.setInputPaths(job, new Path("E:\\teach\\hadoop框架\\资料 \\data\\mr综合案例\\merge-out")); //指定读取数据的原始路径
//指定输出使用的outputformat
job.setOutputFormatClass(SequenceFileOutputFormat.class);
FileOutputFormat.setOutputCompressorClass(job, SnappyCodec.class);
//record压缩
SequenceFileOutputFormat.setOutputCompressionType(job, SequenceFile.CompressionType.RECORD);
SequenceFileOutputFormat.setOutputCompressorClass(job, DefaultCodec.class);
// block压缩
SequenceFileOutputFormat.setOutputCompressionType(job, SequenceFile.CompressionType.BLOCK);
SequenceFileOutputFormat.setOutputCompressorClass(job, DefaultCodec.class);
// 7. 指定job输出结果路径
FileOutputFormat.setOutputPath(job, new Path("E:\\teach\\hadoop框架\\资料 \\data\\mr综合案例\\out"));//指定结果数据输出路径
// 8. 提交作业
final boolean flag = job.waitForCompletion(true);
//jvm退出:正常退出0,非0值则是错误退出
System.exit(flag ? 0 : 1);
}
}