这里,继续对FsShell类中一些命令进行阅读分析,主要是看与拷贝文件有关的几个命令。
该命令实现对文件的拷贝操作,并且支持在不同的文件系统之间进行文件的拷贝。拷贝文件涉及的操作比较复杂,核心拷贝操作还是调用了org.apache.hadoop.fs.FileUtil类的copy方法实现的。 先看该类中定义的其中一个copy方法的实现:
private int copy(String argv[], Configuration conf) throws IOException { int i = 0; int exitCode = 0; String cmd = argv[i++]; String dest = argv[argv.length-1]; // 命令行中最后一个参数 // 若指定了多个输入源文件,则最后一个参数必须是一个目录 if (argv.length > 3) { Path dst = new Path(dest); if (!fs.isDirectory(dst)) { // 最后一个参数必须是目录 throw new IOException("When copying multiple files, " + "destination " + dest + " should be a directory."); } } // 循环对每一个文件进行拷贝操作 for (; i < argv.length - 1; i++) { try { copy(argv[i], dest, conf); // 将文件argv[i]拷贝到dest目录中 } catch (RemoteException e) { // 捕获命令执行发生的异常信息 exitCode = -1; try { String[] content; content = e.getLocalizedMessage().split("/n"); System.err.println(cmd.substring(1) + ": " + content[0]); } catch (Exception ex) { System.err.println(cmd.substring(1) + ": " + ex.getLocalizedMessage()); } } catch (IOException e) { // 捕获IO异常信息 exitCode = -1; System.err.println(cmd.substring(1) + ": " + e.getLocalizedMessage()); } } return exitCode; }
该命令的实现与mv命令类似,这里调用了一个重载的copy命令,实现对一个文件执行拷贝操作。该重载的拷贝方法如下所示:
void copy(String srcf, String dstf, Configuration conf) throws IOException { Path srcPath = new Path(srcf); // 构造Path FileSystem srcFs = srcPath.getFileSystem(getConf()); // 获取到srcPath所在的文件系统srcFs Path dstPath = new Path(dstf); FileSystem dstFs = dstPath.getFileSystem(getConf()); // 获取到dstPath所在的文件系统dstFs Path [] srcs = FileUtil.stat2Paths(srcFs.globStatus(srcPath), srcPath); // 获取到srcFs中满足srcPath模式的FileStatus[]并转换成为Path[] if (srcs.length > 1 && !dstFs.isDirectory(dstPath)) { throw new IOException("When copying multiple files, " + "destination should be a directory."); } for(int i=0; i<srcs.length; i++) { // 循环拷贝操作 FileUtil.copy(srcFs, srcs[i], dstFs, dstPath, false, conf); // 调用FileUtil类的拷贝方法copy完成文件在srcFs与dstFs文件系统之间拷贝文件的操作 } }
现在,我们开始追踪 org.apache.hadoop.fs.FileUtil类的copy方法,看一看拷贝到底是如何实现的。FileUtil类中定义了多个重载的拷贝方法copy,我们只从FsShell类中调用的copy方法开始追踪其实现。上面调用的FileUtil类的copy实现如下所示:
public static boolean copy(FileSystem srcFS, Path src, FileSystem dstFS, Path dst, boolean deleteSource, Configuration conf) throws IOException { return copy(srcFS, src, dstFS, dst, deleteSource, true, conf); // 调用了一个重载的copy方法实现文件在srcFS与dstFS之间进行复制 }
看重载的copy方法的实现,如下所示:
public static boolean copy(FileSystem srcFS, Path src, FileSystem dstFS, Path dst, boolean deleteSource, boolean overwrite, Configuration conf) throws IOException { dst = checkDest(src.getName(), dstFS, dst, overwrite); // 检查目的文件系统dstFS中dst目录是否合法(参照src) if (srcFS.getFileStatus(src).isDir()) { // 若源文件系统srcFS中src是目录 checkDependencies(srcFS, src, dstFS, dst); // 检查文件依赖性:如果srcFS=dstFS,并且dst不是src的子目录,检查通过;如果srcFS与dstFS不是同一个文件系统,依赖性检查通过 if (!dstFS.mkdirs(dst)) { // 在dstFS中创建dst目录,准备向其中拷贝数据 return false; } FileStatus contents[] = srcFS.listStatus(src); // 获取srcFS中src目录下的文件列表 for (int i = 0; i < contents.length; i++) { // 分治思想:分治后执行递归拷贝文件操作 copy(srcFS, contents[i].getPath(), dstFS, new Path(dst, contents[i].getPath().getName()), deleteSource, overwrite, conf); // 递归调用 } } else if (srcFS.isFile(src)) { // 递归出口(如果src是一个普通文件) InputStream in=null; OutputStream out = null; try { in = srcFS.open(src); // 打开srcFS中的src文件,并返回输入流对象 out = dstFS.create(dst, overwrite); // 在目的文件系统dstFS中创建dst文件,并返回输出流,等待写入 IOUtils.copyBytes(in, out, conf, true); // 调用:通过调用IOUtils类的copyBytes方法实现流式拷贝 } catch (IOException e) { IOUtils.closeStream(out); // 关闭out IOUtils.closeStream(in); // 关闭in throw e; } } else { throw new IOException(src.toString() + ": No such file or directory"); } if (deleteSource) { // 如果设置了拷贝完成后删除源文件选项 return srcFS.delete(src, true); // 删除源文件系统srcFS的源文件src } else { return true; } }
IOUtils类中实现了Hadoop文件系统中文件的 流式拷贝操作,我们追踪该工具类的copyBytes方法,分析实现的过程。该方法如下所示:
/** * 从一个流拷贝到另一个流中 */ public static void copyBytes(InputStream in, OutputStream out, Configuration conf, boolean close) throws IOException { copyBytes(in, out, conf.getInt("io.file.buffer.size", 4096), close); // 调用:重载的copyBytes方法实现流式拷贝 }
我们看重载流式拷贝实现方法copyBytes,如下所示:
public static void copyBytes(InputStream in, OutputStream out, int buffSize, boolean close) throws IOException { PrintStream ps = out instanceof PrintStream ? (PrintStream)out : null; // 使用PrintStream为out流增加便捷功能 byte buf[] = new byte[buffSize]; // 字节缓冲区 try { int bytesRead = in.read(buf); // 从输入流in读取bytesRead个字节到buf缓冲区中 while (bytesRead >= 0) { // 确实读取到了字节 out.write(buf, 0, bytesRead); // 将从in中读取到的字节,通过buf缓冲区写入到输出流out中 if ((ps != null) && ps.checkError()) { // 如果ps=(PrintStream)out,测试内部标志,并自动刷新 throw new IOException("Unable to write to output stream."); } bytesRead = in.read(buf); // 继续从in读取字节 } } finally { if(close) { out.close(); // 关闭out in.close(); // 关闭in } } }
上面在从InputStream in拷贝到OutputStream out中的过程中,使用了更加高效的PrintStream流类,它能够为OutputStream增加方便打印各种数据值的表示形式,而且,它不会抛出IO异常,而是将流式拷贝过程中发生的异常,设置为通过调用checkError方法来检测内部的标志。另外,它还可以实现自动刷新,在向输出流中写入字节(通过字节缓冲区)之后,自动刷新。
cp命令的具体实现都在上面进行分析了,应该能够理解在Hadoop中如何在不同文件系统之间执行流式拷贝文件的过程。
该命令实现了从本地文件系统拷贝文件的操作。实现方法为,如下所示:
/** * 从本地文件系统(srcs在本地文件系统中)拷贝srcs到目的文件系统(对应Path为dstf) */ void copyFromLocal(Path[] srcs, String dstf) throws IOException { Path dstPath = new Path(dstf); FileSystem dstFs = dstPath.getFileSystem(getConf()); // 获取到目的文件系统dstFs if (srcs.length == 1 && srcs[0].toString().equals("-")) // 如果只指定了一个参数“-” copyFromStdin(dstPath, dstFs); // 调用:从标准输入流中进行流式拷贝操作 else // 否则 dstFs.copyFromLocalFile(false, false, srcs, dstPath); // 调用目的文件系统dstFs的copyFromLocalFile方法执行拷贝操作 }
我们关注一下copyFromStdin方法拷贝的实现,如下所示:
private void copyFromStdin(Path dst, FileSystem dstFs) throws IOException { if (dstFs.isDirectory(dst)) { // 如果目的文件是目录,不支持源为标准输入流的情况 throw new IOException("When source is stdin, destination must be a file."); } if (dstFs.exists(dst)) { // 如果目的文件系统dstFs中存在文件dst,出错 throw new IOException("Target " + dst.toString() + " already exists."); } FSDataOutputStream out = dstFs.create(dst); // 满足拷贝要求,执行流式拷贝操作 try { IOUtils.copyBytes(System.in, out, getConf(), false); // 调用IOUtils类的copyBytes方法实现,前面已经分析过拷贝过程 } finally { out.close(); // 拷贝完成,关闭输出流out } }
再看一下,如果指定的是待拷贝的文件源不是标准输入流的情况,文件系统FileSystem是如何实现拷贝操作的。实现的方法copyFromLocalFile如下所示:
/** * 将本地的srcs,拷贝到目的文件系统中的dst * delSrc指示了拷贝文件完成之后,是否删除源文件srcs */ public void copyFromLocalFile(boolean delSrc, boolean overwrite, Path[] srcs, Path dst) throws IOException { Configuration conf = getConf(); FileUtil.copy(getLocal(conf), srcs, this, dst, delSrc, overwrite, conf); // 调用FileUtil工具类实现拷贝操作 }
关于FileUtil的copy方法,前面已经详细分析过,不再累述。
像moveFromLocal、moveFromLocal、copyToLocal、moveToLocal、copyMergeToLocal等命令的实现都非常类似,也不做过多的解释了。