Hadoop源码分析(22)

1、 加载FSImage文件

  在之前文档中分析了namenode的启动流程,其中namenode启动主要有两个任务:其一是加载元数据,其二是启动相关的服务。其中加载元数据在文档(10)中分析了其最终会调用FSImage类的loadFSImage方法来加载元数据。这个方法大致可以分为5个部分:是查找fsimage文件;初始化editlog;加载editlog流;加载fsimage文件;执行editlog。随后的文档依次分析了:查找FSImage文件、初始化editlog与加载editlog流。

  接着继续分析加载FSImage文件,在loadFSImage方法中相关代码如下:

hadoop 停不掉 hadoop load_hadoop

  如上图所示其中重点在于调用的loadFSImageFile方法加载数据,而加载的FSImage,在之前文档解析了,其存储在配置文件设置的目录中,而FSImage文件的内容是二进制数据内容如下:

hadoop 停不掉 hadoop load_hadoop_02

  这种数据虽然利于程序读取,但不利于人读取,而hadoop提供了将FSImage文件转换成xml文件的命令,命令内容如下:

hdfs oiv -i fsimage_0000000000000076483 -p XML -o fsimage.xml

  转换成xml文件后其内容如下:

hadoop 停不掉 hadoop load_hadoop 停不掉_03

  这里以某一个段数据为例,解析FSImage文件存储的内容。xml数据如下:

<inode>
<id>16563</id>
<type>DIRECTORY</type>
<name>job_2bd7ecec0f9c1112be6d5accd220ea82</name>
<mtime>1611127906838</mtime>
<permission>root:supergroup:rwxr-xr-x</permission>
<nsquota>-1</nsquota>
<dsquota>-1</dsquota>
</inode>


<inode>
<id>16458</id>
<type>FILE</type>
<name>.tableinfo.0000000001</name>
<replication>2</replication>
<mtime>1585788692978</mtime>
<atime>1586310192376</atime>
<perferredBlockSize>134217728</perferredBlockSize>
<permission>hadoop:supergroup:rw-r--r--</permission>
<blocks>
<block>
<id>1073741834</id>
<genstamp>1010</genstamp>
<numBytes>312</numBytes>
</block>
</blocks>
</inode>

  上述xml中的inode是fsimage转换成xml 中的基础节点。这里的inode实际是对应的namenode中的inode对象,而namenode维护的元数据在内存中其实就是这个inode结构。这里的inode分为两类,在上述xml中以type标签区分,DIRECTORY表示目录,FILE表示文件。对目录来说记录id、name、时间戳、权限等数据便可。对文件来说,除了目录有的几个数据外还有几个参数:perferredBlockSize,这个参数是记录文件的块大小;replication这个是记录块的副本数;还有一个blocks标签,其中有block标签,这个标签代表了文件的块信息。

  简单分析了FSImage文件中的内容,之后再来细看加载该文件的方法,上文提到的loadFSImageFile方法内容如下:

void loadFSImageFile(FSNamesystem target, MetaRecoveryContext recovery,
      FSImageFile imageFile, StartupOption startupOption) throws IOException {
    LOG.info("Planning to load image: " + imageFile);
    StorageDirectory sdForProperties = imageFile.sd;
    storage.readProperties(sdForProperties, startupOption);

    if (NameNodeLayoutVersion.supports(
        LayoutVersion.Feature.TXID_BASED_LAYOUT, getLayoutVersion())) {
      // For txid-based layout, we should have a .md5 file
      // next to the image file
      boolean isRollingRollback = RollingUpgradeStartupOption.ROLLBACK
          .matches(startupOption);
      loadFSImage(imageFile.getFile(), target, recovery, isRollingRollback);
    } else if (NameNodeLayoutVersion.supports(
        LayoutVersion.Feature.FSIMAGE_CHECKSUM, getLayoutVersion())) {
      // In 0.22, we have the checksum stored in the VERSION file.
      String md5 = storage.getDeprecatedProperty(
          NNStorage.DEPRECATED_MESSAGE_DIGEST_PROPERTY);
      if (md5 == null) {
        throw new InconsistentFSStateException(sdForProperties.getRoot(),
            "Message digest property " +
            NNStorage.DEPRECATED_MESSAGE_DIGEST_PROPERTY +
            " not set for storage directory " + sdForProperties.getRoot());
      }
      loadFSImage(imageFile.getFile(), new MD5Hash(md5), target, recovery,
          false);
    } else {
      // We don't have any record of the md5sum
      loadFSImage(imageFile.getFile(), null, target, recovery, false);
    }
  }

  这段代码从第7行到第30行实际就是一个if else语句,这里根据NameNode的版本来确定使用不同来使用不同的参数来加载FSImage文件。但是其最终调用的都是loadFSImage方法,这个方法的内容如下:

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 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());
  }

  这段代码的重点在第8行和第9行,这里会创建一个loader,然后调用其load方法来加载数据。其中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);
        if (Arrays.equals(magic, FSImageUtil.MAGIC_HEADER)) {
          FSImageFormatProtobuf.Loader loader = new FSImageFormatProtobuf.Loader(
              conf, fsn, requireSameLayoutVersion);
          impl = loader;
          loader.load(file);
        } else {
          Loader loader = new Loader(conf, fsn);
          impl = loader;
          loader.load(file);
        }
      } finally {
        IOUtils.cleanup(LOG, is);
      }
    }
  }

  首先是第7行,这里利用传入的FSImage文件创建一个输入流,从文件中读取文件数据。然后是第8行,创建了一个固定长度的byte数组,然后是第9行从文件中读取固定长度的数据到第8行的数组中,以上文的解析的FSImage文件为例,这里读取的内容是HDFSIMG1,然后是第10行比较读取的magic(第8行的数组)是否为定义的MAGIC_HEADER。这个值的定义如下:

public static final byte[] MAGIC_HEADER =
      "HDFSIMG1".getBytes(Charsets.UTF_8);

  这里的两个值实际是相同的,所以它会执行if语句内的内容,再创建一个loader,再用这个loader加载数据。这个loader的load方法内容如下:

void load(File file) throws IOException {
      long start = Time.monotonicNow();
      imgDigest = MD5FileUtils.computeMd5ForFile(file);
      RandomAccessFile raFile = new RandomAccessFile(file, "r");
      FileInputStream fin = new FileInputStream(file);
      try {
        loadInternal(raFile, fin);
        long end = Time.monotonicNow();
        LOG.info("Loaded FSImage in {} seconds.", (end - start) / 1000);
      } finally {
        fin.close();
        raFile.close();
      }
    }

  这段代码的重点在第7行的loadInternal方法,这个方法内容如下:

private void loadInternal(RandomAccessFile raFile, FileInputStream fin)
        throws IOException {
      if (!FSImageUtil.checkFileFormat(raFile)) {
        throw new IOException("Unrecognized file format");
      }
      FileSummary summary = FSImageUtil.loadSummary(raFile);
      if (requireSameLayoutVersion && summary.getLayoutVersion() !=
          HdfsConstants.NAMENODE_LAYOUT_VERSION) {
        throw new IOException("Image version " + summary.getLayoutVersion() +
            " is not equal to the software version " +
            HdfsConstants.NAMENODE_LAYOUT_VERSION);
      }

      FileChannel channel = fin.getChannel();

      FSImageFormatPBINode.Loader inodeLoader = new FSImageFormatPBINode.Loader(
          fsn, this);
      FSImageFormatPBSnapshot.Loader snapshotLoader = new FSImageFormatPBSnapshot.Loader(
          fsn, this);

      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;

      for (FileSummary.Section s : sections) {
        channel.position(s.getOffset());
        InputStream in = new BufferedInputStream(new LimitInputStream(fin,
            s.getLength()));

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

        String n = s.getName();

        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);
        }
          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.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.endStep(Phase.LOADING_FSIMAGE, step);
        }
          break;
        default:
          LOG.warn("Unrecognized section {}", n);
          break;
        }
      }
    }

  首先是第6行,从文件中读取summary的内容,然后是第21行从summary中读取sections信息,然后是第46行到末尾,遍历sections,根据section的类型来执行不同的方法。这里以INODE和INODE_DIR为例来解析,其如何加载数据。其中处理INODE的代码在第63行到第66行,其中重点是第66行loadINodeSection方法,该方法内容如下:

void loadINodeSection(InputStream in) throws IOException {
      INodeSection s = INodeSection.parseDelimitedFrom(in);
      fsn.dir.resetLastInodeId(s.getLastInodeId());
      LOG.info("Loading " + s.getNumInodes() + " INodes.");
      for (int i = 0; i < s.getNumInodes(); ++i) {
        INodeSection.INode p = INodeSection.INode.parseDelimitedFrom(in);
        if (p.getId() == INodeId.ROOT_INODE_ID) {
          loadRootINode(p);
        } else {
          INode n = loadINode(p);
          dir.addToInodeMap(n);
        }
      }
    }

  首先是第2行获取一个INodeSection,然后是第5行利用for循环遍历INodeSection中的节点。然后根据该节点是否是root节点来执行不同的方法,若是则执行loadRootINode节点,若不是则执行loadINode节点并将其添加到map中。

  对于非root节点执行的loadINode节点内容如下:

private INode loadINode(INodeSection.INode n) {
      switch (n.getType()) {
      case FILE:
        return loadINodeFile(n);
      case DIRECTORY:
        return loadINodeDirectory(n, parent.getLoaderContext());
      case SYMLINK:
        return loadINodeSymlink(n);
      default:
        break;
      }
      return null;
    }

  这里就是一个switch语句,根据节点的内容的不同执行不同的方法。其中上文提到的inode节点主要有两类:File和Directory。其中File类型执行的是loadINodeFile方法,Directory执行的是loadINodeDirectory方法。其中loadINodeFile方法内容如下:

private INodeFile loadINodeFile(INodeSection.INode n) {
      assert n.getType() == INodeSection.INode.Type.FILE;
      INodeSection.INodeFile f = n.getFile();
      List<BlockProto> bp = f.getBlocksList();
      short replication = (short) f.getReplication();
      LoaderContext state = parent.getLoaderContext();

      BlockInfoContiguous[] blocks = new BlockInfoContiguous[bp.size()];
      for (int i = 0, e = bp.size(); i < e; ++i) {
        blocks[i] = new BlockInfoContiguous(PBHelper.convert(bp.get(i)), replication);
      }
      final PermissionStatus permissions = loadPermission(f.getPermission(),
          parent.getLoaderContext().getStringTable());

      final INodeFile file = new INodeFile(n.getId(),
          n.getName().toByteArray(), permissions, f.getModificationTime(),
          f.getAccessTime(), blocks, replication, f.getPreferredBlockSize(),
          (byte)f.getStoragePolicyID());

      if (f.hasAcl()) {
        int[] entries = AclEntryStatusFormat.toInt(loadAclEntries(
            f.getAcl(), state.getStringTable()));
        file.addAclFeature(new AclFeature(entries));
      }

      if (f.hasXAttrs()) {
        file.addXAttrFeature(new XAttrFeature(
            loadXAttrs(f.getXAttrs(), state.getStringTable())));
      }

      // under-construction information
      if (f.hasFileUC()) {
        ucFiles.add(file);
        INodeSection.FileUnderConstructionFeature uc = f.getFileUC();
        file.toUnderConstruction(uc.getClientName(), uc.getClientMachine());
        if (blocks.length > 0) {
          BlockInfoContiguous lastBlk = file.getLastBlock();
          // replace the last block of file
          file.setBlock(file.numBlocks() - 1, new BlockInfoContiguousUnderConstruction(
              lastBlk, replication));
        }
      }
      return file;
    }

  首先从第2行到第13行,这里主要是从文件中读取具体的文件信息到内存中,主要信息和上文解析的xml中的内容相同。然后是第15行利用之前读取的数据创建一个INodeFile类。然后是第20行到文件末尾,这里主要是三个if语句,这几个if语句主要是按需求第建立的节点设置一些参数。

  然后再回到loadINode方法中,这个方法加载Directory使用的loadINodeDirectory方法内容如下:

public static INodeDirectory loadINodeDirectory(INodeSection.INode n,
        LoaderContext state) {
      assert n.getType() == INodeSection.INode.Type.DIRECTORY;
      INodeSection.INodeDirectory d = n.getDirectory();

      final PermissionStatus permissions = loadPermission(d.getPermission(),
          state.getStringTable());
      final INodeDirectory dir = new INodeDirectory(n.getId(), n.getName()
          .toByteArray(), permissions, d.getModificationTime());
      final long nsQuota = d.getNsQuota(), dsQuota = d.getDsQuota();
      if (nsQuota >= 0 || dsQuota >= 0) {
        dir.addDirectoryWithQuotaFeature(new DirectoryWithQuotaFeature.Builder().
            nameSpaceQuota(nsQuota).storageSpaceQuota(dsQuota).build());
      }
      EnumCounters<StorageType> typeQuotas = null;
      if (d.hasTypeQuotas()) {
        ImmutableList<QuotaByStorageTypeEntry> qes =
            loadQuotaByStorageTypeEntries(d.getTypeQuotas());
        typeQuotas = new EnumCounters<StorageType>(StorageType.class,
            HdfsConstants.QUOTA_RESET);
        for (QuotaByStorageTypeEntry qe : qes) {
          if (qe.getQuota() >= 0 && qe.getStorageType() != null &&
              qe.getStorageType().supportTypeQuota()) {
            typeQuotas.set(qe.getStorageType(), qe.getQuota());
          }
        }

        if (typeQuotas.anyGreaterOrEqual(0)) {
          DirectoryWithQuotaFeature q = dir.getDirectoryWithQuotaFeature();
          if (q == null) {
            dir.addDirectoryWithQuotaFeature(new DirectoryWithQuotaFeature.
                Builder().typeQuotas(typeQuotas).build());
          } else {
            q.setQuota(typeQuotas);
          }
        }
      }

      if (d.hasAcl()) {
        int[] entries = AclEntryStatusFormat.toInt(loadAclEntries(
            d.getAcl(), state.getStringTable()));
        dir.addAclFeature(new AclFeature(entries));
      }
      if (d.hasXAttrs()) {
        dir.addXAttrFeature(new XAttrFeature(
            loadXAttrs(d.getXAttrs(), state.getStringTable())));
      }
      return dir;
    }

  这个方法与上文加载文件的方法几乎完全一样,差别主要在第8行创建的是INodeDirectory类。

  loadINode方法的主要内容便如同上文解析的,主要是根据文件中的内容创建对应的INodeFile或INodeDirector对象。在调用loadINode的loadINodeSection方法中,在创建好了INode对象后,会调用addToInodeMap方法将这个对象添加到inodemap中,这个方法的内容如下:

public final void addToInodeMap(INode inode) {
    if (inode instanceof INodeWithAdditionalFields) {
      inodeMap.put(inode);
      if (!inode.isSymlink()) {
        final XAttrFeature xaf = inode.getXAttrFeature();
        addEncryptionZone((INodeWithAdditionalFields) inode, xaf);
      }
    }
  }

  这里重点在第3行,这里会将创建好的inode对象存储到inodeMap中。

  至此其对inode的处理操作便完成了。然后回到上文的loadINodeFile方法中,这个方法的switch语句中,除了上述解析的inode,还有许多其他格式的数据,这里再解析一个INODE_DIR格式的处理方式,其处理方法在loadINodeFile方法的第73行,调用的是inodeLoader的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);
        }
      }
    }

  这里整体是使用了一个while循环来遍历数据,首先是第5行和第6行,这里从文件中读取目录的结构信息,然后是第11行拿到目录结构中的父节点,这里调用的dir的getInode方法,这个方法会从上文提到的inodemap读取inode节点。然后是第12行到第14行,使用for循环拿到父节点的所有子节点信息,然后同样利用getInode方法获取子节点,最后调用addToParent将子节点添加到父节点下。由此便能够创建一个文件目录树。

  首先获取节点的getInode方法如下:

public INode getInode(long id) {
    readLock();
    try {
      return inodeMap.get(id);
    } finally {
      readUnlock();
    }
  }

  这个方法就是直接从inodeMap中获取数据。

  然后是addToParent方法,这个方法内容如下:

private void addToParent(INodeDirectory parent, INode child) {
      if (parent == dir.rootDir && FSDirectory.isReservedName(child)) {
        throw new HadoopIllegalArgumentException("File name \""
            + child.getLocalName() + "\" is reserved. Please "
            + " change the name of the existing file or directory to another "
            + "name before upgrading to this release.");
      }
      // NOTE: This does not update space counts for parents
      if (!parent.addChild(child)) {
        return;
      }
      dir.cacheName(child);

      if (child.isFile()) {
        updateBlocksMap(child.asFile(), fsn.getBlockManager());
      }
    }

  这里首先是的第9行调用用父节点的addChild方法将其添加到父节点下面,然后是第14行,如果子节点是文件则更新该节点在BlocksMap中的内容。

+ child.getLocalName() + "\" is reserved. Please "
        + " change the name of the existing file or directory to another "
        + "name before upgrading to this release.");
  }
  // NOTE: This does not update space counts for parents
  if (!parent.addChild(child)) {
    return;
  }
  dir.cacheName(child);

  if (child.isFile()) {
    updateBlocksMap(child.asFile(), fsn.getBlockManager());
  }
}
<p>    这里首先是的第9行调用用父节点的addChild方法将其添加到父节点下面,然后是第14行,如果子节点是文件则更新该节点在BlocksMap中的内容。
</p>