Hadoop学习-04-MapReduce

概述

MapReduce 是一个分布式运算程序的编程框架,是用户开发“基于Hadoop 的数据分析应用”的核心框架。
MapReduce 核心功能是将用户编写的业务逻辑代码和自带默认组件整合成一个完整的分布式运算程序,并发运行在一个 Hadoop 集群上。

优缺点

  • 易于编程,简单的实现一些接口,就可以完成一个分布式程序
  • 良好的扩展性,通过简单的增加机器来扩展它的计算能力
  • 高容错性,其中一台机器挂了,它可以把上面的计算任务转移到另外一个节点上运行, 不至于这个任务运行失败
  • PB级以上的海量数据的离线处理
  • 不擅长实时计算
  • 不擅长流式计算
  • 不擅长DAG(有向无环图)计算

核心思想

image-20210506153914979

  1. 分布式的计算往往需要分成至少两个阶段
  2. 第一个阶段MapTask并发实例,完全并行运行,互不相干
  3. 第二个阶段的ReduceTack并发实例互不相干,但是他们的数据依赖于上一阶段所有MapTask并发实例的输出
  4. MapReduce编程模型只能包含一个Map阶段和一个Reduce阶段,如果用户的逻辑非常复杂,那就只能多个MapReduce程序串行运行

MapReduce进程

一个MapReduce进程在分布式运行时有三类实例进程:

  • MrAppMaster:负责整个程序的过程调度及状态协调。
  • MapTask:负责 Map 阶段的整个数据处理流程。
  • ReduceTask:负责 Reduce 阶段的整个数据处理流程。

MapReduce编程规范

一个MapReduce程序分为三个部分:Mapper、Reducer、Driver

  • Mapper:

    • 用户自定义的Mapper类要继承自己的父类
    • Mapper的输入数据是K-V形式
    • Mapper的业务逻辑写在map方法中
    • Mapper的输出数据也是K-V
    • map方法对每一个K-V调用一次
    public class WordCountMapper extends Mapper<LongWritable, Text,Text, IntWritable> {
        Text k=new Text();
    
        IntWritable v=new IntWritable(1);
    
        @Override
        protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
            //获取一行
            String line=value.toString();
    
            //分割
            String[] s = line.split(" ");
    
            //输出
            for (String s1 : s) {
                k.set(s1);
                context.write(k,v);
            }
    
      
    
  • Reducer

    • 用户自定义的Reducer要继承自己的父类
    • Reducer的输入数据类型对应Mapper的输出数据类型
    • Reducer的业务逻辑写在reduce方法
    • RaduceTask对每一组相同的K的K-V调用一次reduce方法
    public class WordCountReducer extends Reducer<Text, IntWritable,Text, IntWritable> {
    
        int sum;
        IntWritable v=new IntWritable();
    
        @Override
        protected void reduce(Text key, Iterable<IntWritable> values, Context context) throws IOException, InterruptedException {
            sum=0;
            for (IntWritable value : values) {
                sum+=value.get();
            }
            v.set(sum);
            context.write(key,v);
        }
    }
    
  • Driver

    • 相当于YARN集群的客户端,用于提交整个程序到YARN集群,提交的是封装了MapReduce程序相关运行参数的job对象
    public class WordCountDriver {
        public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
            //获取配置信息和Job对象
            Configuration configuration=new Configuration();
            Job job=Job.getInstance(configuration);
            //设置Driver的jar
            job.setJarByClass(WordCountDriver.class);
            //关联 Mapper和Reducer
            job.setMapperClass(WordCountMapper.class);
            job.setReducerClass(WordCountReducer.class);
            //设置Mapper输出的KV类型
            job.setMapOutputKeyClass(Text.class);
            job.setMapOutputValueClass(IntWritable.class);
            //设置最终输出的KV类型
            job.setOutputKeyClass(Text.class);
            job.setOutputValueClass(IntWritable.class);
            //设置输入和输出路径
            FileInputFormat.setInputPaths(job,new Path(args[0]));
            FileOutputFormat.setOutputPath(job,new Path(args[1]));
            //提交job
            boolean b = job.waitForCompletion(true);
            System.exit(b?0:1);
    
        }
    }
    

    启动:hadoop jar map-reduce-test-1.0-SNAPSHOT.jar com.ty.mapreduce.WordCountDriver /user/atguigu/input /user/atguigu/output

序列化

常用数据序列化类型

image-20210506154513431

  • 序列化与反序列化

    序列化就是把内存中的对象,转换成字节序列(或其他数据传输协议)以便于存储到磁 盘(持久化)和网络传输。
    反序列化就是将收到字节序列(或其他数据传输协议)或者是磁盘的持久化数据,转换 成内存中的对象。

  • 为什么不用java的序列化

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

  • Hadoop序列化的特点

    • 紧凑:高效使用存储空间
    • 快速:读写数据的额外开销小
    • 互操作:支持多语言的交互

Writable序列化接口

在 Hadoop 框架内部 传递一个 bean 对象,那么该对象就需要实现序列化接口。

实现序列化步骤:

  1. 实现Writable接口
  2. 必须有空参构造器
  3. 重写序列化的方法
  4. 重写反序列化的方法
  5. 序列化顺序必须和反序列化顺序一致
  6. 需要把结果显示到文件中,需要重写 toString方法
  7. 如果需要将自定义的bean放在key中传输,必须实现Comparable接口,因为MapReduce中的Shuffle过程要求对key必须能够排序
//1 继承  Writable接口
public class FlowBean implements Writable {
    private long upFlow; //上行流量
    private long downFlow; //下行流量
    private long sumFlow; //总流量

    //2 提供无参构造 public FlowBean() { }
    //3 提供三个参数的  getter和  setter方法
    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;
    }

    //4 实现序列化和反序列化方法,注意顺序一定要保持一致 @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 {
        this.upFlow = dataInput.readLong();
        this.downFlow = dataInput.readLong();
        this.sumFlow = dataInput.readLong();
    }

    //5 重写  ToString @Override
    @Override
    public String toString() {
        return upFlow + "\t" + downFlow + "\t" + sumFlow;
    }
}

核心框架原理

MapReduce工作流程

image-20210507095914381

image-20210513121228335

image-20210513121245178

InputFormat过程

切片与MapTask并行度决定机制
  • 数据块:HDFS的存储数据单位
  • 数据切片:逻辑上对数据进行分片,MapReduce程序计算输入数据的单位,一个切片会对应启动一个MapTask
  • MapTask的并行度决定Map阶段的任务处理并发度,进而影响到整个job的处理速度
  • 一个Job的Map阶段并行度由客户端在提交Job时的切片数决定
  • 每一个切片分配一个MapTask并行实例处理
  • 默认情况下,切片大小=BlockSize
  • 切片时不考虑数据集整体,而是针对每一个文件单独切片
