zoukankan      html  css  js  c++  java
  • 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

  • 相关阅读:
    浅析 x1B[1;3;31mxterm.jsx1B[0m 是什么?如何在终端输出带颜色等格式的字符串
    使用xterm报错:Error: Terminal requires a parent element、及删除时报错:xterm.js: Parsing error 的问题
    浅析如何实现浏览器访问远程桌面/服务器界面:NoVNC
    浅析NAS网络存储是什么及其主要用途
    浅析uniapp如何做图片裁剪及遇到问题 uni.canvasToTempFilePath 在APP下返回的是临时路径,如何把路径转为base64的解决方案
    浅析webpack中mode的取值及不同取值的作用/打包方式及摇树优化(tree-shaking)的理解
    P1160 队列安排题解
    P1996 约瑟夫问题题解
    P1449 后缀表达式题解
    P1825 玉米田迷宫题解
  • 原文地址:https://www.cnblogs.com/bianqi/p/12183722.html
Copyright © 2011-2022 走看看