Hadoop源码学习-脚本命令(hadoop fs -ls)执行细节

Hadoop有提供一些脚本命令,以便于我们对HDFS进行管理,可以通过命令hadoop fs进行查看:
这里写图片描述
通过以上使用说明可以发现,里面提供了大多数和我们在本地操作文件系统相类似的命令,例如,cat查看文件内容,chgrp改变用户群组权限,chmod改变用户权限,chown改变用户拥有者权限,还有创建目录,查看目录,移动文件,重命名等等。

hadoop fs -ls

这里,我们来看看命令hadoop fs -ls
这里写图片描述

这个命令大家肯定非常熟悉,在Linux下使用超级频繁,功能就是列出指定目录下的文件及文件夹。那接下来,我们来看看它是怎样查找到此目录下的文件及文件夹的。

在运行hadoop fs -ls /命令之后,真正的命令只是hadoop,后面只是参数,那么,这个hadoop命令到底是哪呢?如果说集群是自己手动搭配的话,那大家肯定知道,这个命令就在${HADOOP_HOME}/bin目录下,但如果集群是自动化部署的时候,你可能一下子找不到这个命令在哪,此时,可以使用以下命令查找:
这里写图片描述

可以看到,这个命令应该是在目录/usr/bin/之下,使用vim /usr/bin/hadoop查看命令详细:

#!/bin/bash

export HADOOP_HOME=${HADOOP_HOME:-/usr/hdp/2.5.0.0-1245/hadoop}
export HADOOP_MAPRED_HOME=${HADOOP_MAPRED_HOME:-/usr/hdp/2.5.0.0-1245/hadoop-mapreduce}
export HADOOP_YARN_HOME=${HADOOP_YARN_HOME:-/usr/hdp/2.5.0.0-1245/hadoop-yarn}
export HADOOP_LIBEXEC_DIR=${HADOOP_HOME}/libexec
export HDP_VERSION=${HDP_VERSION:-2.5.0.0-1245}
export HADOOP_OPTS="${HADOOP_OPTS} -Dhdp.version=${HDP_VERSION}"

exec /usr/hdp/2.5.0.0-1245//hadoop/bin/hadoop.distro "$@"

这个脚本做了两件事,一是export了一些环境变量,使得之后运行的子程序都可以共享这些变量的值;二是执行了命令hadoop.distro命令,并传上了所有参数。

现在,我们来看下命令hadoop.distro做了哪些事,由于代码有点小多,我就不全部贴了,只贴与执行命令hadoop fs -ls /相关的代码。

命令hadoop.distro做的事情是:根据之前传入的参数,然后做一些判断,确定一些变量的值,最后执行的是以下命令:

exec "$JAVA" $JAVA_HEAP_MAX $HADOOP_OPTS $CLASS "$@"

这里,我们看到了一堆变量,其中
$JAVAjava
$JAVA_HEAP_MAX
$HADOOP_OPTS

    # Always respect HADOOP_OPTS and HADOOP_CLIENT_OPTS
    HADOOP_OPTS="$HADOOP_OPTS $HADOOP_CLIENT_OPTS"

    #make sure security appender is turned off
    HADOOP_OPTS="$HADOOP_OPTS -Dhadoop.security.logger=${HADOOP_SECURITY_LOGGER:-INFO,NullAppender}"

$CLASSorg.apache.hadoop.fs.FsShell
$@-ls / 注意:这里已经没有参数fs

因此,命令hadoop.distro也就转换成执行一个JAVA类了,然后继续带上参数。

打开hadoop的源代码,找到类org.apache.hadoop.fs.FsShell,它的main方法如下:

  public static void main(String argv[]) throws Exception {
     FsShell shell = newShellInstance(); //创建FsShell实例
    Configuration conf = new Configuration();  //配置类,
    conf.setQuietMode(false); //设置成“非安静模式”,默认为“安静模式”,在安静模式下,error和information的信息不会被记录。
    shell.setConf(conf);
    int res;
    try {
      res = ToolRunner.run(shell, argv); //ToolRunner就是一个工具类,用于执行实现了接口`Tool`的类
    } finally {
      shell.close();
    }
    System.exit(res);
  }