Job提交源码解析

image-20210507101250694

  • Job提交:
//提交job
boolean result = job.waitForCompletion(true);
  • waitForCompletion方法:
public boolean waitForCompletion(boolean verbose) throws IOException, InterruptedException,ClassNotFoundException {
    //判断当期状态是不是DEFINE,是进入submit方法
    if (state == JobState.DEFINE) {
        submit();
    }
    if (verbose) {
        //监控Job状态,打印任务运行信息
        monitorAndPrintJob();
    } else {
        // get the completion poll interval from the client.
        int completionPollIntervalMillis = 
            Job.getCompletionPollInterval(cluster.getConf());
        //监控任务是否运行完成
        while (!isComplete()) {
            try {
                Thread.sleep(completionPollIntervalMillis);
            } catch (InterruptedException ie) {
            }
        }
    }
    //返回任务运行结果
    return isSuccessful();
}
  • submit方法
public void submit() throws IOException, InterruptedException, ClassNotFoundException {
    //再一次确定状态为DEFINE
    ensureState(JobState.DEFINE);
    //设置新旧API兼容
    setUseNewAPI();
    //与集群或本地建立连接
    connect();
    final JobSubmitter submitter = 
        getJobSubmitter(cluster.getFileSystem(), cluster.getClient());
    status = ugi.doAs(new PrivilegedExceptionAction<JobStatus>() {
      public JobStatus run() throws IOException, InterruptedException, 
      ClassNotFoundException {
          //提交Job到服务器
        return submitter.submitJobInternal(Job.this, cluster);
      }
    });
    //修改状态为RUNNING
    state = JobState.RUNNING;
    LOG.info("The url to track the job: " + getTrackingURL());
   }
  • connect方法
private synchronized void connect()
    throws IOException, InterruptedException, ClassNotFoundException {
    if (cluster == null) {
        cluster = 
            ugi.doAs(new PrivilegedExceptionAction<Cluster>() {
                public Cluster run()
                    throws IOException, InterruptedException, 
                ClassNotFoundException {
                    //创建提交Job的代理
                    //如果没有配置集群连接,创建本地连接
                    return new Cluster(getConfiguration());
                }
            });
    }
}
  • submitter.submitJobInternal方法
JobStatus submitJobInternal(Job job, Cluster cluster) 
    throws ClassNotFoundException, InterruptedException, IOException {
    //.... 
    //创建提交给集群的stag路径
    Path jobStagingArea = JobSubmissionFiles.getStagingDir(cluster, conf);
    //...
    //获取JobId
    JobID jobId = submitClient.getNewJobID();
    //...
    //拷贝Jar包到集群
    copyAndConfigureFiles(job, submitJobDir);
    //...
    //计算切片,生成切片文件
    int maps = writeSplits(job, submitJobDir);
    //...
    //XML配置文件提交
    writeConf(conf, submitJobFile);
    // Now, actually submit the job (using the submit name)
    //提交任务,返回提交状态
    printTokens(jobId, job.getCredentials());
    status = submitClient.submitJob(
        jobId, submitJobDir.toString(), job.getCredentials());
    if (status != null) {
        return status;
    } else {
        throw new IOException("Could not launch job");
    }
}
  • copyAndConfigureFiles方法
private void copyAndConfigureFiles(Job job, Path jobSubmitDir) 
    throws IOException {
    Configuration conf = job.getConfiguration();
    boolean useWildcards = conf.getBoolean(Job.USE_WILDCARD_FOR_LIBJARS,
                                           Job.DEFAULT_USE_WILDCARD_FOR_LIBJARS);
    JobResourceUploader rUploader = new JobResourceUploader(jtFs, useWildcards);
	//提交文件
    rUploader.uploadResources(job, jobSubmitDir);
	//获取工作目录
    job.getWorkingDirectory();
}
  • uploadResources方法
public void uploadResources(Job job, Path submitJobDir) throws IOException {
    try {
        initSharedCache(job.getJobID(), job.getConfiguration());
        //提交文件
        uploadResourcesInternal(job, submitJobDir);
    } finally {
        stopSharedCache();
    }
}
  • uploadResourcesInternal方法
private void uploadResourcesInternal(Job job, Path submitJobDir)
    throws IOException {
    //... 配置文件修改
    //判断提交文件夹是否存在
    if (jtFs.exists(submitJobDir)) {
        throw new IOException("Not submitting job. Job directory " + submitJobDir
                              + " already exists!! This is unexpected.Please check what's there in"
                              + " that directory");
    }
    // Create the submission directory for the MapReduce job.
    submitJobDir = jtFs.makeQualified(submitJobDir);
    submitJobDir = new Path(submitJobDir.toUri().getPath());
    FsPermission mapredSysPerms =
        new FsPermission(JobSubmissionFiles.JOB_DIR_PERMISSION);
    //创建文件夹
    mkdirs(jtFs, submitJobDir, mapredSysPerms);
    //...
    String jobJar = job.getJar();
    //...  获取上传文件
	//提交jar包
    //如果是本地运行不会提交jar包
    uploadFiles(job, files, submitJobDir, mapredSysPerms, replication,
                fileSCUploadPolicies, statCache);
    uploadLibJars(job, libjars, submitJobDir, mapredSysPerms, replication,
                  fileSCUploadPolicies, statCache);
    uploadArchives(job, archives, submitJobDir, mapredSysPerms, replication,
                   archiveSCUploadPolicies, statCache);
    uploadJobJar(job, jobJar, submitJobDir, replication, statCache);
    addLog4jToDistributedCache(job, submitJobDir);

    //...
}
  • 上传Jar完成
  • writeSplits方法
private int writeSplits(org.apache.hadoop.mapreduce.JobContext job,
                        Path jobSubmitDir) throws IOException,
InterruptedException, ClassNotFoundException {
    JobConf jConf = (JobConf)job.getConfiguration();
    int maps;
    //判断新旧API
    if (jConf.getUseNewMapper()) {
        maps = writeNewSplits(job, jobSubmitDir);
    } else {
        maps = writeOldSplits(jConf, jobSubmitDir);
    }
    return maps;
}
  • writeNewSplits方法
private <T extends InputSplit>
    int writeNewSplits(JobContext job, Path jobSubmitDir) throws IOException,
InterruptedException, ClassNotFoundException {
    Configuration conf = job.getConfiguration();
    InputFormat<?, ?> input =
        ReflectionUtils.newInstance(job.getInputFormatClass(), conf);
	//获取切片信息
    List<InputSplit> splits = input.getSplits(job);
    T[] array = (T[]) splits.toArray(new InputSplit[splits.size()]);

    // sort the splits into order based on size, so that the biggest
    // go first
    Arrays.sort(array, new SplitComparator());
    //提交切片文件
    JobSplitWriter.createSplitFiles(jobSubmitDir, conf, 
                                    jobSubmitDir.getFileSystem(conf), array);
    return array.length;
}
  • writeConf方法
