Hadoop3.2.1 【 HDFS 】源码分析 : 文件系统FSImage解析

Table of Contents

一.前言

二.构建&初始化

三.保存命名空间

3.1.saveFSImageInAllDirs()

3.2.FSImageSaver

3.3.saveFSImage(context, sd, nnf);

3.4.FSImageFormatProtobuf.Saver

3.5.saveInternal()方法

3.5.1. 写入文件头[ FSImageUtil.FILE_VERSION  ]

3.5.2. 构建FileSummary 对象

3.5.3. 处理数据的压缩方式:

3.5.4.SectionName

3.5.5.save*()方法

3.5.6. 将FileSummary写入文件

四. 加载FSImage  :  FSImage.loadFSImage()

五. 加载Edits 文件


 

 

一.前言

Namenode会定期将文件系统的命名空间(文件目录树、文件/ 目录元信息) 保存到fsimage文件中, 以防止Namenode掉电或者进程崩溃。 但如果Namenode实时地将内存中的元数据同步到fsimage文件中, 将会非常消耗资源且造成Namenode运行缓慢。 所以Namenode会先将命名空间的修改操作保存在editlog文件中, 然后定期合并fsimage和editlog文件。

FSImage类主要实现了以下功能:

保存命名空间——将当前时刻Namenode内存中的命名空间保存到fsimage文件中。

加载fsimage文件——将磁盘上fsimage文件中保存的命名空间加载到Namenode内存中, 这个操作是保存命名空间操作的逆操作。

加载editlog文件——Namenode加载了fsimage文件后, 内存中只包含了命名空间在保存fsimage文件时的信息, Namenode还需要加载后续对命名空间的修改操作,即editlog文件中记录的内容。 所以FSImage类还提供了加载editlog文件到Namenode内存中的功能.

 

 

 

 

二.构建&初始化

FSImage是在FSNamesystem中的loadFromDisk方法进行初始化的

 

 


  /**
   * Construct the FSImage. Set the default checkpoint directories.
   *
   * Setup storage and initialize the edit log.
   *
   * @param conf Configuration
   * @param imageDirs Directories the image can be stored in.
   * @param editsDirs Directories the editlog can be stored in.
   * @throws IOException if directories are invalid.
   */
  protected FSImage(Configuration conf,
                    Collection<URI> imageDirs,
                    List<URI> editsDirs)
      throws IOException {
    this.conf = conf;

    //构建NNStorage ==> NNStorage负责管理NameNode使用的 StorageDirectories。
    storage = new NNStorage(conf, imageDirs, editsDirs);

    // dfs.namenode.name.dir.restore  默认: false
    // 设置为true可使NameNode尝试恢复以前失败的dfs.NameNode.name.dir。
    // 启用后,将在检查点期间尝试恢复任何失败的目录。
    if(conf.getBoolean(DFSConfigKeys.DFS_NAMENODE_NAME_DIR_RESTORE_KEY,
                       DFSConfigKeys.DFS_NAMENODE_NAME_DIR_RESTORE_DEFAULT)) {
      storage.setRestoreFailedStorage(true);
    }


    // 构建 FSEditLog
    this.editLog = FSEditLog.newInstance(conf, storage, editsDirs);

    //
    archivalManager = new NNStorageRetentionManager(conf, storage, editLog);
  }

 

 

 

 

三.保存命名空间

FSImage类最重要的功能之一就是将当前时刻Namenode的命名空间保存到fsimage文件中。 

3.1.saveFSImageInAllDirs()

Namenode可以定义多个存储路径来保存fsimage文件, 对于每一个存储路径,saveFSImageInAllDirs()方法都会启动一个线程负责在这个路径上保存fsimage文件。 同时, 为了防止保存过程中出现错误, 命名空间信息首先会被保存在一个fsimage.ckpt文件中, 当保存操作全部完成之后, 才会将fsimage.ckpt重命名为fsimage文件。 之后saveFSImageInAllDirs()方法会清理Namenode元数据存储文件夹中过期的editlog文件和fsimage文件。

