【Mapred】jobtracker & tasktracker架构作业是怎么提交的

Streaming那一套就先不管了,提交作业部分的代码肯定是一样的,只不过客户端提交的方式不一样。

很多人都从wordCount看起,看吧,我擦。

Configuration conf = new Configuration();
    String[] otherArgs = new GenericOptionsParser(conf, args).getRemainingArgs();
    if (otherArgs.length != 2) {
      System.err.println("Usage: wordcount <in> <out>");
      System.exit(2);
    }
    Job job = new Job(conf, "word count");
    job.setJarByClass(WordCount.class);
    job.setMapperClass(TokenizerMapper.class);
    job.setCombinerClass(IntSumReducer.class);
    job.setReducerClass(IntSumReducer.class);
    job.setOutputKeyClass(Text.class);
    job.setOutputValueClass(IntWritable.class);
    FileInputFormat.addInputPath(job, new Path(otherArgs[0]));
    FileOutputFormat.setOutputPath(job, new Path(otherArgs[1]));
    System.exit(job.waitForCompletion(true) ? 0 : 1);
main方法如上,一般的都是先搞一个jobconf对象,所谓jobconf对象就是一个专门管理作业配置文件的对象,一般都是继承Configured类,这里就不多废话了。

然后配置好mapper,reducer,输入输出数据类型,数据输入输出路径等等,就调用submit方法或者runjob,这样就得到一个RunningJob对象,作业的执行过程可以通过这个runningJob对象得到管理。但是这里好像又包装了几层,进去waitForCompletion看一下

  public boolean waitForCompletion(boolean verbose
                                   ) throws IOException, InterruptedException,
                                            ClassNotFoundException {
    if (state == JobState.DEFINE) {
      submit();
    }
    if (verbose) {
      jobClient.monitorAndPrintJob(conf, info);
    } else {
      info.waitForCompletion();
    }
    return isSuccessful();
  }

这里还是有submit,直接进去看看吧,下边的就不管了,是对作业运行过程的监控管理。

  public void submit() throws IOException, InterruptedException, 
                              ClassNotFoundException {
    ensureState(JobState.DEFINE);
    setUseNewAPI();//估计1.0.3API有一些变化,先不管了
    
    // Connect to the JobTracker and submit the job
    connect();
    info = jobClient.submitJobInternal(conf);
    super.setJobID(info.getID());
    state = JobState.RUNNING;
   }
首先确定作业的状态,作业看起来有两种状态一种叫define状态,就是没开始执行吧,一种叫running,就是正在运行,先这么理解吧,擦。

下一步就是connect,什么叫connect,connect谁?其实就是JobClient的初始化过程,同时让去连接jobtracker,abaci中就是去找metamaster。

关键代码看下哈:

  public void init(JobConf conf) throws IOException {
    String tracker = conf.get("mapred.job.tracker", "local");//看到没有根据mapred.job.tracker 找到jt,还有本地模式?

    tasklogtimeout = conf.getInt(
      TASKLOG_PULL_TIMEOUT_KEY, DEFAULT_TASKLOG_TIMEOUT);
    this.ugi = UserGroupInformation.getCurrentUser();
    if ("local".equals(tracker)) {
      conf.setNumMapTasks(1);
      this.jobSubmitClient = new LocalJobRunner(conf);
    } else {
      this.jobSubmitClient = createRPCProxy(JobTracker.getAddress(conf), conf);//建立rpc代理,不说了,so easy?
    }        
  }
接着提交代码往下看,connect之后,开始submitJobInternal

进去主要看下关键代码片段吧

1、

Path jobStagingArea = JobSubmissionFiles.getStagingDir(JobClient.this,
            jobCopy);
得到staging目录,我擦,什么是staging目录?通过追踪可以发现,所谓staging目录实际上是

<name>mapreduce.jobtracker.staging.root.dir</name>配置的目录下,比如/user下,那么staging目录就是/user/username/.staging/,也就是各个用户“家”目录下的.staging 目录,这里没有就创建它,至于这个目录干啥用,先不管。但是猜测可能就是用户作业信息存放的地方,tt正是从这里拿数据执行任务。