private void writeConf(Configuration conf, Path jobFile) 
    throws IOException {
    // Write job file to JobTracker's fs        
    FSDataOutputStream out = 
        FileSystem.create(jtFs, jobFile, 
                          new FsPermission(JobSubmissionFiles.JOB_FILE_PERMISSION));
    try {
        //提交XML文件
        conf.writeXml(out);
    } finally {
        out.close();
    }
}

Job提交过程 主要就是将Jar包\切片文件\XML配置文件提交到MapReduce运行的服务器,之后监控Job运行状态,返回运行信息

FileInputFormat切片源码分析

image-20210507101305144

  • 切片机制:

    • 简单的按照文件的内容长度进行切片
    • 切片大小默认为block大小
    • 不考虑数据集整体,而是对每一个文件单独切片
  • input.getSplits(job)方法(writeNewSplits方法中),这里看的是FileInputFormat的getSplits

public List<InputSplit> getSplits(JobContext job) throws IOException {
    StopWatch sw = new StopWatch().start();
    //获取最大最小值
    long minSize = Math.max(getFormatMinSplitSize(), getMinSplitSize(job));
    long maxSize = getMaxSplitSize(job);

    // generate splits
    List<InputSplit> splits = new ArrayList<InputSplit>();
    List<FileStatus> files = listStatus(job);

    boolean ignoreDirs = !getInputDirRecursive(job)
        && job.getConfiguration().getBoolean(INPUT_DIR_NONRECURSIVE_IGNORE_SUBDIRS, false);
    //遍历所有文件,跳过文件夹和忽略文件,对每一个文件进行切片
    for (FileStatus file: files) {
        if (ignoreDirs && file.isDirectory()) {
            continue;
        }
        Path path = file.getPath();
        long length = file.getLen();
        //切片信息
        if (length != 0) {
            BlockLocation[] blkLocations;
            if (file instanceof LocatedFileStatus) {
                blkLocations = ((LocatedFileStatus) file).getBlockLocations();
            } else {
                FileSystem fs = path.getFileSystem(job.getConfiguration());
                blkLocations = fs.getFileBlockLocations(file, 0, length);
            }
            if (isSplitable(job, path)) {
                long blockSize = file.getBlockSize();
                //计算切片大小
                long splitSize = computeSplitSize(blockSize, minSize, maxSize);

                long bytesRemaining = length;
                while (((double) bytesRemaining)/splitSize > SPLIT_SLOP) {
                    int blkIndex = getBlockIndex(blkLocations, length-bytesRemaining);
                    splits.add(makeSplit(path, length-bytesRemaining, splitSize,
                                         blkLocations[blkIndex].getHosts(),
                                         blkLocations[blkIndex].getCachedHosts()));
                    bytesRemaining -= splitSize;
                }

                if (bytesRemaining != 0) {
                    int blkIndex = getBlockIndex(blkLocations, length-bytesRemaining);
                    splits.add(makeSplit(path, length-bytesRemaining, bytesRemaining,
                                         blkLocations[blkIndex].getHosts(),
                                         blkLocations[blkIndex].getCachedHosts()));
                }
            } else { // not splitable
                if (LOG.isDebugEnabled()) {
                    // Log only if the file is big enough to be splitted
                    if (length > Math.min(file.getBlockSize(), minSize)) {
                        LOG.debug("File is not splittable so no parallelization "
                                  + "is possible: " + file.getPath());
                    }
                }
                splits.add(makeSplit(path, 0, length, blkLocations[0].getHosts(),
                                     blkLocations[0].getCachedHosts()));
            }
        } else { 
            //Create empty hosts array for zero length files
            splits.add(makeSplit(path, 0, length, new String[0]));
        }
    }
    // Save the number of input files for metrics/loadgen
    job.getConfiguration().setLong(NUM_INPUT_FILES, files.size());
    sw.stop();
    if (LOG.isDebugEnabled()) {
        LOG.debug("Total # of splits generated by getSplits: " + splits.size()
                  + ", TimeTaken: " + sw.now(TimeUnit.MILLISECONDS));
    }
    return splits;
}
  • computeSplitSize方法
protected long computeSplitSize(long blockSize, long minSize,
                                long maxSize) {
    //minSize=1
    //maxSize=Long.MAX_VALUE
    //如果想要提高切片大小,提高minSize
    //想要降低切片大小,减少maxSize
    return Math.max(minSize, Math.min(maxSize, blockSize));
}
TextInputFormat

FileInputFormat实现类,FileInputFormat常见的实现类:TextInputFormat、KeyValueTextInputFormat、 NLineInputFormat、CombineTextInputFormat 和自定义 InputFormat 等。

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

CombineTextInputFormat

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

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

使用CombineTextInputFormat需要设置虚拟存储切片最大值: setMaxInputSplitSize

切片机制

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

  • 虚拟存储过程:
    • 将输入目录下所有文件大小,依次和设置的 setMaxInputSplitSize 值比较,如果不 大于设置的最大值,逻辑上划分一个块。如果输入文件大于设置的最大值且大于两倍, 那么以最大值切割一块;当剩余数据大小超过设置的最大值且不大于最大值 2 倍,此时 将文件均分成 2 个虚拟存储块(防止出现太小切片)。
      例如 setMaxInputSplitSize 值为 4M,输入文件大小为 8.02M,则先逻辑上分成一个 4M。剩余的大小为 4.02M,如果按照 4M 逻辑划分,就会出现 0.02M 的小的虚拟存储 文件,所以将剩余的 4.02M 文件切分成(2.01M 和 2.01M)两个文件。
  • 切片过程:
    • 判断虚拟存储的文件大小是否大于 setMaxInputSplitSize 值,大于等于则单独形成一个切片。
    • 如果不大于则跟下一个虚拟存储文件进行合并,共同形成一个切片。
    • 测试举例:有 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

Shuffle过程

(1)MapTask 收集我们的 map()方法输出的 kv 对,放到内存缓冲区中
(2)从内存缓冲区不断溢出本地磁盘文件,可能会溢出多个文件
(3)多个溢出文件会被合并成大的溢出文件
(4)在溢出过程及合并的过程中,都要调用 Partitioner 进行分区和针对 key 进行排序
(5)ReduceTask 根据自己的分区号,去各个 MapTask 机器上取相应的结果分区数据
(6)ReduceTask 会抓取到同一个分区的来自不同 MapTask 的结果文件,ReduceTask 会将这些文件再进行合并(归并排序)
(7)合并成大文件后,Shuffle 的过程也就结束了,后面进入 ReduceTask 的逻辑运算过 程(从文件中取出一个一个的键值对 Group,调用用户自定义的 reduce()方法)