/**
   * Namenode可以定义多个存储路径来保存fsimage文件,
   *
   * 1. 对于每一个存储路径,saveFSImageInAllDirs()方法都会启动一个线程负责在这个路径上保存fsimage文件。
   *
   * 2. 同时, 为了防止保存过程中出现错误, 命名空间信息首先会被保存在一个fsimage.ckpt文件中,
   * 当保存操作全部完成之后, 才会将fsimage.ckpt重命名为fsimage文件。
   *
   * 3. 之后saveFSImageInAllDirs()方法会清理Namenode元数据存储文件夹中过期的editlog文件和
   * fsimage文件。
   *
   * @param source
   * @param nnf
   * @param txid
   * @param canceler
   * @throws IOException
   */
  private synchronized void saveFSImageInAllDirs(FSNamesystem source,
      NameNodeFile nnf, long txid, Canceler canceler) throws IOException {


    StartupProgress prog = NameNode.getStartupProgress();
    prog.beginPhase(Phase.SAVING_CHECKPOINT);
    if (storage.getNumStorageDirs(NameNodeDirType.IMAGE) == 0) {
      throw new IOException("No image directories available!");
    }
    if (canceler == null) {
      canceler = new Canceler();
    }


    //构造保存命名空间操作的上下文
    SaveNamespaceContext ctx = new SaveNamespaceContext(source, txid, canceler);
    
    try {

      //在每一个保存路径上启动一个线程, 该线程使用FSImageSaver类保存fsimage文件
      List<Thread> saveThreads = new ArrayList<Thread>();
      // save images into current
      for (Iterator<StorageDirectory> it = storage.dirIterator(NameNodeDirType.IMAGE); it.hasNext();) {
        StorageDirectory sd = it.next();


        // 命名空间具体的保存操作是由FSImageSaver这个类来承担的,
        // FSImageSaver是FSImage中的内部类, 也是一个线程类,
        // 它的run()方法调用了saveFSImage()方法来保存fsimage文件。
        FSImageSaver saver = new FSImageSaver(ctx, sd, nnf);

        Thread saveThread = new Thread(saver, saver.toString());


        saveThreads.add(saveThread);
        saveThread.start();
      }

      //等待所有线程执行完毕
      waitForThreads(saveThreads);


      saveThreads.clear();
      storage.reportErrorsOnDirectories(ctx.getErrorSDs());

      //保存文件失败则抛出异常
      if (storage.getNumStorageDirs(NameNodeDirType.IMAGE) == 0) {
        throw new IOException(
          "Failed to save in any storage directories while saving namespace.");
      }
      if (canceler.isCancelled()) {
        deleteCancelledCheckpoint(txid);
        ctx.checkCancelled(); // throws
        assert false : "should have thrown above!";
      }


      // 将fsimage.ckpt 改名为 fsimage
      renameCheckpoint(txid, NameNodeFile.IMAGE_NEW, nnf, false);



      // Since we now have a new checkpoint, we can clean up some
      // old edit logs and checkpoints.
      // Do not purge anything if we just wrote a corrupted FsImage.
      if (!exitAfterSave.get()) {

        //我们已经完成了fsimage的保存, 那么可以将存储上的一部分editlog和fsimage删除
        //如果没有成功,则失败.
        purgeOldStorage(nnf);
        archivalManager.purgeCheckpoints(NameNodeFile.IMAGE_NEW);
      }
    } finally {
      // Notify any threads waiting on the checkpoint to be canceled
      // that it is complete.


      //通知所有等待的线程
      ctx.markComplete();
      ctx = null;
    }


    prog.endPhase(Phase.SAVING_CHECKPOINT);


  }

 

3.2.FSImageSaver

命名空间具体的保存操作是由FSImageSaver这个类来承担的, FSImageSaver是FSImage中的内部类, 也是一个线程类,它的run()方法调用了saveFSImage()方法来保存fsimage文件。 

 

 /**
   *
   * 命名空间具体的保存操作是由FSImageSaver这个类来承担的,
   * FSImageSaver是FSImage中的内部类, 也是一个线程类,
   * 它的run()方法调用了saveFSImage()方法来保存fsimage文件。
   *
   *
   *
   * FSImageSaver is being run in a separate thread when saving
   * FSImage. There is one thread per each copy of the image.
   *
   * FSImageSaver assumes that it was launched from a thread that holds
   * FSNamesystem lock and waits for the execution of FSImageSaver thread
   * to finish.
   * This way we are guaranteed that the namespace is not being updated
   * while multiple instances of FSImageSaver are traversing it
   * and writing it out.
   */
  private class FSImageSaver implements Runnable {
    private final SaveNamespaceContext context;
    private final StorageDirectory sd;
    private final NameNodeFile nnf;

    public FSImageSaver(SaveNamespaceContext context, StorageDirectory sd,
        NameNodeFile nnf) {
      this.context = context;
      this.sd = sd;
      this.nnf = nnf;
    }

    @Override
    public void run() {
      // Deletes checkpoint file in every storage directory when shutdown.
      Runnable cancelCheckpointFinalizer = () -> {
        try {
          deleteCancelledCheckpoint(context.getTxId());
          LOG.info("FSImageSaver clean checkpoint: txid={} when meet " +
              "shutdown.", context.getTxId());
        } catch (IOException e) {
          LOG.error("FSImageSaver cancel checkpoint threw an exception:", e);
        }
      };
      ShutdownHookManager.get().addShutdownHook(cancelCheckpointFinalizer,
          SHUTDOWN_HOOK_PRIORITY);
      try {
        //保存fsimage文件
        System.out.println("context : "+ context);
        System.out.println("sd : "+ sd);
        System.out.println("nnf : "+ nnf);
        saveFSImage(context, sd, nnf);
      } catch (SaveNamespaceCancelledException snce) {
        LOG.info("Cancelled image saving for " + sd.getRoot() +
            ": " + snce.getMessage());
        // don't report an error on the storage dir!
      } catch (Throwable t) {
        LOG.error("Unable to save image for " + sd.getRoot(), t);
        context.reportErrorOnStorageDirectory(sd);
        try {
          deleteCancelledCheckpoint(context.getTxId());
          LOG.info("FSImageSaver clean checkpoint: txid={} when meet " +
              "Throwable.", context.getTxId());
        } catch (IOException e) {
          LOG.error("FSImageSaver cancel checkpoint threw an exception:", e);
        }
      }
    }
    
    @Override
    public String toString() {
      return "FSImageSaver for " + sd.getRoot() +
             " of type " + sd.getStorageDirType();
    }
  }

3.3.saveFSImage(context, sd, nnf);