2、

JobID jobId = jobSubmitClient.getNewJobId();

拿到jobId,jt给的,咋生成的?看JobID构造方法,
  public synchronized JobID getNewJobId() throws IOException {
    return new JobID(getTrackerIdentifier(), nextJobId++);
  }
public static final String JOBID_REGEX = 
    JOB + SEPARATOR + "[0-9]+" + SEPARATOR + "[0-9]+";
  
合法的格式是:job_一个或多个数字_一个或多个数字

平常我们看见的jobID一般都是job_201310141042_0036这样的,中间的数字好像有时间的特征,

通过追踪,可以看到trackerIdentifier是这么得来的,的确是日期,格式是年月日时分

  private static SimpleDateFormat getDateFormat() {
    return new SimpleDateFormat("yyyyMMddHHmm");
  }

  private static String generateNewIdentifier() {
    return getDateFormat().format(new Date());
  }
private int nextJobId = 1;初始1 ,那说明,作业号是递增,对后面的数字位数并没有限制。是从1递增的,也可以看做是jobtracker从启动之后接受提交作业的总数,int类型我擦,最大20+亿个吧。先不管了,这样jobID就有了。

3、在用户的staging目录构建一个以作业号命名的目录。叫做提交作业目录,对,staging就是用户的提交作业目录的地方。mapreduce.job.dir就是tt用于拿作业信息的地方。

Path submitJobDir = new Path(jobStagingArea, jobId.toString());
jobCopy.set("mapreduce.job.dir", submitJobDir.toString());

4、什么叫做copyAndConfigureFiles(jobCopy, submitJobDir)?

  private void copyAndConfigureFiles(JobConf job, Path jobSubmitDir) 
  throws IOException, InterruptedException {
    short replication = (short)job.getInt("mapred.submit.replication", 10);
    copyAndConfigureFiles(job, jobSubmitDir, replication);

    // Set the working directory
    if (job.getWorkingDirectory() == null) {
      job.setWorkingDirectory(fs.getWorkingDirectory());          
    }
  }
这段代码首先获取submit的副本数?什么叫副本数,作业提交的过程中需要放到submitdir一些作业task运行相关的数据,供tt去获取,提高一定的副本数能让这个过程更快,如果只有一个副本,我擦,很多tt都向一台机器拿数据,这台机器就成了热点,所以配置了这个副本数,集群实际配置是16个。这个数字是否合适,回头再看。

下边的copyAndConfigureFiles比较复杂,就不全部贴出来了,分析代码片段如下:

a、先要根据配置确保submitdir的存在,比如用户a,那就得保证/user/a/.staging/目录准备好,然后会创建三个目录

 public static Path getJobDistCacheFiles(Path jobSubmitDir) {
    return new Path(jobSubmitDir, "files");
  }
  public static Path getJobDistCacheArchives(Path jobSubmitDir) {
    return new Path(jobSubmitDir, "archives");
  }
  public static Path getJobDistCacheLibjars(Path jobSubmitDir) {
    return new Path(jobSubmitDir, "libjars");
  }
files,archives,libjars干啥用的,继续往下看就知道了。

b、创建好作业的这三个目录,就开始往里扔数据了。

还记得streamming提交作业的时候不,用-file 参数指定要分发的东西,比如我的mapper是个sh map.sh,就是说让tt在本机上执行map.sh这个脚本,但是你如果不用-file指定这个map.sh,作业就没法跑,因为这个map.sh是你自己知道,tt不知道啊,必须用-file指定分发的文件位置,就是这个map.sh的位置,这里就是把你指定的这个文件拷到集群上的刚才说到的files目录里,并且按照16副本保存。同时做好cache,要不然到时候再找建立对这个集群文件的链接又费时间,具体怎么cache的,先不管了。

同样的,存档、还有以来的jar文件放到对应的archives和libjars目录中。

c、如果作业本身是java写的,一定要打成jar包,在提交的过程中指定你的jar包在哪里,所以下面的一步就是找到用户提交指定的jar包。然后找到这个jar包,传到集群上去,这个名字也是固定的,就是/user/username/.staging/jobid/job.jar,即统一都叫job.jar,设置副本数啥的就不多说了。

