007 Hadoop之MapReduce深入

1、MapReduce框架原理

MapTask的sort:快速排序(内存) + 归并排序(磁盘)-- 这里的归并排序归属的对象其实就是Reduce
ReduceTask的sort:归并排序(磁盘 or 内存)
在这里插入图片描述
MapTask.java

@Override
  public void run(final JobConf job, final TaskUmbilicalProtocol umbilical)
    throws IOException, ClassNotFoundException, InterruptedException {
    this.umbilical = umbilical;

    if (isMapTask()) {
      // If there are no reducers then there won't be any sort. Hence the map 
      // phase will govern the entire attempt's progress.
      if (conf.getNumReduceTasks() == 0) {
        mapPhase = getProgress().addPhase("map", 1.0f);
      } else {
        // If there are reducers then the entire attempt's progress will be 
        // split between the map phase (67%) and the sort phase (33%).
        mapPhase = getProgress().addPhase("map", 0.667f);
        sortPhase  = getProgress().addPhase("sort", 0.333f);
      }
    }
    .....
}

ReduceTask.java

@Override
  @SuppressWarnings("unchecked")
  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");
    }
    ......
  }

InputFormat默认实现是TextInputFormat
OutputFormat默认实现类是TextOutputFormat

通过下面两个API设置自定义的InputFormat、OutputFormat
job.setOutputFormatClass(实现类名.class);
job.setInputFormatClass(实现类名.class);

1.1、InputFormat

1.1.1、切片
  • 从文件的逻辑上的进行大小的切分,一个切片就会产生一个MapTask;
  • 切片时只考虑文件本身,不考虑数据的整体集;
  • 切片大小和切块大小默认是一致的,为了避免将来切片读取数据的时候有跨机器的情况;
1.1.2、InputFormat的体系结构
  • FileInputFormat是InputFormat的子实现类,实现切片逻辑(getSplits() 负责切片);
  • TextInputFormat是FileInputFormat的子实现类, 实现读取数据的逻辑(createRecordReader() 返回一个RecordReader,在RecordReader中实现了读取数据的方式:按行读取);
  • CombineFileInputFormat是FileInputFormat的子实现类,此类中也实现了一套切片逻辑 (适用于小文件计算场景);

PS:Hadoop中InputFormat默认的实现逻辑FileInputFormat、TextInputFormat

1.1.3、 源码考证TextInputFormat是hadoop的InputFormat默认实现类

job.java

public boolean waitForCompletion(boolean verbose
                                   ) throws IOException, InterruptedException,
                                            ClassNotFoundException {
    if (state == JobState.DEFINE) {
      submit();
    }
}

public void submit() 
         throws IOException, InterruptedException, ClassNotFoundException {
    return submitter.submitJobInternal(Job.this, cluster);
}

JobSubmitter.java

JobStatus submitJobInternal(Job job, Cluster cluster) 
  throws ClassNotFoundException, InterruptedException, IOException {

    int maps = writeSplits(job, submitJobDir);
}

private int writeSplits(org.apache.hadoop.mapreduce.JobContext job,
      Path jobSubmitDir) throws IOException,
      InterruptedException, ClassNotFoundException {
    maps = writeNewSplits(job, jobSubmitDir);
}

int writeNewSplits(JobContext job, Path jobSubmitDir) throws IOException,
      InterruptedException, ClassNotFoundException {
    InputFormat<?, ?> input =
      ReflectionUtils.newInstance(job.getInputFormatClass(), conf);
  }

ChainMapContextImpl.java

@Override
  public Class<? extends InputFormat<?, ?>> getInputFormatClass()
      throws ClassNotFoundException {
    return base.getInputFormatClass();
  }

JobContextImpl.java

public Class<? extends InputFormat<?,?>> getInputFormatClass() 
     throws ClassNotFoundException {
    return (Class<? extends InputFormat<?,?>>) 
    // 找到了默认实现类TextInputFormat
      conf.getClass(INPUT_FORMAT_CLASS_ATTR, TextInputFormat.class);
  }
1.1.4、FileInputFormat.java(实现数据切片)

FileInputFormat.java