saveFSImage()方法会使用一个FSImageFormat.Saver对象来完成保存操作,FSImageFormat.Saver类会以fsimage文件定义的格式保存Namenode的命名空间信息, 需要注意命名空空间信息会先写入fsimage.ckpt文件中。 saveFSImage()方法还会生成fsimage文件的md5校验文件, 以确保fsimage文件的正确性。 


  /**
   * 将 FS image 的内容保存到文件中。
   * Save the contents of the FS image to the file.
   */
  void saveFSImage(SaveNamespaceContext context, StorageDirectory sd,
      NameNodeFile dstType) throws IOException {

    //获取当前命名空间中记录的最新事务的txid
    long txid = context.getTxId();

    // fsimage文件
    File newFile = NNStorage.getStorageFile(sd, NameNodeFile.IMAGE_NEW, txid);

    File dstFile = NNStorage.getStorageFile(sd, dstType, txid);

    // FSImageFormatProtobuf.Saver类负责保存fsimage
    FSImageFormatProtobuf.Saver saver = new FSImageFormatProtobuf.Saver(context);

    //压缩类
    FSImageCompression compression = FSImageCompression.createCompression(conf);

    //调用Saver类保存fsimage文件
    long numErrors = saver.save(newFile, compression);

    if (numErrors > 0) {
      // The image is likely corrupted.
      LOG.error("Detected " + numErrors + " errors while saving FsImage " +
          dstFile);
      exitAfterSave.set(true);
    }

    //保存MD5校验值
    MD5FileUtils.saveMD5File(dstFile, saver.getSavedDigest());
    storage.setMostRecentCheckpointInfo(txid, Time.now());

  }

saveFSImage()方法构造了一个FSImageFormatProtobuf.Saver对象来保存命名空间,FSImageFormatProtobuf是一个工具类, 它提供了以protobuf格式读取和写入fsimage文件的方法。 

    /**
     * save()方法会打开fsimage文件的输出流并且获得文件通道,
     * 然后调用saveInternal()方法将命名空间保存到fsimage文件中。
     *
     * @return number of non-fatal errors detected while writing the image.
     * @throws IOException on fatal error.
     */
    long save(File file, FSImageCompression compression) throws IOException {


      FileOutputStream fout = new FileOutputStream(file);

      fileChannel = fout.getChannel();

      try {
        LOG.info("Saving image file {} using {}", file, compression);
        long startTime = monotonicNow();

        // 保存到fsimage文件
        long numErrors = saveInternal( fout, compression, file.getAbsolutePath());


        LOG.info("Image file {} of size {} bytes saved in {} seconds {}.", file,
            file.length(), (monotonicNow() - startTime) / 1000,
            (numErrors > 0 ? (" with" + numErrors + " errors") : ""));
        return numErrors;
      } finally {
        fout.close();
      }
    }

 

3.4.FSImageFormatProtobuf.Saver

使用protobuf定义的fsimage文件的格式, 它包括了4个部分的信息

■ MAGIC: fsimage的文件头, 是“HDFSIMG1”这个字符串的二进制形式, MAGIC头标识了当前fsimage文件是使用protobuf格式序列化的。 FSImage类在读取fsimage文件时, 会先判断fsimage文件是否包含了MAGIC头, 如果包含了则使用protobuf格式反序列化fsimage文件。

■ SECTIONS: fsimage文件会将同一类型的Namenode元信息保存在一个section中,例如将文件系统元信息保存在NameSystemSection中, 将文件系统目录树中的所有INode信息保存在INodeSection中, 将快照信息保存在SnapshotSection中等。fsimage文件的第二个部分就是Namenode各类元信息对应的所有section, 每类section中都包含了对应Namenode元信息的属性.

■ FileSummary: FileSummary记录了fsimage文件的元信息, 以及fsimage文件保存的所有section的信息。 FileSummary中的ondiskVersion字段记录了fsimage文件的版本号(3.2.1 版本中这个字段的值为1) , layoutVersion字段记录了当前HDFS的文件
系统布局版本号, codec字段记录了fsimage文件的压缩编码, sections字段则记录了fsimage文件中各个section字段的元信息, 每个fsimage文件中记录的section在FileSummary中都有一个与之对应的section字段。 FileSummary的section字段记录了对应的fsimage中section的名称、 在fsimage文件中的长度, 以及这个section在fsimage中的起始位置。 FSImage类在读取fsimage文件时, 会先从fsimage中读取出FileSummary部分, 然后利用FileSummary记录的元信息指导fsimage文件的反序列化操作。

■ FileSummaryLength: FileSummaryLength记录了FileSummary在fsimage文件中所占的长度, FSImage类在读取fsimage文件时, 会首先读取FileSummaryLength获取FileSummary部分的长度, 然后根据这个长度从fsimage中反序列化出FileSummary

 

FSImageFormatProtobuf.Saver类就是以protobuf格式将Namenode的命名空间保存至fsimage文件的工具类.这个类的入口方法是save()方法。 save()方法会打开fsimage文件的输出流并且获得文件通道, 然后调用saveInternal()方法将命名空间保存到fsimage文件中.

3.5.saveInternal()方法

saveInternal()方法首先构造底层fsimage文件的输出流, 构造fsimage文件的描述类FileSummary, 然后在FileSummary中记录ondiskVersion、 layoutVersion、 codec等信息。

接下来saveInternal()方法依次向fsimage文件中写入命名空间信息、 inode信息、 快照信息、 安全信息、 缓存信息、 StringTable信息等。注意上述信息都是以section为单位写入的, 每个section的格式定义请参考fsimage.proto文件。