以上的目录文件权限配置咱就不管了,重要的是分析清楚作业提交的流程。注意,如果你没给你的作业起名,在C这一步就会用你打的作业的jar包的名字作为作业的名字。


再回头看copyAndConfigureFiles,啥意思,就是从本地往集群拷贝吧?什么叫ConfigureFiles就是设置文件权限、副本数以及cache住这些文件路径吧?

当然整个Configure的过程也是比较重要的,对于解决实际问题应该有很多帮助,但是这里就不再详述了,继续往下吧。

以上都是给作业运行需要的文件创建好集群上保存这些东西的地方,同时把文件传到集群上去,并设置副本数,有了submit目录,还要有一个工作目录,工作目录也是配置的:

就是mapred.working.dir参数指定的hdfs的目录,如果没有配的话,工作目录会跑到用户的家目录下。


5、到目前为止,作业的所有配置信息还都保存在一个jobconf对象里,当然这里还想还有个JobContext对象,不管它是啥,总之作业配置信息还没有落地,因此下面就是把作业配置信息写到作业配置文件中,就是打开作业的运行状态页面看到的job.xml文件。

return new Path(jobSubmitDir, "job.xml");
看下面的代码片段,作业配置信息是怎么写的:

int reduces = jobCopy.getNumReduceTasks();//先获取用户配置了多少个reduce任务
          InetAddress ip = InetAddress.getLocalHost();
          if (ip != null) {
            job.setJobSubmitHostAddress(ip.getHostAddress());
            job.setJobSubmitHostName(ip.getHostName());
          }//哪个机器提交的这个作业
          JobContext context = new JobContext(jobCopy, jobId);//记录了运行作业所需材料存储的目录的位置信息等。
          // Check the output specification
          if (reduces == 0 ? jobCopy.getUseNewMapper() : 
            jobCopy.getUseNewReducer()) {//注意这里,如果reduce个数为0,就是纯map作业,比如distcp,那么这里因为版本的变化,确定要不要使用新api
            org.apache.hadoop.mapreduce.OutputFormat<?,?> output =
              ReflectionUtils.newInstance(context.getOutputFormatClass(),
                  jobCopy);
            output.checkOutputSpecs(context);//检查输出目录是否已经配置,是不是存在,以上都不对的话,就会抛出异常。
          } else {
            jobCopy.getOutputFormat().checkOutputSpecs(fs, jobCopy);
          }//这上边这几段代码先不管它接着往下看。
          
          jobCopy = (JobConf)context.getConfiguration();

          // Create the splits for the job
          FileSystem fs = submitJobDir.getFileSystem(jobCopy);
          LOG.debug("Creating splits at " + fs.makeQualified(submitJobDir));
          int maps = writeSplits(context, submitJobDir);//开始写分片信息,写分片的方式有新旧之分,
          jobCopy.setNumMapTasks(maps);

          // write "queue admins of the queue to which job is being submitted"
          // to job file.
          String queue = jobCopy.getQueueName();
          AccessControlList acl = jobSubmitClient.getQueueAdmins(queue);
          jobCopy.set(QueueManager.toFullPropertyName(queue,
              QueueACL.ADMINISTER_JOBS.getAclName()), acl.getACLString());

          // Write job file to JobTracker's fs        
          FSDataOutputStream out = 
            FileSystem.create(fs, submitJobFile,
                new FsPermission(JobSubmissionFiles.JOB_FILE_PERMISSION));

          try {
            jobCopy.writeXml(out);
          } finally {
            out.close();
          }

注意在写job.xml配置文件之前就要先对输入数据分片,什么是分片?比如你要运算的输入数据有2个T,我日你不分片怎么让任务执行?mapred的原理就是让多个tt去处理一部分数据然后把结果汇总起来,所以必须得分片。到了1.0.3版本分片有了新的方法,但是你仍然可以选择旧的方式,下面看看怎么分吧(old方式):