注意:
(1)Shuffle 中的缓冲区大小会影响到 MapReduce 程序的执行效率,原则上说,缓冲区
越大,磁盘 io 的次数越少,执行速度就越快。
(2)缓冲区的大小可以通过参数调整,参数:mapreduce.task.io.sort.mb 默认 100M。

工作机制

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

image-20210513121444329

分区

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

默认Partitioner分区:

image-20210513121635267

自定义分区

  1. 需要继承Partitioner,重写getPartition方法

    public class ProvincePartitioner extends Partitioner<Text, FlowBean> {
        @Override
        public int getPartition(Text text, FlowBean flowBean, int numPartitions) {
    
            int partition ;
    
            return partition;
        }
    }
    
  2. job中定义Partitioner’

    job.setPartitionerClass(ProvincePartitioner.class);
    
  3. 设置ReduceTask,数值与分区数量相同

    job.setNumReduceTasks(6);
    

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

排序

MapTask 和 ReduceTask 均会对数据按照key 进行排序。 该操作属于Hadoop的默认行为。任何应用程序中的数据均会被排序,而不管逻辑上是否需要。

默认排序是按照字典顺序排序,且实现该排序的方法是快速排序。

对于MapTask,它会将处理的结果暂时放到环形缓冲区中,当环形缓冲区使用率达到一定阈值后(80%),再对缓冲区中的数据进行一次快速排序,并将这些有序数据溢写到磁盘上,而当数据处理完毕后,它会对磁盘上所有文件进行归并排序。

对于ReduceTask,它从每个MapTask上远程拷贝相应的数据文件,如果文件大小超过一定阈值,则溢写磁盘上,否则存储在内存中。如果磁盘上文件数目达到一定阈值,则进行一次归并排序以生成一个更大文件;如果内存中文件大小或者 数目超过一定阈值,则进行一次合并后将数据溢写到磁盘上。当所有数据拷贝完毕后,ReduceTask统一对内存和磁盘上的所有数据进行一次归并排序。

排序分类

  • 部分排序: 根据输入记录的键对数据集进行排序, 保证输出每个文件内部有序
  • 全排序: 最终输出只有一个文件,且文件内部有序, 实现方式是只设置一个ReduceTask, 在处理大文件时效率极低
  • 辅助排序: 在Reduce端进行分组, 希望一个或几个字段相同的key进入同一个reduce方法是
  • 二次排序: 在compareTo方法中判断条件为两个时即为二次排序

自定义排序只要将之前的实现Writable接口改为WritableComparable接口,并实现compareTo方法即可

合并

(1)Combiner是MR程序中Mapper和Reducer之外的一种组件。
(2)Combiner组件的父类就是Reducer。
(3)Combiner和Reducer的区别在于运行的位置
Combiner是在每一个MapTask所在的节点运行; Reducer是接收全局所有Mapper的输出结果;
(4)Combiner的意义就是对每一个MapTask的输出进行局部汇总,以减小网络传输量。
(5)Combiner能够应用的前提是不能影响最终的业务逻辑,而且,Combiner的输出kv 应该跟Reducer的输入kv类型要对应起来。

自定义的Conbiner就是继承Reducer,实现reduce方法

之后再job中设置即可

job.setCombinerClass(WordCountCombiner.class);

OutputFormat过程

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

image-20210513140111635

默认输出格式TextOutputFormat

自定义OutputFormat
应用场景:例如:输出数据到MySQL/HBase/Elasticsearch等存储框架中。

自定义OutputFormat步骤

  • 继承FileOutputFormat,重写getRecordWriter方法,
  • 继承RecordWriter,改写输出数据的方法write()。

注意:

//虽 然 我 们 自 定 义 了     outputformat, 但 是 因 为 我 们 的     outputformat 继承自 fileoutputformat
//而  fileoutputformat要输出一个_SUCCESS文件,所以在这还得指定一个输出目录
FileOutputFormat.setOutputPath(job, new Path("D:\\logoutput"));

MapTask工作机制

  • Read阶段

    • MapTask 通过 InputFormat 获得的 RecordReader,从输入 InputSplit 中解析出一个个 key/value。
  • Map阶段

    • 该节点主要是将解析出的 key/value 交给用户编写map()函数处理,并产生一系列新的 key/value。
  • Collect阶段

    • 在用户编写 map()函数中,当数据处理完成后,一般会调用 OutputCollector.collect()输出结果。 在该函 数内部, 它会将生成的 key/value 分区( 调用 Partitioner),并写入一个环形内存缓冲区中。
  • Spill阶段

    • 即“溢写”,当环形缓冲区满后,MapReduce 会将数据写到本地磁盘上,生成一个临时文件。需要注意的是,将数据写入本地磁盘之前,先要对数据进行一次本地排序,并在必要时对数据进行合并、压缩等操作。

    溢写阶段详情:
    步骤 1:利用快速排序算法对缓存区内的数据进行排序,排序方式是,先按照分区编号 Partition 进行排序,然后按照 key 进行排序。这样,经过排序后,数据以分区为单位聚集在 一起,且同一分区内所有数据按照 key 有序。
    步骤 2:按照分区编号由小到大依次将每个分区中的数据写入任务工作目录下的临时文 件 output/spillN.out(N 表示当前溢写次数)中。如果用户设置了 Combiner,则写入文件之 前,对每个分区中的数据进行一次聚集操作。
    步骤 3:将分区数据的元信息写到内存索引数据结构 SpillRecord 中,其中每个分区的元 信息包括在临时文件中的偏移量、压缩前数据大小和压缩后数据大小。如果当前内存索引大 小超过 1MB,则将内存索引写到文件 output/spillN.out.index 中。

  • Merge阶段

    • 当所有数据处理完成后,MapTask 对所有临时文件进行一次合并, 以确保最终只会生成一个数据文件。
    • 当所有数据处理完后,MapTask 会将所有临时文件合并成一个大文件,并保存到文件 output/file.out 中,同时生成相应的索引文件 output/file.out.index。
    • 在进行文件合并过程中,MapTask 以分区为单位进行合并。对于某个分区,它将采用多 轮递归合并的方式。每轮合并 mapreduce.task.io.sort.factor(默认 10)个文件,并将产生的文 件重新加入待合并列表中,对文件排序后,重复以上过程,直到最终得到一个大文件。
    • 让每个MapTask 最终只生成一个数据文件,可避免同时打开大量文件和同时读取大量小文件产生的随机读取带来的开销。

源码调试:

MapTask进入路径:

MapTask运行方法: runNewMapper

private <INKEY,INVALUE,OUTKEY,OUTVALUE>
    void runNewMapper(final JobConf job,
                      final TaskSplitIndex splitIndex,
                      final TaskUmbilicalProtocol umbilical,
                      TaskReporter reporter
                     ) throws IOException, ClassNotFoundException,