public List<InputSplit> getSplits(JobContext job) throws IOException {
	// 创建一个计时器
    StopWatch sw = new StopWatch().start(); 
    // 值为1(默认情况,mapred-site.xml有配置)
    long minSize = Math.max(getFormatMinSplitSize(), getMinSplitSize(job));
    // 值为long最大值(默认情况,mapred-site.xml没配置)
    long maxSize = getMaxSplitSize(job);

    // generate splits
    List<InputSplit> splits = new ArrayList<InputSplit>();
    List<FileStatus> files = listStatus(job);
	
	// 没有配置mapreduce.input.fileinputformat.input.dir.nonrecursive.ignore.subdirs,ignoreDirs = false
    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)) {
          // 获取HDFS中的数据块的大小 128MB
          long blockSize = file.getBlockSize();
          // 计算切片的大小--> 128M 默认情况下永远都是块大小
          // 通过改变mapreduce.input.fileinputformat.split.minsize 配置项来改变minSize大小
          // 通过改变mapreduce.input.fileinputformat.split.maxsize 配置项来改变maxSize大小
          long splitSize = computeSplitSize(blockSize, minSize, maxSize);

          long bytesRemaining = length;
          // private static final double SPLIT_SLOP = 1.1;   // 10% slop
          // 防止很小的数据占用一个MapTask
          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;
          }
		 
		  // 处理剩余的数据(数据量大于 splitSize * 0.1) 
          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;
  }

配置文件:mapred-default.xml
源码中计算公式可以调整切片大小(默认等于Block(128M)大小)
computeSplitSize(Math.max(minSize,Math.min(maxSize,blocksize)))=blocksize=128M

mapreduce.input.fileinputformat.split.minsize=1 (默认值为1)
mapreduce.input.fileinputformat.split.maxsize= Long.MAXValue (Long最大值)

每次切片时,需要判断切完剩下的部分是否大于块的1.1倍,不大于1.1倍就划分一块切片,大于则分两块。

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

1.1.5、TextInputFormat.java(实现数据读取)

TextInputFormat.java

// RecordReader<LongWritable, Text>和Mapper的输入类型一致
public RecordReader<LongWritable, Text> 
    createRecordReader(InputSplit split,
                       TaskAttemptContext context) {
    String delimiter = context.getConfiguration().get(
        "textinputformat.record.delimiter");
    byte[] recordDelimiterBytes = null;
    if (null != delimiter)
      recordDelimiterBytes = delimiter.getBytes(Charsets.UTF_8);
    // 按行读取数据
    return new LineRecordReader(recordDelimiterBytes);
  }

CombineFileInputFormat.java(适用于小文件计算场景)
不特殊处理,每一个小文件创建一个MapTask,非常损耗计算机资源

public List<InputSplit> getSplits(JobContext job) 
    throws IOException {
    long minSizeNode = 0;
    long minSizeRack = 0;
    long maxSize = 0;
    Configuration conf = job.getConfiguration();

    // the values specified by setxxxSplitSize() takes precedence over the
    // values that might have been specified in the config
    if (minSplitSizeNode != 0) {
      minSizeNode = minSplitSizeNode;
    } else {
      minSizeNode = conf.getLong(SPLIT_MINSIZE_PERNODE, 0);
    }
    if (minSplitSizeRack != 0) {
      minSizeRack = minSplitSizeRack;
    } else {
      minSizeRack = conf.getLong(SPLIT_MINSIZE_PERRACK, 0);
    }
    if (maxSplitSize != 0) {
      maxSize = maxSplitSize;
    } else {
      maxSize = conf.getLong("mapreduce.input.fileinputformat.split.maxsize", 0);
      // If maxSize is not configured, a single split will be generated per
      // node.
    }
    if (minSizeNode != 0 && maxSize != 0 && minSizeNode > maxSize) {
      throw new IOException("Minimum split size pernode " + minSizeNode +
                            " cannot be larger than maximum split size " +
                            maxSize);
    }
    if (minSizeRack != 0 && maxSize != 0 && minSizeRack > maxSize) {
      throw new IOException("Minimum split size per rack " + minSizeRack +
                            " cannot be larger than maximum split size " +
                            maxSize);
    }
    if (minSizeRack != 0 && minSizeNode > minSizeRack) {
      throw new IOException("Minimum split size per node " + minSizeNode +
                            " cannot be larger than minimum split " +
                            "size per rack " + minSizeRack);
    }

    // all the files in input set
    List<FileStatus> stats = listStatus(job);
    List<InputSplit> splits = new ArrayList<InputSplit>();
    if (stats.size() == 0) {
      return splits;    
    }

    // In one single iteration, process all the paths in a single pool.
    // Processing one pool at a time ensures that a split contains paths
    // from a single pool only.
    for (MultiPathFilter onepool : pools) {
      ArrayList<FileStatus> myPaths = new ArrayList<FileStatus>();
      
      // pick one input path. If it matches all the filters in a pool,
      // add it to the output set
      for (Iterator<FileStatus> iter = stats.iterator(); iter.hasNext();) {
        FileStatus p = iter.next();
        if (onepool.accept(p.getPath())) {
          myPaths.add(p); // add it to my output set
          iter.remove();
        }
      }
      // create splits for all files in this pool.
      getMoreSplits(job, myPaths, maxSize, minSizeNode, minSizeRack, splits);
    }

    // create splits for all files that are not in any pool.
    getMoreSplits(job, stats, maxSize, minSizeNode, minSizeRack, splits);

    // free up rackToNodes map
    rackToNodes.clear();
    return splits;    
  }

