MapReduce源码分析之LocatedFileStatusFetcher

        LocatedFileStatusFetcher是MapReduce中一个针对给定输入路径数组,使用配置的线程数目来获取数据块位置的实用类。它的主要作用就是利用多线程技术,每个线程对应一个任务,每个任务针对给定输入路径数组Path[],解析出文件状态列表队列BlockingQueue<List<FileStatus>>。其中,输入数据输入路径只不过是一个Path,而输出数据则是文件状态列表队列BlockingQueue<List<FileStatus>>,文件状态FileStatus包含文件路径、长度、数据块大小、数据块副本数、文件所属用户、文件所属组、文件权限、文件最近修改时间、文件最近访问时间、是否为目录等信息。

        LocatedFileStatusFetcher采用了google并发编程包中的可监听Future模式ListenableFuture、可监听线程池ListeningExecutorService、回调函数FutureCallback,并使用了Java并发包中的可重入互斥锁ReentrantLock、多线程间协调通信工具Condition等实现了处理过程的多线程并发执行,并通过阻塞队列、回调函数等解决了目录的递归解析问题,是一种非常好的多线程环境下递归任务、可监听任务的实现。

        那么,MapReduce中LocatedFileStatusFetcher是如何实现的呢?本文将为你带来LocatedFileStatusFetcher的源码分析。

        首先,看下LocatedFileStatusFetcher的成员变量,代码如下:

  // 输入路径数组
  private final Path[] inputDirs;
  // 输入路径过滤器
  private final PathFilter inputFilter;
  // 配置信息
  private final Configuration conf;
  // 递归标志位
  private final boolean recursive;
  // 使用MR新API标志位
  private final boolean newApi;

  // 底层线程池rawExec
  private final ExecutorService rawExec;

  // 可监听线程池,基于底层线程池rawExec
  private final ListeningExecutorService exec;

  // 文件状态列表阻塞队列
  private final BlockingQueue<List<FileStatus>> resultQueue;

  // 无效输入路径错误相关IO异常列表
  private final List<IOException> invalidInputErrors = new LinkedList<IOException>();

  // 处理原始输入路径回调函数
  private final ProcessInitialInputPathCallback processInitialInputPathCallback =
      new ProcessInitialInputPathCallback();

  // 处理输入路径回调函数
  private final ProcessInputDirCallback processInputDirCallback =
      new ProcessInputDirCallback();

  // 正在运行任务数原子计数器
  private final AtomicInteger runningTasks = new AtomicInteger(0);

  // 可重入互斥锁
  private final ReentrantLock lock = new ReentrantLock();

  // 多线程间协调通信工具Condition
  private final Condition condition = lock.newCondition();

  // 任务执行过程中未知错误
  private volatile Throwable unknownError;

        LocatedFileStatusFetcher的成员变量比较多,但是大体可以分为以下几类:

        一、实现基本功能的输入、输出成员变量

        1、Path[] inputDirs:输入路径数组,其作为整体输入数据,每个最终路径都会被LocatedFileStatusFetcher解析成文件状态FileStatus;

        2、PathFilter inputFilter:输入路径过滤器,内置boolean accept(Path path)方法,对输入路径继续过滤,选取符合业务规则的路径;

        3、Configuration conf:配置信息,可以从中获取执行任务的线程数;

        4、boolean recursive:递归标志位,true表示对目录中的目录进行递归处理;

        5、boolean newApi:使用MR新API标志位;

        6、BlockingQueue<List<FileStatus>> resultQueue:文件状态列表阻塞队列,输出数据,即最终返回结果;

        二、多线程需要使用的成员变量

        1、ExecutorService rawExec:底层线程池;

        2、ListeningExecutorService exec:基于底层线程池rawExec的可监听线程池,利用google的并发编程包实现;

        3、ProcessInitialInputPathCallback processInitialInputPathCallback:处理原始输入路径回调函数;

        4、ProcessInputDirCallback processInputDirCallback:处理输入路径回调函数;

        5、AtomicInteger runningTasks:正在运行任务数原子计数器;

        6、ReentrantLock lock:ReentrantLock lock;

        7、Condition condition:多线程间协调通信工具;

        三、存放中间结果或异常的成员变量

        1、List<IOException> invalidInputErrors:无效输入路径错误相关IO异常列表;

        2、Throwable unknownError:任务执行过程中未知错误;

        再看下LocatedFileStatusFetcher的构造函数,代码如下:

  /**
   * 构造函数
   *
   * @param conf configuration for the job
   * @param dirs the initial list of paths
   * @param recursive whether to traverse the patchs recursively
   * @param inputFilter inputFilter to apply to the resulting paths
   * @param newApi whether using the mapred or mapreduce API
   * @throws InterruptedException
   * @throws IOException
   */
  public LocatedFileStatusFetcher(Configuration conf, Path[] dirs,
      boolean recursive, PathFilter inputFilter, boolean newApi) throws InterruptedException,
      IOException {

	// 获取配置信息中的任务使用线程数numThreads,取参数mapreduce.input.fileinputformat.list-status.num-threads,参数未配置默认为1,
	// 这里很明显应该会大于1
    int numThreads = conf.getInt(FileInputFormat.LIST_STATUS_NUM_THREADS,
        FileInputFormat.DEFAULT_LIST_STATUS_NUM_THREADS);

    // 使用Executors.newFixedThreadPool方式构造线程池rawExec,线程个数为numThreads,并且设置为后台线程,线程名格式为GetFileInfo #数字
    rawExec = Executors.newFixedThreadPool(
        numThreads,
        new ThreadFactoryBuilder().setDaemon(true)
            .setNameFormat("GetFileInfo #%d").build());

    // 使用MoreExecutors.listeningDecorator方式利用rawExec构造可监听线程池exec
    exec = MoreExecutors.listeningDecorator(rawExec);

    // 初始化最终返回结果数据结构,即文件状态列表的链式阻塞队列resultQueue
    resultQueue = new LinkedBlockingQueue<List<FileStatus>>();

    // 根据构造函数入参初始化类成员变量,这些成员变量包括输入路径数组、配置信息、递归标志位等全部是外部输入数据
    this.conf = conf;
    this.inputDirs = dirs;
    this.recursive = recursive;
    this.inputFilter = inputFilter;
    this.newApi = newApi;
  }

        LocatedFileStatusFetcher构造函数逻辑很清晰,大体如下:

        1、首先获取配置信息中的任务使用线程数numThreads:

              取参数mapreduce.input.fileinputformat.list-status.num-threads,参数未配置默认为1,这里很明显应该会大于1;

        2、使用Executors.newFixedThreadPool方式构造线程池rawExec,线程个数为numThreads,并且设置为后台线程,线程名格式为GetFileInfo #数字;

        3、使用MoreExecutors.listeningDecorator方式利用rawExec构造可监听线程池exec;

        4、初始化最终返回结果数据结构,即文件状态列表的链式阻塞队列resultQueue;

        5、根据构造函数入参初始化类成员变量,这些成员变量包括输入路径数组、配置信息、递归标志位等全部是外部输入数据。

        到了这里,您已经大概了解了LocatedFileStatusFetcher的结构。但是,您可能对Java并发编程或者google的可监听并发编程不是很了解,为此,这里有必要做个简单介绍,详细信息,读者可通过相关搜索引擎或书籍自行补脑。

        首先说下Future,Future表示一个异步计算任务,当任务完成时可以得到任务执行结果。您可能需要借助Future,通过启用另外的线程不断的查询任务状态,在任务完成时,获取任务执行结果通知或者展示给用户。而google的ListenableFuture顾名思义就是可以监听的Future,通过它在任务完成后自动调用配置好的回调函数,您就可以很方便的及时获取任务执行结果,采取下一步处理,这些回调函数统一都需要实现FutureCallback接口。

        再来说下可重入互斥锁ReentrantLock,它是一个独占锁,即互斥的,意即当前线程获取该锁后,其他线程此时如果想要获取该锁,就必须等待当前线程释放锁。何谓可重入呢?也很简单,当前线程获取该锁后,未释放前,还可以再次获得或者说进入该锁。

        第三个要说的是Condition,它是一个多线程间协调通信的工具类。通过其await()方法,当前线程会释放锁,进入睡眠,等待被唤醒;而其他线程借助Condition的signal()或signalAll()方法,则可以唤醒等待的线程,继续进行相关逻辑处理。

        最后一个要说的是ListeningExecutorService,它是一个可以返回ListenableFuture的接口,其借助Java并发包中的ExecutorService,就可以实现一个可监听的线程池,而本例中的底层线程池是Executors.newFixedThreadPool,它是一个可重用固定线程数的线程池,以共享的无界队列方式来运行这些线程。ListeningExecutorService中可以提交一些实现了Callable接口的线程任务,这些线程任务会被线程池调度,借助其call()方法完成任务执行逻辑。

        截至到目前,相信您应该LocatedFileStatusFetcher使用的并发编程的一些基础知识有一个大致了解了吧!

        好了,我们继续往下分析吧!看先LocatedFileStatusFetcher实现其核心功能的getFileStatuses()方法,代码如下:

  /**
   * Start executing and return FileStatuses based on the parameters specified
   * 基于指定参数开始执行任务,并返回文件状态迭代器
   *
   * @return fetched file statuses
   * @throws InterruptedException
   * @throws IOException
   */
  public Iterable<FileStatus> getFileStatuses() throws InterruptedException,
      IOException {
    // Increment to make sure a race between the first thread completing and the
    // rest being scheduled does not lead to a termination.

	// 正在运行任务数原子计数器runningTasks加1
    runningTasks.incrementAndGet();

    // 遍历输入路径inputDirs
    for (Path p : inputDirs) {

      // 正在运行任务数原子计数器runningTasks加1
      runningTasks.incrementAndGet();

      // 将处理原始输入路径任务ProcessInitialInputPathCallable提交到线程池exec中去执行,并获取可监听Future,即ListenableFuture,
      // 监听任务执行结果ProcessInitialInputPathCallable.Result
      ListenableFuture<ProcessInitialInputPathCallable.Result> future = exec
          .submit(new ProcessInitialInputPathCallable(p, conf, inputFilter));

      // future中添加回调函数ProcessInitialInputPathCallback实例processInitialInputPathCallback
      Futures.addCallback(future, processInitialInputPathCallback);
    }

    // 正在运行任务数原子计数器runningTasks减1
    runningTasks.decrementAndGet();

    // 获取可重入互斥锁ReentrantLock实例lock
    lock.lock();
    try {

      // 正在运行任务数原子计数器runningTasks不为0,且未知错误unknownError没有发生时
      while (runningTasks.get() != 0 && unknownError == null) {

    	// 等待所有任务运行完成
        condition.await();
      }
    } finally {

      // 释放可重入互斥锁ReentrantLock
      lock.unlock();
    }

    // 停止线程池exec
    this.exec.shutdownNow();

    // 有未知错误unknownError的话处理未知错误
    if (this.unknownError != null) {
      if (this.unknownError instanceof Error) {
        throw (Error) this.unknownError;
      } else if (this.unknownError instanceof RuntimeException) {
        throw (RuntimeException) this.unknownError;
      } else if (this.unknownError instanceof IOException) {
        throw (IOException) this.unknownError;
      } else if (this.unknownError instanceof InterruptedException) {
        throw (InterruptedException) this.unknownError;
      } else {
        throw new IOException(this.unknownError);
      }
    }

    // 有无效路径错误invalidInputErrors的话处理无效路径错误
    if (this.invalidInputErrors.size() != 0) {
      if (this.newApi) {
        throw new org.apache.hadoop.mapreduce.lib.input.InvalidInputException(
            invalidInputErrors);
      } else {
        throw new InvalidInputException(invalidInputErrors);
      }
    }

    // 将结果队列resultQueue转换成迭代器并返回
    return Iterables.concat(resultQueue);
  }

        getFileStatuses()方法的执行逻辑大体如下:

        1、首先,正在运行任务数原子计数器runningTasks加1,这个是针对主线程任务的计数;

        2、接着遍历输入路径inputDirs:

              2.1、正在运行任务数原子计数器runningTasks加1,这个是针对每个待处理输入路径的子线程任务的计数;

              2.2、将处理原始输入路径任务提交到线程池exec中去执行,并获取可监听Future,即ListenableFuture,监听任务执行结果:

                       这里,原始输入路径任务为ProcessInitialInputPathCallable,它实现了Callable接口,并有一个内部静态类Result,作为任务处理结果,稍后我们对它做详细分析;

              2.3、future中添加回调函数,待任务处理完成后通过回调函数做进一步处理:

                       这里,回调函数为ProcessInitialInputPathCallback,即处理原始输入路径的回调函数,其实现了FutureCallback接口,并对上述任务执行结果ProcessInitialInputPathCallable.Result进行回调处理;

        3、正在运行任务数原子计数器runningTasks减1,这个是针对主线程任务的计数,含义是主线程任务在其它子线程任务全部执行完成的情况下可以标记为处理完成;

        4、获取可重入互斥锁ReentrantLock实例lock;

        5、当正在运行任务数原子计数器runningTasks不为0,且未知错误unknownError没有发生时,通过condition.await()方法,释放当前锁,进入睡眠,等待被唤醒,直到其他线程唤醒它,并且正在运行任务数原子计数器runningTasks为0,或者未知错误unknownError发生,才说明所有任务已执行完成或不得不终止运行;

        6、释放可重入互斥锁ReentrantLock;

        7、停止线程池exec;

        8、有未知错误unknownError的话处理未知错误;

        9、有无效路径错误invalidInputErrors的话处理无效路径错误;

        10、将结果队列resultQueue转换成迭代器并返回。

        我们先说下这个原始输入路径任务为ProcessInitialInputPathCallable,它实现了Callable接口,并有一个内部静态类Result,作为任务处理结果,代码如下:

  /**
   * Processes an initial Input Path pattern through the globber and PathFilter
   * to generate a list of files which need further processing.
   * 通过globber和路径过滤器PathFilter处理一个初始输入路径模式,产生一个需要进一步处理的文件列表。
   */
  private static class ProcessInitialInputPathCallable implements
      Callable<ProcessInitialInputPathCallable.Result> {

	// 待处理路径
    private final Path path;

    // 配置信息
    private final Configuration conf;

    // 输入路径过滤器
    private final PathFilter inputFilter;

    public ProcessInitialInputPathCallable(Path path, Configuration conf,
        PathFilter pathFilter) {
      this.path = path;
      this.conf = conf;
      this.inputFilter = pathFilter;
    }

    @Override
    public Result call() throws Exception {

      // 构造任务结果Result实例result
      Result result = new Result();

      // 从路径path中获取文件系统FileSystem实例fs
      FileSystem fs = path.getFileSystem(conf);

      // 设置任务结果Result实例result中的fs变量
      result.fs = fs;

      // 通过文件系统FileSystem实例fs的globStatus()方法,将路径path依据输入路径过滤器inputFilter解析成文件状态FileStatus数组matches
      FileStatus[] matches = fs.globStatus(path, inputFilter);

      if (matches == null) {
    	// 如果文件状态FileStatus数组matches为null,说明路径根本不存在,将IO异常通过addError()方法添加到result中
        result.addError(new IOException("Input path does not exist: " + path));
      } else if (matches.length == 0) {
    	// 如果文件状态FileStatus数组matches不为null,但长度为0,说明路径存在但是没有通过过滤器过滤规则,将IO异常通过addError()方法添加到result中
        result.addError(new IOException("Input Pattern " + path
            + " matches 0 files"));
      } else {

    	// 将符合过滤规则的文件状态FileStatus数组matches赋值给任务结果result的matchedFileStatuses
        result.matchedFileStatuses = matches;
      }
      return result;
    }

    private static class Result {

      // 处理过程中发生的IO异常列表errors
      private List<IOException> errors;

      // 匹配的文件状态数组matchedFileStatuses
      private FileStatus[] matchedFileStatuses;

      // 文件系统实例
      private FileSystem fs;

      // 添加IO异常到errors列表
      void addError(IOException ioe) {
        if (errors == null) {
          errors = new LinkedList<IOException>();
        }
        errors.add(ioe);
      }
    }
  }

        我们看到,它有三个成员变量,待处理路径path、配置信息conf、输入路径过滤器inputFilter,并且构造方法就是简单的根据入参初始化这三个成员变量。ProcessInitialInputPathCallable还提供了一个表示任务结果的内部静态类Result,它也有三个成员变量,处理过程中发生的IO异常列表errors、匹配的文件状态匹配的文件状态数组matchedFileStatuses数组matchedFileStatuses、文件系统实例fs,并提供了添加IO异常到errors列表的addError()方法。

        重点看下 ProcessInitialInputPathCallable的call()方法,它是任务得以执行的入口方法,其大体逻辑如下:

        1、构造任务结果Result实例result;

        2、从路径path中获取文件系统FileSystem实例fs;

        3、设置任务结果Result实例result中的fs变量;

        4、通过文件系统FileSystem实例fs的globStatus()方法,将路径path依据输入路径过滤器inputFilter解析成文件状态FileStatus数组matches:

              这里,限于篇幅及主题明确性,我们不做过多介绍,你只要知道它的主要作用就行,我们将在单线程处理的博文中进行详细介绍;

        5、根据matches分别处理任务执行结果:

              5.1、如果文件状态FileStatus数组matches为null,说明路径根本不存在,将IO异常通过addError()方法添加到result中;

              5.2、如果文件状态FileStatus数组matches不为null,但长度为0,说明路径存在但是没有通过过滤器过滤规则,将IO异常通过addError()方法添加到result中;

              5.3、否则将符合过滤规则的文件状态FileStatus数组matches赋值给任务结果result的matchedFileStatuses;

        6、返回任务结果result。

        原始路径处理任务执行完成的回调函数则是通过ProcessInitialInputPathCallback来定义的,代码如下:

  /**
   * The callback handler to handle results generated by
   * {@link ProcessInitialInputPathCallable}
   *
   */
  private class ProcessInitialInputPathCallback implements
      FutureCallback<ProcessInitialInputPathCallable.Result> {

	// 任务执行成功时:不是说结果对错,而是说任务能完整的执行下来
    @Override
    public void onSuccess(ProcessInitialInputPathCallable.Result result) {
      try {

    	// 如果任务结果有IO异常
        if (result.errors != null) {

          // 通过registerInvalidInputError()方法,将IO异常列表errors全部添加到无效输入路径错误相关IO异常列表invalidInputErrors中
          registerInvalidInputError(result.errors);
        }

        // 如果任务结果得到了匹配的文件状态数组
        if (result.matchedFileStatuses != null) {

          // 遍历匹配的文件状态数组matchedFileStatuses,取出每个文件状态FileStatus实例matched,做以下处理:
          for (FileStatus matched : result.matchedFileStatuses) {

        	// 正在运行任务数原子计数器runningTasks加1,这里标识的是子任务数加1
            runningTasks.incrementAndGet();

            // 将处理输入路径任务ProcessInputDirCallable提交到线程池exec中去执行,并获取可监听Future,即ListenableFuture,
            // 监听任务执行结果ProcessInputDirCallable.Result
            ListenableFuture<ProcessInputDirCallable.Result> future = exec
                .submit(new ProcessInputDirCallable(result.fs, matched,
                    recursive, inputFilter));

            // future中添加回调函数ProcessInputDirCallback实例processInputDirCallback
            Futures.addCallback(future, processInputDirCallback);
          }
        }

        // 解析原始路径的任务完成,调用decrementRunningAndCheckCompletion()做后续处理工作:
        // 正在运行任务数原子计数器减1,并判断是否为0,为0,说明全部任务运行完成,通过condition.signal()通知主线程进行处理
        decrementRunningAndCheckCompletion();
      } catch (Throwable t) { // Exception within the callback

    	// 有异常的话,调用registerError()方法,重置任务执行过程中未知错误unknownError,并通过condition.signal()通知主线程,
        // 有未知错误发生,交由主线程处理(主线程在有位置错误unknownError的情况下会结束整个流程),潜台词就是第一次发生未知错误时,不会通知主线程结束整个流程,后续再发生时才会通知
        registerError(t);
      }
    }

    // 任务执行失败时:不是说结果对错,而是说任务不能完整的执行下来
    @Override
    public void onFailure(Throwable t) {
      // Any generated exceptions. Leads to immediate termination.
      // 调用registerError()方法,重置任务执行过程中未知错误unknownError,并通过condition.signal()通知主线程,
      // 有未知错误发生,交由主线程处理(主线程在有位置错误unknownError的情况下会结束整个流程),潜台词就是第一次发生未知错误时,不会通知主线程结束整个流程,后续再发生时才会通知
      registerError(t);
    }
  }

          原始路径处理任务执行完成的回调函数ProcessInitialInputPathCallback实现了FutureCallback接口,并对原始路径处理任务结果ProcessInitialInputPathCallable.Result进行检测处理,主要分为两种情况:

        1、任务执行成功时:不是说结果对错,而是说任务能完整的执行下来

              通过onSuccess()方法来处理,大体逻辑如下:

              1.1、如果任务结果有IO异常,通过registerInvalidInputError()方法,将IO异常列表errors全部添加到无效输入路径错误相关IO异常列表invalidInputErrors中;

              1.2、如果任务结果得到了匹配的文件状态数组,遍历匹配的文件状态数组matchedFileStatuses,取出每个文件状态FileStatus实例matched,做以下处理:

                        1.2.1、正在运行任务数原子计数器runningTasks加1,这里标识的是子任务数加1;

                        1.2.2、将处理输入路径任务ProcessInputDirCallable提交到线程池exec中去执行,并获取可监听Future,即ListenableFuture,监听任务执行结果ProcessInputDirCallable.Result:

                                     这里的ProcessInputDirCallable任务,主要是为给定文件状态FileStatus获取数据块位置,如有必要(即需要递归目录进行处理),添加额外的路径到处理队列,后续递归处理,而给定文件状态FileStatus则是通过解析原始路径任务ProcessInitialInputPathCallable来获得的;

                        1.2.3、future中添加回调函数ProcessInputDirCallback实例processInputDirCallback;

             1.3、解析原始路径的任务完成,调用decrementRunningAndCheckCompletion()做后续处理工作:正在运行任务数原子计数器减1,并判断是否为0,为0,说明全部任务运行完成,通过condition.signal()通知主线程进行处理;

              需要说明的是,上述逻辑执行期间,如果有Throwable发生,则会调用registerError()方法,至于如何处理,参见2任务执行失败时的处理;

        2、任务执行失败时:不是说结果对错,而是说任务不能完整的执行下来

              通过onFailure()方法来处理,调用registerError()方法,重置任务执行过程中未知错误unknownError,并通过condition.signal()通知主线程,有未知错误发生,交由主线程处理(主线程在有位置错误unknownError的情况下会结束整个流程),潜台词就是第一次发生未知错误时,不会通知主线程结束整个流程,后续再发生时才会通知。

        decrementRunningAndCheckCompletion()方法代码如下:

  private void decrementRunningAndCheckCompletion() {

	// 获取可重入互斥锁lock
	lock.lock();
    try {

      // 正在运行任务数原子计数器减1,并判断是否为0,为0,说明全部任务运行完成,通过condition.signal()通知主线程进行处理
      if (runningTasks.decrementAndGet() == 0) {
        condition.signal();
      }
    } finally {

      // 释放可重入互斥锁lock
      lock.unlock();
    }
  }

        而registerError()方法代码如下:

  /**
   * Register fatal errors - example an IOException while accessing a file or a
   * full exection queue
   */
  private void registerError(Throwable t) {

	// 获取可重入互斥锁lock
	lock.lock();
    try {

      // 重置任务执行过程中未知错误unknownError,并通过condition.signal()通知主线程,
      // 有未知错误发生,交由主线程处理(主线程在有位置错误unknownError的情况下会结束整个流程)
      if (unknownError != null) {
        unknownError = t;
        condition.signal();
      }

    } finally {

      // 释放可重入互斥锁lock
      lock.unlock();
    }
  }

        两个方法功能很明确,注释也很详细,且上面已经提到过,这里不再赘述!

        接下来,我们再看下ProcessInputDirCallable任务,它主要是为给定文件状态FileStatus获取数据块位置,如有必要(即需要递归目录进行处理),添加额外的路径到处理队列,后续递归处理,其实现如下:

  /**
   * Retrieves block locations for the given @link {@link FileStatus}, and adds
   * additional paths to the process queue if required.
   * 为给定文件状态获取数据块位置,如有必要,添加额外的路径到处理队列。
   */
  private static class ProcessInputDirCallable implements
      Callable<ProcessInputDirCallable.Result> {

	// 文件系统实例
    private final FileSystem fs;

    // 文件状态实例
    private final FileStatus fileStatus;

    // 递归标志位
    private final boolean recursive;

    // 输入路径过滤器
    private final PathFilter inputFilter;

    // 构造函数
    ProcessInputDirCallable(FileSystem fs, FileStatus fileStatus,
        boolean recursive, PathFilter inputFilter) {
      this.fs = fs;
      this.fileStatus = fileStatus;
      this.recursive = recursive;
      this.inputFilter = inputFilter;
    }

    // 任务执行主方法
    @Override
    public Result call() throws Exception {

      // 构造结果Result
      Result result = new Result();

      // 初始化结果中的文件系统实例fs
      result.fs = fs;

      // 如果文件状态fileStatus对应为目录
      if (fileStatus.isDirectory()) {

    	// 通过文件系统FileSystem实例fs的listLocatedStatus()方法获取fileStatus对应的带数据块位置信息文件状态迭代器iter
    	RemoteIterator<LocatedFileStatus> iter = fs
            .listLocatedStatus(fileStatus.getPath());

    	// 通过迭代器iter遍历每个带数据块位置信息文件状态stat
    	while (iter.hasNext()) {
          LocatedFileStatus stat = iter.next();

          // 通过输入路径过滤器的accept()方法进行过滤
          if (inputFilter.accept(stat.getPath())) {

        	// 如果需要递归,且stat为目录
        	if (recursive && stat.isDirectory()) {

              // 添加到结果result的dirsNeedingRecursiveCalls列表
              result.dirsNeedingRecursiveCalls.add(stat);
            } else {

              // 否则添加到结果result的locatedFileStatuses列表
              result.locatedFileStatuses.add(stat);
            }
          }
        }
      } else {

    	// 如果文件状态fileStatus对应为文件,直接添加到结果result的locatedFileStatuses列表
        result.locatedFileStatuses.add(fileStatus);
      }
      return result;
    }

    // 处理结果
    private static class Result {

      // 已处理完的文件状态链表locatedFileStatuses
      private List<FileStatus> locatedFileStatuses = new LinkedList<FileStatus>();
      // 需要递归的文件状态链表dirsNeedingRecursiveCalls
      private List<FileStatus> dirsNeedingRecursiveCalls = new LinkedList<FileStatus>();

      // 文件系统实例
      private FileSystem fs;
    }
  }

        首先,ProcessInputDirCallable内部有四个成员变量,分别是文件系统实例fs、文件状态实例fileStatus、递归标志位recursive、输入路径过滤器inputFilter,意义都很明确,而构造方法也是根据入参初始化这四个成员变量,不再详述。

        任务执行结果由其静态内部类Result来表示,它包含三个成员变量,已处理完的文件状态链表locatedFileStatuses、需要递归再处理的文件状态链表dirsNeedingRecursiveCalls、文件系统实例fs,意义都很明确,不再详述。

        接下来,我们再看下任务执行的入口方法call()的运行逻辑,归纳如下:

        1、构造任务运行结果Result实例result;

        2、初始化结果中的文件系统实例fs;

        3、如果文件状态fileStatus对应为目录:

              3.1、通过文件系统FileSystem实例fs的listLocatedStatus()方法获取fileStatus对应的带数据块位置信息文件状态迭代器iter:

                       文件系统FileSystem实例fs的listLocatedStatus()方法我们会在单线程任务重点描述,这里你只要记住它的主要功能就是根据文件状态获取数据块位置信息,并返回带数据块位置信息文件状态迭代器,而带数据块位置信息文件状态LocatedFileStatus是文件状态FileStatus的子类,其内部多了一个成员变量BlockLocation[] locations,表示文件所含数据块的位置信息;

              3.2、通过迭代器iter遍历每个带数据块位置信息文件状态stat:通过输入路径过滤器的accept()方法进行过滤,如果需要递归,且stat为目录,添加到结果result的dirsNeedingRecursiveCalls列表,否则添加到结果result的locatedFileStatuses列表;

        4、如果文件状态fileStatus对应为文件,直接添加到结果result的locatedFileStatuses列表;

        5、返回任务执行结果result。

        如同上面提到的解析原始路径任务ProcessInitialInputPathCallable一样,ProcessInputDirCallable任务也需要在任务执行完成后有回调函数做进一步处理,而这个回调函数是通过ProcessInputDirCallback来实现的,代码如下:

  /**
   * The callback handler to handle results generated by
   * {@link ProcessInputDirCallable}. This populates the final result set.
   *
   */
  private class ProcessInputDirCallback implements
      FutureCallback<ProcessInputDirCallable.Result> {

	// 任务执行完成时:不是说结果对错,而是说任务能完整的执行下来
    @Override
    public void onSuccess(ProcessInputDirCallable.Result result) {
      try {

    	// 如果任务执行结果中已处理完的文件状态链表locatedFileStatuses有数据的话,将其添加到最终返回结果队列resultQueue中
        if (result.locatedFileStatuses.size() != 0) {
          resultQueue.add(result.locatedFileStatuses);
        }

        // 如果任务执行结果中需要递归再处理的文件状态链表dirsNeedingRecursiveCalls,再次提交ProcessInputDirCallable任务到线程池ProcessInputDirCallable,
        // runningTasks计数器加1,添加回调函数ProcessInputDirCallback,以实现迭代处理
        if (result.dirsNeedingRecursiveCalls.size() != 0) {
          for (FileStatus fileStatus : result.dirsNeedingRecursiveCalls) {
            runningTasks.incrementAndGet();
            ListenableFuture<ProcessInputDirCallable.Result> future = exec
                .submit(new ProcessInputDirCallable(result.fs, fileStatus,
                    recursive, inputFilter));
            Futures.addCallback(future, processInputDirCallback);
          }
        }

        // 解析路径的任务完成,调用decrementRunningAndCheckCompletion()做后续处理工作:
        // 正在运行任务数原子计数器减1,并判断是否为0,为0,说明全部任务运行完成,通过condition.signal()通知主线程进行处理
        decrementRunningAndCheckCompletion();
      } catch (Throwable t) { // Error within the callback itself.

    	// 有异常的话,调用registerError()方法,重置任务执行过程中未知错误unknownError,并通过condition.signal()通知主线程,
        // 有未知错误发生,交由主线程处理(主线程在有位置错误unknownError的情况下会结束整个流程),潜台词就是第一次发生未知错误时,不会通知主线程结束整个流程,后续再发生时才会通知
        registerError(t);
      }
    }

    // 任务执行失败时:不是说结果对错,而是说任务不能完整的执行下来
    @Override
    public void onFailure(Throwable t) {
      // Any generated exceptions. Leads to immediate termination.

      // 调用registerError()方法,重置任务执行过程中未知错误unknownError,并通过condition.signal()通知主线程,
      // 有未知错误发生,交由主线程处理(主线程在有位置错误unknownError的情况下会结束整个流程),潜台词就是第一次发生未知错误时,不会通知主线程结束整个流程,后续再发生时才会通知
      registerError(t);
    }
  }

        ProcessInputDirCallbacky如同上面介绍的ProcessInitialInputPathCallback一样,也分成功、失败两种情况分别进行处理:

        1、任务执行成功时:不是说结果对错,而是说任务能完整的执行下来

              通过onSuccess()方法来处理,大体逻辑如下:

              1.1、如果任务执行结果中已处理完的文件状态链表locatedFileStatuses有数据的话,将其添加到最终返回结果队列resultQueue中;

              1.2、如果任务执行结果中需要递归再处理的文件状态链表dirsNeedingRecursiveCalls,再次提交ProcessInputDirCallable任务到线程池ProcessInputDirCallable,runningTasks计数器加1,添加回调函数ProcessInputDirCallback,以实现迭代处理;

             1.3、解析路径的任务完成,调用decrementRunningAndCheckCompletion()做后续处理工作:正在运行任务数原子计数器减1,并判断是否为0,为0,说明全部任务运行完成,通过condition.signal()通知主线程进行处理;

              需要说明的是,上述逻辑执行期间,如果有Throwable发生,则会调用registerError()方法,至于如何处理,参见2任务执行失败时的处理;

        2、任务执行失败时:不是说结果对错,而是说任务不能完整的执行下来

              通过onFailure()方法来处理,调用registerError()方法,重置任务执行过程中未知错误unknownError,并通过condition.signal()通知主线程,有未知错误发生,交由主线程处理(主线程在有位置错误unknownError的情况下会结束整个流程),潜台词就是第一次发生未知错误时,不会通知主线程结束整个流程,后续再发生时才会通知。

        至此,整个LocatedFileStatusFetcher的源码分析介绍完毕。

        总结

        LocatedFileStatusFetcher通过多线程的方式,实现了针对给定输入路径数组,使用配置的线程数目来获取数据块位置的核心功能。它通过google的可监听并发技术ListenableFuture、ListeningExecutorService,实现了两层级别的子任务的并发执行、结果监听与回调处理,第一层任务是ProcessInitialInputPathCallable,根据输入路径获取对应文件状态,第二层任务是ProcessInputDirCallable,根据文件状态获取带数据块位置信息的文件状态,每层任务都有一个静态内部类Result来很好的抽象任务运行结果。每层任务都有一个回调函数,在获得任务执行结果后做进一步处理,并且第一层任务执行结束后,在回调函数里提交第二层任务,且第二层任务会根据是否递归的标志位和实际路径情况,在在回调函数里决定是否递归提交第二层任务。另外,LocatedFileStatusFetcher还使用了可重入互斥锁ReentrantLock、多线程间协调通信工具Condition来解决多线程之间的并发同步问题,特别是主任务线程与子任务线程间的主从协调、通信等。不得不说,LocatedFileStatusFetcher是多线程处理递归任务一种非常好的实现,值得我们借鉴和学习!

