Hadoop MapReduce框架原理

1.InputFormat数据输入

1.数据切片与MapTask并行度决定机制

  1. 一个Job的Map阶段并行度由客户端在提交Job时的切片数决定
  2. 每一个Split切片分配一个MapTask并行实例处理
  3. 默认情况下,切片大小 = BlockSize
  4. 切片时不考虑数据集整体,而是逐个针对每一个文件单独切片。

2.FileInputFormat切片源码解析

  1. 程序先找到数据存储的目录

  2. 开始遍历处理(规划切片)目录下的每一个文件

  3. 遍历第一个文件 core.txt
    a. 获取文件大小

    long length = file.getLen();
    

    b. 计算切片大小

    long splitSize = computeSplitSize(blockSize, minSize, maxSize);
    

    c. 默认情况下,切片大小=blockSize
    d. 开始切片,例如300M的文件

    切片序号 开始大小-结束大小
    1 0 - 128M
    2 128-256M
    3 256-300M

    e.将切片信息写到一个切片规划文件中
    f.整个切片的核心过程在getSplit()方法中完成
    g.Inputsplit只记录了切片的元数据信息,比如起始位置、长度以及所在的节点列表等

  4. 提交切片规划文件到YARN上,YARN上的MrAppMaster就可以根据切片规划文件计算开启MapTask个数。

3.FileInputFormat切片大小的参数配置

1.源码中计算切片大小的公式
  long splitSize = computeSplitSize(blockSize, minSize, maxSize);

protected long computeSplitSize(long blockSize, long minSize,long maxSize) {
  return Math.max(minSize, Math.min(maxSize, blockSize));
}

  long maxSize = getMaxSplitSize(job);

public static long getMaxSplitSize(JobContext context) {
  return context.getConfiguration().getLong(SPLIT_MAXSIZE,Long.MAX_VALUE);
}

  long minSize = Math.max(getFormatMinSplitSize(), getMinSplitSize(job));

protected long getFormatMinSplitSize() {
  return 1;
}

mapreduce.input.fileinputformat.split.minsize=1 默认值为1
mapreduce.input.fileinputformat.split.maxsize=Long.MAX_VALUE,默认值为MAX_VALUE
因此,默认情况下,切片大小 = blockSize

2.切片大小设置

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

3.获取切片信息API
// 获取切片的文件名称
String name = inputSplit.getPath().getName();
// 根据文件类型获取切片信息
FileSplit inputSplit = (FileSplit) context.getInputSplit();

4.TextInputFormat

FileInputFormat是一个抽象类,常见的实现类包括TextInputFormat、KeyValueTextInputFormat、NLineInputFormat、CombineTextInputFormat和自定义InputFormat等
TextInputFormat是默认的FileInputFormat实现类。按行读取每条记录。键是存储该行在整个文件中的起始字节偏移量,LongWritable类型。值是这行的内容,不包括任何行终止符(换行符和回车符),Text类型。

5.CombineTextInputFormat

框架默认的TextInputFormat切片机制是对任务按文件规划切片,不管文件多小,都会是一个单独的切片,都会交给一个MapTask,这样如果有大量小文件,就会产生大量的MapTask,处理效率极其低下。

1.应用场景

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

2.虚拟存储切片最大值设置
CombineTextInputFormat.setMaxInputSplitSize(job,4194304);	//4M

注意:虚拟存储切片最大值设置最好根据实际的小文件大小情况来设置具体的值。

3.切片机制

生成切片过程包括:虚拟存储过程和切片过程两部分

1.虚拟存储过程

将输入目录下所有文件大小,依次和设置的setMaxInputSplitSize值比较,如果不大于设置的最大值,逻辑上划分一个块。如果输入文件大于设置的最大值且大于两倍,那么以最大值切割一块;当剩余数据大小超过设置的最大值且不大于最大值2倍,此时将文件均分成2个虚拟存储块(防止出现太小切片)。
例如setMaxInputSplitSize值为4M,输入文件大小为8.02M,则先逻辑上分成一个4M。剩余的大小为4.02M,如果按照4M逻辑划分,就会出现0.02M的小的虚拟存储文件,所以将剩余的4.02M文件划分。