saveInternal()方法以section为单位写入元数据信息时, 还会在FileSummary中记录这个section的长度, 以及section在fsimage文件中的起始位置等信息。 当完成了所有section的写入后, FileSummary对象也就构造完毕了.saveInternal()最后会将FileSummary对象写入fsimage文件中。 

    /**
     * saveInternal()方法首先构造底层fsimage文件的输出流,
     * 构造fsimage文件的描述类 FileSummary ,
     *
     * 然后在FileSummary中记录ondiskVersion、 layoutVersion、 codec等信息。
     * 接下来saveInternal()方法依次向fsimage文件中写入
     *     1.命名空间信息、
     *     2.inode信息、
     *     3.快照信息、
     *     4.安全信息、
     *     5.缓存信息、
     *     6.StringTable
     * 信息等。
     *
     * 注意上述信息都是以section为单位写入的, 每个section的格式定义请参考fsimage.proto文件。
     * saveInternal()方法以section为单位写入元数据信息时,
     * 还会在FileSummary中记录这个section的长度,
     * 以及section在fsimage文件中的起始位置等信息。
     *
     * 当完成了所有section的写入后,
     * FileSummary对象也就构造完毕了,
     * saveInternal()最后会将
     * FileSummary对象写入fsimage文件中。
     *
     * @return number of non-fatal errors detected while writing the FsImage.
     * @throws IOException on fatal error.
     *
     */
    private long saveInternal(FileOutputStream fout, FSImageCompression compression, String filePath) throws IOException {


      StartupProgress prog = NameNode.getStartupProgress();

      //构造输出流, 一边写入数据, 一边写入校验值
      MessageDigest digester = MD5Hash.getDigester();

      int layoutVersion = context.getSourceNamesystem().getEffectiveLayoutVersion();

      underlyingOutputStream = new DigestOutputStream(new BufferedOutputStream(fout), digester);

      underlyingOutputStream.write(FSImageUtil.MAGIC_HEADER);

      fileChannel = fout.getChannel();

      // FileSummary为fsimage文件的描述部分, 也是protobuf定义的
      FileSummary.Builder b = FileSummary.newBuilder()
          .setOndiskVersion(FSImageUtil.FILE_VERSION)
          .setLayoutVersion(
              context.getSourceNamesystem().getEffectiveLayoutVersion());

      //获取压缩格式, 并装饰输出流
      codec = compression.getImageCodec();


      if (codec != null) {
        b.setCodec(codec.getClass().getCanonicalName());
        sectionOutputStream = codec.createOutputStream(underlyingOutputStream);
      } else {
        sectionOutputStream = underlyingOutputStream;
      }

      //保存命名空间信息
      saveNameSystemSection(b);

      // Check for cancellation right after serializing the name system section.
      // Some unit tests, such as TestSaveNamespace#testCancelSaveNameSpace
      // depends on this behavior.

      // 检查是否取消了保存操作
      context.checkCancelled();

      Step step;

      // Erasure coding policies should be saved before inodes
      if (NameNodeLayoutVersion.supports(
          NameNodeLayoutVersion.Feature.ERASURE_CODING, layoutVersion)) {

        step = new Step(StepType.ERASURE_CODING_POLICIES, filePath);
        prog.beginStep(Phase.SAVING_CHECKPOINT, step);

        //保存 ErasureCoding 信息
        saveErasureCodingSection(b);
        prog.endStep(Phase.SAVING_CHECKPOINT, step);

      }


      //保存命名空间中的inode信息
      step = new Step(StepType.INODES, filePath);

      prog.beginStep(Phase.SAVING_CHECKPOINT, step);
      // Count number of non-fatal errors when saving inodes and snapshots.
      // 保存命名空间中的inode信息
      long numErrors = saveInodes(b);

      // 保存快照信息
      numErrors += saveSnapshots(b);
      prog.endStep(Phase.SAVING_CHECKPOINT, step);


      // 保存安全信息
      step = new Step(StepType.DELEGATION_TOKENS, filePath);
      prog.beginStep(Phase.SAVING_CHECKPOINT, step);
      saveSecretManagerSection(b);
      prog.endStep(Phase.SAVING_CHECKPOINT, step);


      // 保存缓存信息
      step = new Step(StepType.CACHE_POOLS, filePath);
      prog.beginStep(Phase.SAVING_CHECKPOINT, step);
      saveCacheManagerSection(b);
      prog.endStep(Phase.SAVING_CHECKPOINT, step);

      // 保存StringTable
      saveStringTableSection(b);

      // We use the underlyingOutputStream to write the header. Therefore flush
      // the buffered stream (which is potentially compressed) first.

      // flush输出流
      flushSectionOutputStream();

      FileSummary summary = b.build();
      //将FileSummary写入文件
      saveFileSummary(underlyingOutputStream, summary);

      //关闭底层输出流
      underlyingOutputStream.close();

      savedDigest = new MD5Hash(digester.digest());
      return numErrors;


    }

3.5.1. 写入文件头[ FSImageUtil.FILE_VERSION  ]

fsimage的文件头, 是“HDFSIMG1”这个字符串的二进制形式. MAGIC头标识了当前fsimage文件是使用protobuf格式序列化的。

3.5.2. 构建FileSummary 对象

      // FileSummary为fsimage文件的描述部分, 也是protobuf定义的    FILE_VERSION : 1
      FileSummary.Builder b = FileSummary.newBuilder()
          .setOndiskVersion(FSImageUtil.FILE_VERSION)
          .setLayoutVersion(
              context.getSourceNamesystem().getEffectiveLayoutVersion());

3.5.3. 处理数据的压缩方式:

      //获取压缩格式, 并装饰输出流
      codec = compression.getImageCodec();


      if (codec != null) {
        b.setCodec(codec.getClass().getCanonicalName());
        sectionOutputStream = codec.createOutputStream(underlyingOutputStream);
      } else {
        sectionOutputStream = underlyingOutputStream;
      }

3.5.4.SectionName

SectionName是一个枚举类一共记录了12种类型. [按写入的fsimage的顺序进行排列]

