03 MapReduce & Yarn
1 MapReduce概述
1.1 MapReduce定义
● 分布式运算程序的编程框架
● 开发“基于Hadoop的数据分析应用”的核心框架
● 核心功能:将用户编写的业务逻辑代码+自带默认组件 —>整合为分布式运算程序,并发运行在Hadoop集群上。
1.2 MapReduce优缺点
1.2.1 优点
● 易于编程 — 实现接口,就可完成一个分布式程序
● 良好的扩展性 — 增加机器扩展计算能力
● 高容错性 — 机器挂了,任务转移
● PB级海量数据离线处理 — 可实现上千台服务器集群并发工作
1.2.2 缺点
● 不擅长实时计算
● 不擅长流式计算 — 输入数据集时静态的
● 不擅长DAG(有向图)计算 — 输出结果写入磁盘,大量IO,导致性能低下
1.3 MapReduce核心思想
● MapTask阶段:并发实例,完全并行运行,互不相干
● ReduceTask阶段:并发实例互不相干,依赖MapTask的输出
● MapReduce模型包含一个Map阶段、一个Reduce阶段。用户逻辑复杂,多个MapReduce程序,串行运行。
1.4 MapReduce进程
一个完整的MapReduce程序有三类实例进程
● MrAppMaster:负责整个程序的过程调度及状态协调。
● MapTask:负责整个数据处理流程(Map阶段)。
● ReduceTask: 负责整个数据处理流程(Reduce阶段)
1.5 官方WordCount源码
● 采用反编译工具反编译源码
● 发现WordCount案例有Map类、Reduce类和驱动类
● 数据的类型是Hadoop自身封装的序列化类型
1.6 WordCount案例
▶ 需求:在给定的文本文件中统计输出每一个单词出现的总次数
需求分析:
MapReduce编程规范,分别编写Mapper,Reducer,Driver。
【案例1】WCMapper继承Mapper
● 类的介绍
① Mapper类 — 当执行MapTask时回调用此类
Mapper<KEYIN, VALUEIN, KEYOUT, VALUEOUT> | |
KEYIN | 读取数据时的偏移量类型 |
VALUEIN | 读取一行一行的内容的类型 |
KEYOUT | 写出时的key的类型(单词的类型) |
VALUEOUT | 写出时的value的类型(单词数量的类型) |
● 方的介绍
② map() — mapper类会调用此方法(此方法在被循环调用-每调用一些传入一行数据)
map(LongWritable key, Text value, Context context) | |
key | 读取数据时的偏移量 |
value | 读取的一行一行的数据 |
context | 上下文(在这用来将Key,value写出去) |
public class WCMapper extends Mapper<LongWritable, Text,Text, LongWritable> {
//封装Key
private Text outKey = new Text();
//封装Value
private LongWritable outValue = new LongWritable();
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
//super.map(key, value, context); 一定不要调用父类中的map方法
//1.将数据切割
//1.1 将Test转成String
String line = value.toString();
//1.2 切割数据
String[] words = line.split(" ");
for (String word : words) {
//2.封装k,v
outKey.set(word);//给key赋值
outValue.set(1);//给value赋值
//3.将k,v写出去
context.write(outKey,outValue);
}
}
}
【案例2】WCReducer继承Reducer
● 类的介绍
① WCReducer — ReduceTask会调用此类
Reducer<KEYIN, VALUEIN, KEYOUT, VALUEOUT> | |
KEYIN | Reducer读取key的类型(Mapper写出的key的类型--在这是单词) |
VALUEIN | Reducer读取value的类型(Mapper写出的value的类型--在这是单词的数量) |
KEYOUT | Reducer写出的key的类型(在这是单词的类型) |
VALUEOUT | Reducer写出的value的类型(在这是单词数量的类型) |
● 方的介绍
reduce() — 该方法会被Reducer类所调用(该方法也是被循环调用每调用一次传入一组数据)
reduce(Text key, Iterable<LongWritable> values, Context context) | |
key | 读取的key 在这就是单词 |
value | 读取的所有的value 在这就是单词的数量 |
context | 上下文(在这用来将key,value写出去) |
public class WCReducer extends Reducer<Text, LongWritable,Text, LongWritable> {
//封装的value
private LongWritable outValue = new LongWritable();
@Override
protected void reduce(Text key, Iterable<LongWritable> values, Context context) throws IOException, InterruptedException {
long sum = 0;//所有value的和
//1.遍历所有的value
for (LongWritable value : values) {
//将LongWritable转成long类型
long l = value.get();
//2.将所有value累加
sum += l;
}
//3.封装k,v (这次的k不用封装哟!)
outValue.set(sum);//给value赋值
//4.将k,v写出去
context.write(key,outValue);
}
}
【案例3】本地运行 (本地->本地)
● 方的介绍
waitForCompletion(boolean verbose) | |
参数 | 是否打印用户进度 |
返回值 | 如果为true表示执行成功 |
public class WCDriver {
public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
//1.创建Job实例
Configuration conf = new Configuration(); //可以往里面放配置参数,但是我们现在先不放
Job job = Job.getInstance(conf);
//2.给Job赋值
//2.1设置Jar加载路径(如果是本地模式可以不设置)
job.setJarByClass(WCDriver.class);
//2.2设置Mapper和Reducer类
job.setMapperClass(WCMapper.class);
job.setReducerClass(WCReducer.class);
//2.3设置Mapper输出的类型
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(LongWritable.class);
//2.4设置最终输出的k,v类型(在这是Reducer)
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(LongWritable.class);
//2.5设置输入和输出路径
FileInputFormat.setInputPaths(job,new Path("D:\\io\\input"));
//注意:输出路径一定不能存在否则报错
FileOutputFormat.setOutputPath(job,new Path("D:\\io\\output"));
//3.提交Job
/*
参数 :是否打印用户进度
返回值 :如果为true表示执行成功
*/
boolean b = job.waitForCompletion(true);
}
}
运行结果:
【案例4】在集群上跑Job (HDFS->HDFS)
① 向mian()方法传入输入、输出路径
② 打包jar
③ 将jar包上传到任意节点上(这里上传到hadoop102的家目录)
④ 执行命令 — hadoop jar 包名 全类名 HDFS的输入路径 HDFS的输出路径
public class WCDriver2 {
public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
//1.创建Job实例
Configuration conf = new Configuration();
Job job = Job.getInstance(conf);
//2.给Job赋值
//2.1设置Jar加载路径(如果是本地模式可以不设置)
job.setJarByClass(WCDriver2.class);
//2.2设置Mapper和Reducer类
job.setMapperClass(WCMapper.class);
job.setReducerClass(WCReducer.class);
//2.3设置Mapper输出的类型
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(LongWritable.class);
//2.4设置最终输出的k,v类型(在这是Reducer)
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(LongWritable.class);
//2.5设置输入和输出路径
//此时的路径不能写死,通过参数闯入
FileInputFormat.setInputPaths(job,new Path(args[0]));
//注意:输出路径一定不能存在否则报错
FileOutputFormat.setOutputPath(job,new Path(args[1]));
//3.提交Job
/*
参数 :是否打印用户进度
返回值 :如果为true表示执行成功
*/
boolean b = job.waitForCompletion(true);
}
}
运行结果:
【案例5】从本地向集群提交job (本地 —> HDFS) [一般调试的时候使用此方法]
① 配置 (下列代码可见)
② 打包,打包后复制jar包的绝对路径
③ 注释掉 job.setJarByClass(WCDriver3.class)
④ 添加 job.setJar(jar包的路径)
⑤ 配置
VM Options : -DHADOOP_USER_NAME=atguigu
ProgramArguments :hdfs://hadoop102:8020/input hdfs://hadoop102:8020/output
public class WCDriver3 {
public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
//1.创建Job实例
Configuration conf = new Configuration();
//1.1 进行配置
//设置在集群运行的相关参数-设置HDFS,NAMENODE的地址
conf.set("fs.defaultFS", "hdfs://hadoop102:8020");
//指定MR运行在Yarn上
conf.set("mapreduce.framework.name","yarn");
//指定MR可以在远程集群运行
conf.set("mapreduce.app-submission.cross-platform","true");
//指定yarn resourcemanager的位置
conf.set("yarn.resourcemanager.hostname", "hadoop103");
//1.2 获取配置后的对象
Job job = Job.getInstance(conf);
//2.给Job赋值
//2.1设置Jar加载路径(如果是本地模式可以不设置)
//job.setJarByClass(WCDriver3.class);
job.setJar("D:\\develop\\1025project\\MRDemo\\target\\MRDemo-1.0-SNAPSHOT.jar");
//2.2设置Mapper和Reducer类
job.setMapperClass(WCMapper.class);
job.setReducerClass(WCReducer.class);
//2.3设置Mapper输出的类型
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(LongWritable.class);
//2.4设置最终输出的k,v类型(在这是Reducer)
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(LongWritable.class);
//2.5设置输入和输出路径
FileInputFormat.setInputPaths(job,new Path(args[0]));
//注意:输出路径一定不能存在否则报错
FileOutputFormat.setOutputPath(job,new Path(args[1]));
//3.提交Job
boolean b = job.waitForCompletion(true);
}
}
2 Hadoop序列化
2.1 序列化概述
2.1.1 什么时序列化
● 序列化:内存中的对象 —> 磁盘
● 反序列化:磁盘 —> 内存中的对象
2.1.3 为什么不用Java的序列化
● Java序列化,会附带很多额外的信息,不便于在网络中高效传输。
● Hadoop自己开发了一套序列化机制(Writable)。
● Hadoop序列化特点:
紧凑:高效使用存储空间
快速:读写数据的额外开销小
可扩展:随着通信协议的升级而可升级
互操作:支持多语言交互
2.2 自定义bean对象实现序列化接口(Writable)
【案例】自定义的类的对象实现序列化 (主要看 1. 和 2. )
① 自定义的类实现Writable接口
② 重写write和readFields方法
注意:反序列化时的顺序和序列化时的顺序应该保持一致
public class FlowBean implements Writable {
private long upFlow;
private long downFlow;
private long sumFlow;
public FlowBean(){
}
//1.序列化时调用此方法
public void write(DataOutput out) throws IOException {
out.writeLong(upFlow);
out.writeLong(downFlow);
out.writeLong(sumFlow);
}
//2.反序列化时调用此方法
public void readFields(DataInput in) throws IOException {
upFlow = in.readLong();
downFlow = in.readLong();
sumFlow = in.readLong();
}
public long getUpFlow() {
return upFlow;
}
public void setUpFlow(long upFlow) {
this.upFlow = upFlow;
}
public long getDownFlow() {
return downFlow;
}
public void setDownFlow(long downFlow) {
this.downFlow = downFlow;
}
public long getSumFlow() {
return sumFlow;
}
public void setSumFlow(long sumFlow) {
this.sumFlow = sumFlow;
}
}
2.3 序列化实例实操
▶ 需求:统计每一个手机号耗费的总上行流量、下行流量、总流量
▶ 编写MapReduce程序
【案例1】FlowBean
public class FlowBean implements Writable {
private long upFlow;
private long downFlow;
private long sumFlow;
public FlowBean(){
}
/*
序列化时调用此方法
*/
public void write(DataOutput out) throws IOException {
out.writeLong(upFlow);
out.writeLong(downFlow);
out.writeLong(sumFlow);
}
/*
反序列化时调用此方法
注意:反序列化时的顺序和序列化时的顺序应该保持一致
*/
public void readFields(DataInput in) throws IOException {
upFlow = in.readLong();
downFlow = in.readLong();
sumFlow = in.readLong();
}
public long getUpFlow() {
return upFlow;
}
public void setUpFlow(long upFlow) {
this.upFlow = upFlow;
}
public long getDownFlow() {
return downFlow;
}
public void setDownFlow(long downFlow) {
this.downFlow = downFlow;
}
public long getSumFlow() {
return sumFlow;
}
public void setSumFlow(long sumFlow) {
this.sumFlow = sumFlow;
}
@Override
public String toString() {
return upFlow + " " + downFlow + " " + sumFlow;
}
}
【案例2】FlowMapper
/*
Mapper类 :当执行MapTask时会调用此类
Mapper<KEYIN, VALUEIN, KEYOUT, VALUEOUT> :
四个泛型分两组:
第一组
KEYIN :读取数据时的偏移量的类型
VALUEIN :读取的一行一行的内容的类型
第二组
KEYOUT :写出时的key的类型(在这是手机号的类型)
VALUEOUT :写出时的value的类型(在这是FlowBean的类型)
*/
public class FlowMapper extends Mapper<LongWritable, Text,Text,FlowBean> {
//写出的key
private Text outKey = new Text();
//写出的value
private FlowBean outValue = new FlowBean();
/**
* mapper类会调用此方法(此方法在被循环调用-每调用一些传入一行数据)
* @param key 读取数据时的偏移量
* @param value 读取的一行一行的数据
* @param context 上下文(在这用来将Key,value写出去)
*/
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
//1.将数据切割
//1.1将Text转成String
String line = value.toString();
//1.2切割数据
String[] phoneInfo = line.split("\t");
//2.封装k,v
outKey.set(phoneInfo[1]);//给key赋值
//给value赋值
outValue.setUpFlow(Long.parseLong(phoneInfo[phoneInfo.length - 3]));
outValue.setDownFlow(Long.parseLong(phoneInfo[phoneInfo.length - 2]));
outValue.setSumFlow(outValue.getUpFlow() + outValue.getDownFlow());
//3.将k,v写出
context.write(outKey,outValue);
}
}
【案例3】FlowReducer
/*
ReduceTask会调用此类
Reducer<KEYIN, VALUEIN, KEYOUT, VALUEOUT> :
泛型分两组
第一组:
KEYIN :Reducer读取key的类型(Mapper写出的key的类型--在这是手机号)
VALUEIN :Reducer读取value的类型(Mapper写出的value的类型--在这是FlowBean)
第二组:
KEYOUT :Reducer写出的key的类型(在这是手机号的类型)
VALUEOUT :Reducer写出的value的类型(在这是FlowBean的类型)
*/
public class FlowReducer extends Reducer<Text,FlowBean,Text,FlowBean> {
//封装value
private FlowBean outValue = new FlowBean();
/*
/**
* 该方法会被Reducer类所调用(该方法也是被循环调用每调用一次传入一组数据)
* @param key 读取的key 在这就是单词
* @param values 读取的所有的value 在这就是单词的数量
* @param context 上下文(在这用来将key,value写出去)
* @throws IOException
* @throws InterruptedException
* key value
* 1532111111 --- > 100 120 220
1532111111 --- > 80 20 100
*/
@Override
protected void reduce(Text key, Iterable<FlowBean> values, Context context) throws IOException, InterruptedException {
long sumUpFlow = 0; //总上行流量
long sumDowFlow = 0; //总下行流量
//1.遍历所有的value
for (FlowBean value : values) {
//2.将上行和下行流量进行累加
sumUpFlow += value.getUpFlow();
sumDowFlow += value.getDownFlow();
}
//3.封装k,v
outValue.setUpFlow(sumUpFlow);
outValue.setDownFlow(sumDowFlow);
outValue.setSumFlow(sumDowFlow+sumUpFlow);
//4.写出K,V
context.write(key,outValue);
}
}
【案例4】FlowDriver
public class FlowDriver {
public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
//1.创建Job实例
Job job = Job.getInstance(new Configuration());
//2.给Job赋值
//2.1设置Jar加载路径(如果是本地模式可以不设置)
//2.2设置Mapper和Reducer类
job.setMapperClass(FlowMapper.class);
job.setReducerClass(FlowReducer.class);
//2.3设置Mapper输出的类型
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(FlowBean.class);
//2.4设置最终输出的k,v类型(在这是Reducer)
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(FlowBean.class);
//2.5设置输入和输出路径
FileInputFormat.setInputPaths(job,new Path("D:\\io\\input"));
//注意:输出路径一定不能存在否则报错
FileOutputFormat.setOutputPath(job,new Path("D:\\io\\output"));
//3.提交Job
/*
参数 :是否打印用户进度
返回值 :如果为true表示执行成功
*/
job.waitForCompletion(true);
}
}
运行结果:
3 MapReduce框架原理
3.1 InputFormat数据输入
3.1.1 切片与MapTask并行度决定机制
3.1.2 Job提交流程源码和切片源码详解
3.1.3 FileInputFormat切片机制
切片机制
参数配置
3.1.4 CombineTextInputFormat切片机制
默认的TextInputFormat切片机制,不管文件多小,都会是一个单独的切片。
1) 应用场景:
CombineTextInputFormat 用于小文件过多的场景,可将多个小文件从逻辑上整成一个切片,然后交给一个MapTask处理。
2)虚拟存储切片最大值设置
CombineTextInputFormat.setMaxInputSplitSize(job, 4194304); // 4m
3)切片机制
生成切片过程包括:虚拟存储过程和切片过程二部分。
3.1.5 CombineTextInputFormat案例实操
3.6 TextInputFormat的KV
● TextInputFormat是默认的FileInputFormat实现类。按行读取每条记录。
● 键是存储该行在整个文件中的起始字节偏移量,LongWritable类型
● 值是这行的内容,不包括任何终止符,Text类型
3.2 MapReduce工作流程
3.3 Shuffle机制
3.3.1 Shuffle机制
3.3.2 Partition分区