文章部分转自:https://blog.csdn.net/dataiyangu/article/details/89481818
自己的话:三更灯火五更鸡,正是男儿读书时
眼泪你别问,joker这个 “男人” 你别恨
Hadoop第六天–MapReduce&Yarn
同HDFS一样,先简单介绍一下MapReduce的结构以及各部分的组成和作用
一、MapReduce概念
Mapreduce是一个分布式运算程序的编程框架,是用户开发“基于hadoop的数据分析应用”的核心框架;
Mapreduce核心功能是将用户编写的业务逻辑代码和自带默认组件整合成一个完整的分布式运算程序,并发运行在一个hadoop集群上。
1.为什么要MapReduce
1)海量数据在单机上处理因为硬件资源限制,无法胜任
2)而一旦将单机版程序扩展到集群来分布式运行,将极大增加程序的复杂度和开发难度
3)引入mapreduce框架后,开发人员可以将绝大部分工作集中在业务逻辑的开发上,而将分布式计算中的复杂性交由框架来处理。
4)mapreduce分布式方案考虑的问题
·运算逻辑要不要先分后合?
·程序如何分配运算任务(切片)?
·两阶段的程序如何启动?如何协调?
·整个程序运行过程中的监控?容错?重试?
分布式方案需要考虑很多问题,但是我们可以将分布式程序中的公共功能封装成框架,让开发人员将精力集中于业务逻辑上。而mapreduce就是这样一个分布式程序的通用框架。
2.MapReduce核心思想
map过程是一个蔬菜到制成食物前的准备工作,reduce将准备好的材料合并进而制作出食物的过程
3个节点每个节点都存一份部分数据如图红色123所示(将数据分成多份,即分割成多块保存到节点中),mapTask是具体干活的,把红色和绿色的123,传过来的数据进行分配调度。
如果用户逻辑非常复杂,那就只能多个MapReduce程序,串行运行的意思是,将第一个MapReduce的结果再作为输入后面在进行一次MapReduce,如此往复。
1)分布式的运算程序往往需要分成至少2个阶段
2)第一个阶段的map task并发实例,完全并行运行,互不相干
3)第二个阶段的reduce task并发实例互不相干,但是他们的数据依赖于上一个阶段的所有maptask并发实例的输出
4)MapReduce编程模型只能包含一个map阶段和一个reduce阶段,如果用户的业务逻辑非常复杂,那就只能多个mapreduce程序,串行运行
3.MapReduce进程
一个完整的mapreduce程序在分布式运行时有三类实例进程:
1)MrAppMaster:负责整个程序的过程调度及状态协调(负责MapTask和ReduceTask的协调和调度)
2)MapTask:负责map阶段的整个数据处理流程
3)ReduceTask:负责reduce阶段的整个数据处理流程
4.MapReduce编程规范(八股文)
用户编写的程序分成三个部分:Mapper,Reducer,Driver(提交运行mr程序的客户端)
Mapper阶段
(1)用户自定义的Mapper要继承自己的父类
(2)Mapper的输入数据是KV对的形式(KV的类型可自定义)K-行号,V-行里面的内容
(3)Mapper中的业务逻辑写在map()方法中
(4)Mapper的输出数据是KV对的形式(KV的类型可自定义)
(5)map()方法(maptask进程)对每一个<K,V>调用一次
Reducer阶段
(1)用户自定义的Reducer要继承自己的父类
(2)Reducer的输入数据类型对应Mapper的输出数据类型,也是KV
(3)Reducer的业务逻辑写在reduce()方法中
(4)Reducetask进程对每一组相同k的<k,v>组调用一次reduce()方法
Driver阶段
整个程序需要一个Drvier来进行提交,提交的是一个描述了各种必要信息的job对象
案例实操:
统计一堆文件中单词出现的个数(WordCount案例)
便于大家分辨,这里写了三个类,将各个阶段分开来写:
第一个类 WordCountMap.java
package com.jasmine;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;
import java.io.IOException;
public class WordCountMap extends Mapper<LongWritable,Text,Text, IntWritable> {
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
String word = value.toString();
String[] words = word.split(" ");
for (String w:words) {
context.write(new Text(w),new IntWritable(1));
}
}
}
第二个类:WordCountReduce.java
package com.jasmine;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;
import java.io.IOException;
public class WordCountReduce extends Reducer<Text, IntWritable,Text,IntWritable> {
@Override
protected void reduce(Text key,Iterable<IntWritable>values,Context context) throws IOException, InterruptedException {
Integer count = 0;
for (IntWritable v:values) {
count++;
}
context.write(key,new IntWritable(count));
}
}
第三个类:WordCountDriver.java
package com.jasmine;
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 org.apache.log4j.BasicConfigurator;
import java.io.IOException;
public class WordCountDriver {
public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
BasicConfigurator.configure(); //自动快速地使用缺省Log4j环境
Configuration conf = new Configuration();
conf.set("yarn.resorcemanager.hostname","jasmine01");
conf.set("fs.deafutFS","hdfs://jasmine01:9000/");
Job job = Job.getInstance(conf);
job.setJarByClass(WordCountDriver.class);
//设置本次job是使用map,reduce
job.setMapperClass(WordCountMap.class);
job.setReducerClass((WordCountReduce.class));
//设置本次map和reduce的输出类型
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(IntWritable.class);
job.setOutputKeyClass(Text.class);
job.setOutputKeyClass(IntWritable.class);
//制定本次job读取源数据时需要用的组件:我们的源文件在hdfs的文本文件中,用TextInputFormat
job.setInputFormatClass(TextInputFormat.class);
//制定本次job输出数据时需要用的组件:我们要输出到hdfs文件中,用TextInputFormat
job.setOutputFormatClass(TextOutputFormat.class);
//设置输入路径
FileInputFormat.setInputPaths(job,new Path("args[0]"));
FileOutputFormat.setOutputPath(job,new Path("args[1]"));
//提交任务,客户端返回
job.submit();
//核心代码:提交jar程序给yarn,客户端不退出,等待接受mapreduce的进度信息,打印进度信息,并等待最终运行的结果
//客户端true:的含义 等着
//result:返回true:则跑完了 false:出错了
boolean result = job.waitForCompletion(true);
System.exit(result ? 0 : 1);
}
}
5.MapReduce程序运行流程分析
提交Job,然后就知道了需要几个Maptask,而且每个task负责其中每个文件中的那一段,然后通过inputformat框架,然后就会去文本文件中一行一行的读,读取完之后以kv对的形式存在Map task中,然后把kv对给了wordcount mapper,即我们自己程序定义的wordcound mapper,key就是行号,value就是这一行的文本数据,讲过map方法之后,通过Context.write写出去,写到outputCollector(出处缓冲区)中,然后根据k进行分区排序,这里的分区决定了后面reduce的个数,之后等到所有的maptask任务都完成了,启动相应数量的reducetask,并告知reducetask处理数据的范文,即一个reducetask只需要处理分区中的一部分,经过reduce task处理完之后,通过输出矿建outputformat输出结果。inputformat和outputfomat都可以自己定义,控制输入和输出。最后的最后就到了上面的例子中,图里面能够看到的part-r-00001
1)在MapReduce程序读取文件的输入目录上存放相应的文件。
2)客户端程序在submit()方法执行前,获取待处理的数据信息,然后根据集群中参数的配置形成一个任务分配规划。
3)客户端提交job.split、jar包、job.xml等文件给yarn,yarn中的resourcemanager启动MRAppMaster。
4)MRAppMaster启动后根据本次job的描述信息,计算出需要的maptask实例数量,然后向集群申请机器启动相应数量的maptask进程。
5)maptask利用客户指定的inputformat来读取数据,形成输入KV对。
6)maptask将输入KV对传递给客户定义的map()方法,做逻辑运算
7)map()运算完毕后将KV对收集到maptask缓存。
8)maptask缓存中的KV对按照K分区排序后不断写到磁盘文件
9)MRAppMaster监控到所有maptask进程任务完成之后,会根据客户指定的参数启动相应数量的reducetask进程,并告知reducetask进程要处理的数据分区。
10)Reducetask进程启动之后,根据MRAppMaster告知的待处理数据所在位置,从若干台maptask运行所在机器上获取到若干个maptask输出结果文件,并在本地进行重新归并排序,然后按照相同key的KV为一个组,调用客户定义的reduce()方法进行逻辑运算。
11)Reducetask运算完毕后,调用客户指定的outputformat将结果数据输出到外部存储。
二、MapReduce理论
1.Writable序列化
序列化就是把内存中的对象,转换成字节序列(或其他数据传输协议)以便于存储(持久化)和网络传输。
反序列化就是将收到字节序列(或其他数据传输协议)或者是硬盘的持久化数据,转换成内存中的对象。
Java的序列化是一个重量级序列化框架(Serializable),一个对象被序列化后,会附带很多额外的信息(各种校验信息,header,继承体系等),不便于在网络中高效传输。所以,hadoop自己开发了一套序列化机制(Writable),精简、高效。
(一)常用数据序列化类型:
常用的数据类型对应的hadoop数据序列化类型
Java类型 Hadoop Writable类型
boolean BooleanWritable
byte ByteWritable
int IntWritable
float FloatWritable
long LongWritable
double DoubleWritable
string Text
map MapWritable
array ArrayWritable
(二)自定义bean对象实现序列化接口:
自定义bean对象要想序列化传输,必须实现序列化接口,需要注意以下7项。
(1)必须实现Writable接口
(2)反序列化时,需要反射调用空参构造函数,所以必须有空参构造
(3)重写序列化方法
(4)重写反序列化方法
(5)注意反序列化的顺序和序列化的顺序完全一致
(6)要想把结果显示在文件中,需要重写toString(),且用”\t”分开,方便后续用
(7)如果需要将自定义的bean放在key中传输,则还需要实现comparable接口,因为mapreduce框中的shuffle过程一定会对key进行排序
如下:
// 1 必须实现Writable接口
public class FlowBean implements Writable {
private long upFlow;
private long downFlow;
private long sumFlow;
//2 反序列化时,需要反射调用空参构造函数,所以必须有
public FlowBean() {
super();
}
/**
* 3重写序列化方法
*
* @param out
* @throws IOException
*/
@Override
public void write(DataOutput out) throws IOException {
out.writeLong(upFlow);
out.writeLong(downFlow);
out.writeLong(sumFlow);
}
/**
* 4 重写反序列化方法
5 注意反序列化的顺序和序列化的顺序完全一致
*
* @param in
* @throws IOException
*/
@Override
public void readFields(DataInput in) throws IOException {
upFlow = in.readLong();
downFlow = in.readLong();
sumFlow = in.readLong();
}
// 6要想把结果显示在文件中,需要重写toString(),且用”\t”分开,方便后续用
@Override
public String toString() {
return upFlow + "\t" + downFlow + "\t" + sumFlow;
}
//7 如果需要将自定义的bean放在key中传输,则还需要实现comparable接口,因为mapreduce框中的shuffle过程一定会对key进行排序
@Override
public int compareTo(FlowBean o) {
// 倒序排列,从大到小
return this.sumFlow > o.getSumFlow() ? -1 : 1;
}
}
案例实操:
统计json文件中各个电影的评分总和
第一个类:UserRateBean
package com.SumRateMovie;
public class UserRateBean {
private String movie;
private Integer rate;
private String timeStamp;
private String uid;
public String getMovie() {
return movie;
}
public void setMovie(String movie) {
this.movie = movie;
}
public Integer getRate() {
return rate;
}
public void setRate(Integer rate) {
this.rate = rate;
}
public String getTimeStamp() {
return timeStamp;
}
public void setTimeStamp(String timeStamp) {
this.timeStamp = timeStamp;
}
public String getUid() {
return uid;
}
public void setUid(String uid) {
this.uid = uid;
}
@Override
public String toString() {
return "UserRateDriver{" +
"movie='" + movie + '\'' +
", rate=" + rate +
", timeStamp='" + timeStamp + '\'' +
", uid='" + uid + '\'' +
'}';
}
}
第二个类:UserRateDriver(三类并一类)
package com.SumRateMovie;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.Mapper;
import org.apache.hadoop.mapreduce.Reducer;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.input.TextInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
import org.apache.hadoop.mapreduce.lib.output.TextOutputFormat;
import org.apache.htrace.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
public class UserRateDriver {
public static class UserRateMap extends Mapper<LongWritable, Text,Text, IntWritable>{
ObjectMapper objectMapper = new ObjectMapper();
protected void map(LongWritable key, Text value,Context context) throws IOException, InterruptedException {
String line = value.toString();
UserRateBean userRateBean = objectMapper.readValue(line,UserRateBean.class);
String movie = userRateBean.getMovie();
Integer rate = userRateBean.getRate();
context.write(new Text(movie),new IntWritable(rate));
}
}
public static class UserRateReducer extends Reducer<Text,IntWritable,Text,IntWritable>{
protected void reduce(Text key,Iterable<IntWritable> values,Context context) throws IOException, InterruptedException {
int sum = 0;
for (IntWritable v:values) {
sum = sum + v.get();
}
context.write(key,new IntWritable(sum));
}
}
public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
Configuration conf = new Configuration();
conf.set("yarn.resorcemanager.hostname","jasmine01");
conf.set("fs.deafutFS","hdfs://jasmine01:9000/");
Job job = Job.getInstance(conf);
job.setJarByClass(UserRateDriver.class);
job.setMapperClass(UserRateMap.class);
job.setReducerClass(UserRateReducer.class);
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(IntWritable.class);
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(IntWritable.class);
job.setInputFormatClass(TextInputFormat.class);
job.setOutputFormatClass(TextOutputFormat.class);
FileInputFormat.setInputPaths(job,new Path(args[0]));
FileOutputFormat.setOutputPath(job,new Path(args[1]));
job.submit();
boolean b = job.waitForCompletion(true);
System.exit(b?0:1);
}
}
2.InputFormat数据切片机制
(一)FileInputFormat切片机制
**job提交流程源码详解**
waitForCompletion()
submit();
(1)建立连接
connect();
//创建提交job的代理
new Cluster(getConfiguration());
// 判断是本地yarn还是远程
initialize(jobTrackAddr, conf);
(2)提交job
submitter.submitJobInternal(Job.this, cluster)
//创建给集群提交数据的Stag路径
Path jobStagingArea = JobSubmissionFiles.getStagingDir(cluster, conf);
//获取jobid ,并创建job路径
JobID jobId = submitClient.getNewJobID();
//拷贝jar包到集群
copyAndConfigureFiles(job, submitJobDir);
rUploader.uploadFiles(job, jobSubmitDir);
//计算切片,生成切片规划文件
writeSplits(job, submitJobDir);
maps = writeNewSplits(job, jobSubmitDir);
input.getSplits(job);
从这里能看到为什么默认切片大小等于块的大小
每次切片后看是否大于块的1.1倍,大于的时候就新加一个切片,不大于就划分为一个切片。
// 向Stag路径写xml配置文件
writeConf(conf, submitJobFile);
conf.writeXml(out);
// 提交job,返回提交状态
status = submitClient.submitJob(jobId, submitJobDir.toString(), job.getCredentials());
**FileInputFormat源码解析(input.getSplits(job))**
(1)找到你数据存储的目录。
(2)开始遍历处理(规划切片)目录下的每一个文件
(3)遍历第一个文件ss.txt
·获取文件大小fs.sizeOf(ss.txt);
·计算切片大小computeSliteSize(Math.max(minSize,Math.max(maxSize,blocksize)))=blocksize=128M
·默认情况下,切片大小=blocksize
·开始切,形成第1个切片:ss.txt—0:128M 第2个切片ss.txt—128:256M 第3个切片ss.txt—256M:300M( 每次切片时,都要判断切完剩下的部分是否大于块的1.1倍,不大于1.1倍就划分一块切片 )
·将切片信息写到一个切片规划文件中
·整个切片的核心过程在getSplit()方法中完成。
·数据切片只是在逻辑上对输入数据进行分片,并不会再磁盘上将其切分成分片进行存储。InputSplit只记录了分片的元数据信息,比如起始位置、长度以及所在的节点列表等。
·注意:block是HDFS上物理上存储的存储的数据,切片是对数据逻辑上的划分。
(4)提交切片规划文件到yarn上,yarn上的MrAppMaster就可以根据切片规划文件计算开启maptask个数。
**FileInputFormat中默认的切片机制**
(1)简单地按照文件的内容长度进行切片
(2)切片大小,默认等于block大小
(3)切片时不考虑数据集整体,而是逐个针对每一个文件单独切片
比如待处理数据有两个文件:
file1.txt 320M
file2.txt 10M
经过FileInputFormat的切片机制运算后,形成的切片信息如下:
file1.txt.split1-- 0~128
file1.txt.split2-- 128~256
file1.txt.split3-- 256~320
file2.txt.split1-- 0~10M
**FileInputFormat切片大小的参数配置**
通过分析源码,在FileInputFormat中,计算切片大小的逻辑:Math.max(minSize, Math.min(maxSize, blockSize));
切片主要由这几个值来运算决定
mapreduce.input.fileinputformat.split.minsize=1 默认值为1
mapreduce.input.fileinputformat.split.maxsize= Long.MAXValue 默认值Long.MAXValue
因此, 默认情况下,切片大小=blocksize。
maxsize(切片最大值):参数如果调得比blocksize小,则会让切片变小,而且就等于配置的这个参数的值。
minsize (切片最小值):参数调的比blockSize大,则可以让切片变得比blocksize还大。
**获取切片信息API**
// 根据文件类型获取切片信息
FileSplit inputSplit = (FileSplit) context.getInputSplit();
// 获取切片的文件名称
String name = inputSplit.getPath().getName();