HDFS文件目录list操作加速优化 前言 当前List操作的问题 社区解决方案 参考资料


在我们使用HDFS作为数据存储文件系统时,恐怕最常使用到的命令就是ls命令了。我们往往先使用这个命令查找出目前我们期待的文件目录信息,然后对查出的这些文件目录做后续的操作。所以说,list操作的执行效率高低对用户以及上层应用层调用程序来说就显得十分重要了。

当前List操作的问题


这里我们会从2个层面来展开此方面的讨论:

  • 一个是针使用用户的角度,也就是CLI命令行使用方式的。
  • 另一个就是针对应用程序调用底层API获取目录信息的。

先来看第一种情况,当使用ls命令行去查询目录树比较深并且其下子文件又比较多的时候,就会出现返回信息特别缓慢的情况。给到用户的第一感觉就是命令行执行了好几十秒,还是没有结果出来。这种情况,我们最直观的优化点,就是能够更快速地出结果。

再来看第二种情况,当然啦,第一钟情况里提到的情况也会包含在第二种情况里,但是额外地,它还有另外一个问题,应用程序在list文件目录操作时,往往不会只查找1次,往往它要根据比如说partition这种查找一系列的路径,这个时候就会有RTT往返时延的问题,以及多次RPC请求的开销。这时,我们可能需要一种Batch-list查询的API。

社区解决方案


因为最近社区有这方面的解决方案,所以笔者就拿来分享一下了,想提前使用的朋友,可以先apply到自己的测试分支中,进行使用。

ls命令执行缓慢


对应上述2种场景,首先来看第一种case,命令行方式的list优化,解决的核心要点在于将同步执行过程改为客户端多线程异步执行,用ForkJoinPool来执行递归的list查询,对子任务进行拆分执行,然后进行结果的再汇总

以下是核心的改动,在基类Command.java里改的:

+  /** multi-thread approach to FsShell commands */
+  protected ForkJoinPool pool;
+
   /** Constructor */
   protected Command() {
     out = System.out;
@@ -174,6 +179,13 @@ public int run(String...argv) {
             "DEPRECATED: Please use '"+ getReplacementCommand() + "' instead.");
       }
       processOptions(args);
+        int threads = getConf().getInt("fs.threads", -1);
+        if(threads != -1) {
+            pool = new ForkJoinPool(threads);
+        } else {
+            // Default is number of cores.
+            pool = new ForkJoinPool();
+        }
       processRawArguments(args);
     } catch (CommandInterruptException e) {
       displayError("Interrupted");
@@ -301,7 +313,7 @@ protected void processPathArgument(PathData item) throws IOException {
     // null indicates that the call is not via recursion, ie. there is
     // no parent directory that was expanded
     depth = 0;
-    processPaths(null, item);
+    pool.invoke(new ProcessPathsAction(null, Arrays.asList(item)));
   }

这种部分全部代码请阅读JIRA HADOOP-15471:Hdfs recursive listing operation is very slow

list多目录查询缓慢


在元数据的加载中,会涉及到List多目录的查询操作,这在Hadoop之上的很多应用程序,比如MR,Hive,Presto中都会调用到。但是,像目前这种方式,HDFS提供的方式只有单一的特定路径的List查找。当然你可以说,我可以去查一个很大的路径,然后再在返回结果中过滤出我们想要的路径。这么做会有几个问题,首先你可能会得到大量无关的文件目录信息,第二会造成不必要的性能损耗,对于NN来说。总的一句话,还是得让每次的查询是一次精准,有效的查询。上文中,笔者已经提到过,这里我们的优化点是减少不必要的RTT,以及RPC调用,从Multiple-list Call变为一次Batch-List Call。

下面用一个例子来更加具体化以上提到的改进点:

比如目前我们需要list出下面3个路径:

/zhangsan/20180525/16
/zhangsan/20180525/17
/zhangsan/20180525/18

正常情况下,我们就会调3次get list操作,而一旦我们有了Batch-List的API,我们只需一次性传入/zhangsan/20180525/16(17)(18),然后经过NN处理,返回多个list结果列表。这样就完成了一次高效的查询操作,相比于之前3次完全独立的查询调用。

目前此方面的改进在JIRA HDFS-13616:Batch listing of multiple directories

另外,根据HDFS-13616上面的讨论,此部分改进在元数据的loading过程中会有近10到20倍的性能提升。

Batch-List API在DFSClient.java中的定义如下:

  public BatchedDirectoryListing batchedListPaths(
      String[] srcs, byte[] startAfter, boolean needLocation)
      throws IOException {
    checkOpen();
    try {
      return namenode.getBatchedListing(srcs, startAfter, needLocation);
    } catch(RemoteException re) {
      throw re.unwrapRemoteException(AccessControlException.class,
          FileNotFoundException.class,
          UnresolvedPathException.class);
    }
  }

在FileSystem层面的API定义如下:

  @Override
  public RemoteIterator<PartialListing<FileStatus>> batchedListStatusIterator(
      final List<Path> paths)
      throws IOException {
    List<Path> absPaths = Lists.newArrayListWithCapacity(paths.size());
    for (Path p : paths) {
      absPaths.add(fixRelativePart(p));
    }
    return new PartialListingIterator<>(absPaths, false);
  }

这里的List Path就是需要list的多个path路径。

参考资料


[1]. https://issues.apache.org/jira/browse/HADOOP-15471. Hdfs recursive listing operation is very slow
[2].https://issues.apache.org/jira/browse/HDFS-13616. Batch listing of multiple directories.