在配置Hadoop环境后,第一次启动NameNode节点之前,需要使用命令hadoop namenode -format对NameNode节点进行格式化,那么这个格式化的过程是怎么样的呢?这篇文章就来分析NameNode格式化的过程。
NameNode格式化过程

NameNode节点以参数-format启动之后,会进入NameNode.createNameNode()方法,在这个方法中有一个switch语句,其中有一项就是FORMAT,switch语句代码如下:

[java] view plain copy

switch (startOpt) {  
      case FORMAT:  
        boolean aborted = format(conf, startOpt.getConfirmationNeeded(),  
            startOpt.getInteractive());  
        System.exit(aborted ? 1 : 0);  
      case FINALIZE:  
        aborted = finalize(conf, true);  
        System.exit(aborted ? 1 : 0);  
      case RECOVER:  
        NameNode.doRecovery(startOpt, conf);  
        return null;  
      default:  
    }  

可见,如果以-format参数启动,就会进入到NameNode.format()方法,对NameNode节点进行格式化。NameNode.format()方法的代码如下:

[java] view plain copy

private static boolean format(Configuration conf,  
    boolean isConfirmationNeeded, boolean isInteractive) throws IOException {  
  Collection<File> dirsToFormat = FSNamesystem.getNamespaceDirs(conf);//保存命名空间镜像目录  
  Collection<File> editDirsToFormat =   
               FSNamesystem.getNamespaceEditsDirs(conf);//保存编辑日志目录  
  for(Iterator<File> it = dirsToFormat.iterator(); it.hasNext();) {  
    File curDir = it.next();  
    if (!curDir.exists())  
      continue;//目录不存在  
    if (isConfirmationNeeded) {//目录存在  
      if (!isInteractive) {  
        System.err.println("Format aborted: " + curDir + " exists.");  
        return true;  
      }  
      System.err.print("Re-format filesystem in " + curDir +" ? (Y or N) ");//提示用户确认删除  
      if (!(System.in.read() == 'Y')) {  
        System.err.println("Format aborted in "+ curDir);  
        return true;  
      }  
      while(System.in.read() != '\n'); // discard the enter-key  
    }  
  }  
  
  FSNamesystem nsys = new FSNamesystem(new FSImage(dirsToFormat,  
                                       editDirsToFormat), conf);  
  nsys.dir.fsImage.format();  
  return false;  
}  

NameNode.format()方法有三个参数,第一个参数是配置信息,第二个参数和第三个参数分别通过StartupOption.getConfirmationNeeded()方法和StartupOption.getInteractive()方法获得,即获取StartupOption枚举中的isConfirmationNeeded和isInteractive这两个成员变量的值。在Hadoop源码分析之NameNode的启动与停止这篇文章中分析过NameNode的启动参数,-force参数的用法是java NameNode [-format [ -force ] [-nonInteractive]]。可以看到NameNode的启动参数-format还可以带有两个启动参数,这里称为子参数吧!isConfirmationNeeded和isInteractive这两个成员变量分别对应着force和nonInteractive,其中nonInteractive表示如果NameNode节点的文件夹在当前的底层文件系统中存在,那么用户将不会收到提示,并且当前的格式化会失败,force表示不管NameNode的目录存不存在,强制格式化NameNode节点,也不会提示用户,如果nonInteractive和force参数同时存在,那么force参数将会被忽略。

在NameNode.format()方法的方法体中,先通过FSNamesystem的静态方法getNamespaceDirs()和getNamespaceEditsDirs()获取到保存命名空间镜像和编辑日志的目录,关于命名空间和编辑日志可以参考文章Hadoop源码分析之NameNode的目录构成与类继承结构这篇文章。然后变量保存命名空间镜像的目录,如果这个目录存在,则根据参数isConfirmationNeeded和isInteractive进行处理:未指定-force参数,但是指定了-nonInteractive参数,isConfirmationNeeded变量就为true,isInteractive变量就为false,这样就放弃格式化,退出NameNode.format()方法,如果未指定-force参数,也未指定nonInteractive参数,那么就会针对每一个目录,提示用户是否执行格式化。如果指定了-force参数,则会强制格式化命名空间镜像目录和编辑日志目录,执行下面的代码。

然后创建一个FSNamesystem对象,其中FSNamesystem封装了大量方法供NameNode操作使用。此处FSNamesystem调用的构造方法代码为:

[java] view plain copy

FSNamesystem(FSImage fsImage, Configuration conf) throws IOException {  
    setConfigurationParameters(conf);  
    this.dir = new FSDirectory(fsImage, this, conf);  
    dtSecretManager = createDelegationTokenSecretManager(conf);  
  }  

FSNamesystem的这个构造方法的第一个参数是FSImage对象,FSImage类主要用于NameNode节点管理其存储空间,它管理着存储空间的生存期,同时,也负责命名空间镜像的保存和加载,也需要与第二名字节点合作执行检查点过程。FSImage和FSEditlog一起完成了NameNode节点的存储工作,FSEditlog类主要用于记录NameNode节点中内存元数据(即目录树)的更改。在NameNode.format()方法中,创建FSNamesysten对象的同时,创建了一个FSImage对象,其中调用的FSImage类带有两个集合类参数的构造方法,这两个参数正是保存命名空间镜像的目录的集合和保存编辑日志的目录的集合。所调用的FSImage的构造方法代码如下:

[java] view plain copy

FSImage(Collection<File> fsDirs, Collection<File> fsEditsDirs)   
    throws IOException {  
    this();  
    setStorageDirectories(fsDirs, fsEditsDirs);  
  }  
  
FSImage() {  
    super(NodeType.NAME_NODE);  
    this.editLog = new FSEditLog(this);  
  }  

在这个构造方法中调用了FSImage无参的构造方法,由于FSImage类是Storage类的子类(在Hadoop源码分析之DateNode的目录构成与类继承结构中分析过这几个类的继承结构),所以调用这个午参构造方法会相继调用Storage类的构造方法设置节点类别为NAME_NODE类型,然后Storage会调用父类StorageInfo的构造方法,设置layoutVersion、namespaceID和cTime变量的值,这些值将会保存到NameNode节点的存储目录的current目录的VERSION文件中,用与保存当前NameNod节点的信息。执行完FSImage的父类构造方法后,再创建一个FSEditlog对象。最后调用FSImage.setStorageDirectories()方法将保存命名空间镜像的目录与保存编辑日志的目录归类再统一保存。

fsNameDirs,fsEditsDirs中的目录可能有三种,一种是只存储命名空间镜像,一种是只存储编辑日志,还有一种是存储以上两种文件,通过方法setStorageDirectories()将这三种目录区分开来,并将所有目录导入内存中,通过列表storageDirs保存,FSImage.setStorageDirectories()的代码如下:

[java] view plain copy

void setStorageDirectories(Collection<File> fsNameDirs,  
                        Collection<File> fsEditsDirs  
                             ) throws IOException {  
    storageDirs = new ArrayList<StorageDirectory>();  
    removedStorageDirs = new ArrayList<StorageDirectory>();  
    // Add all name dirs with appropriate NameNodeDirType   
    for (File dirName : fsNameDirs) {  
      boolean isAlsoEdits = false;  
      for (File editsDirName : fsEditsDirs) {  
        if (editsDirName.compareTo(dirName) == 0) {  
          isAlsoEdits = true;  
          fsEditsDirs.remove(editsDirName);  
          break;  
        }  
      }  
      NameNodeDirType dirType = (isAlsoEdits) ?  
                          NameNodeDirType.IMAGE_AND_EDITS :  
                          NameNodeDirType.IMAGE;  
      addStorageDir(new StorageDirectory(dirName, dirType));  
    }  
      
    // Add edits dirs if they are different from name dirs  
    for (File dirName : fsEditsDirs) {  
      addStorageDir(new StorageDirectory(dirName, NameNodeDirType.EDITS));   
    }  
  }  

方法中storageDirs变量是Storage类中定义的一个List类型的成员变量,用于保存命名空间镜像目录和编辑日志目录。首先遍历保存命名空间镜像的目录fsNamedirs,然后与每一个保存编辑日志的目录比较,如果是同一个目录,则给这个目录赋予类型IMAGE_AND_EDITS,说明这个目录同时保存命名空间镜像和编辑日志。这个方法的作用就是区分参数中的每个目录类型,然后加入到一个集合中。

在继续分析前,先分析几个要用到的DirIterator类,StorageDirectory类和NameNodeDirType类在Hadoop源码分析之DateNode的目录构成与类继承结构文章中分析过了,在这篇文章里就不再分析了。

