前面分析了與操作系統有關的Shell命令,它們用於與操作系統進行命令行方式的交互。在Hadoop中,自定義了FileSystem文件系統,這是基於Unix操作系統之上的文件系統,爲了方便對FileSystem的管理,通過org.apache.hadoop.fs.FsShell類定義了對Hadoop FileSystem文件系統進行命令行方式管理的命令實現。
先給出對Hadoop文件系統進行管理的命令實現類的繼承層次關係:
- ◦org.apache.hadoop.conf.Configured(implements org.apache.hadoop.conf.Configurable)
- ◦org.apache.hadoop.fs.FsShell(implements org.apache.hadoop.util.Tool)
- ◦org.apache.hadoop.hdfs.tools.DFSAdmin
由於DFSAdmin類是對HDFS分佈式文件系統提供基於命令行的管理功能,這裏先不對DFSAdmin進行分析,在後面分析HDFS實現的時候,進行詳細分析理解。
Configured就不用多說了,是Hadoop配置類的高層抽象。
Tool接口支持命令行方式的處理,如果需要通過命令行方式來執行一定的任務,都可以實現該接口,通過該接口定義的run方法來運行命令行。由於它繼承自Configurable 接口,使得實現Tool的接口可以對特定的待執行的任務進行詳細配置,滿足執行一個命令能夠完成任務的要求。下面是接口的定義:
- public interface Tool extends Configurable {
- int run(String [] args) throws Exception;
- }
在Hadoop中,Tool接口主要是爲進行MapReduce並行計算而定義的,這裏FsShell類實現了該接口,其實也是使得命令行執行與任務關聯起來,通過執行命令行,而執行設置的待完成的任務。
下面來看FsShell類的具體實現。
既然,FsShell是與命令行有關的,那麼我們就從其中對指定的命令實現的角度來看,分別對每個命令的實現進行閱讀分析。在分析每個命令實現過程之前,先看一下該類中printUsage方法的執行,該方法能夠打印出全部命令用法的信息,如下所示:
- Usage: java FsShell
- [-ls <path>]
- [-lsr <path>]
- [-du <path>]
- [-dus <path>]
- [-count[-q] <path>]
- [-mv <src> <dst>]
- [-cp <src> <dst>]
- [-rm <path>]
- [-rmr <path>]
- [-expunge]
- [-put <localsrc> ... <dst>]
- [-copyFromLocal <localsrc> ... <dst>]
- [-moveFromLocal <localsrc> ... <dst>]
- [-get [-ignoreCrc] [-crc] <src> <localdst>]
- [-getmerge <src> <localdst> [addnl]]
- [-cat <src>]
- [-text <src>]
- [-copyToLocal [-ignoreCrc] [-crc] <src> <localdst>]
- [-moveToLocal [-crc] <src> <localdst>]
- [-mkdir <path>]
- [-setrep [-R] [-w] <rep> <path/file>]
- [-touchz <path>]
- [-test -[ezd] <path>]
- [-stat [format] <path>]
- [-tail [-f] <file>]
- [-chmod [-R] <MODE[,MODE]... | OCTALMODE> PATH...]
- [-chown [-R] [OWNER][:[GROUP]] PATH...]
- [-chgrp [-R] GROUP PATH...]
- [-help [cmd]]
- Generic options supported are
- -conf <configuration file> specify an application configuration file
- -D <property=value> use value for given property
- -fs <local|namenode:port> specify a namenode
- -jt <local|jobtracker:port> specify a job tracker
- -files <comma separated list of files> specify comma separated files to be copied to the map reduce cluster
- -libjars <comma separated list of jars> specify comma separated jar files to include in the classpath.
- -archives <comma separated list of archives> specify comma separated archives to be unarchived on the compute machines.
- The general command line syntax is
- bin/hadoop command [genericOptions] [commandOptions]
非常清晰明瞭,FsShell所支持的命令行,及其該命令的可以設置的參數,都在上述列表中顯示出來。
另外,對於每個命令的幫助信息,都可以通過printHelp方法得到,例如,如果想要得到命令“ls”的幫助信息,調用printHelp("ls");即可。如果想要得到全部命令的幫助信息,只要給printHelp隨便傳入一個非命令字符串,如printHelp("hashyes3532333");,將打印出全部命令幫助信息,下面是一個幫助信息的片段:
- hadoop fs is the command to execute fs commands. The full syntax is:
- hadoop fs [-fs <local | file system URI>] [-conf <configuration file>]
- [-D <propertyproperty=value>] [-ls <path>] [-lsr <path>] [-du <path>]
- [-dus <path>] [-mv <src> <dst>] [-cp <src> <dst>] [-rm <src>]
- [-rmr <src>] [-put <localsrc> ... <dst>] [-copyFromLocal <localsrc> ... <dst>]
- [-moveFromLocal <localsrc> ... <dst>] [-get [-ignoreCrc] [-crc] <src> <localdst>
- [-getmerge <src> <localdst> [addnl]] [-cat <src>]
- [-copyToLocal [-ignoreCrc] [-crc] <src> <localdst>] [-moveToLocal <src> <localdst>]
- [-mkdir <path>] [-report] [-setrep [-R] [-w] <rep> <path/file>]
- [-touchz <path>] [-test -[ezd] <path>] [-stat [format] <path>]
- [-tail [-f] <path>] [-text <path>]
- [-chmod [-R] <MODE[,MODE]... | OCTALMODE> PATH...]
- [-chown [-R] [OWNER][:[GROUP]] PATH...]
- [-chgrp [-R] GROUP PATH...]
- [-count[-q] <path>]
- [-help [cmd]]
- -fs [local | <file system URI>]: Specify the file system to use.
- If not specified, the current configuration is used,
- taken from the following, in increasing precedence:
- core-default.xml inside the hadoop jar file
- core-site.xml in $HADOOP_CONF_DIR
- 'local' means use the local file system as your DFS.
- <file system URI> specifies a particular file system to
- contact. This argument is optional but if used must appear
- appear first on the command line. Exactly one additional
- argument must be specified.
下面介紹每個命令的實現:
- ls與lsr命令
執行ls命令,能夠列出匹配指定Path下的全部文件,並且不遞歸列出子目錄中文件;lsr能夠列出指定Path下的所有文件,並且如果存在子目錄,也會遞歸列出子目錄中的文件。實現這兩個命令的方法均爲ls方法,如下所示:
- /**
- * 列出滿足模式srcf的全部文件
- * @param srcf 文件模式
- * @param recursive 是否遞歸列出
- */
- private int ls(String srcf, boolean recursive) throws IOException {
- Path srcPath = new Path(srcf);
- FileSystem srcFs = srcPath.getFileSystem(this.getConf()); // 通過構造Path類實例,獲取它所屬的FileSystem文件系統
- FileStatus[] srcs = srcFs.globStatus(srcPath); // 獲取到文件系統srcFs中匹配srcPath模式的全部按照文件名稱排好序的文件(不包括校驗和文件),每個文件對應一個FileStatus
- if (srcs==null || srcs.length==0) {
- throw new FileNotFoundException("Cannot access " + srcf + ": No such file or directory.");
- }
- boolean printHeader = (srcs.length == 1) ? true: false; // 兩種情況:如果獲取到一個文件FileStatus,表示只有一個目錄或者文件,需要打印出列表頭部信息;否則返回多個FileStatus,需要循環並遞歸遍歷,不打印出列表頭部信息
- int numOfErrors = 0;
- for(int i=0; i<srcs.length; i++) {
- numOfErrors += ls(srcs[i], srcFs, recursive, printHeader); // 調用ls,遞歸列出文件
- }
- return numOfErrors == 0 ? 0 : -1;
- }
實際上,執行ls命令真正實現執行的過程在重載的另一個方法ls中,如下所示:
- private int ls(FileStatus src, FileSystem srcFs, boolean recursive, boolean printHeader) throws IOException {
- final String cmd = recursive? "lsr": "ls"; // 根據recursive判斷,是否遞歸列出文件,如果是則命令名稱爲lsr,否則命令爲ls
- final FileStatus[] items = shellListStatus(cmd, srcFs, src); // 調用shellListStatus方法,文件系統從srcFs中獲取src中的全部FileStatus[](如果src是文件而非目錄,直接返回它自身)
- if (items == null) {
- return 1;
- } else {
- int numOfErrors = 0;
- if (!recursive && printHeader) { // 如果指定不遞歸列出
- if (items.length != 0) {
- System.out.println("Found " + items.length + " items");
- }
- }
- int maxReplication = 3, maxLen = 10, maxOwner = 0,maxGroup = 0; //
- for(int i = 0; i < items.length; i++) {
- FileStatus stat = items[i];
- int replication = String.valueOf(stat.getReplication()).length(); // stat對應文件的replication因子
- int len = String.valueOf(stat.getLen()).length(); // stat對應的文件的長度
- int owner = String.valueOf(stat.getOwner()).length(); // stat對應的文件的屬主數
- int group = String.valueOf(stat.getGroup()).length(); // stat對應的文件屬組數
- if (replication > maxReplication) maxReplication = replication; // 有可能一個文件的副本數超過指定的最大replication因子值
- if (len > maxLen) maxLen = len; // 超過文件最大長度的情況
- if (owner > maxOwner) maxOwner = owner; // 超過最大屬主數
- if (group > maxGroup) maxGroup = group; // 超過最大屬組數
- }
- for (int i = 0; i < items.length; i++) {
- FileStatus stat = items[i];
- Path cur = stat.getPath();
- String mdate = dateForm.format(new Date(stat.getModificationTime())); // 格式化stat對應的文件的修改時間
- System.out.print((stat.isDir() ? "d" : "-") + stat.getPermission() + " "); // 輸出stat對應文件權限信息:若是目錄就以"d權限"格式輸出,若是文件則以"-權限"格式輸出
- System.out.printf("%"+ maxReplication + "s ", (!stat.isDir() ? stat.getReplication() : "-")); // 輸出stat對應文件最replication因子信息到輸出流中
- if (maxOwner > 0)
- System.out.printf("%-"+ maxOwner + "s ", stat.getOwner()); // 輸出stat對應文件的屬主數到輸出流中
- if (maxGroup > 0)
- System.out.printf("%-"+ maxGroup + "s ", stat.getGroup()); // 輸出stat對應文件的屬組數到輸出流中
- System.out.printf("%"+ maxLen + "d ", stat.getLen()); // 輸出stat對應文件的最大長度信息到輸出流中
- System.out.print(mdate + " "); // 輸出格式化的文件修改時間
- System.out.println(cur.toUri().getPath()); // 輸出stat對應文件的路徑信息
- if (recursive && stat.isDir()) { // 如果stat對應的是目錄,而且要求遞歸列出
- numOfErrors += ls(stat,srcFs, recursive, printHeader); // 遞歸調用ls
- }
- }
- return numOfErrors;
- }
- }
lsr命令是遞歸列出滿足給定模式的全部文件,也是基於上述方法實現的。
通過上面的ls的實現可知,列出FileSystem文件系統中的數據,是通過獲取到該文件系統中保存的文件的FileStatus實例,因爲FileStatus描述了位於該文件系統中對應文件的詳細信息,然後通過它來打印出文件類表(包含必要的文件屬性信息)。
- du與dus命令
du命令列出滿足給定模式的全部文件對應的長度信息,dus執行後列出了滿足給定模式的每個文件或目錄的磁盤使用情況摘要信息,比du命令執行得到的結果信息要詳細。
du命令實現是通過du方法,如下:
- void du(String src) throws IOException {
- Path srcPath = new Path(src);
- FileSystem srcFs = srcPath.getFileSystem(getConf()); // 獲取到Path對應的FileSystem文件系統
- Path[] pathItems = FileUtil.stat2Paths(srcFs.globStatus(srcPath), srcPath); // 調用:將從srcFs文件系統中獲取到經過srcPath過濾的FileStatus[]轉換爲Path數組
- FileStatus items[] = srcFs.listStatus(pathItems); // 根據得到的滿足過濾條件的Path得到對應的FileStatus
- if ((items == null) || ((items.length == 0) && (!srcFs.exists(srcPath)))){
- throw new FileNotFoundException("Cannot access " + src + ": No such file or directory.");
- } else {
- System.out.println("Found " + items.length + " items");
- int maxLength = 10;
- long length[] = new long[items.length]; // length數組用來保存每個文件對應的長度信息
- for (int i = 0; i < items.length; i++) {
- length[i] = items[i].isDir() ?
- srcFs.getContentSummary(items[i].getPath()).getLength() :
- items[i].getLen(); // 若items[i]對應文件是目錄,通過srcFs獲取到其內容摘要信息的長度,若是普通文件,則得到其長度
- int len = String.valueOf(length[i]).length();
- if (len > maxLength) maxLength = len;
- }
- for(int i = 0; i < items.length; i++) { // 循環遍歷
- System.out.printf("%-"+ (maxLength + BORDER) +"d", length[i]); // 將每個文件或者目錄的長度信息寫入到流中
- System.out.println(items[i].getPath());
- }
- }
- }
獲取文件信息的方式,基本上都是一致的,通過文件系統來得到對應文件的統計信息。dus命令實現通過dus方法,與上面的實現基本類似,與ds實現不同的是,從FileSystem文件系統中獲取到的文件不管是目錄還行普通文件,都獲取到其摘要信息(對應org.apache.hadoop.fs.ContentSummary)的長度,最後返回執行結果。
- mkdir命令
該命令根據跟定的字符串,創建該字符串標識的目錄,實現方法爲mkdir方法,實現比較簡單易懂:
- void mkdir(String src) throws IOException {
- Path f = new Path(src);
- FileSystem srcFs = f.getFileSystem(getConf());
- FileStatus fstatus = null;
- try {
- fstatus = srcFs.getFileStatus(f);
- if (fstatus.isDir()) { // 只有給定的src在文件系統中不存在時,纔可以創建
- throw new IOException("cannot create directory " + src + ": File exists");
- }
- else {
- throw new IOException(src + " exists but " + "is not a directory");
- }
- } catch(FileNotFoundException e) {
- if (!srcFs.mkdirs(f)) { // 通過調用文件系統srcFs的創建目錄方法,執行目錄的創建
- throw new IOException("failed to create " + src);
- }
- }
- }
- touchz命令
該命令創建一個空文件,大小爲0,通過touchz方法實現,實現的原理也是,通過調用文件系統的create方法執行文件的創建,如下所示:
- void touchz(String src) throws IOException {
- Path f = new Path(src);
- FileSystem srcFs = f.getFileSystem(getConf());
- FileStatus st;
- if (srcFs.exists(f)) {
- st = srcFs.getFileStatus(f); // 嘗試,是否能夠從文件系統srcFs中獲取到待創建文件的信息
- if (st.isDir()) { // 如果該文件時一個目錄,不能創建
- throw new IOException(src + " is a directory");
- } else if (st.getLen() != 0) // 如果該文件存在,並且文件不空,也不能創建
- throw new IOException(src + " must be a zero-length file");
- }
- FSDataOutputStream out = srcFs.create(f); // 調用文件系統srcFs的create方法創建0長度新文件
- out.close();
- }
- mv命令
該命令是移動文件,並支持文件的重命名,在FsShell類中通過rename方法實現的。方法實現如下所示:
- private int rename(String argv[], Configuration conf) throws IOException {
- int i = 0;
- int exitCode = 0;
- String cmd = argv[i++]; // 提取出命令名稱
- String dest = argv[argv.length-1]; //命令行中最後一個參數
- // 如果命令行指定了大於3個參數,最後一個一定是一個目錄
- if (argv.length > 3) {
- Path dst = new Path(dest); // 創建目錄
- FileSystem dstFs = dst.getFileSystem(getConf()); // 得到該目錄所在的文件系統dstFs
- if (!dstFs.isDirectory(dst)) { // 如果文件系統dstFs中存在dst,而且它不是一個目錄,出錯
- throw new IOException("When moving multiple files, " + "destination " + dest + " should be a directory.");
- }
- }
- // 循環遍歷多個輸入源文件,也就是在命令名稱與最後一個參數之間的參數字符串
- for (; i < argv.length - 1; i++) {
- try {
- rename(argv[i], dest); // 調用:將每個源文件argv[i]移動到dest目錄中
- } catch (RemoteException e) {
- // 移動文件過程中發生異常,由hadoop server返回,打印出錯信息的第一行
- 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) {
- // 捕獲異常
- exitCode = -1;
- System.err.println(cmd.substring(1) + ": " + e.getLocalizedMessage());
- }
- }
- return exitCode;
- }
接着,看一下上面調用的一個重載的rename方法,將一個文件進行移動和重命名操作:
- void rename(String srcf, String dstf) throws IOException {
- Path srcPath = new Path(srcf); // 源文件
- Path dstPath = new Path(dstf); // 目的文件
- FileSystem srcFs = srcPath.getFileSystem(getConf()); // 源文件系統
- FileSystem dstFs = dstPath.getFileSystem(getConf()); // 目的文件系統
- URI srcURI = srcFs.getUri(); // 源文件系統URI
- URI dstURI = dstFs.getUri(); // 目的文件系統URI
- if (srcURI.compareTo(dstURI) != 0) { // 文件移動只支持在同一個FileSystem文件系統之上進行
- throw new IOException("src and destination filesystems do not match.");
- }
- Path[] srcs = FileUtil.stat2Paths(srcFs.globStatus(srcPath), srcPath); // 得到全部滿足的Path
- Path dst = new Path(dstf);
- if (srcs.length > 1 && !srcFs.isDirectory(dst)) { // 輸入源文件大於1個,如果目的文件不是目錄,出錯
- throw new IOException("When moving multiple files, " + "destination should be a directory.");
- }
- for(int i=0; i<srcs.length; i++) { // 迭代輸入源文件
- if (!srcFs.rename(srcs[i], dst)) { // 調用文件系統srcFs的rename方法實現移動文件並重命名
- FileStatus srcFstatus = null;
- FileStatus dstFstatus = null;
- try {
- srcFstatus = srcFs.getFileStatus(srcs[i]);
- } catch(FileNotFoundException e) {
- throw new FileNotFoundException(srcs[i] +
- ": No such file or directory");
- }
- try {
- dstFstatus = dstFs.getFileStatus(dst);
- } catch(IOException e) {
- }
- if((srcFstatus!= null) && (dstFstatus!= null)) { // 移動文件失敗,捕獲:輸入源文件爲目錄,目的文件不是目錄
- if (srcFstatus.isDir() && !dstFstatus.isDir()) {
- throw new IOException("cannot overwrite non directory " + dst + " with directory " + srcs[i]);
- }
- }
- throw new IOException("Failed to rename " + srcs[i] + " to " + dst);
- }
- }
- }
可以看到,在FsShell類中定義的mv操作,不支持在不同的FileSystem文件系統之間進行文件的移動操作。
- rm與rmr命令
rm命令是刪除文件,rmr是遞歸刪除給定目錄的子目錄中的 文件,實現方式和ls與lsr類似。也存在兩個重載的delete方法實現rm與rmr命令,先看其中一個:
- void delete(String srcf, final boolean recursive) throws IOException {
- Path srcPattern = new Path(srcf); // 根據給定的srcf模式,構造一個Path
- new DelayedExceptionThrowing() { // 延遲拋出執行該刪除命令發生的異常信息
- @Override
- void process(Path p, FileSystem srcFs) throws IOException {
- delete(p, srcFs, recursive); // 調用重載的delete方法,執行刪除操作
- }
- }.globAndProcess(srcPattern, srcPattern.getFileSystem(getConf())); // 收集異常信息,以待命令執行完成後一起拋出
- }
上面,實際上在org.apache.hadoop.fs.FsShell.DelayedExceptionThrowing類中定義的globAndProcess方法中,循環執行了重載的delete方法,也就是真正真正實現刪除的delete方法。也就是說,每調用執行delete方法,能夠刪除一個指定的文件。該方法實現如下所示:
- private void delete(Path src, FileSystem srcFs, boolean recursive) throws IOException {
- if (srcFs.isDirectory(src) && !recursive) { // src是目錄,且指定不進行遞歸刪除,報錯
- throw new IOException("Cannot remove directory /"" + src + "/", use -rmr instead");
- }
- Trash trashTmp = new Trash(srcFs, getConf()); // 構造一個回收站
- if (trashTmp.moveToTrash(src)) { // 將src移動到回收站中(可能是文件或者目錄)
- System.out.println("Moved to trash: " + src);
- return;
- }
- if (srcFs.delete(src, true)) { // 從文件系統srcFs中刪除文件src
- System.out.println("Deleted " + src);
- } else {
- if (!srcFs.exists(src)) { // 若刪除失敗,查找失敗原因
- throw new FileNotFoundException("cannot remove " + src + ": No such file or directory.");
- }
- throw new IOException("Delete failed " + src);
- }
- }
執行刪除文件操作的時候,是將存在於FileSystem文件系統上的文件移動到Hadoop定義的回收站.Trash目錄中。
- cat命令
該命令取出全部滿足給定模式的文件,並緩衝到標準輸出流上。
該命令實現的方法爲cat方法,如下所示:
- void cat(String src, boolean verifyChecksum) throws IOException {
- Path srcPattern = new Path(src);
- new DelayedExceptionThrowing() { // 延遲拋出執行命令捕獲到的異常信息
- @Override
- void process(Path p, FileSystem srcFs) throws IOException {
- if (srcFs.getFileStatus(p).isDir()) {
- throw new IOException("Source must be a file.");
- }
- printToStdout(srcFs.open(p)); // 調用:執行命令
- }
- }.globAndProcess(srcPattern, getSrcFileSystem(srcPattern, verifyChecksum));
- }
調用方法printToStdout真正執行命令,該方法實現如下所示:
- private void printToStdout(InputStream in) throws IOException {
- try {
- IOUtils.copyBytes(in, System.out, getConf(), false); // 使用IOUtils工具類將in流拷貝到System.out流中
- } finally {
- in.close();
- }
- }
可以查閱IOUtils類中的具體實現。這裏,先不對拷貝的具體實現進行分析,在後面會單獨對涉及拷貝操作的實現進行詳細分析。
- stat命令
該命令可以得到一個文件的詳細統計信息,實現方法爲stat方法,實現比較簡單,不再累述。
- tail命令
tail命令執行顯示一個文件的最後1KB內容,在tail方法中實現,如下所示:
- private void tail(String[] cmd, int pos) throws IOException {
- CommandFormat c = new CommandFormat("tail", 1, 1, "f"); // 構造一個解析命令行參數的CommandFormat對象
- String src = null;
- Path path = null;
- try {
- List<String> parameters = c.parse(cmd, pos); // 解析cmd的參數
- src = parameters.get(0); // 文件參數
- } catch(IllegalArgumentException iae) {
- System.err.println("Usage: java FsShell " + TAIL_USAGE);
- throw iae;
- }
- boolean foption = c.getOpt("f") ? true: false; // 判斷是否設置了-f選項
- path = new Path(src);
- FileSystem srcFs = path.getFileSystem(getConf()); // 獲取到Path對應的文件系統
- if (srcFs.isDirectory(path)) { // 若path是目錄,出錯
- throw new IOException("Source must be a file.");
- }
- long fileSize = srcFs.getFileStatus(path).getLen(); // 計算path文件的長度
- long offset = (fileSize > 1024) ? fileSize - 1024: 0; // 計算開始的偏移位置
- while (true) {
- FSDataInputStream in = srcFs.open(path); // 打開文件
- in.seek(offset); // 定位到offset位置
- IOUtils.copyBytes(in, System.out, 1024, false); // 將輸入流in拷貝到Syste.out標準輸出流中
- offset = in.getPos(); // 重新設置開始偏移位置
- in.close(); // 關閉輸入流in
- if (!foption) { // 如果沒有設置-f選項,直接退出
- break;
- }
- fileSize = srcFs.getFileStatus(path).getLen(); // 設置了-f選項,顯示向文件path追加寫入數據的起始位置
- offset = (fileSize > offset) ? offset: fileSize;
- try {
- Thread.sleep(5000);
- } catch (InterruptedException e) {
- break;
- }
- }
- }
- setrep命令
該命令是設置滿足給定模式的文件的副本因子(replication factor)。不僅可以通過該類實現的setReplication方法對單個文件設置副本因子,也可以遞歸設置某個目錄的所有文件的副本因子。實現設置副本因子的方法在該類中有多個,包括重載的方法,先隊下面的方法來分析:
- private void setReplication(String[] cmd, int pos) throws IOException {
- CommandFormat c = new CommandFormat("setrep", 2, 2, "R", "w"); // 解析命令行
- String dst = null;
- short rep = 0; // 初始化副本因子
- try {
- List<String> parameters = c.parse(cmd, pos); // 從位置pos出開始,解析出命令行中的全部參數列表
- rep = Short.parseShort(parameters.get(0)); // 第一個參數就是副本因子的值
- dst = parameters.get(1); // 第二個參數是帶設置副本因子的文件
- } catch (NumberFormatException nfe) {
- System.err.println("Illegal replication, a positive integer expected");
- throw nfe;
- }
- catch(IllegalArgumentException iae) {
- System.err.println("Usage: java FsShell " + SETREP_SHORT_USAGE);
- throw iae;
- }
- if (rep < 1) { // 不能將副本因子設置爲負數
- System.err.println("Cannot set replication to: " + rep);
- throw new IllegalArgumentException("replication must be >= 1");
- }
- List<Path> waitList = c.getOpt("w")? new ArrayList<Path>(): null; // 如果設置了-w選項,會將待設置副本因子完成的文件Path暫時緩存到列表ArrayList中
- setReplication(rep, dst, c.getOpt("R"), waitList); // 調用重載的setReplication方法,設置副本因子
- if (waitList != null) {
- waitForReplication(waitList, rep); // 更新waitList中文件的塊的副本因子信息
- }
- }
看一下重載的setReplication方法設置副本因子的實現過程:
- void setReplication(short newRep, String srcf, boolean recursive, List<Path> waitingList) throws IOException {
- Path srcPath = new Path(srcf);
- FileSystem srcFs = srcPath.getFileSystem(getConf()); // 獲取到srcf所在的文件系統srcFs
- Path[] srcs = FileUtil.stat2Paths(srcFs.globStatus(srcPath), srcPath); // 得到滿足srcf模式的全部Path文件
- for(int i=0; i<srcs.length; i++) { // 對每一個Path數組srcs中的每一個文件Path設置副本因子
- setReplication(newRep, srcFs, srcs[i], recursive, waitingList); // 調用另一個重載的setReplication方法進行副本因子的設置
- }
- }
繼續看上面方法調用的setReplication方法,實現如下所示:
- /**
- * 該方法遞歸設置每個src及其子目錄中文件的副本因子
- */
- private void setReplication(short newRep, FileSystem srcFs, Path src, boolean recursive, List<Path> waitingList) throws IOException {
- if (!srcFs.getFileStatus(src).isDir()) { // 遞歸出口:如果src是一個普通文件(而非目錄)
- setFileReplication(src, srcFs, newRep, waitingList); // 調用setFileReplication方法設置文件src的副本因子
- return;
- }
- FileStatus items[] = srcFs.listStatus(src); // 如果src是目錄,獲取該目錄中所有的文件FileStatus數組
- if (items == null) {
- throw new IOException("Could not get listing for " + src);
- } else {
- for (int i = 0; i < items.length; i++) { // 分治思想:對每一個items[i]進行遞歸設置副本因子
- if (!items[i].isDir()) {
- setFileReplication(items[i].getPath(), srcFs, newRep, waitingList); // items[i]不是目錄,調用setFileReplication方法設置副本因子
- } else if (recursive) { // 如果指定recursive=true,且items[i]是一個目錄
- setReplication(newRep, srcFs, items[i].getPath(), recursive, waitingList); // 遞歸設置副本因子
- }
- }
- }
- }
上面方法調用了setFileReplication方法,設置一個非目錄文件的副本因子,實現過程如下所示:
- private void setFileReplication(Path file, FileSystem srcFs, short newRep, List<Path> waitList) throws IOException {
- if (srcFs.setReplication(file, newRep)) { // 調用文件系統srcFs的設置副本因子的方法,設置副本因子
- if (waitList != null) {
- waitList.add(file); // 將設置副本因子完成的文件file加入到waitList列表
- }
- System.out.println("Replication " + newRep + " set: " + file);
- } else {
- System.err.println("Could not set replication for: " + file);
- }
- }
我們再回到最前面重載的setReplication方法,已經完成了設置副本因子的任務,然後需要執行waitForReplication(waitList, rep);語句。此時,全部需要設置副本因子的文件都已經緩存到waitList列表中,下面看調用該方法對waitList列表中的文件執行的操作:
- /**
- * 等待在waitList列表中的文件,所對應的每一個塊的副本因子,都設置爲指定的值rep
- */
- void waitForReplication(List<Path> waitList, int rep) throws IOException {
- for(Path f : waitList) {
- System.out.print("Waiting for " + f + " ...");
- System.out.flush();
- boolean printWarning = false; // 如果文件f對應的塊超過rep,是否給出警告信息(需要減少塊副本數量,直到等於rep)
- FileStatus status = fs.getFileStatus(f); // 獲取當前文件系統fs上文件f對應的FileStatus信息
- long len = status.getLen(); // 文件f的長度
- for(boolean done = false; !done; ) {
- BlockLocation[] locations = fs.getFileBlockLocations(status, 0, len); // 在當前fs上獲取文件f對應的全部塊的位置信息對象(一個數組)
- int i = 0;
- for(; i < locations.length && locations[i].getHosts().length == rep; i++) { // 遍歷文件f的每個塊
- if (!printWarning && locations[i].getHosts().length > rep) { // 如果文件f的某個塊的位置信息locations[i]中,主機列表長度(其實就是副本因子的值)大於待設置的副本因子rep
- System.out.println("/nWARNING: the waiting time may be long for " + "DECREASING the number of replication."); // 打印警告信息,需要適當刪除該塊副本,以滿足副本因子要求
- printWarning = true; // 對於同一個文件f,只打印一次警告信息(如果滿足f中的條件時)
- }
- } // for
- done = i == locations.length; // 對文件f對應的塊都檢查過以後,設置檢查完成標誌done
- if (!done) { // 沒有經過上述檢查(文件f對應的塊副本小於0的情況下)
- System.out.print(".");
- System.out.flush();
- try {Thread.sleep(10000);} catch (InterruptedException e) {}
- }
- }
- System.out.println(" done");
- }
- }
這裏,有必要了解一下org.apache.hadoop.fs.BlockLocation的含義,可以看BlockLocation類定義的屬性,如下所示:
- private String[] hosts; //hostnames of datanodes
- private String[] names; //hostname:portNumber of datanodes
- private String[] topologyPaths; // full path name in network topology
- private long offset; //offset of the of the block in the file
- private long length;
可見,一個BlockLocation包含了一個文件的一個塊的詳細信息,包括這個塊對應的全部副本(包含它本身) ,比如上述定義的有:所在主機、所在主機及其端口號、在網絡拓撲結構中的全路徑名稱、塊在文件中的偏移位置、塊長度。顯然,這些塊副本長度和在文件中的偏移位置都是相同的,可以共享(分別對應length和offset屬性),其他三個屬性的信息就不相同了(可能存在某兩個相同的情況)。
Hadoop文件系統中,一個文件對應多個塊(Block),每個塊默認大小設置爲64M。那麼,對於由多個塊組成的文件來說,如果想要獲取到該文件的全部塊及其塊副本的信息,就需要通過文件系統中文件的統計信息FileStatus來獲取到一個BlockLocation[],該數組中對應的全部快就能夠構成完整的該文件。
下面通過形式化語言來表達一下上面的含義:
假設一個文件F由n個塊組成,則分別爲:
B(1),B(2),……,B(n)
假設默認塊的大小爲BS,那麼B(1)~B(n-1)一定是大小相同的塊,大小都等於BS,而B(i)<=BS,這是顯而易見的。
文件F的每個塊B(i)都被存儲在指定主機的文件系統中,假設存儲到了主機H(i)上。爲了快速計算,需要快速定位到文件F的Bi塊上,也就是需要進行流式讀取獲取到,那麼F的塊B(i)需要有一個記錄其詳細信息的結構,也就是Hadoop定義的BlockLocation。假設Bi對應的描述信息對象爲BL(i),那麼BL(i)就包含了與塊B(i)相關的全部塊副本的信息,當然每個塊副本同樣包含與BL(i),相同的描述信息的屬性,只是屬性值不同而已。
假設文件F對應的塊B(i)一共具有m個副本:
BR1(i),BR2(i),……,BRm(i)
這些塊副本分別存儲在對應如下的主機上:
H1(i),H2(i),……,Hm(i)
這些塊副本分別對應指定主機的端口號分別如下:
H1(i):P1(i),H2(i):P2(i),……,Hm(i):Pm(i)
這些塊副本對應的拓撲網絡中的完整路徑分別爲:
U1(i),U2(i),……,Um(i)
假設塊Bi的長度爲LENGTH(i),偏移位置爲OFFSET(i),那麼,通過該文件的FileStatus獲取的BlockLocation[i]的內容,形式化的可以描述爲:
- new BlockLocation[]{
- new String[m]{H1(i), H2(i), ……, Hm(i)},
- new String[m]{H1(i):P1(i), H2(i):P2(i), ……, Hm(i):Pm(i)},
- new String[m]{U1(i), U2(i), ……, Um(i)},
- LENGTH(i),
- OFFSET(i)
- }
關於獲取到一個文件(對應的FileStatus)的BlockLocation[],可以看到FileSystem類中getFileBlockLocations方法的實現,如下所示:
- public BlockLocation[] getFileBlockLocations(FileStatus file, long start, long len) throws IOException { // 根據文件F對應的FileStatus,及其位置start和長度信息len就能獲取到
- if (file == null) {
- return null;
- }
- if ( (start<0) || (len < 0) ) {
- throw new IllegalArgumentException("Invalid start or len parameter");
- }
- if (file.getLen() < start) {
- return new BlockLocation[0];
- }
- String[] name = { "localhost:50010" };
- String[] host = { "localhost" };
- return new BlockLocation[] { new BlockLocation(name, host, 0, file.getLen()) };
- }
上面這個方法只能獲取到本機上的一個塊。如果在Hadoop分佈式文件系統中,這個方法就需要被重寫了,使得通過客戶端能夠獲取到指定文件的塊,在不同主機上分佈的塊副本。