序号名称Section对象描述
1NS_INFONameSystemSection命名空间信息
2ERASURE_CODINGErasureCodingSection擦除编码信息
3INODEINodeSection保存inode 信息
4INODE_DIRINodeDirectorySection构建目录信息
5FILES_UNDERCONSTRUCTIONFileUnderConstructionEntry租约管理
6SNAPSHOTSnapshotSection保存快照信息
7SNAPSHOT_DIFFSnapshotDiffSection保存快照信息
8INODE_REFERENCEINodeReferenceSection引用信息
9SECRET_MANAGERSecretManagerSection安全信息
10CACHE_MANAGERCacheManagerSection缓存信息
11STRING_TABLEStringTableSection保存StringTable
12EXTENDED_ACL  


 

public enum SectionName {
    NS_INFO("NS_INFO"),
    STRING_TABLE("STRING_TABLE"),
    EXTENDED_ACL("EXTENDED_ACL"),
    ERASURE_CODING("ERASURE_CODING"),
    INODE("INODE"),
    INODE_REFERENCE("INODE_REFERENCE"),
    SNAPSHOT("SNAPSHOT"),
    INODE_DIR("INODE_DIR"),
    FILES_UNDERCONSTRUCTION("FILES_UNDERCONSTRUCTION"),
    SNAPSHOT_DIFF("SNAPSHOT_DIFF"),
    SECRET_MANAGER("SECRET_MANAGER"),
    CACHE_MANAGER("CACHE_MANAGER");

    private static final SectionName[] values = SectionName.values();

    public static SectionName fromString(String name) {
      for (SectionName n : values) {
        if (n.name.equals(name))
          return n;
      }
      return null;
    }

    private final String name;

    private SectionName(String name) {
      this.name = name;
    }
  }

3.5.5.save*()方法

saveInternal()方法调用了多个save*()方法来记录不同section的元数据信息, 这些方法除了在fsimage文件中写入对应种类的元数据信息外, 还会在FileSummary中记录section的大小, 以及在fsimage中的起始位置。

以saveINodes()方法举例, 改方法构造了一个FSImageFormatPBINode.Saver对象, 并调用这个对象对应的方法保存文件系统目录树中的INode信息、 INodeDirectory信息, 以及处于构建状态的文件信息

    // saveINodes()方法构造了一个FSImageFormatPBINode.Saver对象,
    // 并调用这个对象对应的方法保存文件系统目录树中的INode信息、 INodeDirectory信息,
    // 以及处于构建状态的文件信息
    private long saveInodes(FileSummary.Builder summary) throws IOException {

      FSImageFormatPBINode.Saver saver = new FSImageFormatPBINode.Saver(this,summary);

      // 保存INode信息是由FSImageFormatPBINode.Saver.serializeINodeSection()方法实现的
      saver.serializeINodeSection(sectionOutputStream);
      // 保存info目录信息
      saver.serializeINodeDirectorySection(sectionOutputStream);
      // 租约管理
      saver.serializeFilesUCSection(sectionOutputStream);

      return saver.getNumImageErrors();
    }

保存INode信息是由FSImageFormatPBINode.Saver.serializeINodeSection()方法实现的。

serializeINodeSection()方法会首先构造一个INodeSection对象, 记录文件系统目录树中保存的最后一个inode的inodeid, 以及命名空间中所有inode的个数。 之后迭代处理将所有inode信息写入fsimage文件中, 最后将INodeSection的属性信息记录在FileSummary中。

    /**
     * serializeINodeSection()方法会首先构造一个INodeSection对象,
     * 记录文件系统目录树中保存的最后一个inode的inodeid,
     * 以及命名空间中所有inode的个数。
     *
     * 之后迭代处理将所有inode信息写入fsimage文件中,
     * 最后将INodeSection的属性信息记录在FileSummary中。
     *
     * serializeINodeSection
     * @param out
     * @throws IOException
     */
    void serializeINodeSection(OutputStream out) throws IOException {


      INodeMap inodesMap = fsn.dir.getINodeMap();

      //构造一个 INodeSection, 保存最后一个inode的 inodeid, 以及这个命名空间中所有inode的个数
      INodeSection.Builder b = INodeSection.newBuilder()
          .setLastInodeId(fsn.dir.getLastInodeId()).setNumInodes(inodesMap.size());

      INodeSection s = b.build();

      //序列化至输出流
      s.writeDelimitedTo(out);

      int i = 0;

      //迭代处理inodeMap中所有的inode, 调用save()方法将inode信息保存到fsimage中
      Iterator<INodeWithAdditionalFields> iter = inodesMap.getMapIterator();
      while (iter.hasNext()) {
        INodeWithAdditionalFields n = iter.next();
        // 将所有inode信息写入fsimage文件中
        save(out, n);
        ++i;
        if (i % FSImageFormatProtobuf.Saver.CHECK_CANCEL_INTERVAL == 0) {
          context.checkCancelled();
        }
      }

      //调用commitSection()方法在FileSummary中写入inode section
      parent.commitSection(summary, FSImageFormatProtobuf.SectionName.INODE);
    }