InterruptedException {
    //...

    // get an output object
    if (job.getNumReduceTasks() == 0) {
        output = 
            new NewDirectOutputCollector(taskContext, job, umbilical, reporter);
    } else {
        output = new NewOutputCollector(taskContext, job, umbilical, reporter);
    }

    org.apache.hadoop.mapreduce.MapContext<INKEY, INVALUE, OUTKEY, OUTVALUE> 
        mapContext = 
        new MapContextImpl<INKEY, INVALUE, OUTKEY, OUTVALUE>(job, getTaskID(), 
                                                             input, output, 
                                                             committer, 
                                                             reporter, split);

    org.apache.hadoop.mapreduce.Mapper<INKEY,INVALUE,OUTKEY,OUTVALUE>.Context 
        mapperContext = 
        new WrappedMapper<INKEY, INVALUE, OUTKEY, OUTVALUE>().getMapContext(
        mapContext);

    try {
        //输入初始化
        input.initialize(split, mapperContext);
        //调用Mapper接口的run方法读取kv到内存
        mapper.run(mapperContext);
        //
        mapPhase.complete();
        setPhase(TaskStatus.Phase.SORT);
        statusUpdate(umbilical);
        //关闭输入
        input.close();
        input = null;
        //关闭输出
        output.close(mapperContext);
        output = null;
    } finally {
        closeQuietly(input);
        closeQuietly(output, mapperContext);
    }
}

自定义Mapper的context.write(outK, outV);-> WrapperMapper的write方法->TaskInputOutputContextImpl的write方法->NewOutputCollector的 write方法

建议Debug进入

@Override
public void write(K key, V value) throws IOException, InterruptedException {
    collector.collect(key, value,
                      //获取 分区
                      partitioner.getPartition(key, value, partitions));
}
  • collect方法:
public synchronized void collect(K key, V value, final int partition
                                ) throws IOException {
    reporter.progress();
    if (key.getClass() != keyClass) {
        throw new IOException("Type mismatch in key from map: expected "
                              + keyClass.getName() + ", received "
                              + key.getClass().getName());
    }
    if (value.getClass() != valClass) {
        throw new IOException("Type mismatch in value from map: expected "
                              + valClass.getName() + ", received "
                              + value.getClass().getName());
    }
    if (partition < 0 || partition >= partitions) {
        throw new IOException("Illegal partition for " + key + " (" +
                              partition + ")");
    }
    checkSpillException();
    bufferRemaining -= METASIZE;
    if (bufferRemaining <= 0) {
        // start spill if the thread is not running and the soft limit has been
        // reached
        spillLock.lock();
        try {
            do {
                if (!spillInProgress) {
                    final int kvbidx = 4 * kvindex;
                    final int kvbend = 4 * kvend;
                    // serialized, unspilled bytes always lie between kvindex and
                    // bufindex, crossing the equator. Note that any void space
                    // created by a reset must be included in "used" bytes
                    final int bUsed = distanceTo(kvbidx, bufindex);
                    final boolean bufsoftlimit = bUsed >= softLimit;
                    if ((kvbend + METASIZE) % kvbuffer.length !=
                        equator - (equator % METASIZE)) {
                        // spill finished, reclaim space
                        resetSpill();
                        bufferRemaining = Math.min(
                            distanceTo(bufindex, kvbidx) - 2 * METASIZE,
                            softLimit - bUsed) - METASIZE;
                        continue;
                    } else if (bufsoftlimit && kvindex != kvend) {
                        // spill records, if any collected; check latter, as it may
                        // be possible for metadata alignment to hit spill pcnt
                        startSpill();
                        final int avgRec = (int)
                            (mapOutputByteCounter.getCounter() /
                             mapOutputRecordCounter.getCounter());
                        // leave at least half the split buffer for serialization data
                        // ensure that kvindex >= bufindex
                        final int distkvi = distanceTo(bufindex, kvbidx);
                        final int newPos = (bufindex +
                                            Math.max(2 * METASIZE - 1,
                                                     Math.min(distkvi / 2,
                                                              distkvi / (METASIZE + avgRec) * METASIZE)))
                            % kvbuffer.length;
                        setEquator(newPos);
                        bufmark = bufindex = newPos;
                        final int serBound = 4 * kvend;
                        // bytes remaining before the lock must be held and limits
                        // checked is the minimum of three arcs: the metadata space, the
                        // serialization space, and the soft limit
                        bufferRemaining = Math.min(
                            // metadata max
                            distanceTo(bufend, newPos),
                            Math.min(
                                // serialization max
                                distanceTo(newPos, serBound),
                                // soft limit
                                softLimit)) - 2 * METASIZE;
                    }
                }
            } while (false);
        } finally {
            spillLock.unlock();
        }
    }

    try {
        // serialize key bytes into buffer
        //序列化KV
        int keystart = bufindex;
        keySerializer.serialize(key);
        if (bufindex < keystart) {
            // wrapped the key; must make contiguous
            bb.shiftBufferedKey();
            keystart = 0;
        }
        // serialize value bytes into buffer
        final int valstart = bufindex;
        valSerializer.serialize(value);
        bb.write(b0, 0, 0);
        int valend = bb.markRecord();

        mapOutputRecordCounter.increment(1);
        mapOutputByteCounter.increment(
            distanceTo(keystart, valend, bufvoid));
        // write accounting info
        kvmeta.put(kvindex + PARTITION, partition);
        kvmeta.put(kvindex + KEYSTART, keystart);
        kvmeta.put(kvindex + VALSTART, valstart);
        kvmeta.put(kvindex + VALLEN, distanceTo(valstart, valend));
        // advance kvindex
        kvindex = (kvindex - NMETA + kvmeta.capacity()) % kvmeta.capacity();
    } catch (MapBufferTooSmallException e) {
        LOG.info("Record too large for in-memory buffer: " + e.getMessage());
        spillSingleRecord(key, value, partition);
        mapOutputRecordCounter.increment(1);
        return;
    }
}
  • output关闭方法:
@Override
public void close(TaskAttemptContext context
                 ) throws IOException,InterruptedException {
    try {
        //溢写刷出
        collector.flush();
    } catch (ClassNotFoundException cnf) {
        throw new IOException("can't find class ", cnf);
    }
    //关闭收集器
    collector.close();
}
  • collector.flush();