2.切片过程
  1. 判断虚拟存储的文件大小是否大于setMaxInputSplitSize值,大于等于则单独形成一个切片。
  2. 如果不大于则跟下一个虚拟存储文件进行合并,共同形成一个切片。
  3. 测试举例:有4个小文件大小分别为1.7M、5.1M、3.4M以及6.8M这四个小文件,
    则虚拟存储之后形成6个文件块,大小分别为:1.7M、(2.55M、2.55M)、3.4M以及(3.4M、3.4M)
    最终会形成3个切片,大小分别为:(1.7+2.55)M、(2.55+3.4)M、(3.4+3.4)M
package cn.coreqi.mapreduce.combinewordcount;

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.CombineTextInputFormat;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;

import java.io.IOException;

public class WordCountDriver {

    public static void main(String[] args) throws IOException, InterruptedException, ClassNotFoundException {
        // 1.获取job
        Configuration conf = new Configuration();
        Job job = Job.getInstance(conf);

        // 2.设置jar包路径
        job.setJarByClass(WordCountDriver.class);   //通过反射指定类所在的包地址来获取当前jar包的路径

        // 3.关联mapper和reducer
        job.setMapperClass(WordCountMapper.class);
        job.setReducerClass(WordCountReducer.class);

        // 4.设置mapper输出的KV类型[因为泛型擦除的问题,所以需要手动指定类型]
        job.setMapOutputKeyClass(Text.class);
        job.setMapOutputValueClass(IntWritable.class);

        // 5. 设置最终输出(reducer)的KV类型
        job.setOutputKeyClass(Text.class);
        job.setOutputValueClass(IntWritable.class);

        // 修改默认的InputFormat,默认使用的是TextInputFormat
        job.setInputFormatClass(CombineTextInputFormat.class);
        // 虚拟存储切片最大值设置
        CombineTextInputFormat.setMaxInputSplitSize(job,4194304);   //4M


        // 6. 设置输入路径和输出路径(本地模式)
//        FileInputFormat.setInputPaths(job,new Path("D:\\intput\\inputword"));
//        FileOutputFormat.setOutputPath(job,new Path("D:\\hadoop\\output"));

        // 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);
    }
}

2.Shuffle机制

1.Partition分区

1.问题

将统计结果按照条件输出到不同的分区(文件)中