DirIterator类实现了Iterator接口,是Hadoop实现的一个StorageDirectory对象迭代器。命名空间目录和编辑日志目录保存在一个ArrayList集合中,而ArryaList本身就自带一个迭代器,为什么在HDFS中要自己实现一个迭代器呢?因为DirIterator这个迭代器实现的功能比ArrayList所带的迭代器的功能多一些,它可以根据创建迭代器时给定的目录类型参数来迭代特定类型的目录,DirIterator类的代码如下:

[java] view plain copy

private class DirIterator implements Iterator<StorageDirectory> {  
    StorageDirType dirType;  
    int prevIndex; // for remove()  
    int nextIndex; // for next()  
      
    DirIterator(StorageDirType dirType) {  
      this.dirType = dirType;  
      this.nextIndex = 0;  
      this.prevIndex = 0;  
    }  
      
    public boolean hasNext() {  
      if (storageDirs.isEmpty() || nextIndex >= storageDirs.size())  
        return false;  
      if (dirType != null) {  
        while (nextIndex < storageDirs.size()) {  
          if (getStorageDir(nextIndex).getStorageDirType().isOfType(dirType))  
            break;  
          nextIndex++;  
        }  
        if (nextIndex >= storageDirs.size())  
         return false;  
      }  
      return true;  
    }  
      
    public StorageDirectory next() {  
      StorageDirectory sd = getStorageDir(nextIndex);  
      prevIndex = nextIndex;  
      nextIndex++;  
      if (dirType != null) {  
        while (nextIndex < storageDirs.size()) {  
          if (getStorageDir(nextIndex).getStorageDirType().isOfType(dirType))  
            break;  
          nextIndex++;  
        }  
      }  
      return sd;  
    }  
      
    public void remove() {  
      nextIndex = prevIndex; // restore previous state  
      storageDirs.remove(prevIndex); // remove last returned element  
      hasNext(); // reset nextIndex to correct place  
    }  
  }  

DirIterator类的构造方法带有一个StorageDirType类型的参数,赋值给DirIterator类的dirType成员变量,再看看hasNext()方法和next()方法,两个方法中都有一个if语句判断dirType变量是否为null,如果不为null,就在storageDirs集合中找出一个类型为dirType的元素,如果找不到就返回null(对hasNext()方法来说)。ArrayList类的迭代器不具备这样的功能,如果使用ArrayList的迭代器,则每次获取到一个元素之后,都要判断这个元素是否是所要求的类型,如果不是就再取下一个,这样做显然比较麻烦。

接着分析NameNode.format()方法,这个方法的最后一行代码就是执行格式化,即nsys.dir.fsImage.format();,那么来看看FSImage.format()方法的执行过程,代码如下:

[java] view plain copy

public void format() throws IOException {  
    this.layoutVersion = FSConstants.LAYOUT_VERSION;  
    this.namespaceID = newNamespaceID();//生成namespaceID  
    this.cTime = 0L;  
    this.checkpointTime = FSNamesystem.now();  
    for (Iterator<StorageDirectory> it =   
                           dirIterator(); it.hasNext();) {  
      StorageDirectory sd = it.next();  
      format(sd);  
    }  
  }  

先记录layoutVersion,namespaceID,cTime等值,这些值会保存到VERSION文件中,调用dirIterator()方法对上文分析到的存有命名空间镜像目录和编辑日志目录的集合storageDirs进行迭代,这次迭代是对所有的目录都遍历到,因为dirIterator()方法创建的DirIterator对象的dirType值为null,所以会对集合storageDirs中的所有值进行遍历,对遍历到的每个目录使用FSImage.format()带参数的重载方法进行格式化,这个方法的代码如下:

[java] view plain copy

void format(StorageDirectory sd) throws IOException {  
    sd.clearDirectory(); // create currrent dir,创建current目录  
    sd.lock();//锁住这个目录  
    try {  
      saveCurrent(sd);  
    } finally {  
      sd.unlock();  
    }  
    LOG.info("Storage directory " + sd.getRoot()  
             + " has been successfully formatted.");  
  }  