save()方法首先将当前INode对象分为目录、 文件以及符号链接等几类, 然后调用各个类型对应的save()重载方法。 重载方法的实现也非常简单, 就是构造不同的protobuf Builder类, 然后设置相应字段的值, 并将序列化之后的对象写入fsimage文件的输出流中。 这里以INodeFile为例, 首先构造protobuf Builder类——INodeSection.INodeFile.Builder, 然后设置blocks——也就是当前文件有哪些数据块, 如果当前的INodeFile处于构建状态, 则设置对应的构建信息。 最后将序列化后的inode信息写入输出流中。

    /**
     * @param out
     * @param n
     * @throws IOException
     */
    private void save(OutputStream out, INode n) throws IOException {
      if (n.isDirectory()) {
        save(out, n.asDirectory());
      } else if (n.isFile()) {
        save(out, n.asFile());
      } else if (n.isSymlink()) {
        save(out, n.asSymlink());
      }
    }

    private void save(OutputStream out, INodeDirectory n) throws IOException {
      INodeSection.INodeDirectory.Builder b = buildINodeDirectory(n,
          parent.getSaverContext());
      INodeSection.INode r = buildINodeCommon(n)
          .setType(INodeSection.INode.Type.DIRECTORY).setDirectory(b).build();
      r.writeDelimitedTo(out);
    }

    // 首先构造protobuf Builder类——INodeSection.INodeFile.Builder,
    // 然后设置blocks——也就是当前文件有哪些数据块,
    // 如果当前的INodeFile处于构建状态, 则设置对应的构建信息。
    //
    // 最后将序列化后的inode信息写入输出流中。

    private void save(OutputStream out, INodeFile n) throws IOException {
      INodeSection.INodeFile.Builder b = buildINodeFile(n,
          parent.getSaverContext());
      BlockInfo[] blocks = n.getBlocks();

      if (blocks != null) {
        for (Block block : n.getBlocks()) {
          b.addBlocks(PBHelperClient.convert(block));
        }
      }

      FileUnderConstructionFeature uc = n.getFileUnderConstructionFeature();
      if (uc != null) {
        INodeSection.FileUnderConstructionFeature f =
            INodeSection.FileUnderConstructionFeature
            .newBuilder().setClientName(uc.getClientName())
            .setClientMachine(uc.getClientMachine()).build();
        b.setFileUC(f);
      }

      INodeSection.INode r = buildINodeCommon(n)
          .setType(INodeSection.INode.Type.FILE).setFile(b).build();
      r.writeDelimitedTo(out);
    }


    private void save(OutputStream out, INodeSymlink n) throws IOException {
      INodeSection.INodeSymlink.Builder b = INodeSection.INodeSymlink
          .newBuilder()
          .setPermission(buildPermissionStatus(n))
          .setTarget(ByteString.copyFrom(n.getSymlink()))
          .setModificationTime(n.getModificationTime())
          .setAccessTime(n.getAccessTime());

      INodeSection.INode r = buildINodeCommon(n)
          .setType(INodeSection.INode.Type.SYMLINK).setSymlink(b).build();
      r.writeDelimitedTo(out);
    }

3.5.6. 将FileSummary写入文件

    private static void saveFileSummary(OutputStream out, FileSummary summary)
        throws IOException {
      summary.writeDelimitedTo(out);
      int length = getOndiskTrunkSize(summary);
      byte[] lengthBytes = new byte[4];
      ByteBuffer.wrap(lengthBytes).asIntBuffer().put(length);
      out.write(lengthBytes);
    }



 

四. 加载FSImage  :  FSImage.loadFSImage()

当Namenode启动时, 首先会将fsimage文件中记录的命名空间加载到Namenode内存中, 然后再一条一条地将editlog文件中记录的更新操作加载并合并到命名空间中。 

整体代码流程图:

Namenode会等待各个Datanode向自己汇报数据块信息来组装blockMap, 从而离开安全模式。 Namenode每次启动时都会调用FSImage.loadFSImage()方法执行加载fsimage和editlog文件的操作。 

这里需要关注两个地方, 一个是加载 FSImage 文件fsImage.recoverTransitionRead(startOpt, this, recovery); 另外一个是根据条件判断是否要合并edits文件[间隔周期1小时或者,edits数量操作100万.].

 

我们直接说重要的loadFSImage方法,前面其实还有些方法,但是太啰嗦了,直接看重点吧. 其实就是加载最后一个fsimage文件.

加载分两步:

1 获取加载器 FSImageFormat.LoaderDelegator .

2. 加载文件: loader.load(curFile, requireSameLayoutVersion);

/**
   * Load in the filesystem image from file. It's a big list of
   * filenames and blocks.
   */
  private void loadFSImage(File curFile, MD5Hash expectedMd5,
      FSNamesystem target, MetaRecoveryContext recovery,
      boolean requireSameLayoutVersion) throws IOException {
    // BlockPoolId is required when the FsImageLoader loads the rolling upgrade
    // information. Make sure the ID is properly set.
    target.setBlockPoolId(this.getBlockPoolID());
    //获取加载器 FSImageFormat.LoaderDelegator
    FSImageFormat.LoaderDelegator loader = FSImageFormat.newLoader(conf, target);
    //加载文件
    loader.load(curFile, requireSameLayoutVersion);

    // Check that the image digest we loaded matches up with what
    // we expected
    MD5Hash readImageMd5 = loader.getLoadedImageMd5();
    if (expectedMd5 != null &&
        !expectedMd5.equals(readImageMd5)) {
      throw new IOException("Image file " + curFile +
          " is corrupt with MD5 checksum of " + readImageMd5 +
          " but expecting " + expectedMd5);
    }

    long txId = loader.getLoadedImageTxId();
    LOG.info("Loaded image for txid " + txId + " from " + curFile);
    lastAppliedTxId = txId;
    storage.setMostRecentCheckpointInfo(txId, curFile.lastModified());
  }

我们看一下load方法:

public void load(File file, boolean requireSameLayoutVersion)
        throws IOException {
      Preconditions.checkState(impl == null, "Image already loaded!");

      FileInputStream is = null;
      try {
        is = new FileInputStream(file);
        byte[] magic = new byte[FSImageUtil.MAGIC_HEADER.length];
        IOUtils.readFully(is, magic, 0, magic.length);

        // fsimage文件中包括 magicHeader, 使用的是protobuf序列化方式
        if (Arrays.equals(magic, FSImageUtil.MAGIC_HEADER)) {

          //构造FSImageFormatProtobuf.Loader加载fsimage文件
          FSImageFormatProtobuf.Loader loader = new FSImageFormatProtobuf.Loader(
              conf, fsn, requireSameLayoutVersion);
          impl = loader;
          loader.load(file);

        } else {
          //否则构造FSImageFormat.Loader加载fsimage文件
          Loader loader = new Loader(conf, fsn);
          impl = loader;
          loader.load(file);
        }
      } finally {
        IOUtils.cleanupWithLogger(LOG, is);
      }
    }