2.默认Partition分区
  private class NewOutputCollector<K,V>
    extends org.apache.hadoop.mapreduce.RecordWriter<K,V> {
    private final MapOutputCollector<K,V> collector;
    private final org.apache.hadoop.mapreduce.Partitioner<K,V> partitioner;
    private final int partitions;

    @SuppressWarnings("unchecked")
    NewOutputCollector(org.apache.hadoop.mapreduce.JobContext jobContext,
                       JobConf job,
                       TaskUmbilicalProtocol umbilical,
                       TaskReporter reporter
                       ) throws IOException, ClassNotFoundException {
      collector = createSortingCollector(job, reporter);
      partitions = jobContext.getNumReduceTasks();
	  // 分区数大于1,则走默认Partition分区,根据HashCode分区
      if (partitions > 1) {
        partitioner = (org.apache.hadoop.mapreduce.Partitioner<K,V>)
          ReflectionUtils.newInstance(jobContext.getPartitionerClass(), job);
      } else {	//否则直接返回0,1个分区
        partitioner = new org.apache.hadoop.mapreduce.Partitioner<K,V>() {
          @Override
          public int getPartition(K key, V value, int numPartitions) {
            return partitions - 1;
          }
        };
      }
    }

// 默认的Partition分区
package org.apache.hadoop.mapreduce.lib.partition;

import org.apache.hadoop.classification.InterfaceAudience;
import org.apache.hadoop.classification.InterfaceStability;
import org.apache.hadoop.mapreduce.Partitioner;


@InterfaceAudience.Public
@InterfaceStability.Stable
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存储到哪个分区。

3.自定义Partition步骤
1.自定义类继承Partitioner,重写getPartition()方法
package cn.coreqi.mapreduce.flow;

import cn.coreqi.mapreduce.writable.FlowBean;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Partitioner;

/**
 * KEY, Mapper 输出的Key
 * VALUE, Mapper 输出的Value
 */
public class ProvincePartitioner extends Partitioner<Text, FlowBean> {
    @Override
    public int getPartition(Text text, FlowBean flowBean, int numPartitions) {
        // 根据手机号码进行分区, key是手机号码
        String phoneNum = text.toString();
        String prePhoneNum = phoneNum.substring(0,3);
        int partition = 0;
        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;
    }
}
2.在job驱动中,设置自定义Partitioner
3.自定义Partitioner后,要根据自定义Partitioner的逻辑设置相应数量的ReduceTask
package cn.coreqi.mapreduce.flow;

import cn.coreqi.mapreduce.writable.FlowBean;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
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;

public class FlowDriver {
    public static void main(String[] args) throws IOException, InterruptedException, ClassNotFoundException {
        // 1.获取job
        Configuration conf = new Configuration();
        Job job = Job.getInstance(conf);

        // 2.设置jar包路径
        job.setJarByClass(FlowDriver.class);   //通过反射指定类所在的包地址来获取当前jar包的路径

        // 3.关联mapper和reducer
        job.setMapperClass(FlowMapper.class);
        job.setReducerClass(FlowReducer.class);

        // 4.设置mapper输出的KV类型[因为泛型擦除的问题,所以需要手动指定类型]
        job.setMapOutputKeyClass(Text.class);
        job.setMapOutputValueClass(FlowBean.class);

        // 5. 设置最终输出(reducer)的KV类型
        job.setOutputKeyClass(Text.class);
        job.setOutputValueClass(FlowBean.class);

        // 6. 设置输入路径和输出路径(本地模式)
//        FileInputFormat.setInputPaths(job,new Path("D:\\intput\\inputword"));
//        FileOutputFormat.setOutputPath(job,new Path("D:\\hadoop\\output"));

        //设置自定义Partitioner
        job.setPartitionerClass(ProvincePartitioner.class);
        //设置相应数量的ReduceTask,必须大于1,否则不生效
        job.setNumReduceTasks(5);

        // 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);
    }
}
4.分区总结
  1. 如果自定义Partitioner后,在Job中配置ReduceTask的数量大于自定义Partitioner中getPartition()的结果数,则会多产生几个空的输出文件part-r-000xx[多申请ReduceTask的资源会占用资源,因此要求数量匹配];
  2. 如果自定义Partitioner后,在Job中配置ReduceTask的数量小于自定义Partitioner中getPartition()的结果数并且大于1,则有一部分分区数据无处安放,会抛出Exception。
  3. 如果自定义Partitioner后,在Job中配置ReduceTask的数量为1,则不管MapTask端输出多少个分区文件,最终结果都交给这一个ReduceTask,最终也就只会产生一个结果文件 part-r-00000;
  4. 自定义Partitioner中,getPartition()返回的分区号必须从0开始,逐一累加。
5.分区案例分析

例如:自定义Partitioner,getPartition()的分区数为5,则

  1. job.setNumReduceTasks(1); 不会走自定义Partitioner,正常运行,会产生一个输出文件
  2. job.setNumReduceTasks(2); 抛出异常
  3. job.setNumReduceTasks(6); 大于5,正常运行,会产生空文件

2.WritableComparable排序

排序是MapReduce框架中最重要的操作之一。
MapTask和ReduceTask均会对数据按照Key进行排序。该操作属于Hadoop的默认行为。任何应用程序中的数据均会被排序,而不管逻辑上是否需要。
Map后相同Key的一组进入Reduce中进行处理。
默认排序是按照字典顺序排序,且实现该排序的方法是快速排序。

1.排序概述

对于MapTask,它会将处理的结果暂时放到环形缓存区中,当环形缓冲区使用率达到一定阈值后,再对缓冲区中的数据进行一次快速排序,并将这些有序数据溢写到磁盘上,而当数据处理完毕后,它会对磁盘上所有文件进行归并排序。
对于ReduceTask,它从每个MapTask上远程拷贝相应的数据文件,如果文件大小超过一定阈值,则溢写到磁盘上,否则存储在内存中。如果磁盘上文件数目达到一定阈值,则进行一次归并排序以生成一个更大文件;如果内存中文件大小或者数目超过一定阈值,则进行一次合并后将数据溢写到磁盘上。当所有数据拷贝完毕后,ReduceTask统一对内存和磁盘上的所有数据进行一次归并排序。

2.排序分类
1.部分排序

MapReduce根据输入记录的键对数据集排序。保证输出的每个文件内部有序。

2.全排序

最终输出结果只有一个文件,且文件内部有序。
实现方式是只设置一个ReduceTask
该方法在处理大型文件时效率极低,因为一台机器处理所有文件,完全丧失了MapReduce所提供的并行架构。

3.辅助排序(GroupingComparator分组)

在Reduce端对Key进行分组。
应用于:在接收的Key为Bean对象时,想让一个或几个字段相同(全部字段比较不相同)的Key进入到同一个Reduce方法时,可以采用分组排序。

4.二次排序

在自定义排序过程中,如果compareTo中的判断条件为两个即为二次排序。

3.自定义排序WritableComparable原理分析

Bean对象作为Key传输,需要实现WritableComparable接口并重写compareTo方法,就可以实现排序。

package cn.coreqi.mapreduce.writable;

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

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

/**
 * 1.定义类实现 Writable 接口
 * 2.重写空参构造函数
 * 3.重写序列化和反序列化方法
 * 4.toString()方法
 * 5.如果自定义的Bean需要放在key中传输,则还需要实现Comparable接口
 * 实现WritableComparable 等同于 实现 Writable,Comparable<FlowBean>
 * WritableComparable 继承了  Writable,Comparable接口
 */
public class FlowBean implements WritableComparable<FlowBean> {

    private long upFlow;    //上行流量

    private long downFlow;  //下行流量

    private long sumFlow;   //总流量

    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 setSumFlow() {
        this.sumFlow = this.upFlow + this.downFlow;
    }

    /**
     * 空参构造函数
     */
    public FlowBean(){
        super();
    }

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

    /**
     * 反序列化方法
     * 注意,反序列化时数据的顺序必须和序列化时的顺序完全一致
     * @param dataInput
     * @throws IOException
     */
    @Override
    public void readFields(DataInput dataInput) throws IOException {
        upFlow = dataInput.readLong();
        downFlow = dataInput.readLong();
        sumFlow = dataInput.readLong();
    }

    @Override
    public String toString() {
        return upFlow + "\t" + downFlow + "\t" + sumFlow;
    }

    @Override
    public int compareTo(FlowBean o) {
        int result;
        // 按照总流量大小,倒序排列
        if (this.sumFlow > o.getSumFlow()){
            return -1;
        } else if (this.sumFlow < o.getSumFlow()){
            return 1;
        } else {
            // 如果相同,则二次排序,按照上行流量的正序排序
            if(this.upFlow > o.upFlow){
                return 1;
            } else if (this.upFlow < o.upFlow) {
                return -1;
            } else{
                return 0;
            }
        }
    }
}

3.Combiner 合并

  1. Combiner是MR程序中Mapper和Reducer之外的一种组件
  2. Combiner组件的父类就是Reducer
  3. Combiner和Reducer的区别在于运行的位置
    a. Combiner是在每一个MapTask所在的节点运行;
    b. Reducer是接收全局所有Mapper的输出结果;
  4. Combiner的意义就是对每一个MapTask的输出进行局部汇总,以减少网络传输量
  5. Combiner能够应用的前提是不能影响最终的业务逻辑,而且,Combiner输出的KV应该跟Reducer的输入KV类型对应起来。
1.自定义Combiner实现
1.自定义一个Combiner继承Reducer,重写Reduce方法。
package cn.coreqi.mapreduce.wordcount;

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

import java.io.IOException;

/**
 * KEYIN, mapper阶段输出的key的类型
 * VALUEIN, mapper阶段输出的value的类型
 * KEYOUT, Combiner输出的key的类型 (reducer的key的输入类型,要求一致)
 * VALUEOUT Combiner输出的value的类型 (reducer的value的输入类型,要求一致)
 */
public class WordCountCombiner extends Reducer<Text, IntWritable,Text, IntWritable> {
    private IntWritable outV = new IntWritable();
    @Override
    protected void reduce(Text key, Iterable<IntWritable> values, Reducer<Text, IntWritable, Text, IntWritable>.Context context) throws IOException, InterruptedException {
        // 和reducer逻辑大体相同,combiner也是个reducer,只是在mapper节点上运行
        int sum = 0;
        //累加
        for (IntWritable value : values) {
            sum += value.get();
        }
        outV.set(sum);

        //写出
        context.write(key,outV);
    }
}
2.在Job驱动类中配置:
job.setCombinerClass(WordCountCombiner.class);

特殊说明,如果Combiner和Reducer的处理逻辑一致的话,可以在Job驱动类配置时直接将Reducer作为Combiner进行注册

3.OutputFormat数据输出

1.OutputFormat 接口实现类

OutputFormat时MapReduce输出的基类,所有实现MapReduce输出都实现了OutputFormat接口。
默认输出格式是TextOutputFormat

1.自定义OutputFormat

例如想要将输出的数据保存到Mysql/HBase/ES等中间件中。

1.自定义一个类继承FileOutputFormat。
package cn.coreqi.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 lrw = new LogRecordWriter(job);
        return lrw;
    }
}
2.改写RecordWriter,具体改写输出数据的write()方法。
package cn.coreqi.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 java.io.IOException;

