MapReduce学习总结

一、MapReduce概述

1.MapReduce核心思想

MapReduce核心思想

2.WordCount案例

Mapper类:

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;
/*
    map阶段
    KEYIN:输入数据的key:行偏移量
    VALUE:输入数据的value:当前行
    KEYOUT:输出数据的key:单词
    VALUEOUT:输出数据的value:1
*/
public class WordcountMapper extends Mapper<LongWritable, Text, Text, IntWritable> {
    
    @Override
    protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {

        Text k = new Text();
        IntWritable v = new IntWritable(1);

        // 1.获取一行
        String line = value.toString();
        // 2.切割单词
        String[] words = line.split(" ");
        // 3.循环写出
        for (String word : words) {
            k.set(word);
            context.write(k, v);
        }
    }
}

Reducer类:

import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;

import java.io.IOException;

public class WordcountReducer extends Reducer<Text, IntWritable, Text, IntWritable> {
    
    IntWritable v = new IntWritable();
    
    @Override
    protected void reduce(Text key, Iterable<IntWritable> values, Context context) throws IOException, InterruptedException {

        // 1.累加求和
        int sum = 0;
        for (IntWritable value : values) {
            sum+=value.get();
        }
        // 2.封装value
        v.set(sum);
        // 3.写出
        context.write(key,v);
    }
}

Driver类:

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.mapreduce.lib.input.CombineTextInputFormat;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;

import java.io.IOException;

public class WordcountDriver {
    
    public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException{
        
        args = new String [] {"/Users/cc/Downloads/hadoop-study/input","/Users/cc/Downloads/hadoop-study/output"};
        // 1 获取Job对象
        Configuration conf = new Configuration();
        Job job = Job.getInstance(conf);
        
        // 2 设置jar存储的位置
        job.setJarByClass(WordcountDriver.class);
        // 3 关联Map和Reduce类
        job.setMapperClass(WordcountMapper.class);
        job.setReducerClass(WordcountReducer.class);
        
        // 4 设置Mapper阶段输出数据的key和value类型
        job.setMapOutputKeyClass(Text.class);
        job.setMapOutputValueClass(IntWritable.class);
        // 5 设置最终数据输出的key和value类型
        job.setOutputKeyClass(Text.class);
        job.setOutputValueClass(IntWritable.class);

        // 6 设置输入路径和输出路径
        FileInputFormat.setInputPaths(job, new Path(args[0]));
        FileOutputFormat.setOutputPath(job, new Path(args[1]));
        // 7 提交Job
        boolean result = job.waitForCompletion(true);

        System.exit(result ? 0 : 1);
    }
}

二、Hadoop概述

1.序列化概述

🤔:为什么不用Java序列化?
Java的序列化是一个重量级序列化框架(Serializable),一个对象被序列化后,会附带很多额外的信息(各种校验信息,Header,继承体系等),不便于在网络中高效传输。所以,Hadoop自己开发了一套序列化机制(Writable)。

2.自定义Bean对象实现序列化接口(Writable)

import org.apache.hadoop.io.Writable;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;

/**
 * @Description:自定义Bean对象
 * @Author: lnch
 * @Date: 5/8/20 12:06 上午
 */
public class FlowBean implements Writable {

    private long upFlow; //上行流量
    private long downFlow; //下行流量
    private long sumFlow; //总流量

    // 反序列化时,需要反射调用空参构造函数,所以必须有空参构造
    public FlowBean() {
    }

    public FlowBean(long upFlow, long downFlow) {
        this.upFlow = upFlow;
        this.downFlow = downFlow;
        this.sumFlow = upFlow + downFlow;
    }

    // 序列化方法
    @Override
    public void write(DataOutput dataOutput) throws IOException {
        dataOutput.writeLong(upFlow);
        dataOutput.writeLong(downFlow);
        dataOutput.writeLong(sumFlow);
    }
    //反序列化方法
    @Override
    public void readFields(DataInput dataInput) throws IOException {
        // 必须和序列化的顺序一致
        upFlow = dataInput.readLong();
        downFlow = dataInput.readLong();
        sumFlow = dataInput.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;
    }

    public void set(long upFlow2, long downFlow2) {
        upFlow = upFlow2;
        downFlow = downFlow2;
        sumFlow = upFlow2 + downFlow2;
    }

    // 要想把结果显示在文件中,需要重写toString(),可用”\t”分开,方便后续用
    @Override
    public String toString() {
        return upFlow +
                "\t" + downFlow +
                "\t" + sumFlow;
    }
}

