MR运行流程
MapReduce框架原理
1.InputFormat 数据输入
1)Job提交流程源码解析
概述:
1.提交Job任务
2.建立连接创建Job代理,判断是本地运行环境还是集群运行环境
3.提交任务,创建stag路径
4.获取JobID,创建job路径
5.如果是集群环境,则拷贝jar包到集群,如果是本地环境,则计算分片**(job.split),生成分片文件信息(job.splitmetainfo)**
6.向stag写xml配置文件**(job.xml)**
7.提交Job,返回提交状态
waitForCompletion()
submit();
// 1建立连接
connect();
// 1)创建提交Job的代理
new Cluster(getConfiguration());
// (1)判断是本地运行环境还是yarn集群运行环境
initialize(jobTrackAddr, conf);
// 2 提交job
submitter.submitJobInternal(Job.this, cluster)
// 1)创建给集群提交数据的Stag路径
Path jobStagingArea = JobSubmissionFiles.getStagingDir(cluster, conf);
// 2)获取jobid ,并创建Job路径
JobID jobId = submitClient.getNewJobID();
// 3)拷贝jar包到集群
copyAndConfigureFiles(job, submitJobDir);
rUploader.uploadFiles(job, jobSubmitDir);
// 4)计算切片,生成切片规划文件
writeSplits(job, submitJobDir);
maps = writeNewSplits(job, jobSubmitDir);
input.getSplits(job);
// 5)向Stag路径写XML配置文件
writeConf(conf, submitJobFile);
conf.writeXml(out);
// 6)提交Job,返回提交状态
status = submitClient.submitJob(jobId, submitJobDir.toString(), job.getCredentials());
2)切片源码解析
MR程序启动任务时,会使用FileInputFormat计算任务的分片数
-
FileInputFormat继承InputFormat
-
FileInputFormat默认使用Block大小作为分片大小
Math.max(minSize, Math.min(maxSize, blockSize));
-
除此之外,FileInputFormat设置了一个阈值1.1,如果文件大小不超过分片大小的1.1倍,则直接作为一个分片
-
如果需要调整分片大小,则可以通过修改以下配置
- 想减小分片大小,修改
mapreduce.input.fileinputformat.split.maxsize
,如果参数调试的比blockSize小,
则会让分片变小,而且切片的大小就是调试的参数的值
- 想增加分片大小,修改
mapreduce.input.fileinputformat.split.minsize
,如果参数调试的比blockSize大,则会让分片变大
- 想减小分片大小,修改
-
-
分片的个数与MR任务启动的MapTask的线程数(并发度)对应
3)自定义输入格式
特殊场景下,由于默认TextInputFormat每个文件至少产生一个分片,如果原始数据是大量小文件会导致启动过多MapTask线程导致性能收到影响
- MR中提供了可以跨文件进行分片合并的输入格式化类,CombineTextInpuFormat
// driver中设置使用的输入格式化类
job.setInputFormatClass(CombineTextInputFormat.class);
// 设置CombineTextInputFormat的最大最小分片大小,通常设置为一样值,可以完成跨文件的分片输入
CombineTextInputFormat.setMinInputSplitSize(job, 100 * 1024 * 1024);
CombineTextInputFormat.setMaxInputSplitSize(job, 100 * 1024 * 1024);
- MR也允许开发者根据自己的需要创建自定义的数据输入格式化类
- 自定义一个类 继承 FileInputFormat,实现createRecordReader()方法
- 自定义一个类 继承RecordReader,实现6个方法
init() 初始化方法用于获取分片信息和上下文对象
nextKeyValue() 判断是否还有下一组kv,以及读取数据将数据赋值给当前的k v
getCurKey() 返回当前K
getCurValue() 返回当前V
getProgress() 返回读取进度
close() 释放资源
-
在Driver中使用job.setInputFormatClass(设置自定义输入格式化类)
package com.zch.exercise.day1124; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.FSDataInputStream; import org.apache.hadoop.fs.FileStatus; 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.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; /** * 自定义分片信息要继承RecordReader,而且类型应该和map的输出类型一致 */ public class WholeRecordReader extends RecordReader<Text, Text> { FileSystem fs = null; FileSplit fileSplit = null; boolean flag = true; Text k = new Text(); Text v = new Text(); /** * @param split 定义分区 * @param context 上下文信息 * @throws IOException * @throws InterruptedException */ @Override public void initialize(InputSplit split, TaskAttemptContext context) throws IOException, InterruptedException { //创捷conf对象,创建fs Configuration conf = context.getConfiguration(); this.fs = FileSystem.get(conf); //通过文件分片信息获取文件路径 this.fileSplit = (FileSplit) split; } /** * 用于判断是否还有下一组kv对需要读取 * 如果有 则读取数据 并返回true * * @return * @throws IOException * @throws InterruptedException */ @Override public boolean nextKeyValue() throws IOException, InterruptedException { if (this.flag) { //通过分片信息获取文件路径 Path path = fileSplit.getPath(); //使用文件系统创建输入流,通过路径读取到文件 FSDataInputStream inputStream = fs.open(path); //通过分片信息获取文件的状态 FileStatus[] listStatus = fs.listStatus(path); //文件状态数组: 文件 文件状态,获取文件长度 FileStatus fileStatus = listStatus[0]; long len = fileStatus.getLen(); //创建读取缓存数组 byte[] buf = new byte[(int) len]; //将文件内容读取到一起,作为v inputStream.read(buf); inputStream.close(); v.set(new String(buf)); //根据路径获取文件名称,作为k k.set(path.getName()); this.flag = false; return true; } return false; } /** * 取出当前 k 对象 * * @return * @throws IOException * @throws InterruptedException */ @Override public Text getCurrentKey() throws IOException, InterruptedException { return this.k; } /** * 取出当前v对象 * * @return * @throws IOException * @throws InterruptedException */ @Override public Text getCurrentValue() throws IOException, InterruptedException { return this.v; } /** * 返回0.0 ~ 1.0的flaot值表示当前读取任务的进度 * * @return * @throws IOException * @throws InterruptedException */ @Override public float getProgress() throws IOException, InterruptedException { return flag ? 0.0F : 1.0F; } /** * 释放资源 * * @throws IOException */ @Override public void close() throws IOException { fs.close(); } }
package com.zch.hadoop.mapreduce.inputformat; 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.FileInputFormat; import java.io.IOException; /** * Created with IntelliJ IDEA. * Author: Amos * E-mail: amos@amoscloud.com * Date: 2021/11/24 * Time: 14:07 * Description: */ // 自定义一个输入输入格式化类,将小文件 文件名作为k 完整的文件内容作为v public class WholeFileInputFormat extends FileInputFormat<Text, Text> { /** * 用来创建记录读取器对象 * * @param split 读取数据的分片信息 * @param context MR上下文对象 * @return 记录读取器对象 * @throws IOException * @throws InterruptedException */ @Override public RecordReader<Text, Text> createRecordReader(InputSplit split, TaskAttemptContext context) throws IOException, InterruptedException { WholeRecordReader recordReader = new WholeRecordReader(); recordReader.initialize(split, context); return recordReader; } }
package com.zch.hadoop.mapreduce.inputformat; 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.lib.input.CombineTextInputFormat; 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; /** * Created with IntelliJ IDEA. * Author: Amos * E-mail: amos@amoscloud.com * Date: 2021/11/24 * Time: 14:38 * Description: */ public class Job_WholeFilerMergeDriver { public static class Job_WholeFilerMergeMapper extends Mapper<Text, Text, Text, Text> { @Override protected void map(Text key, Text value, Context context) throws IOException, InterruptedException { context.write(key, value); } } public static void main(String[] args) throws Exception { Configuration conf = new Configuration(); Job job = Job.getInstance(conf); job.setJarByClass(Job_WholeFilerMergeDriver.class); job.setMapperClass(Job_WholeFilerMergeMapper.class); job.setOutputKeyClass(Text.class); job.setOutputValueClass(Text.class); // 设置输入格式化类为自定义的输入格式化 job.setInputFormatClass(WholeFileInputFormat.class); // 设置输出格式化类为输出序列文件 //输入格式为SequenceFileOutputFormat,则使用文件时输入格式也要用相应的SequenceFileOutputFormat job.setOutputFormatClass(SequenceFileOutputFormat.class); FileInputFormat.setInputPaths(job, new Path(args[0])); FileOutputFormat.setOutputPath(job, new Path(args[1])); boolean b = job.waitForCompletion(true); System.exit(b ? 0 : 1); } }
2. MapTask阶段
(1)Read阶段:MapTask通过InputFormat获得的RecordReader,从输入InputSplit中解析出一个个key/value。
(2)Map阶段:该节点主要是将解析出的key/value交给用户编写map()函数处理,并产生一系列新的key/value。
(3)Collect收集阶段:在用户编写map()函数中,当数据处理完成后,一般会调用OutputCollector.collect()输出结果。在该函数内部,它会将生成的key/value分区(调用Partitioner),并写入一个环形内存缓冲区中。
(4)Spill阶段:即“溢写”,当环形缓冲区满后,MapReduce会将数据写到本地磁盘上,生成一个临时文件。需要注意的是,将数据写入本地磁盘之前,先要对数据进行一次本地排序,并在必要时对数据进行合并、压缩等操作。
-
Mapper和Reducer如果需要处理包含多个属性的负责对象时,Hadoop自身提供的Writable不能满足需要,可以自定义个实现Wirtable序列化的bean
-
声明一个类实现Wirtable接口
-
实现序列化方法write(out)
-
实现反序列化方法readFields(ip)
-
重写toString方便数据的输出
注意: 序列化和反序列化的顺序必须一致@Override public void write(DataOutput out) throws IOException { out.writeInt(this.visitCount); out.writeLong(this.upFlow); out.writeLong(this.downFlow); } @Override public void readFields(DataInput in) throws IOException { this.visitCount = in.readInt(); this.upFlow = in.readLong(); this.downFlow = in.readLong(); }
-
3. 环形缓冲区
- MR提供了一个默认100M大小内存区域,用于存储Map输出的kv
- 当缓冲区到达存储空间0.8的阈值时,将已经存储在缓冲区的数据刷写到磁盘中
- 由于使用了环形设计,可以保证数据写入和读取互不影响,使用顺序读写
4. 分区
-
MR在Map结束后Reduce开始前 会对数据按照Key进行分区
-
默认分区数据量为1,如果job中设置了reduceTask数量,则分区数量与reduceTask数量一致
- MR默认使用HashPartitioner,使用Key的hash % reduceTask,HashParitioner可以比较均衡的将Map输出的输入分配给每一个reducer避免数据倾斜
- 如果需要将数据按照要求输出到不同的文件,可以自定义一个类继承Partitioner,自己实现getPartition方法用于计算分区编号。自定义分区器可能导致数据倾斜,可以提前将较大Key单独处理或者继续散列为多个key
-
分区数reduce数量的关系
-
分区数 = reduce数
理想状态 -
分区数 > reduce数
任务报错 -
分区数 < reduce数
生成多余空文件package com.zch.exercise.day1124.partitoner; import org.apache.hadoop.io.LongWritable; import org.apache.hadoop.io.NullWritable; import org.apache.hadoop.io.Text; import org.apache.hadoop.mapreduce.Partitioner; import java.util.Arrays; /** * 自定义分区规则 * 根据24小时分为24个分区 */ public class TimeHourPartitioner extends Partitioner<Text, NullWritable> { @Override public int getPartition( Text text,NullWritable nullWritable, int numPartitions) { String[] split = text.toString().split("\\s+"); String s1 = split[3]; String[] split1 = s1.split("/"); String s2 = split1[2]; String s3 = s2.split(":")[1]; int diYiWei = (int)s3.toCharArray()[0];//48 int diErWei = (int)s3.toCharArray()[1]; int result = 0; if (diYiWei == 48 && diErWei != 48){ result = diErWei - 48; }else { result = 1; } if (diYiWei != 48){ result = Integer.parseInt(s3); } return result - 1; } }
package com.zch.exercise.day1124.partitoner; 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.Mapper; import org.apache.hadoop.mapreduce.Reducer; import org.apache.hadoop.mapreduce.lib.input.FileInputFormat; import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat; import java.io.IOException; public class WholeFileDriver { public static class WholeFileMapper extends Mapper<LongWritable, Text, Text, NullWritable>{ Text k = new Text(); @Override protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException { //将输入的文本利用换行分隔 String[] split = value.toString().split("\\n"); //获取每行数据 String s = split[0]; //将每行数据作为k输出 k.set(s); context.write(k, NullWritable.get()); } } public static class WholeFileReducer 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()); } } public static void main(String[] args) throws Exception{ Configuration conf = new Configuration(); Job job = Job.getInstance(conf); job.setJarByClass(WholeFileDriver.class); job.setMapperClass(WholeFileMapper.class); job.setReducerClass(WholeFileReducer.class); job.setMapOutputKeyClass(Text.class); job.setMapOutputValueClass(NullWritable.class); job.setOutputKeyClass(Text.class); job.setOutputValueClass(NullWritable.class); //设置分区为24个 job.setNumReduceTasks(24); //加载自定义分区对象 job.setPartitionerClass(TimeHourPartitioner.class); //自定义格式 job.setCombinerClass(WholeFileReducer.class); FileInputFormat.setInputPaths(job,new Path("C:\\Users\\Administrator\\Desktop\\part-r-00000")); FileOutputFormat.setOutputPath(job,new Path("C:\\Users\\Administrator\\Desktop\\output1")); boolean b = job.waitForCompletion(true); System.exit(b ? 0 : 1); } }
-
5. 排序
- MR任务会默认按照Map输出数据的Key进行升序排列
- 排序时会使用Key位置上的对象的compareTo方法进行升序排列
- 如果需要让自定义的Writable类放在输出Key位置上,则需要实现WritableComparable接口,并实现compareTo()方法用于进行对象比较
6. Map端预聚合
-
Combiner是MR提供的一个Map端的预聚合机制
-
在Map输出之后,没有产生网络shuffle之前,在Map端对数据进行类似Reducer的聚合操作
-
编写MR任务时,可以直接将Reducer作为Combiner
-
观察如果没有改变计算结果,则可以使用预聚合优化
public static class Job_IPCountCombiner extends Reducer<Text, IntWritable, Text, IntWritable> { @Override protected void reduce(Text key, Iterable<IntWritable> values, Context context) throws IOException, InterruptedException { int count = 0; for (IntWritable value : values) { count += value.get(); } context.write(key, new IntWritable(count)); } }
7. reduce端合并
- 所有的MapTask结束后,会根据分区的编号
- 每个Reduce读取一个分区(来自多个MapTask)的数据,进行合并
- 合并时 将相同K对应的所有Value合并到一个集合中
8. ReduceTask阶段
- setup 创建连接 申请资源
reduce(K key, Iterable<V> values,context )
是ReduceTask的核心方法,用于完成聚合的逻辑- claenup 关闭资源
- run 将上述三个方法组合执行reduce任务
9. 数据输出
- MR任务默认使用TextOutputFormat,将K和V toString后使用
\t
进行分割 每个KV对输出到一行中 - 可以在driver中job.setOuputFormat(xxx.class)改变输出格式化
- 也可以根据需要自定义输出格式化
- 自定义类继承FileOutputFormat,实现方法createRecordWirter
- 自定义类继承RecordWirter,实现方法
write(K key,V value) 拿到reducer输出后的kv对,可以在wirte方法中实现io或者jdbc将数据写出到对应的自定义文件或者数据库
close()关闭连接或者流
package com.zch.hadoop.mapreduce.outputformat;
import com.zch.hadoop.mapreduce.utils.IPGEOUtils;
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.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.*;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
public class CustomOutputFormatDrive {
//编写一个mr任务,将ip归属地不同的数据写道文件中
// 要求输出的文件名为xx地区的用户.txt
// 地区 ip
public static class GEOMapper extends Mapper<LongWritable,Text,Text,Text>{
Text k = new Text();
Text v = new Text();
@Override
protected void map(LongWritable key, Text value, Context context)
throws IOException, InterruptedException {
//将输入的文本数据分隔获取ip
String ip = value.toString().split("\\s+")[0];
//将ip传入工具类中获取地区和城市
List<String> geo = IPGEOUtils.getGEO(ip);
//地区
String pro = geo.get(0);
//城市
String city = geo.get(1);
//将城市名存入text中写出到reduce
//将城市名字和IP组合实现去重操作
k.set(city+":"+ip);
context.write(k, v);
}
}
public static class GEOReduce extends Reducer<Text,Text,Text,Text>{
Text k = new Text();
Text v = new Text();
@Override
protected void reduce(Text key, Iterable<Text> values, Context context) throws IOException, InterruptedException {
String toString = key.toString();
//将map传入的k值分隔,获取城市和ip
String[] split = toString.split(":");
//城市
String city = split[0];
k.set(city);
//ip
String ip = split[1];
v.set(ip);
context.write(k, v);
}
}
public static void main(String[] args) throws Exception {
args[0] = "C:\\Users\\Administrator\\Desktop\\test";
args[1] = "C:\\Users\\Administrator\\Desktop\\output\\";
Configuration conf = new Configuration();
conf.set("ip.geo.output.path", args[1]);
Job job = Job.getInstance(conf);
job.setMapperClass(GEOMapper.class);
job.setReducerClass(GEOReduce.class);
job.setJarByClass(CustomOutputFormatDrive.class);
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(Text.class);
job.setOutputFormatClass(GEOOutputFormat.class);
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(Text.class);
FileInputFormat.setInputPaths(job, new Path(args[0]));
FileOutputFormat.setOutputPath(job, new Path(args[1]));
boolean b = job.waitForCompletion(true);
System.exit(b ? 0 : 1);
}
/**
* 自定义输出流
*/
public static class GEOOutputFormat extends FileOutputFormat<Text, Text> {
@Override
public RecordWriter<Text, Text> getRecordWriter(TaskAttemptContext job) throws IOException, InterruptedException {
return new GEORecordWriter(job);
}
}
/**
* 创建RecordWriter对象
*/
public static class GEORecordWriter extends RecordWriter<Text, Text> {
FileSystem fs;
HashMap<String, FSDataOutputStream> map = new HashMap<>();
public GEORecordWriter(TaskAttemptContext context) {
Configuration conf = context.getConfiguration();
try {
//获取文件系统,引入流对象
fs = FileSystem.get(conf);
} catch (IOException e) {
//todo 添加异常处理
}
}
/**
* 实现文件写出IO方法
*
* @param key 城市名
* @param value ip地址
* @throws IOException
* @throws InterruptedException
*/
@Override
public void write(Text key, Text value) throws IOException, InterruptedException {
FSDataOutputStream outputStream;
String city = key.toString();
Configuration conf = fs.getConf();
//自定义输出路径
String parentPath = conf.get("ip.geo.output.path");
Path path = new Path(parentPath+ city + "地区的用户.txt");
if (!map.containsKey(city)) {
outputStream = fs.create(path);
map.put(city, outputStream);
}
String data = key.toString() + "\t" + value.toString();
//将传入的key value写出到输出路径
map.get(city).write(data.getBytes());
}
@Override
public void close(TaskAttemptContext context) throws IOException, InterruptedException {
//便利map集合,获取map集合的value值为文件的输出流
for (FSDataOutputStream outputStream : map.values()) {
outputStream.flush();
outputStream.close();
}
}
}
}