public class LogRecordWriter extends RecordWriter<Text, NullWritable> {
    private FSDataOutputStream coreqiOut;
    private FSDataOutputStream otherOut;
    public LogRecordWriter(TaskAttemptContext job) {
        try {
            FileSystem fs = FileSystem.get(job.getConfiguration());
            coreqiOut = fs.create(new Path("D:\\hadoop\\coreqi.log"));
            otherOut = fs.create(new Path("D:\\hadoop\\other.log"));
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public void write(Text key, NullWritable value) throws IOException, InterruptedException {
        String urlString = key.toString();
        if(urlString.contains("coreqi")){
            coreqiOut.writeBytes(urlString + "\n");
        } else{
            otherOut.writeBytes(urlString + "\n");
        }
    }

    @Override
    public void close(TaskAttemptContext context) throws IOException, InterruptedException {
        IOUtils.closeStream(coreqiOut);
        IOUtils.closeStream(otherOut);
    }
}
3.在驱动类Job中配置自定义的输出格式组件
job.setOutputFormatClass(LogOutputFormat.class);

4.ReduceTask工作机制

MapTask并行度由切片个数决定,切片个数由输入文件和切片规则决定。
ReduceTask的并行度同样影响整个Job的执行并发度和执行效率,但与MapTask的并发数由切片数决定不同,ReduceTask数量的决定是可以直接手动设置

1.设置ReduceTask并行度(个数)
        // 默认值是1,手动设置为7
        job.setNumReduceTasks(7);
2.注意事项
  1. ReduceTask=0,表示没有Reduce阶段,输出文件个数和Map个数一致。
  2. ReduceTask默认值就是1,所以输出文件个数为一个。
  3. 如果数据分布不均匀,就有可能在Reduce阶段产生数据倾斜。
  4. ReduceTask数量并不是任意设置,还要考虑业务逻辑需求,有些情况下,需要计算全局汇总结果,就只能有1个ReduceTask。
  5. 具体多少个ReduceTask,需要根据集群性能而定。
  6. 如果分区数不是1,但是ReduceTask为1,是否执行分区过程。答案是:不执行分区过程,因为在MapTask源码中,执行分区的前提是先判断ReduceNum个数是否大于1,不大于1肯定不执行。

5.Join多种应用

1.Reduce Join

1.代码
package cn.coreqi.mapreduce.reduceJoin;

import org.apache.hadoop.io.Writable;

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

public class TableBean implements Writable {
    private String id;  //订单Id
    private String pid; //商品Id
    private int amount; //商品数量
    private String pname;   //商品名称
    private String dataSource;  //标记数据来源 order or pd

    public TableBean() {
    }

    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 getDataSource() {
        return dataSource;
    }

    public void setDataSource(String dataSource) {
        this.dataSource = dataSource;
    }

    @Override
    public void write(DataOutput dataOutput) throws IOException {
        dataOutput.writeUTF(id);
        dataOutput.writeUTF(pid);
        dataOutput.writeInt(amount);
        dataOutput.writeUTF(pname);
        dataOutput.writeUTF(dataSource);
    }

    @Override
    public void readFields(DataInput dataInput) throws IOException {
        this.id = dataInput.readUTF();
        this.pid = dataInput.readUTF();
        this.amount = dataInput.readInt();
        this.pname = dataInput.readUTF();
        this.dataSource = dataInput.readUTF();
    }

    @Override
    public String toString() {
        return id + "\t" + pname + "\t" + amount;
    }
}
package cn.coreqi.mapreduce.reduceJoin;

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;

public class TableMapper extends Mapper<LongWritable, Text,Text,TableBean> {
    private String fileName;
    private Text outK = new Text();
    private TableBean outV = new TableBean();
    @Override
    protected void setup(Mapper<LongWritable, Text, Text, TableBean>.Context context) throws IOException, InterruptedException {
        FileSplit split = (FileSplit) context.getInputSplit();
        fileName = split.getPath().getName();
    }

    @Override
    protected void map(LongWritable key, Text value, Mapper<LongWritable, Text, Text, TableBean>.Context context) throws IOException, InterruptedException {
        // 获取一行
        String line = value.toString();
        String[] split = line.split("\t");
        // 判断数据来源是哪个文件的
        if(fileName.contains("order")){
            outK.set(split[1]);
            outV.setId(split[0]);
            outV.setPid(split[1]);
            outV.setAmount(Integer.parseInt(split[2]));
            outV.setPname("");  //空值也必须要处理,否则会抛出异常
            outV.setDataSource("order");
        }else {
           outK.set(split[0]);
            outV.setId("");
            outV.setPid(split[0]);
            outV.setAmount(0);
            outV.setPname(split[1]);  //空值也必须要处理,否则会抛出异常
            outV.setDataSource("pd");
        }
        context.write(outK,outV);
    }
}
package cn.coreqi.mapreduce.reduceJoin;

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;

public class TableReducer extends Reducer<Text,TableBean,TableBean, NullWritable> {
    @Override
    protected void reduce(Text key, Iterable<TableBean> values, Reducer<Text, TableBean, TableBean, NullWritable>.Context context) throws IOException, InterruptedException {
        ArrayList<TableBean> orderBeans = new ArrayList<>();
        TableBean pdBean = new TableBean();
        for (TableBean value : values) {
            if(value.getDataSource().equals("order")){  //订单表
                TableBean tmpTableBean = new TableBean();
                try {
                    BeanUtils.copyProperties(tmpTableBean,value);
                } catch (IllegalAccessException e) {
                    throw new RuntimeException(e);
                } catch (InvocationTargetException e) {
                    throw new RuntimeException(e);
                }
                orderBeans.add(tmpTableBean);
            } else { //商品表
                try {
                    BeanUtils.copyProperties(pdBean,value);
                } catch (IllegalAccessException e) {
                    throw new RuntimeException(e);
                } catch (InvocationTargetException e) {
                    throw new RuntimeException(e);
                }
            }
        }

        for (TableBean orderBean : orderBeans) {
            orderBean.setPname(pdBean.getPname());
            context.write(orderBean,NullWritable.get());
        }
    }
}
package cn.coreqi.mapreduce.reduceJoin;

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.io.IOException;

public class TableDriver {
    public static void main(String[] args) throws IOException, InterruptedException, ClassNotFoundException {
        // 1.获取job
        Configuration conf = new Configuration();
        Job job = Job.getInstance(conf);

        // 2.设置jar包路径
        job.setJarByClass(TableDriver.class);   //通过反射指定类所在的包地址来获取当前jar包的路径

        // 3.关联mapper和reducer
        job.setMapperClass(TableMapper.class);
        job.setReducerClass(TableReducer.class);

        // 4.设置mapper输出的KV类型[因为泛型擦除的问题,所以需要手动指定类型]
        job.setMapOutputKeyClass(Text.class);
        job.setMapOutputValueClass(TableBean.class);

        // 5. 设置最终输出(reducer)的KV类型
        job.setOutputKeyClass(TableBean.class);
        job.setOutputValueClass(NullWritable.class);

        // 6. 设置输入路径和输出路径(本地模式)
//        FileInputFormat.setInputPaths(job,new Path("D:\\intput"));
//        FileOutputFormat.setOutputPath(job,new Path("D:\\hadoop\\output"));

        // 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);
    }
}
2.总结

这种方式中,合并的操作是在Reduce阶段完成,Reduce端的处理压力太大,Map节点的运算负载则很低,资源利用率不高,且在Reduce阶段极易产生数据倾斜。
解决方案:Map端实现数据合并

2.Map Join

1.使用场景

Map Join适用于一张表十分小、一张表很大的场景

2.优点

在Map端缓存多张表,提前处理业务逻辑,增加Map端业务,减少Reduce端数据的压力,尽可能的减少数据倾斜。

3.实现办法:采用DistributedCache
  1. 在Mapper的setup阶段,将文件读取到缓存集合中
  2. 在Driver驱动类中加载缓存
        // 缓存普通文件到Task运行节点
        job.addCacheFile(new URI("file:///D://cache//pd.txt"));
        // 如果是集群运行,需要设置HDFS路径
        job.addCacheFile(new URI("hdfs://192.168.58.130:8020//cache//pd.txt"));
4.案例
package cn.coreqi.mapreduce.mapjoin;

import org.apache.commons.lang3.StringUtils;
import org.apache.hadoop.fs.FSDataInputStream;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
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.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URI;
import java.util.HashMap;

public class MapJoinMapper extends Mapper<LongWritable, Text,Text, NullWritable> {
    private Text outK = new Text();
    private HashMap<String,String> pdMap = new HashMap<>();
    @Override
    protected void setup(Mapper<LongWritable, Text, Text, NullWritable>.Context context) throws IOException, InterruptedException {
        // 获取缓存的文件,并把文件内容封装到集合
        URI[] cacheFiles = context.getCacheFiles();
        FileSystem fs = FileSystem.get(context.getConfiguration());
        FSDataInputStream fis = fs.open(new Path(cacheFiles[0]));

        //从流中读取数据
        BufferedReader reader = new BufferedReader(new InputStreamReader(fis, "UTF-8"));
        String line;
        while (StringUtils.isNotEmpty(line = reader.readLine())){
            String[] fields = line.split("\t");
            pdMap.put(fields[0],fields[1]);
        }
        IOUtils.closeStream(reader);
    }

    @Override
    protected void map(LongWritable key, Text value, Mapper<LongWritable, Text, Text, NullWritable>.Context context) throws IOException, InterruptedException {
        // 获取一行
        String line = value.toString();
        String[] split = line.split("\t");

        // 根据pid从字典中获取pname
        String pname = pdMap.get(split[1]);

        outK.set(split[0] + "\t" + pname + "\t" + split[2]);

        context.write(outK,NullWritable.get());
    }
}
package cn.coreqi.mapreduce.mapjoin;

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.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;

public class MapJoinDriver {
    public static void main(String[] args) throws IOException, InterruptedException, ClassNotFoundException, URISyntaxException {
        // 1.获取job
        Configuration conf = new Configuration();
        Job job = Job.getInstance(conf);

        // 2.设置jar包路径
        job.setJarByClass(MapJoinDriver.class);   //通过反射指定类所在的包地址来获取当前jar包的路径

        // 3.关联mapper和reducer
        job.setMapperClass(MapJoinMapper.class);

        // 4.设置mapper输出的KV类型[因为泛型擦除的问题,所以需要手动指定类型]
        job.setMapOutputKeyClass(Text.class);
        job.setMapOutputValueClass(NullWritable.class);

        // 5. 设置最终输出(reducer)的KV类型
        job.setOutputKeyClass(Text.class);
        job.setOutputValueClass(NullWritable.class);

        // 加载缓存数据
        job.addCacheFile(new URI("file:///D://cache//pd.txt"));
        // Map端Join的逻辑不需要Reduce阶段,设置reduceTask数量为0
        job.setNumReduceTasks(0);

        // 6. 设置输入路径和输出路径(本地模式)
//        FileInputFormat.setInputPaths(job,new Path("D:\\intput"));
//        FileOutputFormat.setOutputPath(job,new Path("D:\\hadoop\\output"));

        // 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);
    }
}

6.数据清洗(ETL)

ETL,是英文 Extract-Transform-Load 的缩写,用来描述将数据从来源端经过抽取(Extract)、转换(Transform)、加载(Load)至目的端的过程。ETL一词较常用在数据仓库,但其对象并不限于数据仓库。
在运行核心业务MapReduce程序之前,往往要先对数据进行清洗,清理掉不符合用户要求的数据。
清理的过程往往只需要运行Mapper程序,不需要运行Reduce程序。

1.案例

package cn.coreqi.mapreduce.etl;

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 WebLogMapper extends Mapper<LongWritable, Text,Text, NullWritable> {
    @Override
    protected void map(LongWritable key, Text value, Mapper<LongWritable, Text, Text, NullWritable>.Context context) throws IOException, InterruptedException {
        String line = value.toString();
        boolean result = parseLog(line, context);
        if(!result){
            return;
        }
        context.write(value,NullWritable.get());
    }

    private boolean parseLog(String line, Mapper<LongWritable, Text, Text, NullWritable>.Context context) {
        String[] fields = line.split(" ");
        return fields.length > 11;
    }
}
package cn.coreqi.mapreduce.etl;

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.io.IOException;

public class WebLogDriver {
    public static void main(String[] args) throws IOException, InterruptedException, ClassNotFoundException {
        // 1.获取job
        Configuration conf = new Configuration();
        Job job = Job.getInstance(conf);

        // 2.设置jar包路径
        job.setJarByClass(WebLogDriver.class);   //通过反射指定类所在的包地址来获取当前jar包的路径

        // 3.关联mapper
        job.setMapperClass(WebLogMapper.class);

        // 4.设置mapper输出的KV类型[因为泛型擦除的问题,所以需要手动指定类型]
        job.setMapOutputKeyClass(Text.class);
        job.setMapOutputValueClass(NullWritable.class);

        // 5. 设置最终输出(reducer)的KV类型
        job.setOutputKeyClass(Text.class);
        job.setOutputValueClass(NullWritable.class);

        //设置reduceTask个数为0
        job.setNumReduceTasks(0);
        // 6. 设置输入路径和输出路径(本地模式)
//        FileInputFormat.setInputPaths(job,new Path("D:\\intput"));
//        FileOutputFormat.setOutputPath(job,new Path("D:\\hadoop\\output"));

        // 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);
    }
}

作者:奇

出处:https://www.cnblogs.com/fanqisoft/p/17900892.html

版权:本作品采用「本文版权归作者和博客园共有,欢迎转载,但必须给出原文链接,并保留此段声明,否则保留追究法律责任的权利。」许可协议进行许可。

posted @   SpringCore  阅读(30)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!
more_horiz
keyboard_arrow_up light_mode palette
选择主题
点击右上角即可分享
微信分享提示