Driver:设定InputFormat的实现类,以下两种方式设置的是同一个Configuration对象

// 指定CombineTextInputFormat中的切片最大值
CombineTextInputFormat.setMaxInputSplitSize(job, 4194304*5);
// 指定InputFormat的具体实现 conf.setClass(INPUT_FORMAT_CLASS_ATTR, cls, InputFormat.class);
job.setInputFormatClass(CombineTextInputFormat.class);

// 等价方式指定InputFormat的实现类
Configuration conf = new Configuration();
conf.set("mapreduce.job.inputformat.class","org.apache.hadoop.mapreduce.lib.input.CombineTextInputFormat");

1.2、shuffle机制

在这里插入图片描述

1.2.1、map task端操作
  • 每个map task都有一个内存缓冲区(默认是100MB),存储着map的输出结果,当缓冲区快满的时候需要将缓冲区的数据以一个临时文件的方式存放到磁盘,当整个map task结束后再对磁盘中这个map task产生的所有临时文件做合并,生成最终的正式输出文件,然后等待reduce task来拉数据。
  • Spill过程:这个从内存往磁盘写数据的过程被称为Spill,中文可译为溢写。整个缓冲区有个溢写的比例spill.percent(默认是0.8),当达到阀值时map task 可以继续往剩余的memory写,同时溢写线程锁定已用memory,先对key(序列化的字节)做排序,如果client程序设置了Combiner,那么在溢写的过程中就会进行局部聚合。
  • Merge过程:每次溢写都会生成一个临时文件,在map task真正完成时会将这些文件归并成一个文件,这个过程叫做Merge。
1.2.2、reduce task端操作
  • 当某台TaskTracker上的所有map task执行完成,对应节点的reduce task开始启动,简单地说,此阶段就是不断地拉取(Fetcher)每个map task所在节点的最终结果,然后不断地做merge形成reduce task的输入文件。
  • Copy过程:Reduce进程启动一些数据copy线程(Fetcher)通过HTTP协议拉取TaskTracker的map阶段输出文件
  • Merge过程:Copy过来的数据会先放入内存缓冲区(基于JVM的heap size设置),如果内存缓冲区不足也会发生map task的spill(sort 默认,combine 可选),多个溢写文件时会发生map task的merge

PS:分区、排序、合并和压缩可以控制,其他的步骤用默认的就好

1.2.3、分区数设定

Driver:设置分区数(默认是一个分区)

// 声明配置对象
Configuration conf = new Configuration();
// 声明Job对象
Job job = Job.getInstance(conf);
// 指定ReduceTask的数量
job.setNumReduceTasks(2);
源码:默认情况如何进行数据的分区划分

WordCountMapper.java(自定义的Mapper)

protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
    // 获取当前输入的数据
    String line = value.toString();
    // 切割数据
    String[] datas = line.split(" ");
    // 遍历集合 封装 输出数据的key和value
    for (String data : datas) {
        outk.set(data);
        context.write(outk, outv);
    }
}

TaskInputOutputContext.java(数据输出对象的接口)

public void write(KEYOUT key, VALUEOUT value) throws IOException, InterruptedException;

ChainMapContextImpl.java(实现数据输出接口的对象)

 public void write(KEYOUT key, VALUEOUT value) throws IOException,
     InterruptedException {
   output.write(key, value);
 }

MapTask.NewOutputCollector.java(环形缓冲区,分区数就是ReduceTask的数量)

public void write(K key, V value) throws IOException, InterruptedException {
  // partitions = jobContext.getNumReduceTasks();
  collector.collect(key, value, partitioner.getPartition(key, value, partitions));
}

Partitioner.java(负责Map阶段输出数据的分区)

public abstract int getPartition(KEY key, VALUE value, int numPartitions);