ToolRunner类结合GenericOptionsParser类来解析命令行参数,
在运行上述ToolRunner.run(shell, argv)代码之后,经过一番解释之后,最后真正执行的仍然是类FsShellrun方法,而且对其参数进行了解析,run方法如下:

  @Override
  public int run(String argv[]) throws Exception {
    // initialize FsShell 包括注册命令类
    init();

    int exitCode = -1;
    if (argv.length < 1) {
      printUsage(System.err); //打印使用方法
    } else {
      String cmd = argv[0]; //取到第一个参数,即 ls
      Command instance = null;
      try {
        // 取得实现了该命令(ls)的命令实例,并且此类已经通过类CommandFactory的addClass方法进行了注册
        instance = commandFactory.getInstance(cmd);
        if (instance == null) {
          throw new UnknownCommandException();
        }
        exitCode = instance.run(Arrays.copyOfRange(argv, 1, argv.length));
      } catch (IllegalArgumentException e) {
        displayError(cmd, e.getLocalizedMessage());
        if (instance != null) {
          printInstanceUsage(System.err, instance);
        }
      } catch (Exception e) {
        // instance.run catches IOE, so something is REALLY wrong if here
        LOG.debug("Error", e);
        displayError(cmd, "Fatal internal error");
        e.printStackTrace(System.err);
      }
    }
    return exitCode;
  }

注意:通过查看类Ls的代码,我们可以发现,它有一个静态方法registerCommands,这个方法就是对类Ls进行注册,但是,这只是一个静态方法,那么,到底是在哪进行了此方法的调用呢?

class Ls extends FsCommand {
  public static void registerCommands(CommandFactory factory) {
    factory.addClass(Ls.class, "-ls");
    factory.addClass(Lsr.class, "-lsr");
  }
  ...

细心的朋友可能已经发现,就在类FsShell的run方法中,调用了一个init方法,而就在此方法中,有一行注册命令的代码,如下:

  protected void init() throws IOException {
    getConf().setQuietMode(true);
    if (commandFactory == null) {
      commandFactory = new CommandFactory(getConf());
      commandFactory.addObject(new Help(), "-help");
      commandFactory.addObject(new Usage(), "-usage");
      // 注册,调用registerCommands方法
      registerCommands(commandFactory);
    }
  }

  protected void registerCommands(CommandFactory factory) {
    // TODO: DFSAdmin subclasses FsShell so need to protect the command
    // registration.  This class should morph into a base class for
    // commands, and then this method can be abstract
    if (this.getClass().equals(FsShell.class)) {
      // 调用CommandFactory类的registerCommands方法
      // 注意,这里传的参数是类FsCommand
      factory.registerCommands(FsCommand.class);
    }
  }

CommandFactory类的registerCommands方法如下:

  public void registerCommands(Class<?> registrarClass) {
    try {
      // 这里触发的是类CommandFactory的registerCommands方法
      registrarClass.getMethod(
          "registerCommands", CommandFactory.class
      ).invoke(null, this);
    } catch (Exception e) {
      throw new RuntimeException(StringUtils.stringifyException(e));
    }
  }

接下来,我拉看看类CommandFactory的registerCommands方法,代码如下:

  public static void registerCommands(CommandFactory factory) {
    factory.registerCommands(AclCommands.class);
    factory.registerCommands(CopyCommands.class);
    factory.registerCommands(Count.class);
    factory.registerCommands(Delete.class);
    factory.registerCommands(Display.class);
    factory.registerCommands(Find.class);
    factory.registerCommands(FsShellPermissions.class);
    factory.registerCommands(FsUsage.class);
    // 我们会用到的就是这个类Ls
    factory.registerCommands(Ls.class);
    factory.registerCommands(Mkdir.class);
    factory.registerCommands(MoveCommands.class);
    factory.registerCommands(SetReplication.class);
    factory.registerCommands(Stat.class);
    factory.registerCommands(Tail.class);
    factory.registerCommands(Test.class);
    factory.registerCommands(Touch.class);
    factory.registerCommands(Truncate.class);
    factory.registerCommands(SnapshotCommands.class);
    factory.registerCommands(XAttrCommands.class);
  }

我们再来看看Ls类

class Ls extends FsCommand {
  public static void registerCommands(CommandFactory factory) {
    factory.addClass(Ls.class, "-ls");
    factory.addClass(Lsr.class, "-lsr");
  }

也就是,在调用init方法的时候,对这些命令类进行了注册。
因此,上面的那个instance,在这里的话,其实就是类Ls的实例。类Ls继承类FsCommand,而类FsCommand是继承类Command,前面instance调用的run方法其实是父类Commandrun方法,此方法主要做了两件事,一是处理配置选项,如-d,-R,-h,二是处理参数,如下:

  public int run(String...argv) {
    LinkedList<String> args = new LinkedList<String>(Arrays.asList(argv));
    try {
      if (isDeprecated()) {
        displayWarning(
            "DEPRECATED: Please use '"+ getReplacementCommand() + "' instead.");
      }
      processOptions(args);
      processRawArguments(args);
    } catch (IOException e) {
      displayError(e);
    }

    return (numErrors == 0) ? exitCode : exitCodeForError();
  }

方法processRawArguments的调用层次关系如下:

     -> processRawArguments(LinkedList)
          |-> expandArguments(LinkedList)
          |   -> expandArgument(String)*
          -> processArguments(LinkedList)
              |-> processArgument(PathData)*
              |   |-> processPathArgument(PathData)
              |   -> processPaths(PathData, PathData...)
              |        -> processPath(PathData)*
              -> processNonexistentPath(PathData)

从这个层次关系中可以看出,整个方法是先进行展开参数,传入的参数是LinkedList<String>,展开后的参数是LinkedList<PathData>PathData类中包含了PathFileStatusFileSystem。其实,当程序运行到这里的时候,命令ls的结果就已经可以通过类PathData中的相关方法获取了。

展开参数后,开始进行处理参数,此时的参数就是LinkedList<PathData>,然后循环处理此List,先是判断目录是否存在,是否需要递归查找,是否只是列出本目录(就是看有没有-R-d参数),我们来看一下到底是如何输出结果的:

  @Override
  protected void processPaths(PathData parent, PathData ... items)
  throws IOException {
    if (parent != null && !isRecursive() && items.length != 0) {
      out.println("Found " + items.length + " items");
    }
    adjustColumnWidths(items); // 计算列宽,重新构建格式字符串
    super.processPaths(parent, items);
  }

看到这里,大家是不是觉得很面熟?没想起来?我们上个图:
这里写图片描述

这下看到了吧,最是输出结果的第一行,找到11项。

接下来重新调整了一下列宽,最后调用了父类的processPaths方法,我们继续来看父类的这个方法,它做了哪些事:

  protected void processPaths(PathData parent, PathData ... items)
  throws IOException {
    // TODO: this really should be iterative
    for (PathData item : items) {
      try {
        processPath(item); // 真正处理每一项,然后打印出来
        if (recursive && isPathRecursable(item)) {
          recursePath(item); // 如果有指定参数 -R,则需要进行递归
        }
        postProcessPath(item); // 这个没理解,DFS还有后序DFS么?有知情者,请告知,谢谢。
      } catch (IOException e) {
        displayError(e);
      }
    }
  }

我们来看一下打印具体每行信息的代码:

  @Override
  protected void processPath(PathData item) throws IOException {
    FileStatus stat = item.stat;
    String line = String.format(lineFormat,
        (stat.isDirectory() ? "d" : "-"),  // 文件夹显示d,文件显示-
        stat.getPermission() + (stat.getPermission().getAclBit() ? "+" : " "), // 获取权限
        (stat.isFile() ? stat.getReplication() : "-"),
        stat.getOwner(), // 获取拥有者
        stat.getGroup(),  // 获取组
        formatSize(stat.getLen()),  // 获取大小
        dateFormat.format(new  Date(stat.getModificationTime())),  // 日期
        item  // 项,即路径
    );
    out.println(line); // 打印行
  }

到这里,命令hadoop fs -ls /的执行过程基本已经结束(关于文件系统内部细节,后续再讲),这就是整个命令执行的过程。最后,我们来总结一下:

  1. 执行shell。执行命令hadoop fs -ls /,首先执行的是shell命令,然后转换成执行Java类。
  2. 执行Java。在执行Java类的时候,使用工具类对其进行配置项解析,并使用反射机制对命令进行了转换,于是后面变成了调用类Lsrun方法。
  3. 调用类Ls的相关方法。类Ls负责处理路径,并打印详情。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值