注:如果需要将自定义的bean放在key中传输,则需要实现WritableComparable接口,重写compareTo方法,因为MapReduce框架中的Shuffle过程要求对key必须能排序。下文有案例待更新……

三、MapReduce工作流程

Map阶段工作流程
Map阶段工作流程
Reduce阶段工作流程
在这里插入图片描述

1. InputFormat数据输入

1.1 切片与MapTask并行度决定机制

数据块:Block是HDFS物理上把数据分成一块一块。
数据切片:数据切片只是在逻辑上对输入进行分片,并不会在磁盘上将其切分成片进行存储。
MapTask数据切片
理解:

  • Driver类作为MapperReducer的客户端,在提交Job任务时,可以设置切片数。
  • 不考虑数据集整体的意思是,切片只针对单个文件,ss.avi是一个文件,存储在前三个Datanode上,ss2.avi是一个文件,存储在第四个Datanode上,那么切片只会在前三个Datanode中逻辑上切片,或者在第四个Datanode中逻辑上切片。
  • 为了减少Datanode间的网络I/O开销,默认切片大小为块大小。

1.2 Job的提交源码和切片源码流程

Job提交源码和切片源码

1.3 FileInputFormat切片机制

(1)源码中计算切片大小的公式
Math.max(minSize, Math.min(maxSize, blockSize));
其中,

mapreduce.input.fileinputformat.split.minsize=1 默认值为1
mapreduce.input.fileinputformat.split.maxsize= Long.MAXValue 默认值Long.MAXValue

(2)切片大小设置
maxsize(切片最大值):参数如果调得比blockSize小,则会让切片变小,而且就等于配置的这个参数的值。
minsize(切片最小值):参数调的比blockSize大,则可以让切片变得比blockSize还大。

1.4 CombineTextInputFormat切片机制

CombineTextInputFormat用于小文件过多的场景,它可以将多个小文件从逻辑上规划到一个切片中,这样,多个小文件就可以交给一个MapTask处理。

CombineTextInputFormat.setMaxInputSplitSize(job, 4194304); // 4m

生成切片过程包括:虚拟存储过程和切片过程二部分。
在这里插入图片描述
案例:
输入4个小文件,期望一个切片处理4个文件

// 如果不设置InputFormat,它默认用的是TextInputFormat.class
job.setInputFormatClass(CombineTextInputFormat.class);

//虚拟存储切片最大值设置20m
CombineTextInputFormat.setMaxInputSplitSize(job, 20971520);

1.5 KeyValueTextInputFormat切片机制

每一行均为一条记录,被分隔符分割为key和value。

//设置分隔符
conf.set(KeyValueLineRecordReader.KEY_VALUE_SEPERATOR, " ");
//设置InputFormat
job.setInputFormatClass(KeyValueTextInputFormat.class);

1.6 NLineInputFormat切片机制

切片不再按照Block来划分,而是按照指定的NlineInputFormat指定的行数N来划分,即切片数=输入文件的总行书/N

    // 7设置每个切片InputSplit中划分三条记录
    NLineInputFormat.setNumLinesPerSplit(job, 3);

    // 8使用NLineInputFormat处理记录数
   job.setInputFormatClass(NLineInputFormat.class);

1.7自定义InputFormat

案例:一次读取一个文件,封装为KV
(1)继承FileInputFormat,重写createRecordReader方法

import org.apache.hadoop.io.BytesWritable;
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;

/**
 * @Description:自定义InputFormat
 * @Author: lnch
 * @Date: 5/10/20 10:12 下午
 */
public class WholeFileInputformat extends FileInputFormat<Text, BytesWritable> {
    
    @Override
    public RecordReader<Text, BytesWritable> createRecordReader(InputSplit split, TaskAttemptContext context) throws IOException, InterruptedException {
        
        //创建自定义RecordReader对象
        WholeRecordReader recordReader = new WholeRecordReader();
        recordReader.initialize(split, context);
        return recordReader;
    }
}

(2)自定义类,继承RecordReader类

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FSDataInputStream;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.BytesWritable;
import org.apache.hadoop.io.IOUtils;
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;

/**
 * @Description:
 * @Author: lnch
 * @Date: 5/10/20 10:41 下午
 */
public class WholeRecordReader extends RecordReader<Text, BytesWritable> {

    FileSplit split;
    Configuration configuration;
    Text k = new Text();
    BytesWritable v = new BytesWritable();
    boolean isProgress = true;

	// 初始化方法,传入切片和配置信息
    @Override
    public void initialize(InputSplit split, TaskAttemptContext context) throws IOException, InterruptedException {
        // 初始化
        this.split = (FileSplit) split;

        configuration = context.getConfiguration();
    }