private int writeOldSplits(JobConf job, Path jobSubmitDir) 
  throws IOException {
    org.apache.hadoop.mapred.InputSplit[] splits =
    job.getInputFormat().getSplits(job, job.getNumMapTasks());//从作业配置文件拿用户配置的map数,如果没配置默认是1
先要getSplits,我日,因为输入文件可以用多种格式,文本,文件啊,二进制啊,都行,但是你要怎么分割,只有知道数据格式的人才知道怎么分吧?对吧,所以实现了inputFormat的类一定要提供自己的分片方案,以FileInputFormat为例,看看咋分的:

  public InputSplit[] getSplits(JobConf job, int numSplits)
    throws IOException {
    FileStatus[] files = listStatus(job);//进去看就知道,先要根据你作业初始化的时候指定的inputpath找到你要输入的文件,拿到文件的状态。
//基本上,输入文件一定在hdfs上。
    
    // Save the number of input files in the job-conf
    job.setLong(NUM_INPUT_FILES, files.length);//有多少个输入文件不需要用户自己指定,系统这就给你算出来了。
    long totalSize = 0;                           // compute total size
    for (FileStatus file: files) {                // check we have valid files
      if (file.isDir()) {
        throw new IOException("Not a file: "+ file.getPath());
      }
      totalSize += file.getLen();
    }//计算输入文件的大小总和,也就是输入数据总量,如果输入路径里边有目录,我擦,那是不允许的。

    long goalSize = totalSize / (numSplits == 0 ? 1 : numSplits);//numSplits就是传进来的map的个数,这是用户指定的,缺省是1,如果配置成0,我日那就是整片了。作为这里的分片个数,如果没指定就整片搞了。goalSize就是每片的大小。

    long minSize = Math.max(job.getLong("mapred.min.split.size", 1),
                            minSplitSize);
//片也不能忒小,所以default应该会给个最小片,集群给的是0,对于fileInputFormat类型,minSplitSize固定等于1,没有地方再去set它。
    // generate splits
    ArrayList<FileSplit> splits = new ArrayList<FileSplit>(numSplits);//准备好东西盛啊
    NetworkTopology clusterMap = new NetworkTopology();//我日这个集群的拓扑逻辑图,这个东西比较经典,回头再去专门分析它
    for (FileStatus file: files) {//对输入的每一个文件进行如下处理
      Path path = file.getPath();
      FileSystem fs = path.getFileSystem(job);
      long length = file.getLen();
      BlockLocation[] blkLocations = fs.getFileBlockLocations(file, 0, length);//拿到这个文件的块位置信息。
      if ((length != 0) && isSplitable(fs, path)) { //FileInputFormat这种输入格式写死了一定可以切分,isSplitable总返回true。
        long blockSize = file.getBlockSize();
        long splitSize = computeSplitSize(goalSize, minSize, blockSize);
//确定最后的切片大小,介于块大小和最小片大小之间。
        long bytesRemaining = length;
        while (((double) bytesRemaining)/splitSize > SPLIT_SLOP) {//这个SPLIT_SLOP=1.1,啥意思,就是说,如果切完之后剩余1.1个切片量的时候就别切了,实际上不会那么精准,肯定是能切到最后不足一个分片就停下了。

          String[] splitHosts = getSplitHosts(blkLocations, 
              length-bytesRemaining, splitSize, clusterMap);//这个很有意思,回头专门研究一下,也就是说知道了文件的offset,有了dn的拓扑图以及块位置信息,你要给我找到分片在哪些机器上,所以从这里就能看出来,分片分得啥,其实就是逻辑上的分片,分得是机器列表,是数据的地理信息的分片。
          splits.add(new FileSplit(path, length-bytesRemaining, splitSize, 
              splitHosts));//盛下一个个的分片。
          bytesRemaining -= splitSize;
        }
        
        if (bytesRemaining != 0) {
          splits.add(new FileSplit(path, length-bytesRemaining, bytesRemaining, 
                     blkLocations[blkLocations.length-1].getHosts()));
        }//把最后那个不足一个分片大小的剩下的数据放进去吧。
      } else if (length != 0) {
        String[] splitHosts = getSplitHosts(blkLocations,0,length,clusterMap);
        splits.add(new FileSplit(path, 0, length, splitHosts));
      } else { 
        //Create empty hosts array for zero length files
        splits.add(new FileSplit(path, 0, length, new String[0]));
      }
    }//这些else就不管了,不分或者怎样,先随他去吧
    LOG.debug("Total # of splits: " + splits.size());
    return splits.toArray(new FileSplit[splits.size()]);
  }

关于如何确定最终的切片大小:

  protected long computeSplitSize(long goalSize, long minSize,
                                       long blockSize) {
    return Math.max(minSize, Math.min(goalSize, blockSize));
    //分块大小与文件块大小中较小的取出来,跟集群配置的允许的最小切片比较,取较大的。
    //
  }
啥意思?也就是说你根据输入数据总量和map数计算出来的切片大小不能超过一个块大小,超过了切片大小就按集群块大小来实施切分。太小了也不行不能低于允许的最下片。

因此分片大小就这样被限制在块大小和最小值之间了。片最大就是hdfs的标准一块!当然这是FileInputFormat这么规定的,其它的切片方案咋整的,那不一定。

至于为啥这个分片大小要这么取,是值得探讨的,如果说map数非常大,导致分片正好处于这个区间内,但是小于块大小,那一个文件切分的话会发生什么?这些问题留到专门的一篇中去介绍吧。先继续看作业的提交流程。

再回到writeOldSplits方法中,拿到各个分片的host信息等之后,对分片进行排序,排序的方案是实际分得结果里各个分片的byte数,虽然之前指定了切片大小,但是因为切片有可能比块大小小,所以有时候会把整个块给一个切片,这种情况也不是没有,另外在每个机器上块大小也不是绝对等于配置的256M或者128M的,所以造成这里需要排序。

  public static void createSplitFiles(Path jobSubmitDir, 
      Configuration conf, FileSystem   fs, 
      org.apache.hadoop.mapred.InputSplit[] splits) 
  throws IOException {
    FSDataOutputStream out = createFile(fs, 
        JobSubmissionFiles.getJobSplitFile(jobSubmitDir), conf);//往/user/username/.staging/jobid/job.split写切分元数据,叫做SplitMetaInfo
    SplitMetaInfo[] info = writeOldSplits(splits, out, conf);
    out.close();
    writeJobSplitMetaInfo(fs,JobSubmissionFiles.getJobSplitMetaFile(jobSubmitDir), 
        new FsPermission(JobSubmissionFiles.JOB_FILE_PERMISSION), splitVersion,
        info);
  }
看上面,切片信息都拿到了,现在需要把这些信息都写到staging目录里,因为tt要从这里拿嘛。

  public static Path getJobSplitFile(Path jobSubmissionDir) {
    return new Path(jobSubmissionDir, "job.split");
  }
首先split目录在/user/username/.staging/jobid/下面,这个文件叫做job.split,
文件切分的元数据到底是啥,写了啥东西,这里也不讲了,回头单独发一篇。

6、根据实际切片数,重置map数。

int maps = writeSplits(context, submitJobDir);
          jobCopy.setNumMapTasks(maps);

7、还要知道是哪个队列,我擦,队列在公平调度算法中基本上没啥用了,唉,回头再说队列的问题吧,反正这里就完成了job.xml的写入

          String queue = jobCopy.getQueueName();
          AccessControlList acl = jobSubmitClient.getQueueAdmins(queue);
          jobCopy.set(QueueManager.toFullPropertyName(queue,
              QueueACL.ADMINISTER_JOBS.getAclName()), acl.getACLString());

          // Write job file to JobTracker's fs        
          FSDataOutputStream out = 
            FileSystem.create(fs, submitJobFile,
                new FsPermission(JobSubmissionFiles.JOB_FILE_PERMISSION));

          try {
            jobCopy.writeXml(out);
          } finally {
            out.close();
          }

8、下面开始真正向jobtracker提交作业了

public JobStatus submitJob(JobID jobId, String jobSubmitDir, Credentials ts)
告诉jt我的作业号,实际上是jt你妈的给我的,然后告诉你我的作业的信息存哪了,告诉你我的身份,我是尼玛合法用户。

synchronized (this) {
      if (jobs.containsKey(jobId)) {
        // job already running, don't start twice
        return jobs.get(jobId).getStatus();
      }
      jobInfo = new JobInfo(jobId, new Text(ugi.getShortUserName()),
          new Path(jobSubmitDir));
    }
    
看上面,jobtracker先检查这个jobid是不是已经存在了,jt有个乔布斯(jobs)替他管理者所有正在处理的作业,结构就是jobid做key,value是JobInProcess。

如果已经有了就告你这个作业号的状态,我擦,什么情况下会重复提交相同id的作业?

如果没有就构造一个JobInfo来描述你的这个请求。很简单不说了这个JobInfo,就是id,用户,作业提交信息(staging)的路径...

    JobInProgress job = null;
    try {
      job = new JobInProgress(this, this.conf, jobInfo, 0, ts);
    } catch (Exception e) {
      throw new IOException(e);
    }
    
接下来,看上面,构造JobInProgress,至于这个东西是啥,比较麻烦,也是回头专门再看吧。

但你知道这是个描述正在处理的作业的结构就行了。

再往下就是检查内存,这部分也后面单独讲。

还有就是恢复作业的处理,这部分代码目前好像没有做处理,先不讲了。

JobStatus status;
      try {
        status = addJob(jobId, job);
      } catch (IOException ioe) {
        LOG.info("Job " + jobId + " submission failed!", ioe);
        status = job.getStatus();
        status.setFailureInfo(StringUtils.stringifyException(ioe));
        failJob(job);
        throw ioe;
      }
      
      return status;
    }