这里面是构造FSImageFormatProtobuf.Loader对象,使用它的load方法加载fsimage文件. 在load方法中最终调用了loadInternal(raFile, fin);方法. 这个方法是加载fsimage文件的相对最底层的方法了.

在加载fsimage操作中, 最终会调用FSImageFormatProtobuf.Loader作为fsimage文件的加载类。 FSImageFormatProtobuf.Loader.loadInternal()方法执行了加载fsimage文件的操作, loadInternal()方法会打开fsimage文件通道, 然后读取fsimage文件中的FileSummary对象, FileSummary对象中记录了fsimage中保存的所有section的信息。loadInternal()会对FileSummary对象中保存的section排序, 然后遍历每个section并调用对应的方法从fsimage文件中加载这个section。

 

代码如下:

// loadInternal()方法会打开fsimage文件通道,
    // 然后读取fsimage文件中的FileSummary对象,
    // FileSummary对象中记录了fsimage中保存的所有section的信息。
    // loadInternal()会对FileSummary对象中保存的section排序,
    // 然后遍历每个section并调用对应的方法从fsimage文件中加载这个section。
    private void loadInternal(RandomAccessFile raFile, FileInputStream fin)
        throws IOException {
      if (!FSImageUtil.checkFileFormat(raFile)) {
        throw new IOException("Unrecognized file format");
      }

      // 从fsimage文件末尾加载FileSummary, 也就是fsimage文件内容的描述
      FileSummary summary = FSImageUtil.loadSummary(raFile);

      if (requireSameLayoutVersion && summary.getLayoutVersion() !=
          HdfsServerConstants.NAMENODE_LAYOUT_VERSION) {
        throw new IOException("Image version " + summary.getLayoutVersion() +
            " is not equal to the software version " +
            HdfsServerConstants.NAMENODE_LAYOUT_VERSION);
      }

      //获取通道
      FileChannel channel = fin.getChannel();

      // 构造FSImageFormatPBINode.Loader和FSImageFormatPBSnapshot.
      // Loader加载INode以及Snapshot
      FSImageFormatPBINode.Loader inodeLoader = new FSImageFormatPBINode.Loader(
          fsn, this);
      FSImageFormatPBSnapshot.Loader snapshotLoader = new FSImageFormatPBSnapshot.Loader(
          fsn, this);

      //对fsimage文件描述中记录的sections进行排序
      ArrayList<FileSummary.Section> sections = Lists.newArrayList(summary
          .getSectionsList());
      Collections.sort(sections, new Comparator<FileSummary.Section>() {
        @Override
        public int compare(FileSummary.Section s1, FileSummary.Section s2) {
          SectionName n1 = SectionName.fromString(s1.getName());
          SectionName n2 = SectionName.fromString(s2.getName());
          if (n1 == null) {
            return n2 == null ? 0 : -1;
          } else if (n2 == null) {
            return -1;
          } else {
            return n1.ordinal() - n2.ordinal();
          }
        }
      });



      StartupProgress prog = NameNode.getStartupProgress();
      /**
       * beginStep() and the endStep() calls do not match the boundary of the
       * sections. This is because that the current implementation only allows
       * a particular step to be started for once.
       */
      Step currentStep = null;

      //遍历每个section, 并调用对应的方法加载这个section
      for (FileSummary.Section s : sections) {

        //在通道中定位这个section的起始位置
        channel.position(s.getOffset());
        InputStream in = new BufferedInputStream(new LimitInputStream(fin,
            s.getLength()));

        in = FSImageUtil.wrapInputStreamForCompression(conf,
            summary.getCodec(), in);

        String n = s.getName();

        //调用对应的方法加载不同的section
        switch (SectionName.fromString(n)) {
        case NS_INFO:
          loadNameSystemSection(in);
          break;
        case STRING_TABLE:
          loadStringTableSection(in);
          break;
        case INODE: {
          currentStep = new Step(StepType.INODES);
          prog.beginStep(Phase.LOADING_FSIMAGE, currentStep);
          inodeLoader.loadINodeSection(in, prog, currentStep);
        }
          break;
        case INODE_REFERENCE:
          snapshotLoader.loadINodeReferenceSection(in);
          break;
        case INODE_DIR:
          inodeLoader.loadINodeDirectorySection(in);
          break;
        case FILES_UNDERCONSTRUCTION:
          inodeLoader.loadFilesUnderConstructionSection(in);
          break;
        case SNAPSHOT:
          snapshotLoader.loadSnapshotSection(in);
          break;
        case SNAPSHOT_DIFF:
          snapshotLoader.loadSnapshotDiffSection(in);
          break;
        case SECRET_MANAGER: {
          prog.endStep(Phase.LOADING_FSIMAGE, currentStep);
          Step step = new Step(StepType.DELEGATION_TOKENS);
          prog.beginStep(Phase.LOADING_FSIMAGE, step);
          loadSecretManagerSection(in, prog, step);
          prog.endStep(Phase.LOADING_FSIMAGE, step);
        }
          break;
        case CACHE_MANAGER: {
          Step step = new Step(StepType.CACHE_POOLS);
          prog.beginStep(Phase.LOADING_FSIMAGE, step);
          loadCacheManagerSection(in, prog, step);
          prog.endStep(Phase.LOADING_FSIMAGE, step);
        }
          break;
        case ERASURE_CODING:
          Step step = new Step(StepType.ERASURE_CODING_POLICIES);
          prog.beginStep(Phase.LOADING_FSIMAGE, step);
          loadErasureCodingSection(in);
          prog.endStep(Phase.LOADING_FSIMAGE, step);
          break;
        default:
          LOG.warn("Unrecognized section {}", n);
          break;
        }
      }
    }

