HDFS使用Backup Node、Checkpoint Node以及Standby Namenode进行checkpoint的机制详解

前言

checkpoint,就是将某一个时间点的内存镜像,完整地存放到磁盘的过程,比如,我们在StandBy NameNode上可以看到这样的image文件:

fsimage_0000000000992806947
fsimage_0000000000992806947.md5

fsimage_0000000000992806947说明这个文件存放了txId为992806947和以前的全部内存操作的镜像,只要将这个image文件load到内存,那么NameNode就有了历史上截止到992806947操作发生时候的内存镜像。fsimage_0000000000992806947.md5是这个image文件的md5校验值。
image文件与edit log文件相配合,就可以让NameNode在重启的时候恢复到关闭前的状态,即NameNode启目录

    • 前言
  • 前言
  • 1. HA模式下的StandBy NameNode进行checkpoint以及image文件的传输
    • 1.1 CheckpointerThread运行机制简介
    • 1.2 checkpoint过程
    • 1.3 checkpoint完成以后,将fsimage文件上传到Active NameNode
  • 2. 使用Checkpoint Node和Backup Node进行checkpoint操作
    • 2.1 Checkpoint Node和Backup Node进行checkpoint操作的简介
    • 2.2 Checkpoint Node和Backup Node的初始化
    • 2.3 使用Checkpointer线程与Active NameNode进行文件同步
    • 2.4 Backup Node与Active NameNode进行基于JournalProtocol RPC协议进行实时edit操作同步
  • 结语
    • 使用默认的StandBy NameNode进行checkpoint以及image文件的传输
      • checkpoint过程
        • 文件传输过程
    • 使用Checkpoint Node和Backup Node进行checkpoint操作
      • 使用Checkpointer与Active NameNode进行文件同步
      • Backup Node与Active NameNode进行基于JournalProtocol RPC协议进行实时edit操作同步

前言

checkpoint,就是将某一个时间点的内存镜像,完整地存放到磁盘的过程,比如,我们在StandBy NameNode上可以看到这样的image文件:

fsimage_0000000000992806947
fsimage_0000000000992806947.md5

fsimage_0000000000992806947说明这个文件存放了txId为992806947以前(包含)的全部内存操作的镜像,只要将这个image文件load到内存,那么NameNode就有了历史上截止到992806947操作发生时候的内存镜像。fsimage_0000000000992806947.md5是这个image文件的md5校验值。
image文件与edit log文件相配合,就可以让NameNode在重启的时候恢复到关闭前的状态,即NameNode启动的时候,会先读取image文件,加载某个时间点之前的内存镜像,然后再读取这个时间点以后的edit log文件,从而实现对整个内存镜像的恢复。即image文件是一个全量文件,而edit log是一个增量文件,二者相互配置,来实现HDFS重启的时候的文件系统的恢复。
注意,按照《Hadoop技术内幕:深入解析Hadoop Common和HDFS架设计与实现原理》一书中的分类:
1. 文件系统的目录树关系,即目录、文件之间的树形关系,以及文件和块之间的对应关系,我们称为NameNode第一关系;
2. 块与节点之间的对应关系,即根据块找到存放该块的DataNode,我们称为NameNode第二关系;

这里的image文件只包含NameNode第一关系,不会包含NameNode第二关系。NameNode第二关系是在NameNode启动以后,由每一个DataNode定时向Active NameNode汇报的,即,DataNode会向Active NameNode报告自己所维护的块信息。这样,当我们需要获取某个块的数据的时候,会先通过Active NameNode进行查询,NameNode会将我们的请求重定向到对应的DataNode。将第二关系与NameNode脱钩,个人感觉是非常好的设计方式,一方面解脱NameNode,无论DataNode如何横向扩展,都不会影响NameNode的体量和效率;另外,这也避免同一元数据在NameNode和DataNode两处维护,如果两方都维护,两方的元数据不一致的处理又是一个难题;
其实,在HA模式下,只有Active NameNode会维护NameNode第二关系,Standby NameNode不会维护此关系。我在上一篇博客《HDFS使用QJM(Quorum Journal Manager)实现的高可用性以及备份机制》中介绍过,Standby NameNode只会通过重放机制,让自己在内存中的NameNode第一关系与Active NameNode基本保持一致。每个DataNode在向NameNode汇报自身的块信息的时候,请求同时发往Active NameNode和Standby NameNode,但是,通过权限检查机制(NameNodeHAContext.checkOperation()),Standby NameNode会抛出异常Operation category WRITE is not supported in state standby,这是INFO级别的异常,可以忽略。
HDFS的checkpoint,有两种实现方式:
1. 通过单独的节点,即Checkpoint Node或者Backup Node完成;
2. HA模式下,通过Standby NameNode完成,完成以后通过http PUT方式,将生成的image上传给Active NameNode;

下面,我们就对HDFS的两种checkpoint方式进行代码层的解析。

关于Checkpoint Node和Backup Node的简单介绍,可以看hadoop的官方文档;

1. HA模式下的StandBy NameNode进行checkpoint以及image文件的传输

checkpoint的目的是为了定时将某个时间点或者操作点之前的全局内存镜像集中为一个独立文件,从而在NameNode启动的时候可以直接读取该文件实现系统元数据的恢复,如果没有checkpoint操作,那么HDFS需要维护数量庞大的edit log文件,非常不利于系统的启动速度和简洁性。定时进行的checkpoint任务实际上与NameNode的核心工作无关,因此,HDFS将这个工作与Active NameNode隔离开,默认由StandBy NameNode负责,即,默认不配置的情况下,dfs.ha.standby.checkpoints配置项的值为true,表示使用StandBy NameNode来定时进行checkpoint操作。StandBy NameNode完成一次checkpoint操作生成image文件以后,会通过http的PUT操作,将这个image文件传送给远程的Active NameNode。

1.1 CheckpointerThread运行机制简介

我们遵循默认设置,使用StandBy NameNode这种方式来进行checkpoint操作,那么在NameNode启动的时候,如果是Standby角色(实际上,HA模式下,两台NameNode刚启动的时候都会进入Standby状态,然后由ZKFC决定哪台需要transition到Active),则会启动一个叫做StandbyCheckpointer.CheckpointerThread的线程,我们来看NameNode以Standby身份启动时候的代码:

 void startStandbyServices(final Configuration conf) throws IOException {
    //略
    editLogTailer = new EditLogTailer(this, conf);//创建一个editLogTailer线程,用来从远程的QJM服务器上拉取editlog,重演到自己到内存
    editLogTailer.start();
    if (standbyShouldCheckpoint) {
      standbyCheckpointer = new StandbyCheckpointer(conf, this);//独立线程,用来不断进行checkpoint操作
      standbyCheckpointer.start();
    }
  }

Standby模式启动,会创建EditLogTailer负责从JournalNode上拉取edit log文件并在内存中重演(具体细节参考我的博客《HDFS使用QJM(Quorum Journal Manager)实现的高可用性以及备份机制》),然后,创建StandbyCheckpointer对象,负责进行checkpoint的定时操作。

StandbyCheckpointer的构造方法,实际上就是创建了一个CheckpointerThread线程:

 public StandbyCheckpointer(Configuration conf, FSNamesystem ns)
      throws IOException {
    //略
    this.thread = new CheckpointerThread();//创建CheckpointerThread线程
    this.uploadThreadFactory = new ThreadFactoryBuilder().setDaemon(true)
        .setNameFormat("TransferFsImageUpload-%d").build();
    setNameNodeAddresses(conf);
  }

这个线程的任务,就是通过判断距离上一次checkpoint操作的时间是否超过阈值(dfs.namenode.checkpoint.period,默认3600s,即1个小时),以及当前没有进行checkpoint操作的数据量是否超过阈值(dfs.namenode.checkpoint.txns,默认1000000)来判断是否应该进行checkpoint操作。

在完成了checkpoint操作,生成了对应的img文件以后,会通过HTTP PUT操作,将这个文件发送到Active NameNode。

我们一起来看checkpoint以及checkpoint完毕以后文件的上传过程。StandbyCheckpointer.doCheckpoint()方法在StandbyCheckpointer.CheckpointerThread线程中被调用,负责checkpoint和image文件传输到Active NameNode的工作。

private void doCheckpoint() throws InterruptedException, IOException {
    ...略
    //对命名空间加cpLock锁,这是StandBy Namenode专用锁,防止checkpoint过程中发生了edit log重演
    namesystem.cpLockInterruptibly();
    try {
      assert namesystem.getEditLog().isOpenForRead() : //状态检查,预期情况下,StandBy NameNode所维护的editlog应该是处于open for read状态,这是StandBy NameNode启动以后的持续状态
        "Standby Checkpointer should only attempt a checkpoint when " +
        "NN is in standby mode, but the edit logs are in an unexpected state";
      FSImage img = namesystem.getFSImage();
      long prevCheckpointTxId = img.getStorage().getMostRecentCheckpointTxId();//上一次checkpoint完成的位置
      long thisCheckpointTxId = img.getLastAppliedOrWrittenTxId();//当前最后一次操作的位置,即本次checkpoint需要到达的位置
      assert thisCheckpointTxId >= prevCheckpointTxId;
      if (thisCheckpointTxId == prevCheckpointTxId) {
        LOG.info("A checkpoint was triggered but the Standby Node has not " +
            "received any transactions since the last checkpoint at txid " +
            thisCheckpointTxId + ". Skipping...");
        return;
      }

      imageType = NameNodeFile.IMAGE;
      img.saveNamespace(namesystem, imageType, canceler);//进行checkpoint操作
      txid = img.getStorage().getMostRecentCheckpointTxId();
      assert txid == thisCheckpointTxId : "expected to save checkpoint at txid=" +
        thisCheckpointTxId + " but instead saved at txid=" + txid;

      // Save the legacy OIV image, if the output dir is defined.
      String outputDir = checkpointConf.getLegacyOivImageDir();
      if (outputDir != null && !outputDir.isEmpty()) {
        img.saveLegacyOIVImage(namesystem, outputDir, canceler);
      }
    } finally {
      namesystem.cpUnlock();
    }

    //采用异步方式,将当前的img文件发送到远程的Active NameNode
    ExecutorService executor =
        Executors.newSingleThreadExecutor(uploadThreadFactory);
    Future upload = executor.submit(new Callable() {
      @Override
      public Void call() throws IOException {
        TransferFsImage.uploadImageFromStorage(activeNNAddress, conf,
            namesystem.getFSImage().getStorage(), imageType, txid, canceler);
        return null;
      }
    });
    executor.shutdown();
    try {
      upload.get();
    } catch (InterruptedException e) {
      //异常
    }
  }

Standby NameNode在进行checkpoint操作之前,需要对整个namespace加锁,这里的锁变量是cpLock(cp是checkpoint的简称)。FSNamesystem提供了两把锁,fsLock负责锁定和保护整个namesystem,即,任何时候只有获取fsLock的线程能够修改namesystem的结构,比如块信息、命名空间状态等;而cpLock是专门给Standby NameNode使用的锁,目的是防止checkpoint过程中发生edit log重演操作,cpLock不负责锁定对block信息的修改,这是因为Standby NameNode是无权维护和修改文件到快信息等的对应关系的,只有Active NameNode会修改,因此无需对整个命名空间加锁。因此,cpLock只被用在edit log重演(代码EditLogTailer.doWork())和本节所讲的checkpoint过程中。

回到doCheckpoint()方法,可以看到,checkpoint操作通过img.saveNamespace(namesystem, imageType, canceler);完成,而img文件的传输,通过调用TransferFsImage.uploadImageFromStorage(...)完成。

1.2 checkpoint过程