HashPartitioner.java(默认分区实现)

public int getPartition(K key, V value, int numReduceTasks) {
    return (key.hashCode() & Integer.MAX_VALUE) % numReduceTasks;
}
1.2.4、自定义分区对象

PhonePartitioner.java(对电话号进行分区)

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

/**
 * 自定义一个分区器对象,需要继承Hadoop的提供的Partitioner对象
 */
// Partitioner<Text, FlowBean>和Mapper输出KV类型一致
public class PhonePartitioner extends Partitioner<Text, FlowBean> {
    /**
     * 定义当前kv所属分区的规则
     * @param text
     * @param flowBean
     * @param numPartitions
     * @return
     *  136 --> 0
     *  137 --> 1
     *  138 --> 2
     *  139 --> 3
     *  其他 --> 4
     */
    public int getPartition(Text text, FlowBean flowBean, int numPartitions) {

        int phonePartitions;
        // 获取手机号
        String phoneNum = text.toString();
        if(phoneNum.startsWith("136")){
            phonePartitions = 0;
        }else if(phoneNum.startsWith("137")){
            phonePartitions = 1;
        }else if(phoneNum.startsWith("138")){
            phonePartitions = 2;
        }else if(phoneNum.startsWith("139")){
            phonePartitions = 3;
        }else {
            phonePartitions =4;
        }
        return phonePartitions;
    }
}

Driver:设置分区数和分区对象

Configuration conf = new Configuration();
Job job = Job.getInstance(conf);
job.setNumReduceTasks(5);
job.setPartitionerClass(PhonePartitioner.class);

// 等价方式指定InputFormat的实现类
Configuration conf = new Configuration();
conf.set("mapreduce.job.partitioner.class","com.atguigu.mr.partitioner.PhonePartitioner");

PS:不同分区数产生的不同运行结果
– 当ReduceTask的数量设置 > 实际用到的分区数 此时会生成空的分区文件
– 当ReduceTask的数量设置 < 实际用到的分区数 此时会报错
– 当ReduceTask的数量设置 = 1 结果文件会输出到一个文件中,分析源码:MapTask.NewOutputCollector.java

NewOutputCollector(org.apache.hadoop.mapreduce.JobContext jobContext,
                   JobConf job,
                   TaskUmbilicalProtocol umbilical,
                   TaskReporter reporter
                   ) throws IOException, ClassNotFoundException {
  collector = createSortingCollector(job, reporter);
  partitions = jobContext.getNumReduceTasks();
  // 判断ReduceTask的数量是否大于1
  if (partitions > 1) {
    // 当ReduceTask的数量设置 > 实际用到的分区数时,通过反射调用的自定义Partitioner or 默认Partitioner
    partitioner = (org.apache.hadoop.mapreduce.Partitioner<K,V>)ReflectionUtils.newInstance(jobContext.getPartitionerClass(), job);
  } else {
    // Partitioner接口是函数式接口,实现函数式接口创建匿名内部类(特殊的Partitioner实现)
    partitioner = new org.apache.hadoop.mapreduce.Partitioner<K,V>() {
      @Override
      public int getPartition(K key, V value, int numPartitions) {
        // 1 - 1 = 0,Map结果文件会永远输入到一个文件中
        return partitions - 1; 
      }
    };
  }
}

@Override
public void write(K key, V value) throws IOException, InterruptedException {
  collector.collect(key, value,
                    partitioner.getPartition(key, value, partitions));
}
1.2.5、WritableComparable排序

hadoop3.x MapReduce序列化机制-自定义序列化类(Writable、WritableComparable)

  • 部分排序:每个输出文件内部有序;
  • 全排序:只有一个输出文件,只设置一个ReduceTask;
  • 辅助排序:弱化了,不重点掌握,比较key为bean时;
  • 二次排序:compareTo中的比较条件有两个;
1.2.6、Combiner合并

Hadoop3.x MapReduce Combiner 合并

  • Combiner用于减小Mapper和Reducer之间数据传输量
  • Combiner的父类就是Reducer
  • Combiner在每一个MapTask所在的节点运行
  • Combiner的使用不能影响最终的逻辑

Combiner 合并是 Shuffle 阶段的一个可选操作,旨在提前对数据进行一次合并,以减少 Reducer 的压力。Combiner 大概会发生两次:第一次发生在环形缓冲区分区排序后,溢写到硬盘前;第二次是在多个溢写文件合并,然后归并排序后。可以把 Combiner 理解为提前的 Reducer,只不过这个 Reducer 不是针对一个分区上的数据,而是针对单个 MapTask 处理的数据。

