HDFS数据恢复模式

前言

在现有的HDFS中,为了保证元数据的高可用性,我们可以在配置项dfs.namenode.name.dir中配置多个元数据存储目录来达到多备份的作用。这样一来,如果其中一个目录文件损坏了,我们可以选择另外可用的文件。那么问题来了,如果所有备用的元数据都损坏了,不能用了,这个时候怎么办,那么是否就意味着集群就永远启动不起来了呢?这将会是一个多么糟糕的结果啊。在这里,我们就要引出本文的主题:HDFS的数据恢复模式(Recovery Mode)。

HDFS数据恢复模式概述


HDFS数据恢复模式的使用场景如前文中所提到的,当系统遭遇到硬件问题或软件层面的问题导致文件损坏,从而导致NameNode无法正常启动,这个时候数据恢复模式就派上用场了。更全面地来说,HDFS数据恢复模式实质上是一种元数据自我恢复的启动模式。所以它并不是DataNode上真实数据的恢复,这一点可能容易被人误解

其次,数据恢复针对的情况是损坏状态下的editlog,而不是fsImage,fsImage是恢复后生成的

HDFS数据恢复模式原理


当editlog文件损坏的时候,如果我们启动了NameNode,很显然NameNode会在apply editlog的时候抛出异常,从而导致NameNode启动失败。而在Recovery Mode模式下,NameNode则会智能地跳过这些错误情况,从而保证NameNode启动成功。在启动完NameNode之后,它会生出一个新的Fsimage,然后再次退出,下次集群管理员就可以正常的方式来启动集群了,因为此时用的是新的fsImage。图示过程如下:



图 1-1 HDFS数据恢复模式原理图

HDFS数据恢复模式核心代码的实现


本节我们从代码层面来学习HDFS数据恢复模式是如何实现的,主要涉及到的类有NameNode,FSNamesystem,FSImage,FSEditLogLoader。