跟踪img.saveNamespace(namesystem, imageType, canceler);代码,最终调用FSImage.saveFSImageInAllDirs()方法完成checkpoint,这个方法会生成image文件,并存放到一个或者多个image存储目录,这是因为出于高可用考虑,我们是可以配置一个以上的img dir的,让相同的img文件存放在不同的磁盘上:

       <property>
                <name>dfs.namenode.name.dirname>
                <value>/data1/data/hadoop/namenode,/data2/data/hadoop/namenodevalue>
        property>
private synchronized void saveFSImageInAllDirs(FSNamesystem source,
      NameNodeFile nnf, long txid, Canceler canceler) throws IOException {
    ...略
    //获取配置的img 目录
    if (storage.getNumStorageDirs(NameNodeDirType.IMAGE) == 0) {
      throw new IOException("No image directories available!");
    }
    ...略
    SaveNamespaceContext ctx = new SaveNamespaceContext(
        source, txid, canceler);//ctx中保存了生成img文件所需要的上下文信息,最重要的,这个img文件的end txId,即这个checkpoint的目标偏移位置

    try {
      List saveThreads = new ArrayList();
      // save images into current
      for (Iterator it //对于每一个storage目录,都创建一个独立线程,同时进行img文件的生成,提高生成效率
             = storage.dirIterator(NameNodeDirType.IMAGE); it.hasNext();) {
        StorageDirectory sd = it.next();//
        FSImageSaver saver = new FSImageSaver(ctx, sd, nnf);
        Thread saveThread = new Thread(saver, saver.toString());
        saveThreads.add(saveThread);
        saveThread.start();
      }
      waitForThreads(saveThreads);//等待所有的saveThread完成存储操作
      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.");
      }
     ...略
      //完成了img文件的存取,实际上是存为一个中间文件,以NameNodeFile.IMAGE_NEW开头。
      //这时候就可以把这些中间文件rename称为最终正式文件了
      ////最后一个参数false,代表不需要rename md5文件,这是因为在FSImageSaver中生成临时文件的时候已经生成了最终的md5文件
      renameCheckpoint(txid, NameNodeFile.IMAGE_NEW, nnf, false);
    } finally {  }
  }

通过storage.dirIterator(NameNodeDirType.IMAGE)获取image文件目录的一个Iterator,然后逐个遍历这些目录并将image文件存放下来。对于多image目录的情况,会为每一个目录创建一个单独线程,多目录并行进行。但是,通过waitForThreads(saveThreads);,会同步等待所有目录完成img存储才会执行下一步,因此,尽管内部并行,但是saveFSImageInAllDirs(...)是一个同步方法,方法返回时,肯定已经完成了将image文件生成到所有目录。

FSImageSaver类负责完成对把img文件存放到某个img目录,我们来看FSImageSaver.saveFSImage(...)方法:

 /**
   * Save the contents of the FS image to the file.
   */
  void saveFSImage(SaveNamespaceContext context, StorageDirectory sd,
      NameNodeFile dstType) throws IOException {
    long txid = context.getTxId();//checkpoint的目标偏移位置
    File newFile = NNStorage.getStorageFile(sd, NameNodeFile.IMAGE_NEW, txid);
    File dstFile = NNStorage.getStorageFile(sd, dstType, txid);

    FSImageFormatProtobuf.Saver saver = new FSImageFormatProtobuf.Saver(context);
    FSImageCompression compression = FSImageCompression.createCompression(conf);
    //按照压缩配置,将img存入到以fsimage.ckpt开头的中间文件中
    saver.save(newFile, compression);
    MD5FileUtils.saveMD5File(dstFile, saver.getSavedDigest());//保存md5文件
    storage.setMostRecentCheckpointInfo(txid, Time.now());
  }

saveFSImage()方法可以看到,生成image文件的时候,并不是直接将内存镜像dump到对应的image磁盘目录,而是会产生一个以fsimage.ckpt开头的中间文件,如:fsimage.ckpt_0000000000992806947,然后生成对应的MD5校验文件,如:fsimage.ckpt_0000000000992806947.md5。当多目录image文件全部完成了中间文件的生成,再调用renameCheckpoint(...)方法,将所有目录的中间文件rename为最终的格式为fsimage_0000000000992806947的文件;

本文不去详述如何将内存镜像读出、压缩、序列化的细节,也不去详述image文件的结构,这涉及到NameNode的文件系统管理,需要较长篇幅去讲解。我会另起博文进行讲解。

1.3 checkpoint完成以后,将fsimage文件上传到Active NameNode

如上文所讲,doCheckpoint()操作包括了checkpoint和image文件的传输,我们来看checkpoint完成以后,image文件的传输。
doCheckpoint()方法

   //采用异步方式,将当前的img文件发送到远程的Active NameNode
    ExecutorService executor =
        Executors.newSingleThreadExecutor(uploadThreadFactory);
    Future upload = executor.submit(new Callable() {
      @Override
      public Void call() throws IOException {
        TransferFsImage.uploadImageFromStorage(activeNNAddress, conf,
            namesystem.getFSImage().getStorage(), imageType, txid, canceler);
        return null;
      }
    });
    executor.shutdown();
    try {
      upload.get();
    } catch (InterruptedException e) {
      //异常处理 略
    }

可以看到,通过ExecutorService创建了一个独立线程负责文件传输过程。这是因为image文件一般较大,传输较为耗时,因此如果此时正好发生NameNode从standby到active的transition过程,这个transition会发生长时间阻塞,因此会创建一个单独线程执行。

dfs.namenode.http-address前缀配置项设置了我们的NameNode的http地址:比如,我们在hdfs-site.xml中这样配置:

        <property>
                <name>dfs.namenode.http-address.datahdfsmaster.namenode1name>
               <value>10.130.277.29:50070value>
        property>

此时,我们名字为datahdfsmaster的nameservice下的名字为namenode1的namenode的http地址是10.130.277.29:50070,这就是namenode1的http地址。

通过跟踪TransferFsImage.uploadImageFromStorage()的代码,我们可以非常详细地看到根据Active NameNode的URI构建HttpURLConnection对象的构建,以及设置requestMethod为PUT、根据dfs.image.transfer.chunksize设置PUT请求的chunk size、设置http请求的超时时间的全过程和细节,这里也不再详述;


2. 使用Checkpoint Node和Backup Node进行checkpoint操作

当然,如果我们认为这样也不是很好,可能会影响StandBy NameNode的工作,那么我们可以单独起一个进程,专门负责checkpoint工作,即使用单独的Checkpoint Node(以下简称CN)或者Backup Node(以下简称BN)负责checkpoint的定时工作。

注意,Checkpoint Node和Backup Node在某种程度上几乎替代了Standby NameNode的功能,因此,在HA模式下,无法启动Checkpoint Node和Backup Node。必须使用非HA模式才可以启动并使用Checkpoint Node。

2.1 Checkpoint Node和Backup Node进行checkpoint操作的简介

Checkpoint Node会通过单独线程,定时从Active NameNode拉取edit log文件拷贝到本地,并且将这些edit log在自己内存中进行重演。如果checkpoint条件具备,将进行checkpoint操作,文件存入本地,并通过HTTP PUT的方式将fsimage文件传输给Active NameNode。

Backup Node除了具有Checkpoint Node的上述所有功能外,还会通过RPC方式(属于stream的方式,区别于单独定时拷贝)实时接收Active NameNode的edit操作。因此,同Checkpoint Node相比,Backup Node的内存映象在时间差上几乎与Active NameNode一致。

下面,我们具体来介绍Checkpoint Node、Backup Node的edit log和fsimage的处理机制,以及Backup Node同Active NameNode之间基于RPC通信实现的实时edit log传输。

2.2 Checkpoint Node和Backup Node的初始化

我们从NameNode的启动过程代码可以看到,通过在NameNode启动的时候设定启动参数,可以将这个节点设定为Checkpoint Node或者Backup Node:

public static NameNode createNameNode(String argv[], Configuration conf)
      throws IOException {
    LOG.info("createNameNode " + Arrays.asList(argv));
    ...略
    StartupOption startOpt = parseArguments(argv);
    setStartupOption(conf, startOpt);
    switch (startOpt) {
      case FORMAT: {//NameNode 格式化
        ...略
      }
      case BACKUP://启动一个 backup node
      case CHECKPOINT: {//启动一个checkpoint node
        NamenodeRole role = startOpt.toNodeRole();
        DefaultMetricsSystem.initialize(role.toString().replace(" ", ""));
        return new BackupNode(conf, role);//无论是BACKUP还是CHECKPOINT,都使用BackupNode对象进行管理,唯一不同是角色不同,一个是NamenodeRole.BACKUP,一个是NamenodeRole.CHECKPOINT
      }
      ....略
      default: {
        DefaultMetricsSystem.initialize("NameNode");
        return new NameNode(conf);//正常启动一个NameNode
      } } }

Checkpoint Node或者Backup Node的实现类都是BackupNode,Backup Node 是NameNode的子类,这说明Backup Node基本上与NameNode的功能一致,所维护的元数据信息相同,只是有了定时checkpoint等功能,同时,Backup Node没有对集群元数据进行修改的权限。

NamenodeRole枚举类型标记了NameNode节点的角色:

  /**
   * Defines the NameNode role.
   */
  static public enum NamenodeRole {
    NAMENODE  ("NameNode"),//普通的NameNode节点,如Active NameNode或者Standby NameNode
    BACKUP    ("Backup Node"),//BACKUP Node
    CHECKPOINT("Checkpoint Node");//Checkpoint Node
    ...略
  }