	//主要业务逻辑的实现方法
    @Override
    public boolean nextKeyValue() throws IOException, InterruptedException {
        // 核心业务逻辑处理

        byte[] buf = new byte[(int) split.getLength()];
        if(isProgress) {
            // 1.获取fs对象
            Path path = split.getPath();
            FileSystem fs = path.getFileSystem(configuration);

            // 2.获取输入流
            FSDataInputStream fis = fs.open(path);

            // 3.拷贝
            IOUtils.readFully(fis, buf, 0, buf.length);

            // 4.封装v
            v.set(buf,0, buf.length);

            // 5.封装k
            k.set(path.toString());

            // 6.关闭资源
            IOUtils.closeStream(fis);

            isProgress = false;

            return true;
        }
        return false;
    }

    @Override
    public Text getCurrentKey() throws IOException, InterruptedException {

        return k;
    }

    @Override
    public BytesWritable getCurrentValue() throws IOException, InterruptedException {

        return v;
    }

    @Override
    public float getProgress() throws IOException, InterruptedException {
        return 0;
    }

    @Override
    public void close() throws IOException {

    }
}

(3)设置InputFormat和OutputFormat

// 7. 设置输入的inputFormat
job.setInputFormatClass(WholeFileInputformat.class);

// 8. 设置输出的outputFormat
job.setOutputFormatClass(SequenceFileOutputFormat.class);

2. Shuffle机制

Map方法之后,Reduce方法之前的数据处理过程称之为Shuffle。
Shuffle机制

2.1 Partition分区

2.1.1 默认Partitione分区
public class HashPartitioner<K, V> extends Partitioner<K, V> {

  public int getPartition(K key, V value, int numReduceTasks) {
    return (key.hashCode() & Integer.MAX_VALUE) % numReduceTasks;
  }
}

默认分区是根据key的hashCode对ReduceTasks个数取模得到的。用户没法控制哪个key存储到哪个分区。其中,默认numReduceTasks为1。

2.1.2 自定义Partitione分区

继承Partitioner类,重写getPartition方法

import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Partitioner;

/**
 * @Description:自定义partitioner
 * @Author: lnch
 * @Date: 5/11/20 10:49 下午
 */
public class ProvincePartitioner extends Partitioner<Text, FlowBean> {
    @Override
    public int getPartition(Text key, FlowBean value, int numPartitions) {

        // key是手机号
        // value是流量信息

        // 获取手机号前3位
        String prePhoneNum = key.toString().substring(0, 3);

        int partition = 4;

        if ("136".equals(prePhoneNum)) {
            partition = 0;
        }else if ("137".equals(prePhoneNum)) {
            partition = 1;
        }else if ("138".equals(prePhoneNum)) {
            partition = 2;
        }else if ("139".equals(prePhoneNum)) {
            partition = 3;
        }else {
            partition = 4;
        }

        return partition;
    }
}
// 设置自定义Partitioner
job.setPartitionerClass(ProvincePartitioner.class);
// 设置ReduceTask任务数
job.setNumReduceTasks(5);

总结:
(1)如果ReduceTask的数量> getPartition的结果数,则会多产生几个空的输出文件part-r-000xx;
(2)如果1<ReduceTask的数量<getPartition的结果数,则有一部分分区数据无处安放,会Exception;
(3)如果ReduceTask的数量=1,则不管MapTask端输出多少个分区文件,最终结果都交给这一个ReduceTask,最终也就只会产生一个结果文件 part-r-00000;
(4)分区号必须从零开始,逐一累加。

2.2 WritableComparable排序

2.2.1 排序的分类

(1)部分排序
MapReduce根据输入记录的键对数据集排序。保证输出的每个文件内部有序。
(2)全排序
最终输出结果只有一个文件,且文件内部有序。实现方式是只设置一个ReduceTask。但该方法在处理大型文件时效率极低,因为一台机器处理所有文件,完全丧失了MapReduce所提供的并行架构
(3)二次排序
在自定义排序过程中,如果compareTo中的判断条件为两个即为二次排序。
(4)辅助排序(GroupingComparator分组)
在Reduce端对key进行分组。应用于:在接收的key为bean对象时,想让一个或几个字段相同(全部字段比较不相同)的key进入到同一个reduce方法时,可以采用分组排序。

2.2.1 全排序

bean对象做为key传输,需要实现WritableComparable接口重写compareTo方法,就可以实现排序。
自定义Bean对象,实现WritableComparable接口,重写序列化方法、反序列化方法,和compareTo排序方法

import org.apache.hadoop.io.WritableComparable;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;