对于不同类型的section, loadInternal()方法会调用不同的方法加载这个section, 例如对于INodeSection会调用InodeLoader.loadINodeSection()方法加载。 load*()方法的实现都比较简单, 就是按照protobuf格式加载不同的section. 慢慢恢复/构建FSNamesystem对象中的内容.

 

下面是加载loadINodeSection的代码示例:

void loadINodeSection(InputStream in, StartupProgress prog,
        Step currentStep) throws IOException {

      INodeSection s = INodeSection.parseDelimitedFrom(in);

      fsn.dir.resetLastInodeId(s.getLastInodeId());

      long numInodes = s.getNumInodes();

      LOG.info("Loading " + numInodes + " INodes.");
      prog.setTotal(Phase.LOADING_FSIMAGE, currentStep, numInodes);
      Counter counter = prog.getCounter(Phase.LOADING_FSIMAGE, currentStep);
      for (int i = 0; i < numInodes; ++i) {
        INodeSection.INode p = INodeSection.INode.parseDelimitedFrom(in);
        if (p.getId() == INodeId.ROOT_INODE_ID) {
          // 加载root
          loadRootINode(p);
        } else {
          //加载子节点
          INode n = loadINode(p);
          dir.addToInodeMap(n);
        }
        counter.increment();
      }
    }

下面试loadINodeDirectorySection 方法, 构建目录体系

void loadINodeDirectorySection(InputStream in) throws IOException {
      final List<INodeReference> refList = parent.getLoaderContext()
          .getRefList();
      while (true) {
        INodeDirectorySection.DirEntry e = INodeDirectorySection.DirEntry
            .parseDelimitedFrom(in);
        // note that in is a LimitedInputStream
        if (e == null) {
          break;
        }
        INodeDirectory p = dir.getInode(e.getParent()).asDirectory();
        for (long id : e.getChildrenList()) {
          INode child = dir.getInode(id);
          addToParent(p, child);
        }
        for (int refId : e.getRefChildrenList()) {
          INodeReference ref = refList.get(refId);
          addToParent(p, ref);
        }
      }
    }

 

五. 加载Edits 文件

FSImage.loadEdits()方法会构造一个FSEditLogLoader对象. 然后遍历Namenode所有存储路径上保存的editlog文件的输入流  并调用FSEditLogLoader.loadFSEdits()方法加载指定路径上的editlog文件。

/**
   * FSImage.loadEdits()方法会构造一个FSEditLogLoader对象,
   * 然后遍历Namenode所有存储路径上保存的editlog文件的输入流
   * 并调用FSEditLogLoader.loadFSEdits()方法加载指定路径上的editlog文件。
   *
   * @param editStreams
   * @param target
   * @param maxTxnsToRead
   * @param startOpt
   * @param recovery
   * @return
   * @throws IOException
   */
  public long loadEdits(Iterable<EditLogInputStream> editStreams,
      FSNamesystem target, long maxTxnsToRead,
      StartupOption startOpt, MetaRecoveryContext recovery)
      throws IOException {
    LOG.debug("About to load edits:\n  " + Joiner.on("\n  ").join(editStreams));
    StartupProgress prog = NameNode.getStartupProgress();
    prog.beginPhase(Phase.LOADING_EDITS);

    //记录命名空间中加载的最新的事务id
    long prevLastAppliedTxId = lastAppliedTxId;
    long remainingReadTxns = maxTxnsToRead;
    try {

      //构造FSEditLogLoader对象用于加栽editlog文件
      FSEditLogLoader loader = new FSEditLogLoader(target, lastAppliedTxId);


      //遍历所有存储路径上editlog文件对应的输入流
      // Load latest edits
      for (EditLogInputStream editIn : editStreams) {
        LogAction logAction = loadEditLogHelper.record();
        if (logAction.shouldLog()) {
          String logSuppressed = "";
          if (logAction.getCount() > 1) {
            logSuppressed = "; suppressed logging for " +
                (logAction.getCount() - 1) + " edit reads";
          }
          LOG.info("Reading " + editIn + " expecting start txid #" +
              (lastAppliedTxId + 1) + logSuppressed);
        }
        try {

          //调用FSEditLogLoader.loadFSEdits()从某个存储路径上的editlog文件加载修改操作
          remainingReadTxns -= loader.loadFSEdits(editIn, lastAppliedTxId + 1,
                  remainingReadTxns, startOpt, recovery);
        } finally {

          // lastAppliedTxId记录从editlog加载的最新的事务id
          // Update lastAppliedTxId even in case of error, since some ops may
          // have been successfully applied before the error.
          lastAppliedTxId = loader.getLastAppliedTxId();
        }


        // If we are in recovery mode, we may have skipped over some txids.
        if (editIn.getLastTxId() != HdfsServerConstants.INVALID_TXID
            && recovery != null) {
          lastAppliedTxId = editIn.getLastTxId();
        }
        if (remainingReadTxns <= 0) {
          break;
        }
      }
    } finally {
      //关闭所有editlog文件的输入流
      FSEditLog.closeAllStreams(editStreams);
    }
    prog.endPhase(Phase.LOADING_EDITS);
    return lastAppliedTxId - prevLastAppliedTxId;
  }


 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值