public void flush() throws IOException, ClassNotFoundException,
InterruptedException {
    LOG.info("Starting flush of map output");
    if (kvbuffer == null) {
        LOG.info("kvbuffer is null. Skipping flush.");
        return;
    }
    spillLock.lock();
    try {
        while (spillInProgress) {
            reporter.progress();
            spillDone.await();
        }
        checkSpillException();

        final int kvbend = 4 * kvend;
        if ((kvbend + METASIZE) % kvbuffer.length !=
            equator - (equator % METASIZE)) {
            // spill finished
            resetSpill();
        }
        if (kvindex != kvend) {
            kvend = (kvindex + NMETA) % kvmeta.capacity();
            bufend = bufmark;
            LOG.info("Spilling map output");
            LOG.info("bufstart = " + bufstart + "; bufend = " + bufmark +
                     "; bufvoid = " + bufvoid);
            LOG.info("kvstart = " + kvstart + "(" + (kvstart * 4) +
                     "); kvend = " + kvend + "(" + (kvend * 4) +
                     "); length = " + (distanceTo(kvend, kvstart,
                                                  kvmeta.capacity()) + 1) + "/" + maxRec);
            //排序并溢写
            sortAndSpill();
        }
    } catch (InterruptedException e) {
        throw new IOException("Interrupted while waiting for the writer", e);
    } finally {
        spillLock.unlock();
    }
    assert !spillLock.isHeldByCurrentThread();
    // shut down spill thread and wait for it to exit. Since the preceding
    // ensures that it is finished with its work (and sortAndSpill did not
    // throw), we elect to use an interrupt instead of setting a flag.
    // Spilling simultaneously from this thread while the spill thread
    // finishes its work might be both a useful way to extend this and also
    // sufficient motivation for the latter approach.
    try {
        spillThread.interrupt();
        spillThread.join();
    } catch (InterruptedException e) {
        throw new IOException("Spill failed", e);
    }
    // release sort buffer before the merge
    kvbuffer = null;
    //合并文件
    mergeParts();
    Path outputPath = mapOutputFile.getOutputFile();
    fileOutputByteCounter.increment(rfs.getFileStatus(outputPath).getLen());
    // If necessary, make outputs permissive enough for shuffling.
    if (!SHUFFLE_OUTPUT_PERM.equals(
        SHUFFLE_OUTPUT_PERM.applyUMask(FsPermission.getUMask(job)))) {
        Path indexPath = mapOutputFile.getOutputIndexFile();
        rfs.setPermission(outputPath, SHUFFLE_OUTPUT_PERM);
        rfs.setPermission(indexPath, SHUFFLE_OUTPUT_PERM);
    }
}
  • sortAndSpill
private void sortAndSpill() throws IOException, ClassNotFoundException,
InterruptedException {
    //approximate the length of the output file to be the length of the
    //buffer + header lengths for the partitions
    final long size = distanceTo(bufstart, bufend, bufvoid) +
        partitions * APPROX_HEADER_LENGTH;
    FSDataOutputStream out = null;
    FSDataOutputStream partitionOut = null;
    try {
        // create spill file
        final SpillRecord spillRec = new SpillRecord(partitions);
        final Path filename =
            mapOutputFile.getSpillFileForWrite(numSpills, size);
        out = rfs.create(filename);

        final int mstart = kvend / NMETA;
        final int mend = 1 + // kvend is a valid record
            (kvstart >= kvend
             ? kvstart
             : kvmeta.capacity() + kvstart) / NMETA;
        //快排, 溢写排序方法
        sorter.sort(MapOutputBuffer.this, mstart, mend, reporter);
        int spindex = mstart;
        final IndexRecord rec = new IndexRecord();
        final InMemValBytes value = new InMemValBytes();
        for (int i = 0; i < partitions; ++i) {
            IFile.Writer<K, V> writer = null;
            try {
                long segmentStart = out.getPos();
                partitionOut = CryptoUtils.wrapIfNecessary(job, out, false);
                writer = new Writer<K, V>(job, partitionOut, keyClass, valClass, codec,
                                          spilledRecordsCounter);
                if (combinerRunner == null) {
                    // spill directly
                    DataInputBuffer key = new DataInputBuffer();
                    while (spindex < mend &&
                           kvmeta.get(offsetFor(spindex % maxRec) + PARTITION) == i) {
                        final int kvoff = offsetFor(spindex % maxRec);
                        int keystart = kvmeta.get(kvoff + KEYSTART);
                        int valstart = kvmeta.get(kvoff + VALSTART);
                        key.reset(kvbuffer, keystart, valstart - keystart);
                        getVBytesForOffset(kvoff, value);
                        writer.append(key, value);
                        ++spindex;
                    }
                } else {
                    int spstart = spindex;
                    while (spindex < mend &&
                           kvmeta.get(offsetFor(spindex % maxRec)
                                      + PARTITION) == i) {
                        ++spindex;
                    }
                    // Note: we would like to avoid the combiner if we've fewer
                    // than some threshold of records for a partition
                    if (spstart != spindex) {
                        combineCollector.setWriter(writer);
                        RawKeyValueIterator kvIter =
                            new MRResultIterator(spstart, spindex);
                        combinerRunner.combine(kvIter, combineCollector);
                    }
                }

                // close the writer
                writer.close();
                if (partitionOut != out) {
                    partitionOut.close();
                    partitionOut = null;
                }

                // record offsets
                rec.startOffset = segmentStart;
                rec.rawLength = writer.getRawLength() + CryptoUtils.cryptoPadding(job);
                rec.partLength = writer.getCompressedLength() + CryptoUtils.cryptoPadding(job);
                spillRec.putIndex(rec, i);

                writer = null;
            } finally {
                if (null != writer) writer.close();
            }
        }

        if (totalIndexCacheMemory >= indexCacheMemoryLimit) {
            // create spill index file
            Path indexFilename =
                mapOutputFile.getSpillIndexFileForWrite(numSpills, partitions
                                                        * MAP_OUTPUT_INDEX_RECORD_LENGTH);
            spillRec.writeToFile(indexFilename, job);
        } else {
            indexCacheList.add(spillRec);
            totalIndexCacheMemory +=
                spillRec.size() * MAP_OUTPUT_INDEX_RECORD_LENGTH;
        }
        LOG.info("Finished spill " + numSpills);
        ++numSpills;
    } finally {
        if (out != null) out.close();
        if (partitionOut != null) {
            partitionOut.close();
        }
    }
}

ReduceTask工作机制

  • Copy阶段
    • ReduceTask 从各个 MapTask 上远程拷贝一片数据,并针对某一片数据,如果其大小超过一定阈值,则写到磁盘上,否则直接放到内存中。
  • Sort阶段
    • 在远程拷贝数据的同时,ReduceTask 启动了两个后台线程对内存和磁 盘上的文件进行合并,以防止内存使用过多或磁盘上文件过多。按照 MapReduce 语义,用 户编写 reduce()函数输入数据是按 key 进行聚集的一组数据。为了将 key 相同的数据聚在一 起,Hadoop 采用了基于排序的策略。由于各个 MapTask 已经实现对自己的处理结果进行了 局部排序,因此,ReduceTask 只需对所有数据进行一次归并排序即可。
  • Reduce阶段
    • reduce()函数将计算结果写到 HDFS 上。