1.3、OutputFormat

OutputFormat.java

public abstract RecordWriter<K, V> 
    getRecordWriter(TaskAttemptContext context
                    ) throws IOException, InterruptedException;

public abstract void checkOutputSpecs(JobContext context
                                        ) throws IOException, 
                                                 InterruptedException;

OutputFormat定义最终数据的写出需要的方法
子类FileOutputFormat实现了抽象方法checkOutputSpecs:检查目标文件夹是否设置、是否存在
子类TextOutputFormat实现了抽象方法getRecordWriter:为获得RecordWriter,用于job的输出
Hadoop 自定义OutputFormat
HDFS基本操作命令和读写原理
Hadoop学习(六)—使用IOUtils对文件的上传和下载
Hadoop 3.2.0 idea 开发环境搭建 与 HDFS读取写入API操作
hadoop(四)-hadoop的 inputformat、outputformat、recordreader、recordwriter

1.4、Join

注意hadoop和java堆内存分配和使用的区别
hadoop–Reduce Join
Hadoop|Reduce中Iterable迭代器K,V对象复用机制
Hadoop中 Map Join与计数器

Mapper中获取切片对象

@Override
protected void setup(Context context) throws IOException, InterruptedException {
   inputSplit = (FileSplit) context.getInputSplit();
}

1.5、MapReduce开发总结

Internal method for submitting jobs to the system.
The job submission process involves:
1、Checking the input and output specifications of the job. 检测输入输出路径的合法性
2、Computing the InputSplits for the job. 计算当前job的切片信息
3、Setup the requisite accounting information for the DistributedCache of the job, if necessary. 添加分布式缓存文件(方法:job.addCacheFile())
4、Copying the job’s jar and configuration to the map-reduce system directory on the distributed file-system. 将必要内容拷贝到job执行的临时目录:切片信息、配置文件、jar包(本地模式没有)
5、Submitting the job to the JobTracker and optionally monitoring it’s status. 提交job
Params:
job – the configuration to submit
cluster – the handle to the Cluster

job.java

public boolean waitForCompletion(boolean verbose
                                 ) throws IOException, InterruptedException,
                                          ClassNotFoundException {
  // public enum JobState {DEFINE, RUNNING};
  if (state == JobState.DEFINE) {
    submit();
  }
  // 监控状态
}

public void submit() 
      throws IOException, InterruptedException, ClassNotFoundException {
	 // 确定当期Job的状态
	 ensureState(JobState.DEFINE);
	 // 设置使用新的API
	 setUseNewAPI();
	 // 连接HDFS、YARN:生成cluster对象
	 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);
	   }
	 });
	 // 提交后的状态改为运行了
	 state = JobState.RUNNING;
	 LOG.info("The url to track the job: " + getTrackingURL());
}

JobSubmitter.java