看上面,就是把作业加到可调度的作业集合中,拿到作业状态JobStatus。

  private synchronized JobStatus addJob(JobID jobId, JobInProgress job) 
  throws IOException {
    totalSubmissions++;

    synchronized (jobs) {
      synchronized (taskScheduler) {
        jobs.put(job.getProfile().getJobID(), job);
        for (JobInProgressListener listener : jobInProgressListeners) {
          listener.jobAdded(job);
        }
      }
    }
    myInstrumentation.submitJob(job.getJobConf(), jobId);
    job.getQueueMetrics().submitJob(job.getJobConf(), jobId);

    LOG.info("Job " + jobId + " added successfully for user '" 
             + job.getJobConf().getUser() + "' to queue '" 
             + job.getJobConf().getQueueName() + "'");
    AuditLogger.logSuccess(job.getUser(), 
        Operation.SUBMIT_JOB.name(), jobId.toString());
    return job.getStatus();
  }

看上面,totalSubmissions++,说明jobtracker弄了一个全局计数器,记录总提交得作业数

然后同步乔布斯,还记得乔布斯吗?

Map<JobID, JobInProgress> jobs =  
    Collections.synchronizedMap(new TreeMap<JobID, JobInProgress>());
同步作业调度器,把这个新加的作业塞进去。

