文章内容输出来源:拉勾教育大数据高薪训练营
第 1 节 MapReduce思想
MapReduce思想在⽣活中处可见。我们或多或少都曾接触过这种思想。MapReduce的思想核⼼是分而治之,充分利用了并⾏处理的优势。即使是发布过论文实现分布式计算的谷歌也只是实现了这种思想,⽽不是⾃己原创。
MapReduce任务过程是分为两个处理阶段:
- Map阶段:Map阶段的主要作用是“分”,即把复杂的任务分解为若干个“简单的任务”来并行处理。Map阶段的这些任务可以并行计算,彼此间没有依赖关系。
- Reduce阶段:Reduce阶段的主要作用是“合”,即对map阶段的结果进行全局汇总。
再次理解MapReduce的思想
第 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阶段如何处理的逻辑;
1. Hadoop序列化
为什么进行序列化?
序列化主要是我们通过⽹络通信传输数据时或者把对象持久化到文件,需要把对象序列化成⼆进制的结构。
观察源码时发现自定义Mapper类与自定义Reducer类都有泛型类型约束,⽐如⾃定义Mapper有四个形参类型,但是形参类型并不是常见的java基本类型。
为什么Hadoop要选择建立⾃己的序列化格式⽽不使⽤java⾃带serializable?
- 序列化在分布式程序中非常重要,在Hadoop中,集群中多个节点的进程间的通信是通过RPC(远程过程调用:Remote Procedure Call)实现;RPC将消息序列化成⼆进制流发送到远程节点,远程节点再将接收到的二进制数据反序列化为原始的消息,因此RPC往追求如下特点:
- 紧凑:数据更紧凑,能充分利用⽹络带宽资源
- 快速:序列化和反序列化的性能开销更低
- Hadoop使⽤的是⾃己的序列化格式Writable,它比java的序列化serialization更紧凑速度更快。⼀个对象使用Serializable序列化后,会携带很多额外信息⽐如校验信息,Header,继承体系等。
Java基本类型与Hadoop常用序列化类型
Java基本类型 | Hadoop Writable类型 |
boolean | BooleanWritable |
byte | ByteWritable |
int | IntWritable |
float | FloatWritable |
long | LongWritable |
double | DoubleWritable |
String | Text |
map | MapWritable |
array | ArrayWritable |
第 3 节 MapReduce编程规范及示例编写
3.1 Mapper类
- ⽤户⾃定义⼀个Mapper类继承Hadoop的Mapper类
- Mapper的输⼊数据是KV对的形式(类型可以⾃定义)
- Map阶段的业务逻辑定义在map()⽅法中
- Mapper的输出数据是KV对的形式(类型可以⾃定义)
注意:map()⽅法是对输入的⼀个KV对调⽤一次!!
3.2 Reducer类
- ⽤户⾃定义Reducer类要继承Hadoop的Reducer类
- Reducer的输⼊数据类型对应Mapper的输出数据类型(KV对)
- Reducer的业务逻辑写在reduce()⽅方法中
- Reduce()⽅法是对相同K的一组KV对调⽤执⾏一次
3.3 Driver阶段
创建提交YARN集群运行的Job对象,其中封装了MapReduce程序运行所需要的相关参数输⼊数据路径,输出数据路径等,也相当于是一个YARN集群的客户端,主要作⽤就是提交我们MapReduce程序运行。
3.4 WordCount代码实现
3.4.1 需求
在给定的⽂本⽂件中统计输出每⼀个单词出现的总次数
输入数据:wc.txt;
输出:
apache 2
clickhouse 2
hadoop 1
mapreduce 1
spark 2
xiaoming 1
3.4.2 具体步骤
按照MapReduce编程规范,分别编写Mapper,Reducer,Driver。
1. 新建maven工程
- 导⼊hadoop依赖
<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>
注意:以上依赖第⼀次需要联网下载!!
- 添加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
2. 整体思路梳理(仿照源码)
Map阶段:
1. map()⽅法中把传入的数据转为String类型
2. 根据空格切分出单词
3. 输出<单词,1>
Reduce阶段:
1. 汇总各个key(单词)的个数,遍历value数据进行累加
2. 输出key的总数
Driver:
1. 获取配置文件对象,获取job对象实例
2. 指定程序jar的本地路径
3. 指定Mapper/Reducer类
4. 指定Mapper输出的kv数据类型
5. 指定最终输出的kv数据类型
6. 指定job处理的原始数据路径
7. 指定job输出结果路径
8. 提交作业
3. 编写Mapper类
import java.io.IOException;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;
public class WordcountMapper extends Mapper<LongWritable, Text, Text, IntWritable>{
Text k = new Text();
IntWritable v = new IntWritable(1);
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
// 1 获取⼀行
String line = value.toString();
// 2 切割
String[] words = line.split(" ");
// 3 输出
for (String word : words) {
k.set(word);
context.write(k, v);
}
}
}
继承的Mapper类型选择新版本API:
4. 编写Reducer类
import java.io.IOException;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;
public class WordcountReducer extends Reducer<Text, IntWritable, Text, IntWritable>{
int sum;
IntWritable v = new IntWritable();
@Override
protected void reduce(Text key, Iterable<IntWritable> values,Context context) throws IOException, InterruptedException {
// 1 累加求和
sum = 0;
for (IntWritable count : values) {
sum += count.get();
}
// 2 输出
v.set(sum);
context.write(key,v);
}
}
选择继承的Reducer类
5. 编写Driver驱动类
import java.io.IOException;
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;
public class WordcountDriver {
public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
// 1 获取配置信息以及封装任务
Configuration configuration = new Configuration();
Job job = Job.getInstance(configuration);
// 2 设置jar加载路径
job.setJarByClass(WordcountDriver.class);
// 3 设置map和reduce类
job.setMapperClass(WordcountMapper.class);
job.setReducerClass(WordcountReducer.class);
// 4 设置map输出
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(IntWritable.class);
// 5 设置最终输出kv类型
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(IntWritable.class);
// 6 设置输入和输出路径
FileInputFormat.setInputPaths(job, new Path(args[0]));
FileOutputFormat.setOutputPath(job, new Path(args[1]));
// 7 提交
boolean result = job.waitForCompletion(true);
System.exit(result ? 0 : 1);
}
}
6. 运行任务
- 本地模式
直接Idea中运行驱动类即可
idea运行需要传入参数:
运⾏结束,去到输出结果路径查看结果
注意本地idea运行mr任务与集群没有任何关系,没有提交任务到yarn集群,是在本地使用多线程方式模拟的mr的运行。
- Yarn集群模式
-
- 把程序打成jar包,改名为wc.jar;上传到Hadoop集群
选择合适的Jar包
准备原始数据文件,上传到HDFS的路径,不能是本地路径,因为跨节点运行⽆法获取数据!!
- 启动Hadoop集群(Hdfs,Yarn)
- 使用Hadoop命令提交任务运行
hadoop jar wc.jar com.lagou.wordcount.WordcountDriver /user/lagou/input /user/lagou/output
Yarn集群任务运行成功展示图
第 4 节 序列化Writable接口
基本序列化类型往不能满足所有需求,⽐如在Hadoop框架内部传递一个自定义bean对象,那么该对象就需要实现Writable序列化接⼝。
4.1 实现Writable序列化步骤如下
1. 必须实现Writable接口
2. 反序列化时,需要反射调⽤空参构造函数,所以必须有空参构造
public CustomBean() {
super();
}
3. 重写序列化方法
@Override
public void write(DataOutput out) throws IOException {
....
}
4. 重写反序列化方法
@Override
public void readFields(DataInput in) throws IOException {
....
}
5. 反序列化的字段顺序和序列化字段的顺序必须完全一致
6. ⽅便展示结果数据,需要重写bean对象的toString()⽅法,可以⾃定义分隔符
7. 如果自定义Bean对象需要放在Mapper输出KV中的K,则该对象还需实现Comparable接口,因为MapReduce框中的Shuffle过程要求对key必须能排序!!
@Override
public int compareTo(CustomBean o) {
// ⾃自定义排序规则
return this.num > o.getNum() ? -1 : 1;
}
4.2 Writable接⼝案例
1. 需求
统计每台智能⾳音箱设备内容播放时⻓长
原始⽇志格式
输出结果
2. 编写MapReduce程序
1. 创建SpeakBean对象
package com.lagou.hdfs;
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 long sumDuration;
//2 反序列化时,需要反射调⽤空参构造函数,所以必须有
public SpeakBean() {
}
public SpeakBean(long selfDuration, long thirdPartDuration) {
this.selfDuration = selfDuration;
this.thirdPartDuration = thirdPartDuration;
this.sumDuration=this.selfDuration+this.thirdPartDuration;
}
//3 写序列化方法
public void write(DataOutput out) throws IOException {
out.writeLong(selfDuration);
out.writeLong(thirdPartDuration);
out.writeLong(sumDuration);
}
//4 反序列化⽅法
//5 反序列化⽅法读顺序必须和写序列化方法的写顺序必须一致
public void readFields(DataInput in) throws IOException {
this.selfDuration = in.readLong();
this.thirdPartDuration = in.readLong();
this.sumDuration = in.readLong();
}
// 6 编写toString方法,⽅便后续打印到⽂本
@Override
public String toString() {
return selfDuration +
"\t" + thirdPartDuration +
"\t" + sumDuration ;
}
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;
}
}
2. 编写Mapper类
package com.lagou.hdfs;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;
import java.io.IOException;
public class SpeakDurationMapper extends Mapper<LongWritable, Text, Text, SpeakBean> {
SpeakBean v = new SpeakBean();
Text k = new Text();
@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 deviceId = fields[1];
// 取出⾃有和第三方时长数据
long selfDuration = Long.parseLong(fields[fields.length - 3]);
long thirdPartDuration = Long.parseLong(fields[fields.length - 2]);
k.set(deviceId);
v.set(selfDuration, thirdPartDuration);
// 4 写出
context.write(k, v);
}
}
3. 编写Reducer
package com.lagou.hdfs;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;
import java.io.IOException;
public class SpeakDurationReducer extends Reducer<Text, SpeakBean, Text,
SpeakBean> {
@Override
protected void reduce(Text key, Iterable<SpeakBean> values, Context context)throws IOException, InterruptedException {
long self_Duration = 0;
long thirdPart_Duration = 0;
// 1 遍历所有bean,将其中的⾃有,第三⽅时⻓分别累加
for (SpeakBean sb : values) {
self_Duration += sb.getSelfDuration();
thirdPart_Duration += sb.getThirdPartDuration();
}
// 2 封装对象
SpeakBean resultBean = new SpeakBean(self_Duration, thirdPart_Duration);
// 3 写出
context.write(key, resultBean);
}
}
4. 编写驱动
package com.lagou.hdfs;
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 SpeakerDriver {
public static void main(String[] args) throws IllegalArgumentException, IOException, ClassNotFoundException, InterruptedException {
// 输⼊输出路径需要根据⾃己电脑上实际的输⼊输出路径设置
args = new String[] { "e:/input/input", "e:/output1" };
// 1 获取配置信息,或者job对象实例
Configuration configuration = new Configuration();
Job job = Job.getInstance(configuration);
// 6 指定本程序的jar包所在的本地路径
job.setJarByClass(SpeakerDriver.class);
// 2 指定本业务job要使用的mapper/Reducer业务类
job.setMapperClass(SpeakDurationMapper.class);
job.setReducerClass(SpeakDurationReducer.class);
// 3 指定mapper输出数据的kv类型
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(SpeakBean.class);
// 4 指定最终输出的数据的kv类型
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(SpeakBean.class);
// 5 指定job的输入原始⽂件所在目录
FileInputFormat.setInputPaths(job, new Path(args[0]));
FileOutputFormat.setOutputPath(job, new Path(args[1]));
// 7 将job中配置的相关参数,以及job所用的java类所在的jar包,提交给yarn去运行
boolean result = job.waitForCompletion(true);
System.exit(result ? 0 : 1);
}
}
mr编程技巧总结
- 结合业务设计Map输出的key和v,利用key相同则去往同一个reduce的特点!!
- map()⽅法中获取到只是一行⽂本数据尽量不做聚合运算
- reduce()⽅法的参数要清楚含义