JobStatus submitJobInternal(Job job, Cluster cluster) 
  throws ClassNotFoundException, InterruptedException, IOException {

    //validate the jobs output specs 
    // 检查OutputFormat文件是否存在
    checkSpecs(job);

	// 加载默认配置文件、自定义配置文件
    Configuration conf = job.getConfiguration();
    addMRFrameworkToDistributedCache(conf);
	
	// 创建存放jar包、配置文件的临时目录
    Path jobStagingArea = JobSubmissionFiles.getStagingDir(cluster, conf);
    //configure the command line options correctly on the submitting dfs
    // 获得本地的IP
    InetAddress ip = InetAddress.getLocalHost();
    if (ip != null) {
      submitHostAddress = ip.getHostAddress();
      submitHostName = ip.getHostName();
      conf.set(MRJobConfig.JOB_SUBMITHOST,submitHostName);
      conf.set(MRJobConfig.JOB_SUBMITHOSTADDR,submitHostAddress);
    }
    // 生成Job的唯一标识
    JobID jobId = submitClient.getNewJobID();
    job.setJobID(jobId);
    // 拼接临时目录和JobID
    Path submitJobDir = new Path(jobStagingArea, jobId.toString());
    JobStatus status = null;
    try {
      conf.set(MRJobConfig.USER_NAME,
          UserGroupInformation.getCurrentUser().getShortUserName());
      conf.set("hadoop.http.filter.initializers", 
          "org.apache.hadoop.yarn.server.webproxy.amfilter.AmFilterInitializer");
      conf.set(MRJobConfig.MAPREDUCE_JOB_DIR, submitJobDir.toString());
      LOG.debug("Configuring job " + jobId + " with " + submitJobDir 
          + " as the submit dir");
      // get delegation token for the dir
      TokenCache.obtainTokensForNamenodes(job.getCredentials(),
          new Path[] { submitJobDir }, conf);
      
      populateTokenCache(conf, job.getCredentials());

      // generate a secret to authenticate shuffle transfers
      if (TokenCache.getShuffleSecretKey(job.getCredentials()) == null) {
        KeyGenerator keyGen;
        try {
          keyGen = KeyGenerator.getInstance(SHUFFLE_KEYGEN_ALGORITHM);
          keyGen.init(SHUFFLE_KEY_LENGTH);
        } catch (NoSuchAlgorithmException e) {
          throw new IOException("Error generating shuffle secret key", e);
        }
        SecretKey shuffleKey = keyGen.generateKey();
        TokenCache.setShuffleSecretKey(shuffleKey.getEncoded(),
            job.getCredentials());
      }
      if (CryptoUtils.isEncryptedSpillEnabled(conf)) {
        conf.setInt(MRJobConfig.MR_AM_MAX_ATTEMPTS, 1);
        LOG.warn("Max job attempts set to 1 since encrypted intermediate" +
                "data spill is enabled");
      }
      
      // 拷贝配置文件,创建临时目录:里面有jar包,但是本地模式没有
      copyAndConfigureFiles(job, submitJobDir);

      Path submitJobFile = JobSubmissionFiles.getJobConfPath(submitJobDir);
      
      // Create the splits for the job
      LOG.debug("Creating splits at " + jtFs.makeQualified(submitJobDir));
      // 完成切片,确定maptask
      int maps = writeSplits(job, submitJobDir);
      conf.setInt(MRJobConfig.NUM_MAPS, maps);
      LOG.info("number of splits:" + maps);

      int maxMaps = conf.getInt(MRJobConfig.JOB_MAX_MAP,
          MRJobConfig.DEFAULT_JOB_MAX_MAP);
      if (maxMaps >= 0 && maxMaps < maps) {
        throw new IllegalArgumentException("The number of map tasks " + maps +
            " exceeded limit " + maxMaps);
      }

      // write "queue admins of the queue to which job is being submitted"
      // to job file.
      String queue = conf.get(MRJobConfig.QUEUE_NAME,
          JobConf.DEFAULT_QUEUE_NAME);
      AccessControlList acl = submitClient.getQueueAdmins(queue);
      conf.set(toFullPropertyName(queue,
          QueueACL.ADMINISTER_JOBS.getAclName()), acl.getAclString());

      // removing jobtoken referrals before copying the jobconf to HDFS
      // as the tasks don't need this setting, actually they may break
      // because of it if present as the referral will point to a
      // different job.
      TokenCache.cleanUpTokenReferral(conf);

      if (conf.getBoolean(
          MRJobConfig.JOB_TOKEN_TRACKING_IDS_ENABLED,
          MRJobConfig.DEFAULT_JOB_TOKEN_TRACKING_IDS_ENABLED)) {
        // Add HDFS tracking ids
        ArrayList<String> trackingIds = new ArrayList<String>();
        for (Token<? extends TokenIdentifier> t :
            job.getCredentials().getAllTokens()) {
          trackingIds.add(t.decodeIdentifier().getTrackingId());
        }
        conf.setStrings(MRJobConfig.JOB_TOKEN_TRACKING_IDS,
            trackingIds.toArray(new String[trackingIds.size()]));
      }

      // Set reservation info if it exists
      ReservationId reservationId = job.getReservationId();
      if (reservationId != null) {
        conf.set(MRJobConfig.RESERVATION_ID, reservationId.toString());
      }

      // Write job file to submit dir
      // 拼接后的临时目录:写配置文件job.xml、切片文件
      writeConf(conf, submitJobFile);
      
      //
      // Now, actually submit the job (using the submit name)
      //
      printTokens(jobId, job.getCredentials());
      // 提交job,运行完成后临时文件中的内容就没了
      status = submitClient.submitJob(
          jobId, submitJobDir.toString(), job.getCredentials());
      if (status != null) {
        return status;
      } else {
        throw new IOException("Could not launch job");
      }
    } finally {
      if (status == null) {
        LOG.info("Cleaning up the staging area " + submitJobDir);
        if (jtFs != null && submitJobDir != null)
          jtFs.delete(submitJobDir, true);

      }
    }
  }
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值