文章目录
MapReduce OutputFormat & 内核源码
1.1 OutputFormat数据输出
1.1.1 OutputFormat接口实现类
OutputFormat
是MapReduce
输出的基类
,所有实现MapReduce输出都实现了 OutputFormat接口。下面我们介绍几种常见的OutputFormat实现类:
1.OutputFormat实现类(默认
输出格式为TextOutputFormat 按行读取
)
2.自定义OutputFormat
应用场景:如,输出数据
到MYSQL/HBase/Elasticsearch
等存储框架中
步骤:自定义一个类继承FileOutputFormat
--> 改写RecordWriter
,具体改写输出数据的write()方法
1.1.2 自定义OutputFormat案例实操
1)需求
过滤输入的 log 日志,包含 atguigu
的网站输出到 D:\java_learning\output\outputformat1
,不包含 atguigu
的网站输出到 D:\java_learning\output\outputformat2
。
(1)输入data
(2)期望输出数据
两个txt文件:
2)需求分析
3)案例实操
(1)编写 LogMapper 类
package com.root.mapreduce.OutPutFormat;
import org.apache.hadoop.io.IntWritable;
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 LogMapper extends Mapper<LongWritable, Text,Text, NullWritable> {
@Override
protected void map(LongWritable key, Text value, Mapper<LongWritable, Text, Text, NullWritable>.Context context) throws IOException, InterruptedException {
//map不需要分割 也不需要其他操作 直接输出
context.write(value,NullWritable.get());
}
}
(2)编写 LogReducer 类
package com.root.mapreduce.OutPutFormat;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;
import java.io.IOException;
public class LogReducer extends Reducer<Text, NullWritable, Text, NullWritable> {
@Override
protected void reduce(Text key, Iterable<NullWritable> values, Reducer<Text, NullWritable, Text, NullWritable>.Context context) throws IOException, InterruptedException {
//http://www.baidu.com
//http://www.baidu.com
//有可能出现两条相同的数据,为了以防丢失数据,如下操作
for (NullWritable value : values) {
context.write(key, NullWritable.get());
}
}
}
(3)自定义一个 LogOutputFormat 类
package com.root.mapreduce.OutPutFormat;
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 LogOutputFormat extends FileOutputFormat<Text, NullWritable> {
@Override
public RecordWriter<Text, NullWritable> getRecordWriter(TaskAttemptContext job) throws IOException, InterruptedException {
//创建一个自定义的 RecordWriter 返回
LogRecordWriter lrw=new LogRecordWriter(job);
return lrw;
}
}
(4)编写 LogRecordWriter 类
package com.root.mapreduce.OutPutFormat;
import org.apache.hadoop.fs.FSDataOutputStream;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
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 org.apache.hadoop.yarn.webapp.hamlet2.Hamlet;
import java.io.IOException;
public class LogRecordWriter extends RecordWriter<Text, NullWritable> {
private FSDataOutputStream atguiguOut;
private FSDataOutputStream otherOut;
public LogRecordWriter(TaskAttemptContext job) {
//创建两条流
try {
//获取文件系统对象
FileSystem fs = FileSystem.get(job.getConfiguration());
//用文件系统对象创建两个输出流对应不同的目录
atguiguOut = fs.create(new Path("D:\\java_learning\\output\\outputformat1\\atguigu.log"));
otherOut = fs.create(new Path("D:\\java_learning\\output\\outputformat2\\other.log"));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
public void write(Text text, NullWritable nullWritable) throws IOException, InterruptedException {
//具体写
String s = text.toString();
//根据一行的 log 数据是否包含 atguigu,判断两条输出流输出的内容
if (s.contains("atguigu")){
atguiguOut.writeBytes(s+"\n");
}else {
otherOut.writeBytes(s+"\n");
}
}
@Override
public void close(TaskAttemptContext taskAttemptContext) throws IOException, InterruptedException {
//关流
IOUtils.closeStream(atguiguOut);
IOUtils.closeStream(otherOut);
}
}
(5)编写 LogDriver 类
package com.root.mapreduce.OutPutFormat;
import com.root.mapreduce.wordcount.WordCountDriver;
import com.root.mapreduce.wordcount.WordCountMapper;
import com.root.mapreduce.wordcount.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;
import java.sql.Driver;
public class LogDriver {
public static void main(String[] args) throws IOException, InterruptedException, ClassNotFoundException {
//1.获取job
Configuration conf = new Configuration();
Job ins = Job.getInstance(conf);
//2.设置jar包路径
ins.setJarByClass(LogDriver.class);
//3.关联mapper和reducer
ins.setMapperClass(LogMapper.class);
ins.setReducerClass(LogReducer.class);
//4.设置map输出的kv类型
ins.setMapOutputKeyClass(Text.class);
ins.setMapOutputValueClass(NullWritable.class);
//5.设置最终输出的kv类型
ins.setOutputKeyClass(Text.class);
ins.setOutputValueClass(NullWritable.class);
//5+:设置自定义的outputformat
ins.setOutputFormatClass(LogOutputFormat.class);
//6.设置输入路径和输出路径
FileInputFormat.setInputPaths(ins, new Path("D:\\java_learning\\input\\inputOutputfomat"));
//虽然我们自定义了outputformat,但是因为我们的outputformat继承自fileoutputformat
//而fileoutputformat还要输出一个_SUCCESS文件,所以还要再指定一个目录(产生的_SUCCESS文件及其校验文件都在此产生)
FileOutputFormat.setOutputPath(ins, new Path("D:\\java_learning\\output\\output111"));
//7.提交job
boolean result = ins.waitForCompletion(true);
System.exit(result ? 0 : 1);
}
}
4)结果展示
1.2 MapReduce内核源码解析
1.2.1 MapTask工作机制
(1)Read 阶段:MapTask
通过 InputFormat
获得的 RecordReade
r,从输入 InputSplit
中解析
出一个个 key/value
。
(2)Map 阶段:该节点主要是
将解析出的 key/value 交给用户编写 map()函数处理,并
产生一系列
新的 key/value`。
(3)Collect 收集阶段:在用户编写
map()函数中,当
数据处理完成后,一般会
调用OutputCollector.collect()输出结果。在该函数内部,它会将
生成的 key/value 分区(调用Partitioner),并
写入一个环形内存缓冲区`中。
(4)Spill 阶段:即“溢写”
,当环形缓冲区满
后,MapReduce 会将数据
写到本地磁盘
上,生成一个临时文件
。需要注意的是,将数据写入本地磁盘之前
,先要对数据进行一次本地排序
,并在必要时对数据进行合并、压缩等
操作。
溢写阶段
详情:
步骤 1:利用快速排序
算法对缓存区内的数据进行排序,排序方式是,先按照分区编号Partition
进行排序,然后按照 key
进行排序。这样,经过排序后,数据以分区为单位聚集在一起,且同一分区内所有数据按照 key 有序。
步骤 2:按照分区编号由小到大
依次将每个分区中的数据写入
任务工作目录下的临时文件 output/spillN.out
(N 表示当前溢写次数)中。如果用户设置了 Combiner,则写入文件之前,对每个分区中的数据进行一次聚集操作。
步骤 3:将分区数据的元信息写到内存索引数据结构 SpillRecord
中,其中每个分区的元信息包括在临时文件中的偏移量、压缩前数据大小和压缩后数据大小。如果当前内存索引大小超过 1MB,则将内存索引写到文件 output/spillN.out.index 中。
(5)Merge 阶段:当所有数据处理完成
后,MapTask 对所有临时文件
进行一次合并
,以确保最终只会生成一个数据文件
。当所有数据处理完后,MapTask 会将所有临时文件合并成一个大文件
,并保存到文件output/file.out 中
,同时生成相应的索引文件 output/file.out.index
。在进行文件合并过程中,MapTask 以分区为单位进行合并
。对于某个分区,它将采用多轮递归合并的方式。每轮合并 mapreduce.task.io.sort.factor(默认 10)个文件,并将产生的文件重新加入待合并列表中,对文件排序后,重复以上过程,直到最终得到一个大文件。让每个 MapTask 最终只生成一个数据文件,可避免同时打开大量文件和同时读取大量小文件产生的随机读取带来的开销。
1.2.2 ReduceTask工作机制
(1)Copy 阶段:ReduceTask
从各个 MapTask
上远程拷贝一片数据
,并针对某一片数据
,如果其大小超过一定阈值
,则写到磁盘上
,否则直接放到内存中
。
(2)Sort 阶段:在远程拷贝数据的同时
,ReduceTask
启动了两个后台线程
对内存和磁盘上
的文件进行合并
,以防止内存使用过多或磁盘上文件过多。按照 MapReduce 语义,用户编写 reduce()函数输入数据
是按 key
进行聚集
的一组数据。为了将 key 相同的数据聚在一起,Hadoop 采用了基于排序的策略。由于各个 MapTask 已经实现对自己的处理结果进行了局部排序,因此,ReduceTask 只需对所有数据进行一次归并排序即可。
(3)Reduce 阶段:reduce()函数
将计算结果写到 HDFS 上
。
1.2.3 ReduceTask并行度决定机制
回顾:MapTask 并行度由切片个数决定,切片个数由输入文件和切片规则决定(控制minSize,maxSize)。(参考Hadoop框架—MapReduce框架原理(上))
思考:ReduceTask 并行度由谁决定?
1)设置 ReduceTask 并行度(个数)
ReduceTask 的并行度同样影响整个 Job 的执行并发度和执行效率,但与 MapTask 的并发数由切片数决定不同,ReduceTask 数量的决定是可以直接手动设置:
// 默认值是 1,手动设置为 4
job.setNumReduceTasks(4);
2)实验:测试 ReduceTask 多少合适
(1)实验环境:1 个 Master 节点,16 个 Slave 节点:CPU:8GHZ,内存: 2G
(2)实验结论
3)注意事项
(1)ReduceTask=0
,表示没有Reduce阶段
,输出文件个数
和Map个数一致
。
(2)ReduceTask默认值
就是1
,所以输出文件
个数为一个
。
(3)如果数据分布不均匀
,就有可能在Reduce阶段
产生数据倾斜
(4)ReduceTask数量
并不是任意设置
,还要考虑业务逻辑需求
,有些情况下,需要计算全
局汇总结果,就只能有1个ReduceTask。
(5)具体
多少个ReduceTask,需要根据集群性能
而定。
(6)如果分区数不是1
,但是ReduceTask
为1
,是否执行分区过程。答案是:不执行分区过程。因为在MapTask的源码中,执行分区的前提是先判断ReduceNum个数是否大于1。不大于1肯定不执行:(参考Hadoop框架—MapReduce框架原理(中)Partition分区案例实操.)
再看输出结果:
1.2.4 MapTask & ReduceTask源码解析
1)MapTask 源码解析流程
context.write(k, NullWritable.get()); //自定义的 map 方法的写出,进入
output.write(key, value);
//MapTask727 行,收集方法,进入两次
collector.collect(key, value,partitioner.getPartition(key, value, partitions));
HashPartitioner(); //默认分区器
collect() //MapTask1082 行 map 端所有的 kv 全部写出后会走下面的 close 方法
close() //MapTask732 行
collector.flush() // 溢出刷写方法,MapTask735 行,提前打个断点,进入
sortAndSpill() //溢写排序,MapTask1505 行,进入
sorter.sort() QuickSort //溢写排序方法,MapTask1625 行,进入
mergeParts(); //合并文件,MapTask1527 行
collector.close(); //MapTask739 行,收集器关闭,即将进入 ReduceTask
2)ReduceTask 源码解析流程
if (isMapOrReduce()) //reduceTask324 行,提前打断点
initialize() // reduceTask333 行,进入
init(shuffleContext); // reduceTask375 行,走到这需要先给下面的打断点
totalMaps = job.getNumMapTasks(); // ShuffleSchedulerImpl 第 120 行,提前打断点
merger = createMergeManager(context); //合并方法,Shuffle 第 80 行
// MergeManagerImpl 第 232 235 行,提前打断点
this.inMemoryMerger = createInMemoryMerger(); //内存合并
this.onDiskMerger = new OnDiskMerger(this); //磁盘合并
rIter = shuffleConsumerPlugin.run();
eventFetcher.start(); //开始抓取数据,Shuffle 第 107 行,提前打断点
eventFetcher.shutDown(); //抓取结束,Shuffle 第 141 行,提前打断点
copyPhase.complete(); //copy 阶段完成,Shuffle 第 151 行
taskStatus.setPhase(TaskStatus.Phase.SORT); //开始排序阶段,Shuffle 第 152 行
sortPhase.complete(); //排序阶段完成,即将进入 reduce 阶段 reduceTask382 行
reduce(); //reduce 阶段调用的就是我们自定义的 reduce 方法,会被调用多次
cleanup(context); //reduce 完成之前,会最后调用一次 Reducer 里面的 cleanup 方法