/**
 * @Description: 自定义Bean对象,实现排序
 * @Author: lnch
 * @Date: 5/12/20 10:02 下午
 */
public class FlowBean implements WritableComparable<FlowBean> {

    private long upFlow; //上行流量
    private long downFlow; //下行流量
    private long sumFlow; //总流量

    // 反序列化时,需要反射调用空参构造函数,所以必须有
    public FlowBean() {
    }

    public FlowBean(long upFlow, long downFlow) {
        this.upFlow = upFlow;
        this.downFlow = downFlow;
        sumFlow = upFlow + downFlow;
    }

    // 比较,按照总流量大小倒序排序
    @Override
    public int compareTo(FlowBean bean) {

        int result;
        // 核心的比较条件判断
        if (sumFlow > bean.getSumFlow()) {
            result = -1;
        }else if (sumFlow < bean.getSumFlow()) {
            result = 1;
        }else {
            result = 0;
        }
        return result;
    }

    // 序列化方法
    @Override
    public void write(DataOutput out) throws IOException {
        out.writeLong(upFlow);
        out.writeLong(downFlow);
        out.writeLong(sumFlow);
    }

    // 反序列化方法
    @Override
    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 +
                "\t" + downFlow +
                "\t" + sumFlow;
    }
}
2.2.2 区内排序

在全排序的基础上自定义分区类,见2.1.2