Backup Node的初始化方法initialize()重写了NameNode的initialize()方法,这个方法主要完成三项工作:
1. 握手:通过NameNodeProtocol协议的versionRequest()接口,与Active NameNode进行版本协商等工作,即握手;Active NameNode会响应自己的storage信息,比如,namespaceId,clusterId, blockpoolId,BN/CN将这些信息设置到自己的storage信息中。在下面的RPC通信中,BN/CN会携带这些信息,然后Active NameNode会对收到的CN/BN请求中携带的storage信息进行校验,只有校验一致,才会成功返回,否则,认为是异常请求;
2. 注册:通过NameNodeProtocol协议的registerSubordinateNamenode()接口,向Active NameNode注册自己。注册请求中携带了在握手阶段协商一致的storage信息、clusterId等信息,Active NameNode作为服务端收到注册请求以后会对这些信息进行校验。这样,Active NameNode就可以在发生startLogSegment()等操作的时候,通过JournalProtocol协议的startLogSegment()接口告知BN,同时,当发生edit操作的时候,通过JournalProtocol接口的journal()接口,将这个操作发送给BN;
3. 线程启动启动Checkpointer`线程,负责不断从Active NameNode拉取edit log文件;Checkpointer线程从启动到开始运行,涉及到的数据传输和RPC调用过程如下:

这是BN初始化过程中的RPC调用图:

HDFS使用Backup Node、Checkpoint Node以及Standby Namenode进行checkpoint的机制详解_第1张图片

这是BackupNode.initialize(),握手、注册以及线程启动在这里调用:

protected void initialize(Configuration conf) throws IOException {
    // Trash is disabled in BackupNameNode,
    // but should be turned back on if it ever becomes active.
    conf.setLong(CommonConfigurationKeys.FS_TRASH_INTERVAL_KEY, 
                 CommonConfigurationKeys.FS_TRASH_INTERVAL_DEFAULT);
    //通过handshake,获取了远程的Active Namenode的namespace信息
    NamespaceInfo nsInfo = handshake(conf);
    super.initialize(conf);
    namesystem.setBlockPoolId(nsInfo.getBlockPoolID());

    if (false == namesystem.isInSafeMode()) {
      namesystem.setSafeMode(SafeModeAction.SAFEMODE_ENTER);
    }

    // Backup node should never do lease recovery,
    // therefore lease hard limit should never expire.
    namesystem.leaseManager.setLeasePeriod(
        HdfsConstants.LEASE_SOFTLIMIT_PERIOD, Long.MAX_VALUE);

    // register with the active name-node 
    registerWith(nsInfo);
    // Checkpoint daemon should start after the rpc server started
    runCheckpointDaemon(conf);
    InetSocketAddress addr = getHttpAddress();
    if (addr != null) {
      conf.set(BN_HTTP_ADDRESS_NAME_KEY, NetUtils.getHostPortString(getHttpAddress()));
    }
  }

在完成了handshake()即namespaceId、cluster id等信息等协商以后,CN/BN开始向ActiveNameNode注册自己:
注册的代码为BackupNode.registerWith()

  private void registerWith(NamespaceInfo nsInfo) throws IOException {
    BackupImage bnImage = (BackupImage)getFSImage();
    NNStorage storage = bnImage.getStorage();
    // verify namespaceID
    if (storage.getNamespaceID() == 0) { // new backup storage
      storage.setStorageInfo(nsInfo);
      storage.setBlockPoolID(nsInfo.getBlockPoolID());
      storage.setClusterID(nsInfo.getClusterID());
    } else {
      nsInfo.validateStorage(storage);
    }
    bnImage.initEditLog(StartupOption.REGULAR);
    setRegistration();
    NamenodeRegistration nnReg = null;
    while(!isStopRequested()) {
      try {
        //通过getRegistration()获取请求体,发送给Active NameNode。由于请求体中携带了角色信息,
          //远程的Active NameNode会根据角色判断是否加入到自己的output stream中。通过registerBackupNode.registerBackupNode()
         //可以看到,只有backup的角色会被添加到输出流中去
        nnReg = namenode.registerSubordinateNamenode(getRegistration());
        break;
      } catch(SocketTimeoutException e) {  // name-node is busy
       ...略
      }
    }

    ...略
    nnRpcAddress = nnReg.getAddress();
  }

我们跟踪registerWith()方法,可以看到注册实际上使用的是NamenodeProtocol协议的registerSubordinateNamenode()接口。Active Namenode收到注册请求以后,调用FSNamesystem.registerBackupNode()来进行服务端对请求的处理与响应:

void registerBackupNode(NamenodeRegistration bnReg,
      NamenodeRegistration nnReg) throws IOException {
    writeLock();//获取全局锁
    try {
    ...略
    //只有当它的角色是BACKUP的时候,才会加入到FSEditLog.journalSet中,因此,Active NameNode只会向Backup节点发送自己的edit操作
      //只接受BACKUP节点的注册信息
      if (bnReg.getRole() == NamenodeRole.BACKUP) {
        getFSImage().getEditLog().registerBackupNode(
            bnReg, nnReg);
      }
    } finally {
      writeUnlock();
    }
  }

从上述代码可以看到,Active NameNode对对请求者的身份类型进行了判断。BN和CN在这里产生区别

  • 如果身份是NamenodeRole.BACKUP,那么Active NameNode会把这个客户端加入到自己的output stream中,这样,所有的Active NameNode上的edit操作,都会通过JournalPrototol协议的journal()接口(Active NameNode是JournalPrototol协议客户端,而Backup Node是JournalPrototol协议服务端),发送给这个Backup Node。Backup Node收到这些操作内容以后,会将这些操作在内存中进行replay,即重演,这样,Backup Node的内存与Active NameNode的内存基本一致。根据官方文档,由于Backup Node可以通过这种stream的方式实时获取Active NameNode上的操作,因此没有必要从Active NameNode拷贝fsimage文件和edit log文件;
The Backup node does not need to download fsimage and edits files from the active NameNode in order to create a checkpoint, as would be required with a Checkpoint node or Secondary NameNode, since it already has an up-to-date state of the namespace state in memory. The Backup node checkpoint process is more efficient as it only needs to save the namespace into the local fsimage file and reset edits.
  • 而如果客户端身份是NamenodeRole.CHECKPOINT,就没有这个待遇了,Checkpoint Node虽然也会像Backup Node一样在启动的时候注册(上文说过,它们是同一个实现类),但是,注册的时候,Active NameNode不会将其加入到output stream中,即Active NameNode是不会通过JournalPrototol协议向Checkpoint Node发送实时edit操作的,因此,Checkpoint Node只能通过Checkpointer.java线程同步远程Active NameNode上的文件,而无法通过JournalPrototol实时接收Active NameNode的写操作。Checkpoint Node只能通过不断从Active NameNode下载fsimage文件和edit log文件,然后将这些文件读取到内存并定时生车功能checkpoint操作,然后,将checkpoint文件上传到Active NameNode。

    HDFS使用Backup Node、Checkpoint Node以及Standby Namenode进行checkpoint的机制详解_第2张图片

因此,由于Backup Node与Checkpoint Node的fsimage和edit log的同步代码完全相同,除此以外,就是Backup Node比Checkpoint Node多出的实时数据传输过程,因此,下文将只针对Backup Node,分成两节,分别讲解fsimage和edit log的文件传输,以及Backup Node基于RPC实现的与NameNode之间的edit操作的传输。

2.3 使用Checkpointer线程与Active NameNode进行文件同步

BackupNode.initialize()方法的第三个工作,就是通过调用runCheckpointDaemon(...)启动一个Checkpointer的线程(注意,这不是上一节讲到的Standby Namenode节点使用的StandbyCheckpointer.CheckpointerThread),这个线程负责从远程的Active NameNode上读取edit log 和image文件存入本地:

 /**
   * Start a backup node daemon.
   */
  private void runCheckpointDaemon(Configuration conf) throws IOException {
    checkpointManager = new Checkpointer(conf, this);
    checkpointManager.start();//线程启动
  }

这是Checkpointer线程的run()方法:

@Override
  public void run() {
    ...while(shouldRun) {
      try {
        long now = now();
        boolean shouldCheckpoint = false;
        if(now >= lastCheckpointTime + periodMSec) {//判断是否达到checkpoint时间间隔要求
          shouldCheckpoint = true;
        } else {
          long txns = countUncheckpointedTxns();
          if(txns >= checkpointConf.getTxnCount())//判断是否到达checkpoint数据量要求
            shouldCheckpoint = true;
        }
        if(shouldCheckpoint) {
          doCheckpoint();
          lastCheckpointTime = now;
        }
      } catch(IOException e) {
        ...略
      }
      try {
        Thread.sleep(periodMSec);  } catch(InterruptedException ie) {  }
    }
  }

run()方法同Standby NameNode.CheckpointerThread.run()方法相似,也是会判断是否已经满足了checkpoint条件,包括距离上一次checkpoint操作的时间是否超过阈值(dfs.namenode.checkpoint.txns,默认1000000),以及当前没有进行checkpoint操作的数据量是否超过阈值(dfs.namenode.checkpoint.period,默认3600s,即1个小时),如果任意条件满足,则调用doCheckpoint()操作进行checkpoint:

void doCheckpoint() throws IOException {
    BackupImage bnImage = getFSImage();
    NNStorage bnStorage = bnImage.getStorage();

    long startTime = monotonicNow();
    bnImage.freezeNamespaceAtNextRoll();
    //服务端调用,查看NameNodeRpcServer.startCheckpoint(),服务端会结束掉正在写的sgement文件,开启一个新的segment文件
    //客户端调用,查看
    NamenodeCommand cmd = 
      getRemoteNamenodeProxy().startCheckpoint(backupNode.getRegistration());
    CheckpointCommand cpCmd = null;
    //关于这些返回值的含义,可以参考FSImage.startCheckpoint()方法
    switch(cmd.getAction()) {
    //If backup storage contains image that is newer than or incompatible with 
    // what the active name-node has, then the backup node should shutdown. wuchang
      case NamenodeProtocol.ACT_SHUTDOWN://如果发现backup node的image比namenode的更新,或者storage的版本不一致,肯定更有问题,这时候backup node需要关闭
        shutdown();
        throw new IOException("Name-node " + backupNode.nnRpcAddress
                                           + " requested shutdown.");
      case NamenodeProtocol.ACT_CHECKPOINT://校验通过
        cpCmd = (CheckpointCommand)cmd;
        break;
      default:
        throw new IOException("Unsupported NamenodeCommand: "+cmd.getAction());
    }

    //BackupImage.namenodeStartedLogSegment()如果正在发生,那么如果处于frozen,则必须等待
    bnImage.waitUntilNamespaceFrozen();

    //查看服务端调用 NameNodeRPCServer.startCheckpoint()
    CheckpointSignature sig = cpCmd.getSignature();

    // Make sure we're talking to the same NN!
    sig.validateStorageInfo(bnImage);

    long lastApplied = bnImage.getLastAppliedTxId();
    LOG.debug("Doing checkpoint. Last applied: " + lastApplied);
    RemoteEditLogManifest manifest = //从NameNode处获取edit log的文件清单列表
      getRemoteNamenodeProxy().getEditLogManifest(bnImage.getLastAppliedTxId() + 1);

    boolean needReloadImage = false;
    if (!manifest.getLogs().isEmpty()) {
      RemoteEditLog firstRemoteLog = manifest.getLogs().get(0);//获取第一个远程的edit log文件
      // we don't have enough logs to roll forward using only logs. Need
      // to download and load the image.
      //如果从远程获取的edit log文件的transaction 与自己目前最后一次已经获取的log文件的transaction存在gap,需要进行reload
      if (firstRemoteLog.getStartTxId() > lastApplied + 1) {
        //sig.mostRecentCheckpointTxId存放了Active NameNode在最后一个checkpoint的位点
        LOG.info("Unable to roll forward using only logs. Downloading " +
            "image with txid " + sig.mostRecentCheckpointTxId);
        MD5Hash downloadedHash = TransferFsImage.downloadImageToStorage(
            backupNode.nnHttpAddress, sig.mostRecentCheckpointTxId, bnStorage,
            true);//从远程获取这个位点的image文件
        bnImage.saveDigestAndRenameCheckpointImage(NameNodeFile.IMAGE,
            sig.mostRecentCheckpointTxId, downloadedHash);
        lastApplied = sig.mostRecentCheckpointTxId;//更新lastApplied id
        needReloadImage = true;
      }

      if (firstRemoteLog.getStartTxId() > lastApplied + 1) {//在下载了最新的image文件以后,依然存在gap,则抛出异常
        throw new IOException("No logs to roll forward from " + lastApplied);
      }

      // get edits files
      for (RemoteEditLog log : manifest.getLogs()) {//依次下载这些edit log文件
        TransferFsImage.downloadEditsToStorage(
            backupNode.nnHttpAddress, log, bnStorage);
      }

      //刚才已经下载了新的image文件,因此需要将这个image文件reload到内存
      if(needReloadImage) {
        LOG.info("Loading image with txid " + sig.mostRecentCheckpointTxId);
        File file = bnStorage.findImageFile(NameNodeFile.IMAGE,
            sig.mostRecentCheckpointTxId);
        bnImage.reloadFromImageFile(file, backupNode.getNamesystem());
      }
      rollForwardByApplyingLogs(manifest, bnImage, backupNode.getNamesystem());//将edit log应用到内存
    }

    long txid = bnImage.getLastAppliedTxId();

    backupNode.namesystem.writeLock();
    try {
      backupNode.namesystem.setImageLoaded();
      if(backupNode.namesystem.getBlocksTotal() > 0) {
        backupNode.namesystem.setBlockTotal();
      }
      bnImage.saveFSImageInAllDirs(backupNode.getNamesystem(), txid);//将当前内存镜像dump到fsimage文件
      bnStorage.writeAll();
    } finally {
      backupNode.namesystem.writeUnlock();
    }

    //将image文件上传给active namenode
    if(cpCmd.needToReturnImage()) {
      TransferFsImage.uploadImageFromStorage(backupNode.nnHttpAddress, conf,
          bnStorage, NameNodeFile.IMAGE, txid);
    }

    getRemoteNamenodeProxy().endCheckpoint(backupNode.getRegistration(), sig);//结束checkpoint过程

    //只有backup 节点需要进行converge操作,追赶txid到最新的状态
    //如果是checkpoint node,没有这种实时性需求,只需要依靠fsimage文件和edit log文件拷贝就可以完成
    if (backupNode.getRole() == NamenodeRole.BACKUP) {
      bnImage.convergeJournalSpool(); //调用完毕以后,状态成为IN_SYNC
    }
    backupNode.setRegistration(); // keep registration up to date

    long imageSize = bnImage.getStorage().getFsImageName(txid).length();
    LOG.info("Checkpoint completed in "
        + (monotonicNow() - startTime)/1000 + " seconds."
        + " New Image Size: " + imageSize);
  }

进行checkpoint之前,通过调用NameNodeProtocol协议的startCheckpoint()接口,告知NameNode,自己即将进行checkpoint操作。我们可以查看服务端NameNodeRpcServer.startCheckpoint()得知Active NameNode接收到请求以后的调用流程,可以看到服务端在收到startCheckpoint()以后的处理流程:

  • 自我状态校验:判断自己当前是不是Active NameNode(防止Standby NameNode成为了被checkpoint的节点);如果校验失败,则抛出异常;
  • 请求者身份校验:判断请求者的身份是否合法,即是否是NamenodeRole.CHECKPOINT或者NamenodeRole.BACKUP,如果校验失败,则返回一个shutdown指令,要求请求者停机;
  • 请求者的存储信息等版本校验:对请求者BN/CN的storage信息与自身的storage信息(比如clusterId、namespaceId等)进行校验,正常情况下,BN/CN在initialize()的handshake过程中会获取到Active NameNode的storage信息,所以,这个校验会通过,因此校验会通过。因此这一步是防止未经许可的BN/CN请求进行checkpoint;如果校验失败,则返回一个shutdown指令,要求请求者停机;
  • 缓存的edit log进行flush操作:在我的上一篇博客中讲过NameNode通过RPC协议将自己的edit log实时发往JournalNode的过程,这个过程使用了双缓存。那么在收到了startCheckpoint()的请求以后,Active NameNode会进行缓存的刷新,把缓存的未发送的数据发送到远程Backup Node,然后关闭当前的edit log文件,开始一个新的edit log文件;这样,最近关闭的文件就可以被BN/CN的checkpoint请求到了。注意,BN/CN从Active NameNode请求读取edit log文件的时候,是不会请求处于in-progress状态的文件的。
  • 开始进行checkpoint操作:当完成了startCheckpoint()请求以后,如果校验通过,就可以进行下一步的fsimage和edit log文件的同步了。

在详细介绍doCheckpoint()之前,通过以下调用关系图,我们可以了解doCheckpoint()的基本工作流:

HDFS使用Backup Node、Checkpoint Node以及Standby Namenode进行checkpoint的机制详解_第3张图片

从图中可以看到,Checkpointer线程首先会通过RPC同远程的NameNode进行适当沟通,比如,通过上文中讲到的startCheckpoint()告知远程的Active NameNode自己即将进行checkpoint操作,然后,还会通过getEditLogManifest()的RPC获取远程文件列表,然后通过http的方式将列表中的文件拷贝到自己本地,最后,通过endCheckpoint()的RPC调用,结束checkpoint过程。

由此可见,doCheckpoint()方法是整个线程的核心方法,涉及到流程复杂的与Active NameNode之间的通信,因此,我来详细讲解:


STEP 1. 内存namespace锁定
锁定的目的,是让RPC streaming传输过来的实时的edit操作只能写入edit文件,不可以load到内存。

在上文中我讲到,BN除了通过这种方式实现fsimage和edit log文件的同步以外,还会通过RPC stream的方式实时接收edit。这两种方式互为补充,才使得BN的内存镜像能够与Active NameNode保持实时一致。同时,任何时候,两者只有一个能够将edit 操作在内存中重演,必须互斥。我们首先来看BackNode的状态定义:

  volatile BNState bnState;
  static enum BNState {
    DROP_UNTIL_NEXT_ROLL,// BN/CN初始启动的状态,只要来一次写操作,状态就会切换到JOURNAL_ONLY
    JOURNAL_ONLY,//处于该状态下,来自RPC Streaming的edit只可以追加到edit log文件,不可以应用到内存
    IN_SYNC;//处于该状态下,来自RPC Streaming的edit会先在内存中重演,同时写入到edit lot文件
  }

通过bnImage.freezeNamespaceAtNextRoll();,将stopApplyingEditsOnNextRoll标记为置为true,这样,通过RPC方式的JournalProtocol的startLogSegment()接口告知BN的时候,BN会将目前的状态置为JOURNAL_ONLY,即不会将新的edit操作load到内存。
然后,通过bnImage.waitUntilNamespaceFrozen();,一致等待状态变为JOURNAL_ONLY,在这种状态下,通过RPC stream传过来的edits操作不会load到内存,从而达到互斥目的;
通过上面的锁定和等待,当前状态已经切换为JOURNAL_ONLY,因此,Checkpointer线程就可以从NameNode读取fsimage文件和edit log文件,然后load到内存了;开始STEP 2;

STEP 2.请求文件清单列表
确认内存已经被自己加锁,则开始请求Active NameNode的fsimage和image文件清单列表:

    RemoteEditLogManifest manifest = //从NameNode处获取edit log的文件清单列表
      getRemoteNamenodeProxy().getEditLogManifest(bnImage.getLastAppliedTxId() + 1);

这是通过NameNodeProtocol的RPC协议的getEditLogManifest()接口完成的:

  /**
   * Return a structure containing details about all edit logs
   * available to be fetched from the NameNode.
   * @param sinceTxId return only logs that contain transactions >= sinceTxId
   */
  @Idempotent
  public RemoteEditLogManifest getEditLogManifest(long sinceTxId)
    throws IOException;

参数sinceTxId规定了这些edit log的起始位置,从代码中可以看到,这个请求参数设置为bnImage.getLastAppliedTxId() + 1,即,Backup Node希望请求到的edit log能够刚好从自己内存中的最大的txId的下一个值开始;

STEP 3. 根据文件清单请求edit log文件并load到内存

通过文件清单列表获取文件清单,远程Active NameNode真正返回给我们的文件列表的起始txId,也许并不一定就是我们所请求的sinceTxId。这时候会进行判断判断,如果Active NameNode返回的edit log文件的最小的txId大于自己请求的sinceTxId,中间存在空隙,此时,这些返回的edit是不能够apply到内存中的,因为txId必须严格连续递增,不可丢失任何一个。我们看Active NameNode端在对于BN的getEditLogManifest()请求的处理 NameNodeRpcServer.getEditLogManifest() -> FSEditLog.getEditLogManifest() -> JournalSet.getRemoteEditLogs() -> FileJournalManager->getRemoteEditLogs()

public List getRemoteEditLogs(long firstTxId,
      boolean inProgressOk) throws IOException {
    File currentDir = sd.getCurrentDir();//获取当前这个Journal对应的存放EditLog的目录
    List allLogFiles = matchEditLogs(currentDir);//通过正则匹配获取候选的EditLogFile
    List ret = Lists.newArrayListWithCapacity(
        allLogFiles.size());
    for (EditLogFile elf : allLogFiles) {
      ...略
      if (elf.getFirstTxId() >= firstTxId) {//如果当前的segment的第一个txid大于所请求的txId,则加入到返回结果中
        ret.add(new RemoteEditLog(elf.firstTxId, elf.lastTxId,
            elf.isInProgress()));
      } else if (elf.getFirstTxId() < firstTxId && firstTxId <= elf.getLastTxId()) {
        //如果当前的segment文件的startTxId和endTxId位于请求的txId前后,则segment也是满足条件的
        ret.add(new RemoteEditLog(elf.firstTxId, elf.lastTxId,
            elf.isInProgress()));
      }
    }

    Collections.sort(ret);//对edit log文件排序
    return ret;
  }

从上述代码可以看到客户端如何根据getEditLogManifest()中的sinceTxId参数决定返回哪些文件。
从其执行逻辑可以看到,可能存在这种情况,服务器端返回的edit log中最小的txId的值大于请求的sinceTxId,比如,Active NameNode当前的fsimage文件和edits文件是这样的:

fsimage_0000000000000001300
fsimage_0000000000000001300.md5
edits_0000000000000001301-0000000000000001400
edits_0000000000000001401-0000000000000001500
edits_inprogress_0000000000000001501

BN请求的edit log的sinceTxId为0000000000000001200,此时就只能返回edits_0000000000000001301-0000000000000001400edits_0000000000000001401-0000000000000001500 ,即最小的txId值大于请求的sinceTxId。遇到这种情况,BN的处理方式为直接将Active NameNode的fsimage文件fsimage_0000000000000001300下载下来,然后实现txid的完全连续一致,这相当于将Active NameNode的fsimage和edit log进行了一次完全的复制。fsimage复制过来以后,通过bnImage.reloadFromImageFile(file, backupNode.getNamesystem());将fsimage load到内存,然后才能开始把请求过来的edit log load到内存。当然,如果将Active NameNode的fsimage文件下载下来发现txId依然无法实现连续,只能抛出异常了。完成了以上操作,就通过rollForwardByApplyingLogs(manifest, bnImage, backupNode.getNamesystem());将edit log文件 load到内存。

STEP 4.checkpoint文件的生成并上传给Active NameNode

经过以上步骤,BN完成了内存中namesystem与Active NameNode的一致。此时,就开始进行checkpoint了,把内存的整个镜像dump成fsimage文件。然后,把这个checkpoint文件上传到Active NameNode。

STEP 5.Backup Node对处于打开状态的edit log的汇集
我们知道,Backup Node除了有Checkpointer线程进行文件拷贝,同时使用RPC stream进行edit log的流式传输。在上文中讲到BNState这个控制Backup Node的状态,当前,doCheckpoint()方法执行过程中,处于BNState.JOURNAL_ONLY,来自RPC stream的edits操作只会写入到edits文件,不会load到内存。因此,通过调用bnImage.convergeJournalSpool();,来将这个未打开的edits文件中的edits load到内存,这样,Backup Node内存中的景象几乎与Active NameNode保持一致了;
完成了converge操作,BN的状态就切换为BNState了,这时候,通过RPC stream收到的edit log在存入edit log文件的同时,也应用到内存,让内存保持与Active NameNode实时一致;关于RCP stream针对不同状态的处理方式,看下一节介绍。


2.4 Backup Node与Active NameNode进行基于JournalProtocol RPC协议进行实时edit操作同步

Backup Node与Active NameNode进行基于JournalProtocol RPC协议进行实时edit操作同步,让Backup Node的内存状态能够实时与Active NameNode保持一致。

基于JournalProtocol协议,Active NameNode属于协议的客户端,而BN属于协议的服务器端。

JournalProtocol协议有两个接口

  • startLogSegment() 接口调用发生在Active NameNode调用了FSEditLog.startLogSegment()的时候。通过我的另外一篇博客《HDFS使用QJM(Quorum Journal Manager)实现的高可用性以及备份机制》,可以知道,Active NameNode调用FSEditLog.startLogSegment()是为了结束一个旧的edit文件并开始一个新的edit文件。这时候会通过调用JournalProtocol 协议的startLogSegment() 接口通知BN,BN此时也会结束当前的edit log文件并开始一个新的edit log文件。
  • journal()接口调用发生在Active NameNode发生了edit log写操作,因此Active NameNode会调用这个接口将这个写操作实时发送给BN;

HDFS使用Backup Node、Checkpoint Node以及Standby Namenode进行checkpoint的机制详解_第4张图片

上一节讲Checkpointer线程的时候讲到了RPC streaming与Checkpointer之间的同步,我们可以从journal()方法看到通过JournalProtocol协议的journal()接口收到了Active NameNode的一批edit操作以后,BN根据当前的不同状态进行的不同处理,看BackupImage.journal(...)

  synchronized void journal(long firstTxId, int numTxns, byte[] data) throws IOException {
    if (LOG.isTraceEnabled()) {
      LOG.trace("Got journal, " +
          "state = " + bnState +
          "; firstTxId = " + firstTxId +
          "; numTxns = " + numTxns);
    }

    switch(bnState) {
      case DROP_UNTIL_NEXT_ROLL:
        return;//什么都不做,既不用apply到内存,也不写入到本地
      case IN_SYNC://处于IN_SYNC状态,则将这些消息应用到内存
        applyEdits(firstTxId, numTxns, data);//将收到的edits应用到内存的namespace
        break;//break以后
      case JOURNAL_ONLY://处于JOURNAL_ONLY,则这一批来自rpc的消息不可以load到内存的namespace,只能够追加到磁盘的edit文件
        break;//需要把收到的edit log写入到本地
      default:
        throw new AssertionError("Unhandled state: " + bnState);//异常状态
    }
    logEditsLocally(firstTxId, numTxns, data);//将这些操作追加到当前的edit log磁盘文件,下一次Checkpointer运行,通过调用convergeJournalSpool(),可以负责把这个处于in-progress状态的文件里面的edits操作load到namespace
  }

上一节讲到Checkpointer线程的时候,在Checkpointer的doCheckpoint()方法执行期间,整个BN处于JOURNAL_ONLY状态,因此,通过RPC方式收到的edits只会追加到edits文件,不会调用applyEdits(firstTxId, numTxns, data); load到内存,这样,Checkpointer线程拷贝过来的edits文件或者fsimage文件就可以load到内存了,因此不会造成干扰。等到Checkpointer结束一轮运行,状态切换为IN_SYNC,那么内存的操作全就交付给RPC了。
关于在IN_SYNC状态下journal()方法如何将实时收到的edit操作存入文件并load到内存,不在本文讨论范围之列,有兴趣的读者可以自行阅读代码。


结语

总之,HDFS使用Backup Node、Checkpoint Node以及Standby Namenode进行checkpoint操作,原理均相同。它们的存在,既保证了Active NameNode的数据备份,又将Active NameNode从不影响核心业务的checkpoint操作中解脱出来。当Active NameNode发生异常,Standby Namenode可以很快接管HDFS。
对于Backup Node ,个人认为设计得非常好的地方,在于通过良好的同步控制,让http的方式的文件拷贝和RPC stream的方式的实时edit传输交替配合,让Backup Node的内存镜像与Active NameNode时刻保持近乎一致;动的时候,会先读取image文件,加载某个时间点之前的内存镜像,然后再读取这个时间点以后的edit log文件,从而实现对整个内存镜像的恢复。
注意,按照《Hadoop技术内幕:深入解析Hadoop Common和HDFS架设计与实现原理》一书中的分类:
1. 文件系统的目录树关系,即目录、文件之间的树形关系,以及文件和块之间的对应关系,我们称为NameNode第一关系
2. 块与节点之间的对应关系,即根据块找到存放该块的DataNode,我们称为NameNode第二关系

这里的image文件只包含NameNode第一关系,不会包含NameNode第二关系。NameNode第二关系是在NameNode启动以后,由每一个DataNode定时向Active NameNode汇报的,即,DataNode会向Active NameNode报告自己所维护的块信息。这样,当我们需要获取某个块的数据的时候,会先通过Active NameNode进行查询,NameNode会将我们的请求重定向到对应的DataNode。将第二关系与NameNode脱钩,个人感觉是非常好的设计方式,一方面解脱NameNode,无论DataNode如何横向扩展,都不会影响NameNode的体量和效率;另外,这也避免同一元数据在NameNode和DataNode两处维护,如果两方都维护,两方的元数据不一致的处理又是一个难题;
其实,在HA模式下,只有Active NameNode会维护NameNode第二关系,Standby NameNode不会维护此关系。我在上一篇博客《》中介绍过,Standby NameNode只会通过重放机制,让自己在内存中的NameNode第一关系与Active NameNode基本保持一致。每个DataNode在向NameNode汇报自身的块信息的时候,请求同时发往Active NameNode和Standby NameNode,但是,通过权限检查机制(NameNodeHAContext.checkOperation()),Standby NameNode会抛出异常Operation category WRITE is not supported in state standby,这是INFO级别的异常,可以忽略。
HDFS的checkpoint,有两种实现方式:
1. 通过单独的节点,即Checkpoint Node或者Backup Node完成;
2. HA模式下,通过Standby NameNode完成,完成以后通过http PUT方式,将生成的image上传给Active NameNode;

下面,我们就对HDFS的两种checkpoint方式进行代码层的解析。

关于Checkpoint Node和Backup Node的简单介绍,可以看hadoop的官方文档;

使用默认的StandBy NameNode进行checkpoint以及image文件的传输

checkpoint的目的是为了定时将某个时间点或者操作点之前的全局内存镜像集中为一个独立文件,从而在NameNode启动的时候可以直接读取该文件实现系统元数据的恢复,如果没有checkpoint操作,那么HDFS需要维护数量庞大的edit log文件,非常不利于系统的关系。定时进行的checkpoint任务实际上与NameNode的核心工作无关,因此,HDFS将这个工作与Active NameNode隔离开,默认由StandBy NameNode负责,即,默认不配置的情况下,dfs.ha.standby.checkpoints配置项的值为true,表示使用StandBy NameNode来定时进行checkpoint操作。StandBy NameNode完成一次checkpoint操作生成image文件以后,会通过http的PUT操作,将这个image文件传送给远程的Active NameNode。

我们遵循默认设置,使用StandBy NameNode这种方式来进行checkpoint操作,那么在NameNode启动的时候,如果是Standby角色(实际上,HA模式下,两台NameNode刚启动的时候都会进入Standby状态,然后由ZKFC决定哪台需要transition到Active),则会启动一个叫做StandbyCheckpointer.CheckpointerThread的线程,我们来看NameNode以Standby身份启动时候的代码:

 void startStandbyServices(final Configuration conf) throws IOException {
    //略
    editLogTailer = new EditLogTailer(this, conf);//创建一个editLogTailer线程,用来从远程的QJM服务器上拉取editlog,重演到自己到内存
    editLogTailer.start();
    if (standbyShouldCheckpoint) {
      standbyCheckpointer = new StandbyCheckpointer(conf, this);//独立线程,用来不断进行checkpoint操作
      standbyCheckpointer.start();
    }
  }

Standby模式启动,会创建EditLogTailer负责从JournalNode上拉取edit log文件并在内存中重演(具体细节参考我的博客《》),然后,就是创建StandbyCheckpointer对象,负责进行checkpoint的定时操作。

StandbyCheckpointer的构造方法,实际上就是创建了一个CheckpointerThread线程:

 public StandbyCheckpointer(Configuration conf, FSNamesystem ns)
      throws IOException {
    //略
    this.thread = new CheckpointerThread();//创建CheckpointerThread线程
    this.uploadThreadFactory = new ThreadFactoryBuilder().setDaemon(true)
        .setNameFormat("TransferFsImageUpload-%d").build();
    setNameNodeAddresses(conf);
  }

这个线程的任务,就是通过判断距离上一次checkpoint操作的时间是否超过阈值(dfs.namenode.checkpoint.txns,默认1000000),以及当前没有进行checkpoint操作的数据量是否超过阈值(dfs.namenode.checkpoint.period,默认3600s,即1个小时)来判断是否应该进行checkpoint操作。

在完成了checkpoint操作,生成了对应的img文件以后,会通过HTTP PUT操作,将这个文件发送到Active NameNode。

我们一起来看checkpoint以及checkpoint完毕以后文件的上传过程。StandbyCheckpointer.doCheckpoint()方法在StandbyCheckpointer.CheckpointerThread线程中被调用,负责checkpoint和image文件传输到Active NameNode的工作。

private void doCheckpoint() throws InterruptedException, IOException {
    ...略
    //对命名空间加cpLock锁,这是StandBy Namenode专用锁,防止checkpoint过程中发生了edit log重演
    namesystem.cpLockInterruptibly();
    try {
      assert namesystem.getEditLog().isOpenForRead() : //状态检查,预期情况下,StandBy NameNode所维护的editlog应该是处于open for read状态,这是StandBy NameNode启动以后的持续状态
        "Standby Checkpointer should only attempt a checkpoint when " +
        "NN is in standby mode, but the edit logs are in an unexpected state";
      FSImage img = namesystem.getFSImage();
      long prevCheckpointTxId = img.getStorage().getMostRecentCheckpointTxId();//上一次checkpoint完成的位置
      long thisCheckpointTxId = img.getLastAppliedOrWrittenTxId();//当前最后一次操作的位置,即本次checkpoint需要到达的位置
      assert thisCheckpointTxId >= prevCheckpointTxId;
      if (thisCheckpointTxId == prevCheckpointTxId) {
        LOG.info("A checkpoint was triggered but the Standby Node has not " +
            "received any transactions since the last checkpoint at txid " +
            thisCheckpointTxId + ". Skipping...");
        return;
      }

      imageType = NameNodeFile.IMAGE;
      img.saveNamespace(namesystem, imageType, canceler);//进行checkpoint操作
      txid = img.getStorage().getMostRecentCheckpointTxId();
      assert txid == thisCheckpointTxId : "expected to save checkpoint at txid=" +
        thisCheckpointTxId + " but instead saved at txid=" + txid;

      // Save the legacy OIV image, if the output dir is defined.
      String outputDir = checkpointConf.getLegacyOivImageDir();
      if (outputDir != null && !outputDir.isEmpty()) {
        img.saveLegacyOIVImage(namesystem, outputDir, canceler);
      }
    } finally {
      namesystem.cpUnlock();
    }

    //采用异步方式,将当前的img文件发送到远程的Active NameNode
    ExecutorService executor =
        Executors.newSingleThreadExecutor(uploadThreadFactory);
    Future upload = executor.submit(new Callable() {
      @Override
      public Void call() throws IOException {
        TransferFsImage.uploadImageFromStorage(activeNNAddress, conf,
            namesystem.getFSImage().getStorage(), imageType, txid, canceler);
        return null;
      }
    });
    executor.shutdown();
    try {
      upload.get();
    } catch (InterruptedException e) {
      //异常
    }
  }

Standby NameNode在进行checkpoint操作之前,需要对整个namespace加锁,这里的锁变量是cpLock(cp是checkpoint的简称)。FSNamesystem提供了两把锁,fsLock负责锁定和保护整个namesystem,即,任何时候只有获取fsLock的线程能够修改namesystem的结构,比如块信息、命名空间状态等;而cpLock是专门给Standby NameNode使用的锁,目的是防止checkpoint过程中发生edit log重演操作,cpLock不负责锁定对block信息的修改,这是因为Standby NameNode是无权维护和修改文件到快信息等的对应关系的,只有Active NameNode会修改,因此无需对整个命名空间加锁。因此,cpLock只被用在edit log重演(代码EditLogTailer.doWork())和本节所讲的checkpoint过程中。

回到doCheckpoint()方法,可以看到,checkpoint操作通过img.saveNamespace(namesystem, imageType, canceler);完成,而img文件的传输,通过调用TransferFsImage.uploadImageFromStorage(...)完成。

checkpoint过程

跟踪img.saveNamespace(namesystem, imageType, canceler);代码,最终调用FSImage.saveFSImageInAllDirs()方法完成checkpoint,这个方法会生成image文件,并存放到一个或者多个image存储目录,这是因为出于高可用考虑,我们是可以配置一个以上的img dir的,让相同的img文件存放在不同的磁盘上:

       <property>
                <name>dfs.namenode.name.dirname>
                <value>/data1/data/hadoop/namenode,/data2/data/hadoop/namenodevalue>
        property>
private synchronized void saveFSImageInAllDirs(FSNamesystem source,
      NameNodeFile nnf, long txid, Canceler canceler) throws IOException {
    ...略
    //获取配置的img 目录
    if (storage.getNumStorageDirs(NameNodeDirType.IMAGE) == 0) {
      throw new IOException("No image directories available!");
    }
    ...略
    SaveNamespaceContext ctx = new SaveNamespaceContext(
        source, txid, canceler);//ctx中保存了生成img文件所需要的上下文信息,最重要的,这个img文件的end txId,即这个checkpoint的目标偏移位置

    try {
      List saveThreads = new ArrayList();
      // save images into current
      for (Iterator it //对于每一个storage目录,都创建一个独立线程,同时进行img文件的生成,提高生成效率
             = storage.dirIterator(NameNodeDirType.IMAGE); it.hasNext();) {
        StorageDirectory sd = it.next();//
        FSImageSaver saver = new FSImageSaver(ctx, sd, nnf);
        Thread saveThread = new Thread(saver, saver.toString());
        saveThreads.add(saveThread);
        saveThread.start();
      }
      waitForThreads(saveThreads);//等待所有的saveThread完成存储操作
      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.");
      }
     ...略
      //完成了img文件的存取,实际上是存为一个中间文件,以NameNodeFile.IMAGE_NEW开头。
      //这时候就可以把这些中间文件rename称为最终正式文件了
      ////最后一个参数false,代表不需要rename md5文件,这是因为在FSImageSaver中生成临时文件的时候已经生成了最终的md5文件
      renameCheckpoint(txid, NameNodeFile.IMAGE_NEW, nnf, false);
    } finally {  }
  }

通过storage.dirIterator(NameNodeDirType.IMAGE)获取image文件目录的一个Iterator,然后逐个遍历这些目录并将image文件存放下来。对于多image目录的情况,会为每一个目录创建一个单独线程,多目录并行进行。但是,通过waitForThreads(saveThreads);,会同步等待所有目录完成img存储才会执行下一步,因此,尽管内部并行,但是saveFSImageInAllDirs(...)是一个同步方法,方法返回时,肯定已经完成了将image文件生成到所有目录。

FSImageSaver类负责完成对把img文件存放到某个img目录,我们来看FSImageSaver.saveFSImage(...)方法:

 /**
   * Save the contents of the FS image to the file.
   */
  void saveFSImage(SaveNamespaceContext context, StorageDirectory sd,
      NameNodeFile dstType) throws IOException {
    long txid = context.getTxId();//checkpoint的目标偏移位置
    File newFile = NNStorage.getStorageFile(sd, NameNodeFile.IMAGE_NEW, txid);
    File dstFile = NNStorage.getStorageFile(sd, dstType, txid);

    FSImageFormatProtobuf.Saver saver = new FSImageFormatProtobuf.Saver(context);
    FSImageCompression compression = FSImageCompression.createCompression(conf);
    //按照压缩配置,将img存入到以fsimage.ckpt开头的中间文件中
    saver.save(newFile, compression);
    MD5FileUtils.saveMD5File(dstFile, saver.getSavedDigest());//保存md5文件
    storage.setMostRecentCheckpointInfo(txid, Time.now());
  }

saveFSImage()方法可以看到,生成image文件的时候,并不是直接将内存镜像dump到对应的image磁盘目录,而是会产生一个以fsimage.ckpt开头的中间文件,如:fsimage.ckpt_0000000000992806947,然后生成对应的MD5校验文件,如:fsimage.ckpt_0000000000992806947.md5。当多目录image文件全部完成了中间文件的生成,再调用renameCheckpoint(...)方法,将所有目录的中间文件rename为最终的格式为fsimage_0000000000992806947的文件;

本文不去详述如何将内存镜像读出、压缩、序列化的细节,也不去详述image文件的结构,这涉及到NameNode的文件系统管理,需要较长篇幅去讲解。我会另起博文进行讲解。

文件传输过程

如上文所讲,doCheckpoint()操作包括了checkpoint和image文件的传输,我们来看checkpoint完成以后,image文件的传输。
doCheckpoint()方法

   //采用异步方式,将当前的img文件发送到远程的Active NameNode
    ExecutorService executor =
        Executors.newSingleThreadExecutor(uploadThreadFactory);
    Future upload = executor.submit(new Callable() {
      @Override
      public Void call() throws IOException {
        TransferFsImage.uploadImageFromStorage(activeNNAddress, conf,
            namesystem.getFSImage().getStorage(), imageType, txid, canceler);
        return null;
      }
    });
    executor.shutdown();
    try {
      upload.get();
    } catch (InterruptedException e) {
      //异常处理 略
    }

可以看到,通过ExecutorService创建了一个独立线程负责文件传输过程。这是因为image文件一般较大,传输较为耗时,因此如果此时正好发生NameNode从standby到active的transition过程,这个transition会发生长时间阻塞,因此会创建一个单独线程执行。

dfs.namenode.http-address前缀配置项设置了我们的NameNode的http地址:比如,我们在hdfs-site.xml中这样配置:

        <property>
                <name>dfs.namenode.http-address.datahdfsmaster.namenode1name>
                <value>10.130.277.29:50070value>
        property>

此时,我们名字为datahdfsmaster的nameservice下的名字为namenode1的namenode的http地址是10.130.277.29:50070,这就是namenode1的http地址。

通过跟踪TransferFsImage.uploadImageFromStorage()的代码,我们可以非常详细地看到根据Active NameNode的URI构建HttpURLConnection对象的构建,以及设置requestMethod为PUT、根据dfs.image.transfer.chunksize设置PUT请求的chunk size、设置http请求的超时时间的全过程和细节,这里也不再详述;

使用Checkpoint Node和Backup Node进行checkpoint操作

当然,如果我们认为这样也不是很好,可能会影响StandBy NameNode的工作,那么我们可以单独起一个进程,专门负责checkpoint工作,即使用单独的Checkpoint Node(以下简称CN)或者Backup Node(以下简称BN)负责checkpoint的定时工作。

注意,Checkpoint Node和Backup Node在某种程度上几乎替代了Standby NameNode的功能,因此,在HA模式下,无法启动Checkpoint Node和Backup Node。必须使用非HA模式才可以启动并使用Checkpoint Node。

Checkpoint Node会通过单独线程,定时从Active NameNode拉取edit log文件拷贝到本地,并且将这些edit log在自己内存中进行重演。如果checkpoint条件具备,将进行checkpoint操作,文件存入本地,并通过HTTP PUT的方式将fsimage文件传输给Active NameNode。

Backup Node除了具有Checkpoint Node的上述所有功能外,还会通过RPC方式(属于stream的方式,区别于单独定时拷贝)实时接收Active NameNode的edit操作。因此,同Checkpoint Node相比,Backup Node的内存映象在时间差上几乎与Active NameNode一致。

下面,我们具体来介绍Checkpoint Node、Backup Node的edit log和fsimage的处理机制,以及Backup Node同Active NameNode之间基于RPC通信实现的实时edit log传输。

我们从NameNode的启动过程代码可以看到,通过在NameNode启动的时候设定启动参数,可以将这个节点设定为Checkpoint Node或者Backup Node:

public static NameNode createNameNode(String argv[], Configuration conf)
      throws IOException {
    LOG.info("createNameNode " + Arrays.asList(argv));
    ...略
    StartupOption startOpt = parseArguments(argv);
    setStartupOption(conf, startOpt);
    switch (startOpt) {
      case FORMAT: {//NameNode 格式化
        ...略
      }
      case BACKUP://启动一个 backup node
      case CHECKPOINT: {//启动一个checkpoint node
        NamenodeRole role = startOpt.toNodeRole();
        DefaultMetricsSystem.initialize(role.toString().replace(" ", ""));
        return new BackupNode(conf, role);//无论是BACKUP还是CHECKPOINT,都使用BackupNode对象进行管理,唯一不同是角色不同,一个是NamenodeRole.BACKUP,一个是NamenodeRole.CHECKPOINT
      }
      ....略
      default: {
        DefaultMetricsSystem.initialize("NameNode");
        return new NameNode(conf);//正常启动一个NameNode
      } } }

Checkpoint Node或者Backup Node的实现类都是BackupNode,Backup Node 是NameNode的子类,这说明Backup Node基本上与NameNode的功能一致,所维护的元数据信息相同,只是有了定时checkpoint等功能,同时,Backup Node没有对集群元数据进行修改的权限。

NamenodeRole枚举类型标记了NameNode节点的角色:

  /**
   * Defines the NameNode role.
   */
  static public enum NamenodeRole {
    NAMENODE  ("NameNode"),//普通的NameNode节点,如Active NameNode或者Standby NameNode
    BACKUP    ("Backup Node"),//BACKUP Node
    CHECKPOINT("Checkpoint Node");//Checkpoint Node
    ...略
  }

Backup Node的初始化方法initialize()重写了NameNode的initialize()方法,这个方法主要完成三项工作:
1. 握手:通过handshake,与Active NameNode进行版本协商等工作;通过handshare,CN/BN获取了远程Active NameNode的storage信息,比如,namespaceId,clusterId, blockpoolId,将这些信息设置到自己的storage信息中。在下面的RPC通信中,NameNode会对收到的CN/BN请求中携带的storage信息进行校验,只有校验一致,才会成功返回,否则,认为是异常请求;
2. 注册:向Active NameNode注册自己,这样,Active NameNode就可以在startLogSegment()等操作进行的时候,通过RPC调用,告知自己;
3. 启动:启动Checkpointer线程,负责不断从Active NameNode拉取edit log文件;

protected void initialize(Configuration conf) throws IOException {
    // Trash is disabled in BackupNameNode,
    // but should be turned back on if it ever becomes active.
    conf.setLong(CommonConfigurationKeys.FS_TRASH_INTERVAL_KEY, 
                 CommonConfigurationKeys.FS_TRASH_INTERVAL_DEFAULT);
    //通过handshake,获取了远程的Active Namenode的namespace信息
    NamespaceInfo nsInfo = handshake(conf);
    super.initialize(conf);
    namesystem.setBlockPoolId(nsInfo.getBlockPoolID());

    if (false == namesystem.isInSafeMode()) {
      namesystem.setSafeMode(SafeModeAction.SAFEMODE_ENTER);
    }

    // Backup node should never do lease recovery,
    // therefore lease hard limit should never expire.
    namesystem.leaseManager.setLeasePeriod(
        HdfsConstants.LEASE_SOFTLIMIT_PERIOD, Long.MAX_VALUE);

    // register with the active name-node 
    registerWith(nsInfo);
    // Checkpoint daemon should start after the rpc server started
    runCheckpointDaemon(conf);
    InetSocketAddress addr = getHttpAddress();
    if (addr != null) {
      conf.set(BN_HTTP_ADDRESS_NAME_KEY, NetUtils.getHostPortString(getHttpAddress()));
    }
  }

在完成了handshare()即namespaceId、cluster id等信息等协商以后,CN/BN开始向ActiveNameNode注册自己:
注册的代码为BackupNode.registerWith()

  private void registerWith(NamespaceInfo nsInfo) throws IOException {
    BackupImage bnImage = (BackupImage)getFSImage();
    NNStorage storage = bnImage.getStorage();
    // verify namespaceID
    if (storage.getNamespaceID() == 0) { // new backup storage
      storage.setStorageInfo(nsInfo);
      storage.setBlockPoolID(nsInfo.getBlockPoolID());
      storage.setClusterID(nsInfo.getClusterID());
    } else {
      nsInfo.validateStorage(storage);
    }
    bnImage.initEditLog(StartupOption.REGULAR);
    setRegistration();
    NamenodeRegistration nnReg = null;
    while(!isStopRequested()) {
      try {
        //通过getRegistration()获取请求体,发送给Active NameNode。由于请求体中携带了角色信息,
          //远程的Active NameNode会根据角色判断是否加入到自己的output stream中。通过registerBackupNode.registerBackupNode()
         //可以看到,只有backup的角色会被添加到输出流中去
        nnReg = namenode.registerSubordinateNamenode(getRegistration());
        break;
      } catch(SocketTimeoutException e) {  // name-node is busy
       ...略
      }
    }

    ...略
    nnRpcAddress = nnReg.getAddress();
  }

我们跟踪registerWith()方法,可以看到注册实际上使用的是NamenodeProtocol协议的registerSubordinateNamenode()接口。Active Namenode收到注册请求以后,调用FSNamesystem.registerBackupNode()来进行服务端对请求的处理与响应:

void registerBackupNode(NamenodeRegistration bnReg,
      NamenodeRegistration nnReg) throws IOException {
    writeLock();//获取全局锁
    try {
    ...略
    //只有当它的角色是BACKUP的时候,才会加入到FSEditLog.journalSet中,因此,Active NameNode只会向Backup节点发送自己的edit操作
      //只接受BACKUP节点的注册信息
      if (bnReg.getRole() == NamenodeRole.BACKUP) {
        getFSImage().getEditLog().registerBackupNode(
            bnReg, nnReg);
      }
    } finally {
      writeUnlock();
    }
  }

从上述代码可以看到,Active NameNode对对请求者的身份类型进行了判断。BN和CN在这里产生区别

  • 如果身份是NamenodeRole.BACKUP,那么Active NameNode会把这个客户端加入到自己的output stream中,这样,所有的Active NameNode上的edit操作,都会通过JournalPrototol协议的journal()接口(Active NameNode是JournalPrototol协议客户端,而Backup Node是JournalPrototol协议服务端),发送给这个Backup Node。Backup Node收到这些操作内容以后,会将这些操作在内存中进行replay,即重演,这样,Backup Node的内存与Active NameNode的内存基本一致。根据官方文档,由于Backup Node可以通过这种stream的方式实时获取Active NameNode上的操作,因此没有必要从Active NameNode拷贝fsimage文件和edit log文件;
The Backup node does not need to download fsimage and edits files from the active NameNode in order to create a checkpoint, as would be required with a Checkpoint node or Secondary NameNode, since it already has an up-to-date state of the namespace state in memory. The Backup node checkpoint process is more efficient as it only needs to save the namespace into the local fsimage file and reset edits.
  • 而如果客户端身份是NamenodeRole.CHECKPOINT,就没有这个待遇了,Checkpoint Node虽然也会像Backup Node一样在启动的时候注册(上文说过,它们是同一个实现类),但是,注册的时候,Active NameNode不会将其加入到output stream中,即Active NameNode是不会通过JournalPrototol协议向Checkpoint Node发送实时edit操作的,因此,Checkpoint Node只能通过Checkpointer.java线程同步远程Active NameNode上的文件,而无法通过JournalPrototol实时接收Active NameNode的写操作。Checkpoint Node只能通过不断从Active NameNode下载fsimage文件和edit log文件,然后将这些文件读取到内存并定时生车功能checkpoint操作,然后,将checkpoint文件上传到Active NameNode。

因此,由于Backup Node与Checkpoint Node的fsimage和edit log的同步代码完全相同,除此以外,就是Backup Node比Checkpoint Node多出的实时数据传输过程,因此,下文将只针对Backup Node,分成两节,分别讲解fsimage和edit log的文件传输,以及Backup Node基于RPC实现的与NameNode之间的edit操作的传输。

使用Checkpointer与Active NameNode进行文件同步

BackupNode.initialize()方法的第三个工作,就是通过调用runCheckpointDaemon(...)启动一个Checkpointer的线程(注意,这不是上一节讲到的Standby Namenode节点使用的StandbyCheckpointer.CheckpointerThread),这个线程负责从远程的Active NameNode上读取edit log 和image文件存入本地:

 /**
   * Start a backup node daemon.
   */
  private void runCheckpointDaemon(Configuration conf) throws IOException {
    checkpointManager = new Checkpointer(conf, this);
    checkpointManager.start();//线程启动
  }

这是Checkpointer线程的run()方法:

@Override
  public void run() {
    ...while(shouldRun) {
      try {
        long now = now();
        boolean shouldCheckpoint = false;
        if(now >= lastCheckpointTime + periodMSec) {//判断是否达到checkpoint时间间隔要求
          shouldCheckpoint = true;
        } else {
          long txns = countUncheckpointedTxns();
          if(txns >= checkpointConf.getTxnCount())//判断是否到达checkpoint数据量要求
            shouldCheckpoint = true;
        }
        if(shouldCheckpoint) {
          doCheckpoint();
          lastCheckpointTime = now;
        }
      } catch(IOException e) {
        ...略
      }
      try {
        Thread.sleep(periodMSec);  } catch(InterruptedException ie) {  }
    }
  }

run()方法同Standby NameNode的CheckpointerThread.run()方法相似,也是会判断是否已经满足了checkpoint条件,包括距离上一次checkpoint操作的时间是否超过阈值(dfs.namenode.checkpoint.txns,默认1000000),以及当前没有进行checkpoint操作的数据量是否超过阈值(dfs.namenode.checkpoint.period,默认3600s,即1个小时),如果任意条件满足,则调用doCheckpoint()操作进行checkpoint:

void doCheckpoint() throws IOException {
    BackupImage bnImage = getFSImage();
    NNStorage bnStorage = bnImage.getStorage();

    long startTime = monotonicNow();
    bnImage.freezeNamespaceAtNextRoll();
    //服务端调用,查看NameNodeRpcServer.startCheckpoint(),服务端会结束掉正在写的sgement文件,开启一个新的segment文件
    //客户端调用,查看
    NamenodeCommand cmd = 
      getRemoteNamenodeProxy().startCheckpoint(backupNode.getRegistration());
    CheckpointCommand cpCmd = null;
    //关于这些返回值的含义,可以参考FSImage.startCheckpoint()方法
    switch(cmd.getAction()) {
    //If backup storage contains image that is newer than or incompatible with 
    // what the active name-node has, then the backup node should shutdown. wuchang
      case NamenodeProtocol.ACT_SHUTDOWN://如果发现backup node的image比namenode的更新,或者storage的版本不一致,肯定更有问题,这时候backup node需要关闭
        shutdown();
        throw new IOException("Name-node " + backupNode.nnRpcAddress
                                           + " requested shutdown.");
      case NamenodeProtocol.ACT_CHECKPOINT://校验通过
        cpCmd = (CheckpointCommand)cmd;
        break;
      default:
        throw new IOException("Unsupported NamenodeCommand: "+cmd.getAction());
    }

    //BackupImage.namenodeStartedLogSegment()如果正在发生,那么如果处于frozen,则必须等待
    bnImage.waitUntilNamespaceFrozen();

    //查看服务端调用 NameNodeRPCServer.startCheckpoint()
    CheckpointSignature sig = cpCmd.getSignature();

    // Make sure we're talking to the same NN!
    sig.validateStorageInfo(bnImage);

    long lastApplied = bnImage.getLastAppliedTxId();
    LOG.debug("Doing checkpoint. Last applied: " + lastApplied);
    RemoteEditLogManifest manifest = //从NameNode处获取edit log的文件清单列表
      getRemoteNamenodeProxy().getEditLogManifest(bnImage.getLastAppliedTxId() + 1);

    boolean needReloadImage = false;
    if (!manifest.getLogs().isEmpty()) {
      RemoteEditLog firstRemoteLog = manifest.getLogs().get(0);//获取第一个远程的edit log文件
      // we don't have enough logs to roll forward using only logs. Need
      // to download and load the image.
      //如果从远程获取的edit log文件的transaction 与自己目前最后一次已经获取的log文件的transaction存在gap,需要进行reload
      if (firstRemoteLog.getStartTxId() > lastApplied + 1) {
        //sig.mostRecentCheckpointTxId存放了Active NameNode在最后一个checkpoint的位点
        LOG.info("Unable to roll forward using only logs. Downloading " +
            "image with txid " + sig.mostRecentCheckpointTxId);
        MD5Hash downloadedHash = TransferFsImage.downloadImageToStorage(
            backupNode.nnHttpAddress, sig.mostRecentCheckpointTxId, bnStorage,
            true);//从远程获取这个位点的image文件
        bnImage.saveDigestAndRenameCheckpointImage(NameNodeFile.IMAGE,
            sig.mostRecentCheckpointTxId, downloadedHash);
        lastApplied = sig.mostRecentCheckpointTxId;//更新lastApplied id
        needReloadImage = true;
      }

      if (firstRemoteLog.getStartTxId() > lastApplied + 1) {//在下载了最新的image文件以后,依然存在gap,则抛出异常
        throw new IOException("No logs to roll forward from " + lastApplied);
      }

      // get edits files
      for (RemoteEditLog log : manifest.getLogs()) {//依次下载这些edit log文件
        TransferFsImage.downloadEditsToStorage(
            backupNode.nnHttpAddress, log, bnStorage);
      }

      //刚才已经下载了新的image文件,因此需要将这个image文件reload到内存
      if(needReloadImage) {
        LOG.info("Loading image with txid " + sig.mostRecentCheckpointTxId);
        File file = bnStorage.findImageFile(NameNodeFile.IMAGE,
            sig.mostRecentCheckpointTxId);
        bnImage.reloadFromImageFile(file, backupNode.getNamesystem());
      }
      rollForwardByApplyingLogs(manifest, bnImage, backupNode.getNamesystem());//将edit log应用到内存
    }

    long txid = bnImage.getLastAppliedTxId();

    backupNode.namesystem.writeLock();
    try {
      backupNode.namesystem.setImageLoaded();
      if(backupNode.namesystem.getBlocksTotal() > 0) {
        backupNode.namesystem.setBlockTotal();
      }
      bnImage.saveFSImageInAllDirs(backupNode.getNamesystem(), txid);//将当前内存镜像dump到fsimage文件
      bnStorage.writeAll();
    } finally {
      backupNode.namesystem.writeUnlock();
    }

    //将image文件上传给active namenode
    if(cpCmd.needToReturnImage()) {
      TransferFsImage.uploadImageFromStorage(backupNode.nnHttpAddress, conf,
          bnStorage, NameNodeFile.IMAGE, txid);
    }

    getRemoteNamenodeProxy().endCheckpoint(backupNode.getRegistration(), sig);//结束checkpoint过程

    //只有backup 节点需要进行converge操作,追赶txid到最新的状态
    //如果是checkpoint node,没有这种实时性需求,只需要依靠fsimage文件和edit log文件拷贝就可以完成
    if (backupNode.getRole() == NamenodeRole.BACKUP) {
      bnImage.convergeJournalSpool(); //调用完毕以后,状态成为IN_SYNC
    }
    backupNode.setRegistration(); // keep registration up to date

    long imageSize = bnImage.getStorage().getFsImageName(txid).length();
    LOG.info("Checkpoint completed in "
        + (monotonicNow() - startTime)/1000 + " seconds."
        + " New Image Size: " + imageSize);
  }

doCheckpoint()的主要执行流程如下:
1. 进行checkpoint之前,通过调用NameNodeProtocol协议的startCheckpoint()接口,告知NameNode,自己即将进行checkpoint操作。我们可以查看服务端NameNodeRpcServer.startCheckpoint()得知Active NameNode接收到请求以后的调用流程,可以看到服务端在收到startCheckpoint()以后的处理流程:

  • 自我状态校验:判断自己当前是不是Active NameNode(防止Standby NameNode成为了被checkpoint的节点);如果校验失败,则抛出异常;
  • 请求者身份校验:判断请求者的身份是否合法,即是否是NamenodeRole.CHECKPOINT或者NamenodeRole.BACKUP,如果校验失败,则返回一个shutdown指令,要求请求者停机;
  • 请求者的存储信息等版本校验:对请求者BN/CN的storage信息与自身的storage信息(比如clusterId、namespaceId等)进行校验,正常情况下,BN/CN在initialize()的handshake过程中会获取到Active NameNode的storage信息,所以,这个校验会通过,因此校验会通过。因此这一步是防止未经许可的BN/CN请求进行checkpoint;如果校验失败,则返回一个shutdown指令,要求请求者停机;
  • 缓存的edit log进行flush操作:在我的上一篇博客中讲过NameNode通过RPC协议将自己的edit log实时发往JournalNode的过程,这个过程使用了双缓存。那么在收到了startCheckpoint()的请求以后,Active NameNode会进行缓存的刷新,把缓存的未发送的数据发送到远程Backup Node,然后关闭当前的edit log文件,开始一个新的edit log文件;这样,最近关闭的文件就可以被BN/CN的checkpoint请求到了。注意,BN/CN从Active NameNode请求读取edit log文件的时候,是不会请求处于in-progress状态的文件的。

    1. 当完成了startCheckpoint()请求以后,如果校验通过,就可以进行下一步的fsimage和edit log文件的同步了,这个同步操作发生在doCheckpoint()方法中。

doCheckpoint()方法是整个线程的核心方法,涉及到流程复杂的与Active NameNode之间的通信,因此,我来详细讲解:


STEP 1. 内存namespace锁定
锁定内存的namespace:锁定的目的,是让RPC streaming传输过来的实时的edit操作只能写入edit文件,不可以load到内存

在上文中我讲到,BN除了通过这种方式实现fsimage和edit log文件的同步以外,还会通过RPC stream的方式实时接收edit。这两种方式互为补充,才使得BN的内存镜像能够与Active NameNode保持实时一致。同时,任何时候,两者只有一个能够将edit 操作在内存中重演,必须互斥。我们首先来看BackNode的状态定义:

  volatile BNState bnState;
  static enum BNState {
    DROP_UNTIL_NEXT_ROLL,// BN/CN初始启动的状态,只要来一次写操作,状态就会切换到JOURNAL_ONLY
    JOURNAL_ONLY,//处于该状态下,来自RPC Streaming的edit只可以追加到edit log文件,不可以应用到内存
    IN_SYNC;//处于该状态下,来自RPC Streaming的edit会先在内存中重演,同时写入到edit lot文件
  }

通过bnImage.freezeNamespaceAtNextRoll();,将stopApplyingEditsOnNextRoll标记为置为true,这样,通过RPC方式的JournalProtocol的startLogSegment()接口告知BN的时候,BN会将目前的状态置为JOURNAL_ONLY,即不会将新的edit操作load到内存。
然后,通过bnImage.waitUntilNamespaceFrozen();,一致等待状态变为JOURNAL_ONLY,在这种状态下,通过RPC stream传过来的edits操作不会load到内存,从而达到互斥目的;
通过上面的锁定和等待,当前状态已经切换为JOURNAL_ONLY,因此,Checkpointer线程就可以从NameNode读取fsimage文件和edit log文件,然后load到内存了;开始STEP 2;

STEP 2.请求文件清单列表
确认内存已经被自己加锁,则开始请求Active NameNode的fsimage和image文件清单列表:

    RemoteEditLogManifest manifest = //从NameNode处获取edit log的文件清单列表
      getRemoteNamenodeProxy().getEditLogManifest(bnImage.getLastAppliedTxId() + 1);

这是通过NameNodeProtocol的RPC协议的getEditLogManifest()接口完成的:

  /**
   * Return a structure containing details about all edit logs
   * available to be fetched from the NameNode.
   * @param sinceTxId return only logs that contain transactions >= sinceTxId
   */
  @Idempotent
  public RemoteEditLogManifest getEditLogManifest(long sinceTxId)
    throws IOException;

参数sinceTxId规定了这些edit log的起始位置,从代码中可以看到,这个请求参数设置为bnImage.getLastAppliedTxId() + 1,即,Backup Node希望请求到的edit log能够刚好从自己内存中的最大的txId的下一个值开始;

STEP 3. 根据文件清单请求edit log文件并load到内存

这时候会有一个异常判断,如果Active NameNode返回的edit log文件的最小的txId大于自己请求的sinceTxId,中间存在gap,此时,这些返回的edit是不能够apply到内存中的,因为txId必须严格连续递增,不可丢失任何一个。我们看Active NameNode端在对于BN的getEditLogManifest()请求的处理(NameNodeRpcServer.getEditLogManifest() -> FSEditLog.getEditLogManifest() -> JournalSet.getRemoteEditLogs() -> FileJournalManager->getRemoteEditLogs()):

public List getRemoteEditLogs(long firstTxId,
      boolean inProgressOk) throws IOException {
    File currentDir = sd.getCurrentDir();//获取当前这个Journal对应的存放EditLog的目录
    List allLogFiles = matchEditLogs(currentDir);//通过正则匹配获取候选的EditLogFile
    List ret = Lists.newArrayListWithCapacity(
        allLogFiles.size());
    for (EditLogFile elf : allLogFiles) {
      ...略
      if (elf.getFirstTxId() >= firstTxId) {//如果当前的segment的第一个txid大于所请求的txId,则加入到返回结果中
        ret.add(new RemoteEditLog(elf.firstTxId, elf.lastTxId,
            elf.isInProgress()));
      } else if (elf.getFirstTxId() < firstTxId && firstTxId <= elf.getLastTxId()) {
        //如果当前的segment文件的startTxId和endTxId位于请求的txId前后,则segment也是满足条件的
        ret.add(new RemoteEditLog(elf.firstTxId, elf.lastTxId,
            elf.isInProgress()));
      }
    }

    Collections.sort(ret);//对edit log文件排序
    return ret;
  }

从上述代码可以看到客户端如何根据getEditLogManifest()中的sinceTxId参数决定返回哪些文件。
因此存在这种情况,服务器端返回的edit log中最小的txId的值大于请求的sinceTxId。这种情况是可能发生的,比如Active NameNode当前fsimage文件的txId到130,然后edit log文件从131开始,BN请求的edit log的sinceTxId为120,此时就只能返回从131开始的edit log。遇到这种情况,BN的处理方式为直接将Active NameNode的fsimage文件下载下来,然后实现txid的完全连续一致,这相当于将Active NameNode的fsimage和edit log进行了一次完全的复制。fsimage复制过来以后,通过bnImage.reloadFromImageFile(file, backupNode.getNamesystem());将fsimage load到内存,然后才能开始把请求过来的edit log load到内存。当然,如果将Active NameNode的fsimage文件下载下来发现txId依然无法实现连续,只能抛出异常了。完成了以上操作,就通过rollForwardByApplyingLogs(manifest, bnImage, backupNode.getNamesystem());将edit log文件 load到内存。

STEP 4.checkpoint文件的生成并上传给Active NameNode

经过以上步骤,BN完成了内存中namesystem与Active NameNode的一致。此时,就开始进行checkpoint了,把内存的整个镜像dump成fsimage文件。然后,把这个checkpoint文件上传到Active NameNode。

STEP 5.Backup Node对处于打开状态的edit log的汇集
我们知道,Backup Node除了有Checkpointer线程进行文件拷贝,同时使用RPC stream进行edit log的流式传输。在上文中讲到BNState这个控制Backup Node的状态,当前,doCheckpoint()方法执行过程中,处于BNState.JOURNAL_ONLY,来自RPC stream的edits操作只会写入到edits文件,不会load到内存。因此,通过调用bnImage.convergeJournalSpool();,来将这个未打开的edits文件中的edits load到内存,这样,Backup Node内存中的景象几乎与Active NameNode保持一致了;
完成了converge操作,BN的状态就切换为BNState了,这时候,通过RPC stream收到的edit log在存入edit log文件的同时,也应用到内存,让内存保持与Active NameNode实时一致;关于RCP stream针对不同状态的处理方式,看下一节介绍。


Backup Node与Active NameNode进行基于JournalProtocol RPC协议进行实时edit操作同步

上一节讲Checkpointer线程的时候讲到了RPC streaming与Checkpointer之间的同步,我们可以从journal()方法看到通过JournalProtocol协议的journal()接口收到了Active NameNode的一批edit操作以后,BN根据当前的不同状态进行的不同处理,看BackupImage.journal(...)

  synchronized void journal(long firstTxId, int numTxns, byte[] data) throws IOException {
    if (LOG.isTraceEnabled()) {
      LOG.trace("Got journal, " +
          "state = " + bnState +
          "; firstTxId = " + firstTxId +
          "; numTxns = " + numTxns);
    }

    switch(bnState) {
      case DROP_UNTIL_NEXT_ROLL:
        return;//什么都不做,既不用apply到内存,也不写入到本地
      case IN_SYNC://处于IN_SYNC状态,则将这些消息应用到内存
        applyEdits(firstTxId, numTxns, data);//将收到的edits应用到内存的namespace
        break;//break以后
      case JOURNAL_ONLY://处于JOURNAL_ONLY,则这一批来自rpc的消息不可以load到内存的namespace,只能够追加到磁盘的edit文件
        break;//需要把收到的edit log写入到本地
      default:
        throw new AssertionError("Unhandled state: " + bnState);//异常状态
    }
    logEditsLocally(firstTxId, numTxns, data);//将这些操作追加到当前的edit log磁盘文件,下一次Checkpointer运行,通过调用convergeJournalSpool(),可以负责把这个处于in-progress状态的文件里面的edits操作load到namespace
  }

我们先看JOURNAL_ONLY状态,上一节讲到Checkpointer线程的时候,整个BN处于JOURNAL_ONLY状态,因此,通过RPC方式收到的edits只会追加到edits文件,不会调用applyEdits(firstTxId, numTxns, data); load到内存,这样,Checkpointer线程拷贝过来的edits文件或者fsimage文件就可以load到内存了。等Checkpointer结束一轮运行,状态切换为IN_SYNC,那么内存的操作全就交付给RPC了。


总之,HDFS使用Backup Node、Checkpoint Node以及Standby Namenode进行checkpoint操作,原理均相同。它们的存在,既保证了Active NameNode的数据备份,又将Active NameNode从不影响核心业务的checkpoint操作中解脱出来。当Active NameNode发生异常,Standby Namenode可以很快接管HDFS。
对于Backup Node ,个人认为设计得非常好的地方,在于通过良好的同步控制,让http的方式的文件拷贝和RPC stream的方式的实时edit传输交替配合,让Backup Node的内存镜像与Active NameNode时刻保持近乎一致;

你可能感兴趣的:(hadoop)