并行度决定机制

ReduceTask 并行度由谁决定?

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

// 默认值是  1,手动设置为  4 
job.setNumReduceTasks(4);

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

源码解析

  • ReduceTask324行run方法
public void run(JobConf job, final TaskUmbilicalProtocol umbilical)
    throws IOException, InterruptedException, ClassNotFoundException {
    job.setBoolean(JobContext.SKIP_RECORDS, isSkipping());
	
    if (isMapOrReduce()) {
        copyPhase = getProgress().addPhase("copy");
        sortPhase  = getProgress().addPhase("sort");
        reducePhase = getProgress().addPhase("reduce");
    }
    // start thread that will handle communication with parent
    TaskReporter reporter = startReporter(umbilical);

    boolean useNewApi = job.getUseNewReducer();
    initialize(job, getJobID(), reporter, useNewApi);

    // check if it is a cleanupJobTask
    if (jobCleanup) {
        runJobCleanupTask(umbilical, reporter);
        return;
    }
    if (jobSetup) {
        runJobSetupTask(umbilical, reporter);
        return;
    }
    if (taskCleanup) {
        runTaskCleanupTask(umbilical, reporter);
        return;
    }

    // Initialize the codec
    codec = initCodec();
    RawKeyValueIterator rIter = null;
    ShuffleConsumerPlugin shuffleConsumerPlugin = null;

    Class combinerClass = conf.getCombinerClass();
    CombineOutputCollector combineCollector = 
        (null != combinerClass) ? 
        new CombineOutputCollector(reduceCombineOutputCounter, reporter, conf) : null;

    Class<? extends ShuffleConsumerPlugin> clazz =
        job.getClass(MRConfig.SHUFFLE_CONSUMER_PLUGIN, Shuffle.class, ShuffleConsumerPlugin.class);

    shuffleConsumerPlugin = ReflectionUtils.newInstance(clazz, job);
    LOG.info("Using ShuffleConsumerPlugin: " + shuffleConsumerPlugin);

    ShuffleConsumerPlugin.Context shuffleContext = 
        new ShuffleConsumerPlugin.Context(getTaskID(), job, FileSystem.getLocal(job), umbilical, 
                                          super.lDirAlloc, reporter, codec, 
                                          combinerClass, combineCollector, 
                                          spilledRecordsCounter, reduceCombineInputCounter,
                                          shuffledMapsCounter,
                                          reduceShuffleBytes, failedShuffleCounter,
                                          mergedMapOutputsCounter,
                                          taskStatus, copyPhase, sortPhase, this,
                                          mapOutputFile, localMapFiles);
    //shuffle初始化
    shuffleConsumerPlugin.init(shuffleContext);

    rIter = shuffleConsumerPlugin.run();

    // free up the data structures
    mapOutputFilesOnDisk.clear();
	//排序阶段完成,进入reduce阶段
    sortPhase.complete();                         // sort is complete
    setPhase(TaskStatus.Phase.REDUCE); 
    statusUpdate(umbilical);
    Class keyClass = job.getMapOutputKeyClass();
    Class valueClass = job.getMapOutputValueClass();
    RawComparator comparator = job.getOutputValueGroupingComparator();

    if (useNewApi) {
        //reduce阶段
        runNewReducer(job, umbilical, reporter, rIter, comparator, 
                      keyClass, valueClass);
    } else {
        runOldReducer(job, umbilical, reporter, rIter, comparator, 
                      keyClass, valueClass);
    }

    shuffleConsumerPlugin.close();
    done(umbilical, reporter);
}
  • init方法
@Override
public void init(ShuffleConsumerPlugin.Context context) {
    this.context = context;

    this.reduceId = context.getReduceId();
    this.jobConf = context.getJobConf();
    this.umbilical = context.getUmbilical();
    this.reporter = context.getReporter();
    this.metrics = ShuffleClientMetrics.create();
    this.copyPhase = context.getCopyPhase();
    this.taskStatus = context.getStatus();
    this.reduceTask = context.getReduceTask();
    this.localMapFiles = context.getLocalMapFiles();
	//ShuffleSchedulerImpl构造器中会设置MapTask个数
    scheduler = new ShuffleSchedulerImpl<K, V>(jobConf, taskStatus, reduceId,
                                               this, copyPhase, context.getShuffledMapsCounter(),
                                               context.getReduceShuffleBytes(), context.getFailedShuffleCounter());
    //合并方法
    merger = createMergeManager(context);
}
  • createMergeManager
protected MergeManager<K, V> createMergeManager(
      ShuffleConsumerPlugin.Context context) {
    return new MergeManagerImpl<K, V>(reduceId, jobConf, context.getLocalFS(),
        context.getLocalDirAllocator(), reporter, context.getCodec(),
        context.getCombinerClass(), context.getCombineCollector(), 
        context.getSpilledRecordsCounter(),
        context.getReduceCombineInputCounter(),
        context.getMergedMapOutputsCounter(), this, context.getMergePhase(),
        context.getMapOutputFile());
  }
  • MergeManagerImpl
public MergeManagerImpl(TaskAttemptID reduceId, JobConf jobConf, 
                        FileSystem localFS,
                        LocalDirAllocator localDirAllocator,  
                        Reporter reporter,
                        CompressionCodec codec,
                        Class<? extends Reducer> combinerClass,
                        CombineOutputCollector<K,V> combineCollector,
                        Counters.Counter spilledRecordsCounter,
                        Counters.Counter reduceCombineInputCounter,
                        Counters.Counter mergedMapOutputsCounter,
                        ExceptionReporter exceptionReporter,
                        Progress mergePhase, MapOutputFile mapOutputFile) {
   	//内存合并
    this.inMemoryMerger = createInMemoryMerger();
    this.inMemoryMerger.start();
	//磁盘合并
    this.onDiskMerger = new OnDiskMerger(this);
    this.onDiskMerger.start();

    this.mergePhase = mergePhase;
}
  • shuffleConsumerPlugin.run();