2.2.3 二次排序
@Override
public int compareTo(OrderBean bean) {

    // 先按照订单id进行升序排序,如果相同,按照价格降序排序
    int result;
    if (order_id > bean.getOrder_id()) {
        result = 1;
    }else if (order_id < bean.getOrder_id()) {
        result = -1;
    }else {

        if (price > bean.getPrice()) {
            result = -1;
        }else if (price < bean.getPrice()) {
            result = 1;
        }else {
            result = 0;
        }
    }
2.2.4 辅助排序(GroupingComparator分组)

辅助排序在Reduce阶段,见下文……

2.3 Combiner合并

(1)Combiner是MR程序中Mapper和Reducer之外的一种组件。
(2)Combiner组件的父类就是Reducer。
(3)Combiner和Reducer的区别在于运行的位置:

  • Combiner是在每一个MapTask所在的节点运行;
  • Reducer是接收全局所有Mapper的输出结果;
    (4)Combiner的意义就是对每一个MapTask的输出进行局部汇总,以减小网络传输量。
    (5)Combiner能够应用的前提是不能影响最终的业务逻辑,而且,Combiner的输出kv应该跟Reducer的输入kv类型要对应起来。适用于汇总,但不适用于求平均数。
    在这里插入图片描述
    其实方案一中的WordcountCombiner和方案二中的WordcountReducer内容一摸一样,只是将Reducer方法在Map阶段多执行一次,进行局部汇总。
 // 指定需要使用Combiner,以及用哪个类作为Combiner的逻辑
job.setCombinerClass(WordcountReducer.class);

2.4 辅助排序(GroupingComparator分组)

partioner是在MapTask阶段将数据写入环形缓冲区中进行的分区操作,其目的是为了划分出几个结果文件(ReduceTask,但是partioner必须小于ReduceTask个数),而是什么决定将一组数据发送给一次Reduce类中的reduce方法中呢?换句话说,Reduce类中的reduce方法中key一样,values有多个,是什么情况下的key是一样的,能不能自定义。其实这就是 GroupingComparator分组(辅助排序)的作用。
————————————————
版权声明:本文为CSDN博主「qq_43193797」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_43193797/article/details/86093138
辅助排序案例需求
继承WritableComparator类,创建一个构造方法,将比较对象的类传给父类,重写compare方法。注意:区分WritableComparablae排序和辅助排序的区别,前者实现了WritableComparablae类,重写的是compareTo方法,是在Map阶段执行的,后者s继承了WritableComparator类,重写的是compare方法,而且要求一个特殊的构造方法,将比较对象的类传给父类。

import org.apache.hadoop.io.WritableComparable;
import org.apache.hadoop.io.WritableComparator;

/**
 * @Description:辅助排序
 * @Author: lnch
 * @Date: 5/13/20 10:11 下午
 */
public class OrderGroupingComparator extends WritableComparator {

    //创建一个构造将比较对象的类传给父类
    protected OrderGroupingComparator() {
        super(OrderBean.class, true);
    }

    @Override
    public int compare(WritableComparable a, WritableComparable b) {

        // 要求只要id相同,就认为是相同的key
        OrderBean aBean = (OrderBean) a;
        OrderBean bBean = (OrderBean) b;

        int result;
        if (aBean.getOrder_id() > bBean.getOrder_id()) {
            result = 1;
        }else if (aBean.getOrder_id() < bBean.getOrder_id()) {
            result = -1;
        }else {
            result = 0;
        }

        return result;
    }
}
 // 8 设置reduce端的分组 
 job.setGroupingComparatorClass(OrderGroupingComparator.class);

3. Outputformat

OutputFormat是MapReduce输出的基类,所有实现MapReduce输出都实现了 OutputFormat接口。

3.1.1 文本输出TextOutputFormat

默认的输出格式是TextOutputFormat,它把每条记录写为文本行。它的键和值可以是任意类型,因为TextOutputFormat调用toString()方法把它们转换为字符串。

3.1.2 SequenceFileOutputFormat

将SequenceFileOutputFormat输出作为后续 MapReduce任务的输入,这便是一种好的输出格式,因为它的格式紧凑,很容易被压缩。

3.1.3 自定义OutputFormat

案例:要在一个MapReduce程序中根据数据的不同输出两类结果到不同目录,这类灵活的输出需求可以通过自定义OutputFormat来实现。
(1)自定义一个类继承FileOutputFormat
(2)改写RecordWriter,具体改写输出数据的方法write()

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;

/**
 * @Description:
 * @Author: lnch
 * @Date: 5/14/20 10:49 下午
 */
public class FilterOutputFormat extends FileOutputFormat<Text, NullWritable> {

    @Override
    public RecordWriter<Text, NullWritable> getRecordWriter(TaskAttemptContext job) throws IOException, InterruptedException {

        return new FRecordWriter(job);
    }
}
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 java.io.IOException;

/**
 * @Description:
 * @Author: lnch
 * @Date: 5/14/20 10:51 下午
 */
public class FRecordWriter extends RecordWriter<Text, NullWritable> {

    FSDataOutputStream fosatguigu;
    FSDataOutputStream fosother;

    public FRecordWriter(TaskAttemptContext job) {

        try {
            // 1.获取文件系统
            FileSystem fs = FileSystem.get(job.getConfiguration());
            // 2.创建输出到atguigu.log的输出流
            fosatguigu = fs.create(new Path("/Users/lianchao/Downloads/hadoop-study/output/atguigu.log"));
            // 3.创建输出到other.log的输出流
            fosother = fs.create(new Path("/Users/lianchao/Downloads/hadoop-study/output/other.log"));
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

    @Override
    public void write(Text key, NullWritable value) throws IOException, InterruptedException {

        // 判断key当中是否有atguigu,如果有,写出到atguigu.log,如果没有,写出到other.log
        if (key.toString().contains("atguigu")) {
            // atguigu输出流
            fosatguigu.write(key.toString().getBytes());
        }else {
            fosother.write(key.toString().getBytes());
        }

    }

    @Override
    public void close(TaskAttemptContext taskAttemptContext) throws IOException, InterruptedException {

        IOUtils.closeStream(fosatguigu);
        IOUtils.closeStream(fosother);
    }
}
// 要将自定义的输出格式组件设置到job中
job.setOutputFormatClass(FilterOutputFormat.class);

4. Join多种应用

4.1 Reduce Join

Map端的主要工作:为来自不同表或文件的key/value对,打标签以区别不同来源的记录。然后用连接字段作为key,其余部分和新加的标志作为value,最后进行输出。
Reduce端的主要工作:在Reduce端以连接字段作为key的分组已经完成,我们只需要在每一个分组当中将那些来源于不同文件的记录(在Map阶段已经打标志)分开,最后进行合并就ok了。
1)创建商品和订合并后的Bean类

 import org.apache.hadoop.io.Writable;

import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;

/**
 * @Description:
 * @Author: lnch
 * @Date: 5/20/20 8:46 下午
 */
public class TableBean implements Writable {

    // id pid amount
    // pid	pname

    private String id; // 订单id
    private String pid; // 产品id
    private int amount; // 数量
    private String pname; // 产品名称
    private String flag; // 定义一个标记,标记是订单表还是产品表

    public TableBean() {
    }

    public TableBean(String id, String pid, int amount, String pname, String flag) {
        this.id = id;
        this.pid = pid;
        this.amount = amount;
        this.pname = pname;
        this.flag = flag;
    }

    @Override
    public void write(DataOutput out) throws IOException {

        // 序列化方法
        out.writeUTF(id);
        out.writeUTF(pid);
        out.writeInt(amount);
        out.writeUTF(pname);
        out.writeUTF(flag);

    }

    @Override
    public void readFields(DataInput in) throws IOException {

        // 反序列化方法
        id = in.readUTF();
        pid = in.readUTF();
        amount = in.readInt();
        pname = in.readUTF();
        flag = in.readUTF();

    }

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public String getPid() {
        return pid;
    }

    public void setPid(String pid) {
        this.pid = pid;
    }

    public int getAmount() {
        return amount;
    }

    public void setAmount(int amount) {
        this.amount = amount;
    }

    public String getPname() {
        return pname;
    }

    public void setPname(String pname) {
        this.pname = pname;
    }

    public String getFlag() {
        return flag;
    }

    public void setFlag(String flag) {
        this.flag = flag;
    }

    @Override
    public String toString() {
        return  id + "\t" +
                amount + "\t" +
                pname;
    }
}

2)编写TableMapper类

import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;
import org.apache.hadoop.mapreduce.lib.input.FileSplit;

import java.io.IOException;

/**
 * @Description:
 * @Author: lnch
 * @Date: 5/20/20 9:48 下午
 */
public class TableMapper extends Mapper<LongWritable, Text, Text, TableBean> {

    String name;

    @Override
    protected void setup(Context context) throws IOException, InterruptedException {

        // 获取文件的名称
        FileSplit inputSplit = (FileSplit) context.getInputSplit();
        name = inputSplit.getPath().getName();

    }

    TableBean tableBean = new TableBean();
    Text k = new Text();

    @Override
    protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {

        //        id	pid	amount
        //        1001	01	1

        //        pid	pname
        //        01	小米

        // 获取一行
        String line = value.toString();

        if (name.startsWith("order")) { // 订单表

            String[] fields = line.split("\t");

            // 封装key和value
            tableBean.setId(fields[0]);
            tableBean.setPid(fields[1]);
            tableBean.setAmount(Integer.parseInt(fields[2]));
            tableBean.setPname("");
            tableBean.setFlag("order");

            k.set(fields[1]);
        }else { //产品表

            String[] fields = line.split("\t");

            // 封装key和value
            tableBean.setId("");
            tableBean.setPid(fields[0]);
            tableBean.setAmount(0);
            tableBean.setPname(fields[1]);
            tableBean.setFlag("pd");

            k.set(fields[0]);
        }

        // 写出
        context.write(k, tableBean);
    }
}

3)编写TableReducer类