对目录进行格式化的逻辑比较简单,先创建current目录,再对这个目录进行加锁,防止其他线程访问这个目录,三后向current目录中保存VERSION文件,最后对该文件解除锁,这几个过程中比较复杂的就是对目录加锁和将内容保存到current目录中的文件中。StorageDirectory.lock()方法调用StorageDirectory.tryLock()方法对目录进行加锁,如果加锁成功,则会返回一个FileLock对象,如果加锁失败,则返回对象为null,StorageDirectory.tryLock()方法的代码如下:

[java] view plain copy

/** 
     * 在当前目录上获取独占锁,当数据节点运行时,${dfs.data.dir}下会有一个名为“in_use.lock”的文件,就是由tryLock()方法创建并上锁的 
     * “in_use.lock”文件会在数据节点退出时删除,对应的实现代码是lockF.deleteOnExit() 
     */  
    FileLock tryLock() throws IOException {  
      boolean deletionHookAdded = false;  
      File lockF = new File(root, STORAGE_FILE_LOCK);  
      if (!lockF.exists()) {  
        lockF.deleteOnExit();//退出时删除文件“in_use.lock”  
        deletionHookAdded = true;  
      }  
      RandomAccessFile file = new RandomAccessFile(lockF, "rws");  
      FileLock res = null;  
      try {  
        res = file.getChannel().tryLock();  
      } catch(OverlappingFileLockException oe) {  
        file.close();  
        return null;  
      } catch(IOException e) {  
        LOG.error("Cannot create lock on " + lockF, e);  
        file.close();  
        throw e;  
      }  
      if (res != null && !deletionHookAdded) {  
        //在NameNode启动之前,in_use.lock文件存在,那么前面就没有调用deleteOnExit方法,  
        //但是只要存在in_use.lock文件,就说明成功对目录进行加锁了,所以需要调用deleteOnExit方法  
        lockF.deleteOnExit();  
      }  
      return res;  
    }  

tryLock()方法先创建in_use.lock文件(关于in_use.lock文件可以参考博文Hadoop源码分析之NameNode的目录构成与类继承结构),root变量就是当前要进行格式化的的目录,然后就创建获取对当前目录的锁对象,之所以在方法的对后还有一个if语句,是因为在NameNode启动之前,in_use.lock文件可能存在,那么前面就没有调用deleteOnExit方法, 但是只要存在in_use.lock文件,对其他线程来说就成功对目录进行加锁了,即使FileLock对象res为null,所以需要调用deleteOnExit方法,其中deleteOnExit()方法作用是在Java虚拟机退出时,删除调用该方法的文件。

对目录加锁成功之后,就创建日志输出流,调用FSImage.saveCurrent()往目录的文件中写入当前NameNode节点内存中的数据。方法代码如下:

[java] view plain copy

protected void saveCurrent(StorageDirectory sd) throws IOException {  
    File curDir = sd.getCurrentDir();  
    NameNodeDirType dirType = (NameNodeDirType)sd.getStorageDirType();  
    // save new image or new edits  
    if (!curDir.exists() && !curDir.mkdir())  
      throw new IOException("Cannot create directory " + curDir);  
    if (dirType.isOfType(NameNodeDirType.IMAGE))  
      saveFSImage(getImageFile(sd, NameNodeFile.IMAGE));  
    if (dirType.isOfType(NameNodeDirType.EDITS))  
      editLog.createEditLogFile(getImageFile(sd, NameNodeFile.EDITS));  
    // write version and time files  
    sd.write();  
  }  

saveCurrent()方法负责将内存中的信息保存到其参数指定的目录中,在进行NameNode格式化的过程中,保存的仅仅是内存元数据文件/目录树中根节点的信息,首先获取到目录类型,如果是镜像目录,则将当前内存中的信息写出到这个目录下的文件中,如果是编辑日志目录,则在这个目录下创建编辑日志文件。方法的最后在当前目录中写入VERSION文件。写入镜像和创建编辑日志部分的代码分析在分析FSImage和FSEditLog类时再来分析。

执行完上述过程之后,NameNode节点的格式化就完成了,注意,在NameNode.format()方法最后返回的是false,这样,NameNode.format()方法调用完成之后,返回到NameNode.createNameNod()方法中,将该false值赋值给aborted变量,然后执行System.exit(0),NameNode节点正常退出。NameNode的格式化就完成了。

Reference

《Hadoop技术内幕:深入理解Hadoop Common和HDFS架构设计与实现原理》