Hadoop学习笔记系列:05-MapReduce的详细过程图解及源码解读之Client

MapRdeuce的执行逻辑图

image

一个MapReduce作业是客户端需要执行的一个工作单元:它包括输入数据,MapReduce程序和配置信息。Hadoop将作业分为若干个task来执行,其中主要包括两类:map任务和reduce任务。这些任务运行在集群的节点上,并通过YARN进行调度。一个完整的MapReduce程序由client,map,reduce这三大块组成。

Client概述

  • 检查作业的输入、输出的配置是否完整
  • 完成Input Splits(输入分片)的划分与计算
  • 复制作业的jar包、配置文件到分布式文件系统HDFS的目录中
  • 提交作业到JobTracker,同时监控作业的状态

Split 分片

Hadoop将MapReduce的输入数据划分成为等长的的小数据块,称为输入分片(Input Split),如上图所示。Hadoop为每个分片构建一个map任务,并由改任务来运行用户自定义的map函数,从而处理分片中的每一条记录。

分片的目的

将一个大的数据文件切分成多个等长的小的分片,意味着处理每个分片的时间要小于处理整个输入数据所花费的时间。因此,将一个较大的数据输入文件,切分成多个较小的分片,并且并行的处理每个分片,将大大的缩短我们处理整个数据输入的时间。

分片的大小

如果分片太大,以为着分片的数量减少,处理单个分片的时间也相应较长,不能合理的发挥出集群的并行处理能力。如果分片足够小,理论上来说整个过程将获得更好的负载均衡,因为一台较快的计算机能处理的分片数据比一台较慢的计算机更多,所以随着分片被切分的更细,负载均衡的质量会更高。

但是,如果分片切分的太细,那么管理分片的总时间和构建map任务的总时间又会增加,从而使整个作业的执行时间增加。所以对于大多数作业来说,一个合理的分片大小趋向于HDFS的一个块的大小,默认是128M。当然这个值也可以调整。

为什么分片的大小最好是趋向于HDFS的一个块的大小

  • Hadoop会尽可能的在存储有输入数据文件的节点上运行map任务,这样就可以获取到最佳性能,因为它无需使用宝贵的集群带宽资源,这就是数据本地化优化

  • 但是有时候对于一个map任务的输入分片来说,存储该分片的HDFS数据块副本的所有节点有可能正在运行其他的map任务,没有空闲的资源来运行map任务处理这一分片,这时hadoop需要找到一个空闲的节点来运行该map分片任务,这将导致该分片需要从存储数据块的节点网络传输到现在的节点,从而增加网络资源的消耗和等待的时长(仅仅是非常偶然的情况)。所以如果一个分片跨越了两个数据块,那么对于任何一个HDFS节点来说,基本上都不可能同时存储这两个数据块,因此对于分片中的部分数据就需要通过网络传输到map任务运行的节点,从而降低了整个数据处理的效率。所以一个合理的分片大小就是和HDFS的数据块的大小趋同。

TIPS:切片并不是将文件真的切分成多个部分,只是逻辑上描述一个数据范围的机制

源码分析

一个完整的mapreduce作业的客户端的主要做的事情:

  • 检查作业的输入、输出的配置是否完整
  • 完成Input Splits(输入分片)的划分与计算
  • 复制作业的jar包、配置文件到分布式文件系统HDFS的目录中
  • 提交作业到JobTracker,同时监控作业的状态

在一个job中,作业的执行或提交一般调用如下两个方法,这是MapReduce作业的入口

//执行作业
job.waitForCompletion(true)

//或者调用job.sumbit()
job.submit();

无论是上面哪个方法,最终都会调用到org.apache.hadoop.mapreduce.Job类中的submit方法,主要源码如下:

public void submit() 
     throws IOException, InterruptedException, ClassNotFoundException {
ensureState(JobState.DEFINE);
setUseNewAPI();
connect();
final JobSubmitter submitter = 
    getJobSubmitter(cluster.getFileSystem(), cluster.getClient());
status = ugi.doAs(new PrivilegedExceptionAction<JobStatus>() {
  public JobStatus run() throws IOException, InterruptedException, 
  ClassNotFoundException {
    return submitter.submitJobInternal(Job.this, cluster);
  }
});
state = JobState.RUNNING;
LOG.info("The url to track the job: " + getTrackingURL());
}

在这个方法中构建了一个JobSubmitter,由这个对象来执行核心的操作,调用JobSubmitter的submitJobInternal方法执行分片计算,复制配置文件、Jar包到HDFS,以及提交作业。

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

    //验证作业的输入、输出等配置项
    checkSpecs(job);
    
    Path submitJobDir = new Path(jobStagingArea, jobId.toString());
    JobStatus status = null;

    //创建作业的分片
    LOG.debug("Creating splits at " + jtFs.makeQualified(submitJobDir));
    int maps = writeSplits(job, submitJobDir);
    conf.setInt(MRJobConfig.NUM_MAPS, maps);
    LOG.info("number of splits:" + maps);
    
    //提交作业的配置文件
    writeConf(conf, submitJobFile);
    
    //
    // 提交作业
    //
    printTokens(jobId, job.getCredentials());
    status = submitClient.submitJob(
      jobId, submitJobDir.toString(), job.getCredentials());

  }

writeSplits 最终会调用到FileInputFormat类的getSplits方法:

  • 该方法会为每一个输入文件进行分片计算操作。
  • hadoop默认的分片最小值是1,可以通过FileInputFormat.setMinInputSplitSize(job,6000);方法进行设置;
  • hadoop默认的分片最大值是Long的最大值,可以通过FileInputFormat.setMaxInputSplitSize(job,60000);进行设置
  • 由如下代码可知:如果用户不指定分片的大小,那么默认的大小就是文件块的大小
  • 分片信息由:文件的路径,起始偏移量,分片大小,文件块所在的主机列表,和文件块的副本所在的主机列表
public List<InputSplit> getSplits(JobContext job) throws IOException {
    StopWatch sw = new StopWatch().start();
    //分片的最小值:默认是1
    long minSize = Math.max(getFormatMinSplitSize(), getMinSplitSize(job));
    //分片的最大值:默认是Long的最大值,可以由参数“mapreduce.input.fileinputformat.split.maxsize”进行设置
    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();
          
          //比较块大小,设置的最小分片值,最大的分片值,取Math.max(minSize, Math.min(maxSize, blockSize));最大值
          
          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;
  }
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值