import org.apache.commons.beanutils.BeanUtils;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;

import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;

/**
 * @Description:
 * @Author: lnch
 * @Date: 5/20/20 11:03 下午
 */
public class TableReducer extends Reducer<Text, TableBean, TableBean, NullWritable> {
    
    @Override
    protected void reduce(Text key, Iterable<TableBean> values, Context context) throws IOException, InterruptedException {

        // 存储所有订单集合
        ArrayList<TableBean> orderBeans = new ArrayList<TableBean>();
        // 存放产品的信息
        TableBean pdBean = new TableBean();

        for (TableBean tableBean : values) {

            if ("order".equals(tableBean.getFlag())) { // 订单表

                TableBean tempBean = new TableBean();

                try {
                    BeanUtils.copyProperties(tempBean, tableBean);

                    orderBeans.add(tempBean);
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                } catch (InvocationTargetException e) {
                    e.printStackTrace();
                }

            }else {
                try {
                    BeanUtils.copyProperties(pdBean, tableBean);
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                } catch (InvocationTargetException e) {
                    e.printStackTrace();
                }
            }

        }
        for (TableBean orderBean : orderBeans) {
            orderBean.setPname(pdBean.getPname());

            context.write(orderBean, NullWritable.get());
        }
    }
}

4)编写TableDriver类

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
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;


/**
 * @Description:
 * @Author: lnch
 * @Date: 5/21/20 12:06 上午
 */
public class TableDriver {
    public static void main(String[] args) throws Exception {

// 0 根据自己电脑路径重新配置
        args = new String[]{"/Users/lianchao/Downloads/hadoop-study/input/inputjoin","/Users/lianchao/Downloads/hadoop-study/output/outputjoin"};

// 1 获取配置信息,或者job对象实例
        Configuration configuration = new Configuration();
        Job job = Job.getInstance(configuration);

        // 2 指定本程序的jar包所在的本地路径
        job.setJarByClass(TableDriver.class);

        // 3 指定本业务job要使用的Mapper/Reducer业务类
        job.setMapperClass(TableMapper.class);
        job.setReducerClass(TableReducer.class);

        // 4 指定Mapper输出数据的kv类型
        job.setMapOutputKeyClass(Text.class);
        job.setMapOutputValueClass(TableBean.class);

        // 5 指定最终输出的数据的kv类型
        job.setOutputKeyClass(TableBean.class);
        job.setOutputValueClass(NullWritable.class);

        // 6 指定job的输入原始文件所在目录
        FileInputFormat.setInputPaths(job, new Path(args[0]));
        FileOutputFormat.setOutputPath(job, new Path(args[1]));

        // 7 将job中配置的相关参数,以及job所用的java类所在的jar包, 提交给yarn去运行
        boolean result = job.waitForCompletion(true);
        System.exit(result ? 0 : 1);
    }
}