JobInProgressListener就属于作业调度器的范畴了,至于JobTrackerInstrumentation,不用管它,貌似是想做metric或者类似日志的东西,但是其实啥也没干,所有不管它了。

到这里基本上可以对作业提交总结一下:

1、作业提交首先要配置好你的作业,map数,reduce数,输入输出文件路径,输入输出文件格式类等,还要给作业起名。

2、作业staging阶段,所谓staging就是在hdfs上为这个作业弄个submit目录,将要分发的文件啊,压缩文件啊,还有依赖的jar包啊,put到集群上去。

然后还有job.jar(作业本身打的jar包,如果是java作业的话)。

3、对输入数据进行切分,怎么切要根据输入数据的格式,这个不同的有不同的getsplits的方式,但是切分的实质是找到分片对应的数据所在的位置,分片大小和重置map数就不多说了。

4、将切分的元数据写到job.split文件中。

5、注意,切分完了重置map数之后才开始写job.xml

6、作业提交给jobtracker,实际上是把新的jobInprogress塞到jobs的过程,这个jobs就是被作业调度器调度的对象。

作业提交实际上只是提交,只是把一切都准备好,弄个描述这个作业的对象交给乔布斯保存,或者叫cache就完了。

而作业得到执行或者调度,就不是客户端能控制的啦,而是作业调度器要干的了...

















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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值