时间: 2024-10-26 03:19:29

MapReduce源码分析之LocatedFileStatusFetcher的相关文章

MapReduce源码分析之新API作业提交(二):连接集群

         MapReduce作业提交时连接集群是通过Job的connect()方法实现的,它实际上是构造集群Cluster实例cluster,代码如下: private synchronized void connect() throws IOException, InterruptedException, ClassNotFoundException { // 如果cluster为null,构造Cluster实例cluster, // Cluster为连接MapReduce集群的一种工

MapReduce源码分析之JobSubmitter(一)

        JobSubmitter,顾名思义,它是MapReduce中作业提交者,而实际上JobSubmitter除了构造方法外,对外提供的唯一一个非private成员变量或方法就是submitJobInternal()方法,它是提交Job的内部方法,实现了提交Job的所有业务逻辑.本文,我们将深入研究MapReduce中用于提交Job的组件JobSubmitter.         首先,我们先看下JobSubmitter的类成员变量,如下: // 文件系统FileSystem实例 pr

MapReduce源码分析之JobSplitWriter

        JobSplitWriter被作业客户端用于写分片相关文件,包括分片数据文件job.split和分片元数据信息文件job.splitmetainfo.它有两个静态成员变量,如下: // 分片版本,当前默认为1 private static final int splitVersion = JobSplit.META_SPLIT_VERSION; // 分片文件头部,为UTF-8格式的字符串"SPL"的字节数组"SPL" private static

MapReduce源码分析之作业Job状态机解析(一)简介与正常流程浅析

        作业Job状态机维护了MapReduce作业的整个生命周期,即从提交到运行结束的整个过程.Job状态机被封装在JobImpl中,其主要包括14种状态和19种导致状态发生的事件.         作业Job的全部状态维护在类JobStateInternal中,如下所示: public enum JobStateInternal { // 作业新建状态,当作业Job被新创建时所处的状态 NEW, // 作业启动状态,此时运行时间已被设置,任务处于开始被调度阶段 SETUP, // 作

MapReduce源码分析之InputFormat

        InputFormat描述了一个Map-Reduce作业中的输入规范.Map-Reduce框架依靠作业的InputFormat实现以下内容:         1.校验作业的输入规范:         2.分割输入文件(可能为多个),生成逻辑输入分片InputSplit(往往为多个),每个输入分片InputSplit接着被分配给单独的Mapper:         3.提供记录读取器RecordReader的实现,RecordReader被用于从逻辑输入分片InputSplit收集

MapReduce源码分析之Task中关于对应TaskAttempt存储Map方案的一些思考

        我们知道,MapReduce有三层调度模型,即Job-->Task-->TaskAttempt,并且:         1.通常一个Job存在多个Task,这些Task总共有Map Task和Redcue Task两种大的类型(为简化描述,Map-Only作业.JobSetup Task等复杂的情况这里不做考虑):         2.每个Task可以尝试运行1-n此,而且通常很多情况下都是1次,只有当开启了推测执行原理且存在拖后腿Task,或者Task之前执行失败时,Task

Hadoop2源码分析-MapReduce篇

1.概述 前面我们已经对Hadoop有了一个初步认识,接下来我们开始学习Hadoop的一些核心的功能,其中包含mapreduce,fs,hdfs,ipc,io,yarn,今天为大家分享的是mapreduce部分,其内容目录如下所示: MapReduce V1 MapReduce V2 MR V1和MR V2的区别 MR V2的重构思路 本篇文章的源码是基于hadoop-2.6.0-src.tar.gz来完成的.代码下载地址,请参考<Hadoop2源码分析-准备篇>. 2.MapReduce V

《MapReduce 2.0源码分析与编程实战》一导读

前 言 MapReduce 2.0源码分析与编程实战 我们处于一个数据大爆炸的时代.每时每刻.各行各业都在产生和积累海量的数据内容.这些数据中蕴含着进行业务活动.获取商业信息.做出管理决策的重要信息.如何处理这些数据并获取有价值的信息,是众多组织和单位面临的共同问题.而这个问题的解决又依赖两项技术,一是能够对产生的业务数据进行统一管理和综合,并且能够无限扩展存储空间:二是能够有效处理获得的海量数据,在限定时间内获得处理结果的处理程序. 因此,寻求一个合理可靠的大数据处理决方案是目前数据处理的热点

Yarn源码分析之MRAppMaster上MapReduce作业处理总流程(二)

        本文继<Yarn源码分析之MRAppMaster上MapReduce作业处理总流程(一)>,接着讲述MapReduce作业在MRAppMaster上处理总流程,继上篇讲到作业初始化之后的作业启动,关于作业初始化主体流程的详细介绍,请参见<Yarn源码分析之MRAppMaster上MapReduce作业初始化解析>一文.         (三)启动         作业的启动是通过MRAppMaster的startJobs()方法实现的,其代码如下: /** * Th