4.2 Map Join

思考:在Reduce端处理过多的表,非常容易产生数据倾斜。怎么办?
在Map端缓存多张表,提前处理业务逻辑,这样增加Map端业务,减少Reduce端数据的压力,尽可能的减少数据倾斜。
具体办法:采用DistributedCache
(1)在Mapper的setup阶段,将文件读取到缓存集合中。

import org.apache.commons.lang.StringUtils;
import org.apache.hadoop.io.IOUtils;
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.FileInputStream;
import java.io.InputStreamReader;

import java.io.BufferedReader;
import java.io.IOException;
import java.net.URI;
import java.util.HashMap;

/**
 * @Description:
 * @Author: lnch
 * @Date: 5/21/20 8:20 下午
 */
public class DistributedCacheMapper extends Mapper<LongWritable, Text, Text, NullWritable> {

    HashMap<String, String> pdMap = new HashMap<String, String>();

    @Override
    protected void setup(Context context) throws IOException, InterruptedException {

        // 缓存小表
        URI[] cacheFiles = context.getCacheFiles();
        String path = cacheFiles[0].getPath();

        BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(path), "UTF-8"));

        String line;
        while (StringUtils.isNotEmpty(line = reader.readLine())) {
//            pid	pname
//            01	小米
            // 1.切割
            String[] fields = line.split("\t");

            pdMap.put(fields[0], fields [1]);
        }
        IOUtils.closeStream(reader);
    }

    Text k = new Text();
    @Override
    protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {

//        id	pid	amount
//        1001	01	1

//        pid	pname
//        01	小米
        // 1.读取一行
        String line = value.toString();

        // 2.切割
        String[] fields = line.split("\t");

        // 3.获取pid
        String pid = fields[1];

        // 4.取出pname
        String pname = pdMap.get(pid);

        // 5.拼接
        line = line + "\t" + pname;

        // 6.写出
        k.set(line);

        context.write(k, NullWritable.get());

    }
}

(2)在驱动函数中加载缓存。

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
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.net.URI;

/**
 * @Description:
 * @Author: lnch
 * @Date: 5/21/20 8:15 下午
 */
public class DistributedCacheDriver {
    public static void main(String[] args) throws Exception {

        args = new String[]{"/Users/lianchao/Downloads/hadoop-study/input/inputcache/inputorder", "/Users/lianchao/Downloads/hadoop-study/output/outputcache"};

        // 1 获取job信息
        Configuration configuration = new Configuration();
        Job job = Job.getInstance(configuration);

        // 2 设置加载jar包路径
        job.setJarByClass(DistributedCacheDriver.class);

        // 3 关联map
        job.setMapperClass(DistributedCacheMapper.class);

        // 4 设置最终输出数据类型
        job.setOutputKeyClass(Text.class);
        job.setOutputValueClass(NullWritable.class);

        // 5 设置输入输出路径
        FileInputFormat.setInputPaths(job, new Path(args[0]));
        FileOutputFormat.setOutputPath(job, new Path(args[1]));

        // 6 加载缓存数据
        job.addCacheFile(new URI("/Users/lianchao/Downloads/hadoop-study/input/inputcache/inputpd/pd.txt"));

        // 7 Map端Join的逻辑不需要Reduce阶段,设置reduceTask数量为0
        job.setNumReduceTasks(0);

        // 8 提交
        boolean result = job.waitForCompletion(true);
        System.exit(result ? 0 : 1);
    }
}

四、压缩

压缩方式

1. 数据流的压缩与解压缩

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.IOUtils;
import org.apache.hadoop.io.compress.CompressionCodec;
import org.apache.hadoop.io.compress.CompressionCodecFactory;
import org.apache.hadoop.io.compress.CompressionInputStream;
import org.apache.hadoop.io.compress.CompressionOutputStream;
import org.apache.hadoop.util.ReflectionUtils;

import java.io.*;

/**
 * @Description:压缩
 * @Author: lnch
 * @Date: 5/26/20 5:37 下午
 */