数据恢复模式的启动入口是hdfs namenode -recover命令,相应地就对应到了NameNode的处理方法,代码如下:

  public static NameNode createNameNode(String argv[], Configuration conf)
      throws IOException {
    LOG.info("createNameNode " + Arrays.asList(argv));
    if (conf == null)
      conf = new HdfsConfiguration();
    // Parse out some generic args into Configuration.
    GenericOptionsParser hParser = new GenericOptionsParser(conf, argv);
    argv = hParser.getRemainingArgs();
    // Parse the rest, NN specific args.
    StartupOption startOpt = parseArguments(argv);
    ...

    switch (startOpt) {
      case FORMAT: {
      ...
      case RECOVER: {
        // reconver参数对应的启动模式
        NameNode.doRecovery(startOpt, conf);
        return null;
      }
      ...

然后我们进入doRecovery方法,

  private static void doRecovery(StartupOption startOpt, Configuration conf)
      throws IOException {
    String nsId = DFSUtil.getNamenodeNameServiceId(conf);
    String namenodeId = HAUtil.getNameNodeId(conf, nsId);
    initializeGenericKeys(conf, nsId, namenodeId);
    ...
    try {
      // 进入FSNamesystem内的处理方法,此刻开始正式进入数据恢复过程
      fsn = FSNamesystem.loadFromDisk(conf);
      fsn.getFSImage().saveNamespace(fsn);
      ...

中间会经过FSNamesystem.loadFSImage(startOpt),进入此方法,

  private void loadFSImage(StartupOption startOpt) throws IOException {
    final FSImage fsImage = getFSImage();

    // format before starting up if requested
    if (startOpt == StartupOption.FORMAT) {

      fsImage.format(this, fsImage.getStorage().determineClusterId());// reuse current id

      startOpt = StartupOption.REGULAR;
    }
    boolean success = false;
    writeLock();
    try {
      // 构造数据恢复上下文对象
      MetaRecoveryContext recovery = startOpt.createRecoveryContext();
      // 进行新的镜像文件的生成过程
      final boolean staleImage
          = fsImage.recoverTransitionRead(startOpt, this, recovery);
      ...

这里我们进入最终的FSImage的loadFSImage方法,

  private boolean loadFSImage(FSNamesystem target, StartupOption startOpt,
      MetaRecoveryContext recovery)
      throws IOException {
    final boolean rollingRollback
        = RollingUpgradeStartupOption.ROLLBACK.matches(startOpt);
    final EnumSet<NameNodeFile> nnfs;
    ...
    Iterable<EditLogInputStream> editStreams = null;

    initEditLog(startOpt);

    ...
    Exception le = null;
    FSImageFile imageFile = null;
    // 遍历fsImage所存储的多个备份目录
    for (int i = 0; i < imageFiles.size(); i++) {
      try {
        imageFile = imageFiles.get(i);
        // 加载此镜像文件,加载成功一个即可
        loadFSImageFile(target, recovery, imageFile, startOpt);
        break;
      } catch (IllegalReservedPathException ie) {
        ...
      }
    }
    ...

    if (!rollingRollback) {
      // 然后进行editlog的加载
      long txnsAdvanced = loadEdits(editStreams, target, startOpt, recovery);
      needToSave |= needsResaveBasedOnStaleCheckpoint(imageFile.getFile(),
          txnsAdvanced);
    }
    ...

OK,到了这里,我们终于找到了editlog加载的入口了,loadEdits也将是我们所要重点关注的,因为在此过程中,实现了Recovery Mode中最为关键的跳过错误记录的逻辑

FSEditLogLoader的loadEdits过程


我们直接进入loadEdit方法,

  long loadFSEdits(EditLogInputStream edits, long expectedStartingTxId,
      StartupOption startOpt, MetaRecoveryContext recovery) throws IOException {
    StartupProgress prog = NameNode.getStartupProgress();
    Step step = createStartupProgressStep(edits);
    prog.beginStep(Phase.LOADING_EDITS, step);
    fsNamesys.writeLock();
    try {
      long startTime = monotonicNow();
      FSImage.LOG.info("Start loading edits file " + edits.getName());
      // 传入期望的起始txid,恢复模式上下文
      long numEdits = loadEditRecords(edits, false, expectedStartingTxId,
          startOpt, recovery);
      ...

我们继续进入此方法,

  long loadEditRecords(EditLogInputStream in, boolean closeOnExit,
      long expectedStartingTxId, StartupOption startOpt,
      MetaRecoveryContext recovery) throws IOException {
    ...
    try {
      while (true) {
        try {
          FSEditLogOp op;
          try {
            // 从editlog输入流中读取下一个操作记录
            op = in.readOp();
            if (op == null) {
              break;
            }
          } catch (Throwable e) {
            // 如果出现文件损坏的情况,此处会抛出异常
            //...
            // 如果不是处于数据恢复的启动方式下,则会抛出异常
            if (recovery == null) {
               // We will only try to skip over problematic opcodes when in
               // recovery mode.
              throw new EditLogInputException(errorMessage, e, numEdits);
            }
            MetaRecoveryContext.editLogLoaderPrompt(
                "We failed to read txId " + expectedTxId,
                recovery, "skipping the bad section in the log");
            // 如果处于Recovery Mode模式,则此处会跳过错误记录,重新定位到下一个有效的操作记录
            in.resync();
            continue;
          }
          ...

从上面的方法中,我们基本就知道了它是如何跳过错误的editlog记录的。读到了有效的editlog记录之后,就是把它apply到内存的操作了,

          ...
          recentOpcodeOffsets[(int)(numEdits % recentOpcodeOffsets.length)] =
            in.getPosition();
          if (op.hasTransactionId()) {
            // 如果当前读到的事务id大于期待的值,说明中间有被忽略的editlog记录,打出提醒信息
            if (op.getTransactionId() > expectedTxId) { 
              MetaRecoveryContext.editLogLoaderPrompt("There appears " +
                  "to be a gap in the edit log.  We expected txid " +
                  expectedTxId + ", but got txid " +
                  op.getTransactionId() + ".", recovery, "ignoring missing " +
                  " transaction IDs");
            } else if (op.getTransactionId() < expectedTxId) { 
              MetaRecoveryContext.editLogLoaderPrompt("There appears " +
                  "to be an out-of-order edit in the edit log.  We " +
                  "expected txid " + expectedTxId + ", but got txid " +
                  op.getTransactionId() + ".", recovery,
                  "skipping the out-of-order edit");
              continue;
            }
          }
          try {
            if (LOG.isTraceEnabled()) {
              LOG.trace("op=" + op + ", startOpt=" + startOpt
                  + ", numEdits=" + numEdits + ", totalEdits=" + totalEdits);
            }
            // Apply此记录到NameNode内存中
            long inodeId = applyEditLogOp(op, fsDir, startOpt,
                in.getVersion(true), lastInodeId);
            ...

然后以上操作都完成之后,NameNode会执行一次saveNamespace的动作,就会生成一个新的可用的fsImage了,代码如下:

  private static void doRecovery(StartupOption startOpt, Configuration conf)
      throws IOException {
    ...
    NameNode.initMetrics(conf, startOpt.toNodeRole());
    FSNamesystem fsn = null;
    try {
      fsn = FSNamesystem.loadFromDisk(conf);
      // 加载完editlog之后,执行一次saveNamespace生成一个新的fsImage
      fsn.getFSImage().saveNamespace(fsn);
      MetaRecoveryContext.LOG.info("RECOVERY COMPLETE");
      ...

到了这里,整个代码的执行流程就结束了。执行流程图如下:



图 1-2 HDFS数据恢复模式执行流程图

有效editlog的定位寻找


最后我们再来关注一下之前跳过editlog的具体操作逻辑,就是rsync方法,

  public void resync() {
    if (cachedOp != null) {
      return;
    }
    // 定位到下一个有效的editlog操作记录
    cachedOp = nextValidOp();
  }

然后我们定位到其具体的子类实现,位于类EditLogFileInputStream中,

  protected FSEditLogOp nextValidOp() {
    try {
      // 这里的true参数代表着要跳过坏的editlog记录
      return nextOpImpl(true);
    } catch (Throwable e) {
      LOG.error("nextValidOp: got exception while reading " + this, e);
      return null;
    }
  }

上面方法最终将会执行到FSEditLogOp.Reader.readOp方法,执行逻辑如下,

    public FSEditLogOp readOp(boolean skipBrokenEdits) throws IOException {
      // 循环读取Op操作记录,直到找到一个有效的记录
      while (true) {
        try {
          return decodeOp();
        } catch (IOException e) {
          in.reset();
          if (!skipBrokenEdits) {
            throw e;
          }
        } catch (RuntimeException e) {
          ...
          // 判断是否跳过坏的edit记录,如果不跳过,此处将会抛出异常
          if (!skipBrokenEdits) {
            throw e;
          }
        } catch (Throwable e) {
          in.reset();
          // 判断是否跳过坏的edit记录,如果不跳过,此处将会抛出异常
          if (!skipBrokenEdits) {
            throw new IOException("got unexpected exception " +
                e.getMessage(), e);
          }
        }
        ...
      }
    }

HDFS数据恢复模式使用


前面已经提到过,HDFS数据恢复模式实质上是一种NameNode的启动方式,我们可以通过指定启动NameNode的参数来选择是否以这样的方式启动NameNode,输入hdfs namenode -help即可获取这些参数,

$ hdfs namenode -help
Usage: java NameNode [-backup] | 
    [-checkpoint] | 
    ...
    [-bootstrapStandby] | 
    [-recover [ -force] ] | // 以数据恢复模式的启动方式,force参数表示后面所有的提醒都默认选择第一个
    [-metadataVersion ]  ]    

此种方式与之前脚本启动方式略有不同的一点在于,此类方式是前台启动的,用户能直观地看到NameNode的启动过程。

参考资料


[1].https://issues.apache.org/jira/browse/HDFS-3004
[2].https://issues.apache.org/jira/secure/attachment/12542798/recovery-mode.pdf

posted @ 2020-01-12 19:08  回眸,境界  阅读(215)  评论(0编辑  收藏  举报