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 "$@"
这里,我们看到了一堆变量,其中
$JAVA :java
$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}"
$CLASS :org.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) 代码之后,经过一番解释之后,最后真正执行的仍然是类FsShell 的run 方法,而且对其参数进行了解析,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方法其实是父类Command 的run 方法,此方法主要做了两件事,一是处理配置选项,如-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 类中包含了Path ,FileStatus ,FileSystem 。其实,当程序运行到这里的时候,命令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 / 的执行过程基本已经结束(关于文件系统内部细节,后续再讲),这就是整个命令执行的过程。最后,我们来总结一下:
- 执行shell。执行命令
hadoop fs -ls / ,首先执行的是shell命令,然后转换成执行Java类。
- 执行Java。在执行Java类的时候,使用工具类对其进行配置项解析,并使用反射机制对命令进行了转换,于是后面变成了调用类
Ls 的run 方法。
- 调用类
Ls 的相关方法。类Ls 负责处理路径,并打印详情。
|