@Override
public RawKeyValueIterator run() throws IOException, InterruptedException {
    // Scale the maximum events we fetch per RPC call to mitigate OOM issues
    // on the ApplicationMaster when a thundering herd of reducers fetch events
    // TODO: This should not be necessary after HADOOP-8942
    int eventsPerReducer = Math.max(MIN_EVENTS_TO_FETCH,
                                    MAX_RPC_OUTSTANDING_EVENTS / jobConf.getNumReduceTasks());
    int maxEventsToFetch = Math.min(MAX_EVENTS_TO_FETCH, eventsPerReducer);

    // Start the map-completion events fetcher thread
    final EventFetcher<K,V> eventFetcher = 
        new EventFetcher<K,V>(reduceId, umbilical, scheduler, this,
                              maxEventsToFetch);
    //开始抓取数据
    eventFetcher.start();

    // Start the map-output fetcher threads
    boolean isLocal = localMapFiles != null;
    final int numFetchers = isLocal ? 1 :
    jobConf.getInt(MRJobConfig.SHUFFLE_PARALLEL_COPIES, 5);
    Fetcher<K,V>[] fetchers = new Fetcher[numFetchers];
    if (isLocal) {
        fetchers[0] = new LocalFetcher<K, V>(jobConf, reduceId, scheduler,
                                             merger, reporter, metrics, this, reduceTask.getShuffleSecret(),
                                             localMapFiles);
        fetchers[0].start();
    } else {
        for (int i=0; i < numFetchers; ++i) {
            fetchers[i] = new Fetcher<K,V>(jobConf, reduceId, scheduler, merger, 
                                           reporter, metrics, this, 
                                           reduceTask.getShuffleSecret());
            fetchers[i].start();
        }
    }

    // Wait for shuffle to complete successfully
    while (!scheduler.waitUntilDone(PROGRESS_FREQUENCY)) {
        reporter.progress();

        synchronized (this) {
            if (throwable != null) {
                throw new ShuffleError("error in shuffle in " + throwingThreadName,
                                       throwable);
            }
        }
    }

    // Stop the event-fetcher thread
    //抓取结束
    eventFetcher.shutDown();

    // Stop the map-output fetcher threads
    for (Fetcher<K,V> fetcher : fetchers) {
        fetcher.shutDown();
    }

    // stop the scheduler
    scheduler.close();
	//copy阶段完成
    copyPhase.complete(); // copy is already complete
    //开始排序阶段
    taskStatus.setPhase(TaskStatus.Phase.SORT);
    reduceTask.statusUpdate(umbilical);

    // Finish the on-going merges...
    RawKeyValueIterator kvIter = null;
    try {
        kvIter = merger.close();
    } catch (Throwable e) {
        throw new ShuffleError("Error while doing final merge " , e);
    }

    // Sanity check
    synchronized (this) {
        if (throwable != null) {
            throw new ShuffleError("error in shuffle in " + throwingThreadName,
                                   throwable);
        }
    }

    return kvIter;
}

Join

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

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

(1)在 Mapper 的 setup 阶段,将文件读取到缓存集合中。
(2)在 Driver 驱动类中加载缓存。

//缓存普通文件到  Task运行节点。
job.addCacheFile(new URI("file:///e:/cache/pd.txt")); 
//如果是集群运行,需要设置 HDFS路径
job.addCacheFile(new URI("hdfs://hadoop102:8020/cache/pd.txt"));

数据清洗(ETL)

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

压缩

  • 压缩的好处和坏处

    • 压缩的优点:以减少磁盘 IO、减少磁盘存储空间。
    • 压缩的缺点:增加 CPU 开销
  • 运算密集型的 Job,少用压缩

  • IO 密集型的 Job,多用压缩

压缩算法对比介绍

压缩格式Hadoop自带算法文件扩展名是否可以切片换成压缩格式后原程序是否需要修改
DEFLATEDEFAULE.default不需要
GzipDEFAULT.gz不需要
bzip2bzip2.bz2不需要
LZO否,需要安装LZO.lzo需要建索引,指定输入格式
SnappySnappy.snappy不需要

压缩性能比较

压缩算法原始文件大小压缩文件大小压缩速度解压速度
gzip8.3G1.8G17.5MB/s58MB/s
bzip28.3G1.1G2.4MB/s9.5MB/s
LZO8.3G2.9G49.3MB/s74.6MB/s

压缩方式选择时重点考虑:压缩/解压缩速度、压缩率(压缩后存储大小)、压缩后是否可以支持切片。

image-20210513151217364

为了支持多种压缩/解压缩算法,Hadoop 引入了编码/解码器

压缩格式对应编码解码器
DEFLATEorg.apache.hadoop.io.compress.DefaultCodec
gziporg.apache.hadoop.io.compress.GzipCodec
bzip2org.apache.hadoop.io.compress.BZip2Codec
LZOcom.hadoop.compression.lzo.LzopCodec
Snappyorg.apache.hadoop.io.compress.SnappyCodec

要在 Hadoop 中启用压缩,可以配置如下参数

参数默认值阶段建议
io.compression.codecs (在 core-site.xml 中配置)无,这个需要在命令行输入 hadoop checknative 查看输入压缩Hadoop 使用文件扩展 名判断是否支持某种 编解码器
mapreduce.map.output.compr ess(在 mapred-site.xml 中 配置)falsemapper 输出这个参数设为 true 启 用压缩
mapreduce.map.output.compr ess.codec(在 mapred- site.xml 中配置)org.apache.hadoop.io.com press.DefaultCodecmapper 输出企业多使用 LZO 或 Snappy 编解码器在此 阶段压缩数据
mapreduce.output.fileoutpu tformat.compress(在 mapred-site.xml 中配置)falsereducer 输出这个参数设为 true 启 用压缩
mapreduce.output.fileoutpu tformat.compress.codec(在 mapred-site.xml 中配置)org.apache.hadoop.io.com press.DefaultCodecreducer 输出使用标准工具或者编 解码器,如 gzip 和 bzip2

总结

  • 输入数据接口:InputFormat
    • 默认使用的实现类是:TextInputFormat
    • TextInputFormat 的功能逻辑是:一次读一行文本,然后将该行的起始偏移量作为
      key,行内容作为 value 返回。
    • CombineTextInputFormat 可以把多个小文件合并成一个切片处理,提高处理效率。
  • 逻辑处理接口:Mapper
    • 用户根据业务需求实现其中三个方法:map() setup() cleanup ()
  • Partitioner 分区
    • 有默认实现 HashPartitioner,逻辑是根据 key 的哈希值和 numReduces 来返回一个
      分区号;key.hashCode()&Integer.MAXVALUE % numReduces
    • 如果业务上有特别的需求,可以自定义分区。
  • Comparable 排序
    • 当我们用自定义的对象作为 key 来输出时,就必须要实现 WritableComparable 接
      口,重写其中的 compareTo()方法。
    • 部分排序:对最终输出的每一个文件进行内部排序。
    • 全排序:对所有数据进行排序,通常只有一个 Reduce。
    • 二次排序:排序的条件有两个。
  • Combiner 合并
    • Combiner 合并可以提高程序执行效率,减少 IO 传输。但是使用时必须不能影响原有的 业务处理结果。
  • 逻辑处理接口:Reducer
    用户根据业务需求实现其中三个方法:reduce() setup() cleanup ()
  • 输出数据接口:OutputFormat
    • 默认实现类是 TextOutputFormat,功能逻辑是:将每一个 KV 对,向目标文本文件
      输出一行。
    • 用户还可以自定义 OutputFormat。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值