MapReduce OutputFormat & 内核源码

MapReduce OutputFormat & 内核源码

1.1 OutputFormat数据输出

1.1.1 OutputFormat接口实现类

OutputFormatMapReduce输出的基类,所有实现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 获得的 RecordReader,从输入 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,但是ReduceTask1,是否执行分区过程。答案是:不执行分区过程。因为在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 方法
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值