public class TestCompress {
    public static void main(String[] args) throws Exception {

        // 压缩(第二个参数为对应的编码器)
//        DEFLATE	org.apache.hadoop.io.compress.DefaultCodec
//        gzip	org.apache.hadoop.io.compress.GzipCodec
//        bzip2	org.apache.hadoop.io.compress.BZip2Codec
        //compress("/Users/lianchao/Downloads/hadoop-study/input/compress/hello.txt","org.apache.hadoop.io.compress.GzipCodec");
        decompress("/Users/lianchao/Downloads/hadoop-study/input/compress/hello.txt.gz");
    }

    /*解压缩*/
    private static void decompress(String fileName) throws IOException {

        // 1压缩方式检查
        CompressionCodecFactory factory = new CompressionCodecFactory(new Configuration());
        CompressionCodec codec = factory.getCodec(new Path(fileName));

        if (codec == null) {
            System.out.println("cannot find codec for file " + fileName);
            return;
        }

        // 2获取输入流
        FileInputStream fis = new FileInputStream(new File(fileName));

        CompressionInputStream cis = codec.createInputStream(fis);

        // 3获取输出流
        FileOutputStream fos = new FileOutputStream(new File(fileName + ".decode"));

        // 4流的对拷
        IOUtils.copyBytes(cis, fos, 1024*1024, false);

        // 5关闭资源
        IOUtils.closeStream(fos);
        IOUtils.closeStream(cis);
        IOUtils.closeStream(fis);
    }

    /*压缩*/
    private static void compress(String fileName, String method) throws Exception {

        // 1获取输入流
        FileInputStream fis = new FileInputStream(new File(fileName));

        Class<?> classCodec = Class.forName(method);
        CompressionCodec codec = (CompressionCodec) ReflectionUtils.newInstance(classCodec, new Configuration());
        // 2获取输出流
        FileOutputStream fos = new FileOutputStream(new File(fileName + codec.getDefaultExtension()));
        CompressionOutputStream cos = codec.createOutputStream(fos);
        // 3流的对拷
        IOUtils.copyBytes(fis, cos, 1024*1024, false);
        // 4关闭资源
        IOUtils.closeStream(cos);
        IOUtils.closeStream(fos);
        IOUtils.closeStream(fis);
    }
}

2. Map输出端采用压缩

// 开启map端输出压缩
conf.setBoolean("mapreduce.map.output.compress", true);
// 设置map端输出压缩方式
conf.setClass("mapreduce.map.output.compress.codec", BZip2Codec.class, CompressionCodec.class);

3. Reduce输出端采用压缩

// 设置reduce端输出压缩开启
FileOutputFormat.setCompressOutput(job, true);

// 设置压缩的方式
FileOutputFormat.setOutputCompressorClass(job, BZip2Codec.class);

五、Yarn资源调度器

1. Yarn的工作机制

Yarn的工作机制
工作机制概述:客户端执行job.waitForCompletion,首先判断是localRunner还是YarnRunner(这里只描述YarnRunner的过程)。YarnRunner向ResourceManager申请一个Application,Resourcemanager返回资源提交路径和Job_id,客户端将切片,.xml文件以及jar包提交到该路径。资源提交完毕后,客户端申请运行mrAppMaster。之后,ResourceManager将请求初始化成Task,Task进入资源调度器(常见的调度策略有三种,FIFO,Capacity调度器和公平调度器,详见下文)。随后空闲的Nodemanager从调度器中领取到Task任务,由Container为其分配cpu,ram等计算资源。
该NodeManager从资源提交路径中下载Job资源,根据切片数量,向ResourceManager申请开启相同数量的MapTask。之后,其他空闲的NodeManager领取MapTask,并由Container分配相应的计算资源。最初领取Task的NodeManager,向领取MapTask的NodeManager发送程序启动脚本,开启Map任务,Map任务结束之后,最初领取Task的NodeManager向ResourceManager申请开启ReduceTask,ReduceTask将每一个Map任务执行完的有序数据,按分区拷贝,执行Reduce任务。

2. 资源调度器

Hadoop2.7.2默认的资源调度器是Capacity Scheduler

2.1 FIFO调度策略

FIFO调度策略
遵循队列先进先出。

2.2 Capacity调度策略

Capacity调度策略
容量调度策略是多个FIFO队列的组合,需要注意的是,该调度器会对同一个用户提交的作业所占的资源进行限定,任务来了之后,会选择最闲的队列进入。

2.3 Fair调度策略

Fair调度策略
公平调度策略需要注意的是调度器对每个Job按照资源缺额进行资源分配,同一个队列中的Job也是并发执行的。

3. 任务推测执行

作业完成时间取决于最慢的任务完成时间,推测执行机制就是为最慢的任务开启备份任务,谁先运行完,采用谁的结果。
推测执行算法原理:
推测执行算法原理

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值