【Hadoop源码解读】【DataNode】DN trash功能解读

DN trash功能解读

理解trash功能需要我们先熟悉一下HDFS的删除操作,具体内容大家可以参考另外两篇博客(补充博客地址)

一、Trash原理介绍

整个Trash功能大家可以参考HDFS-12996阅读其设计文档了解详细的介绍。在这里我们只介绍一下背景。
HDFS滚动升级期间,为了保证回滚过程DN数据不会和NN元数据之间产生错乱和丢失(回滚时会恢复元数据到升级前的状态)。为此设计DN的Trash功能来保留升级前集群的数据。将对应的数据删除操作转为移动到Trash目录,以提供后续的数据恢复。
升级期间的目录结构(源自设计文档实际生产中rolling-upgrade-trash名为trash)
image

二、Trash全流程

解读之前我们可以先带着一个问题入手,那就是DN的trash目录是何时创建的?这个问题可以帮助我们更好的理顺思路。

如果说NameNodeRpcServer是理解NameNode的入口,那么BPServiceActor就是理解DN和NN之间操作的最好入口,我们先来看看该类都负责做些什么?

/**
 * A thread per active or standby namenode to perform:
 * <ul>
 * <li> Pre-registration handshake with namenode</li>
 * <li> Registration with namenode</li>
 * <li> Send periodic heartbeats to the namenode</li>
 * <li> Handle commands received from the namenode</li>
 * </ul>
 */
@InterfaceAudience.Private
class BPServiceActor implements Runnable {

  static final Logger LOG = DataNode.LOG;
  // ......

该类的主要功能包括向NN预注册与注册、发送心跳到NN、处理来自NN的指令。如果Hadoop社区有什么做的比较好的那就是类注释写的十分清晰,能够一目了然他在做什么,如果看完这里还是有疑惑,大家可以搜索类名,一般都会有人进行解读。这种方式可以帮助我们快速理解源码。
自从HDFS-6005之后滚动升级DN不再需要重启DN而是根据NN的状态来判断集群所处的升级过程。因此DN处理滚动升级的入口都移动到了心跳之中。因此我们可以阅读BPServiceActor#offerService方法来了解相关处理过程。

private void offerService() throws Exception {
    LOG.info("For namenode " + nnAddr + " using"
    + " BLOCKREPORT_INTERVAL of " + dnConf.blockReportInterval + "msecs"
    + " CACHEREPORT_INTERVAL of " + dnConf.cacheReportInterval + "msecs"
    + " Initial delay: " + dnConf.initialBlockReportDelayMs + "msecs"
    + "; heartBeatInterval=" + dnConf.heartBeatInterval
    + (lifelineSender != null ?
    "; lifelineIntervalMs=" + dnConf.getLifelineIntervalMs() : ""));

        //
    // Now loop for a long time....
    //
    while (shouldRun()) {
        try {
    // 折叠部分我们不关注的代码
    ......
    // If the state of this NN has changed (eg STANDBY->ACTIVE)
    // then let the BPOfferService update itself.
    //
    // Important that this happens before processCommand below,
    // since the first heartbeat to a new active might have commands
    // that we should actually process.
    bpos.updateActorStatesFromHeartbeat(
            this, resp.getNameNodeHaState());
    state = resp.getNameNodeHaState().getState();

    if (state == HAServiceState.ACTIVE) {
        // 处理滚动升级的入口函数
        handleRollingUpgradeStatus(resp);
    }
    commandProcessingThread.enqueue(resp.getCommands());
    }
    } else {
        if (scheduler.lastCmdReciveTime > scheduler.maxIdleTimeWindow) {
            // In this case some heart beat may be missed
    scheduler.scheduleNextLifeline(monotonicNow());
        }
    }
    // 折叠部分我们不关注的代码
    ......
    } // while (shouldRun())
} // offerService

此处所有的心跳收到回应以后都要先过来判断一下升级状态,确认集群是否开始升级。
接着往里追,我们看BPServiceActor#handleRollingUpgradeStatus方法。

 private void handleRollingUpgradeStatus(HeartbeatResponse resp) throws IOException {
    RollingUpgradeStatus rollingUpgradeStatus = resp.getRollingUpdateStatus();
    if (rollingUpgradeStatus != null &&
        rollingUpgradeStatus.getBlockPoolId().compareTo(bpos.getBlockPoolId()) != 0) {
      // Can this ever occur?
      LOG.error("Invalid BlockPoolId " +
          rollingUpgradeStatus.getBlockPoolId() +
          " in HeartbeatResponse. Expected " +
          bpos.getBlockPoolId());
    } else {
      // 此处的逻辑可能存在冗余校验,真实处理的过程在BPOfferService中
      bpos.signalRollingUpgrade(rollingUpgradeStatus);
    }
}

到目前为止我们知道到了DN的滚动升级状态是怎么来的,接下来我们继续往下看,看看trash目录的生成到底和那些操作有关。

void signalRollingUpgrade(RollingUpgradeStatus rollingUpgradeStatus)
throws IOException {
    // 非升级场景直接退出
    if (rollingUpgradeStatus == null) {
        return;
    }
    String bpid = getBlockPoolId();
    // Finalized状态执行清除Trash的动作,其他升级状态开启Trash功能。
    if (!rollingUpgradeStatus.isFinalized()) {
        dn.getFSDataset().enableTrash(bpid);
        dn.getFSDataset().setRollingUpgradeMarker(bpid);
    } else {
        dn.getFSDataset().clearTrash(bpid);
        dn.getFSDataset().clearRollingUpgradeMarker(bpid);
    }
}

可以发现Trash功能在升级期间一定会开启,因此我们来看看enableTrash做了什么。查看FsDatasetImpl#enableTrash代码,可以知道其最终的实现是在DataStorage#enableTrash里。

 /**
* Enable trash for the specified block pool storage. Even if trash is
* enabled by the caller, it is superseded by the 'previous' directory
* if a layout upgrade is in progress.
*/
public void enableTrash(String bpid) {
    // 这里add方法不会重复add,trashEnabledBpids的数据结构是set
    if (trashEnabledBpids.add(bpid)) {
      getBPStorage(bpid).stopTrashCleaner();
      LOG.info("Enabled trash for bpid {}",  bpid);
    }
}

public void clearTrash(String bpid) {
    if (trashEnabledBpids.contains(bpid)) {
        getBPStorage(bpid).clearTrash();
        trashEnabledBpids.remove(bpid);
        LOG.info("Cleared trash for bpid {}", bpid);
    }
}

public boolean trashEnabled(String bpid) {
  return trashEnabledBpids.contains(bpid);
}

可以发现我们开启Trash功能是通过把块池加入到trashEnabledBpids队列里。这个队列提供三种操作增删查,通过着三种操作来标记Trash的生命周期。

到这里BPServiceActor这条线就追完了,这里做一个小结,盘点一下这条线都干了什么。

  1. 每一次心跳都去判断一下集群的升级状态
  2. 如果在滚动升级中就去开启Trash功能
  3. 通过trashEnabledBpids.add(bpid)的方法标记Trash功能的开启状态。

接下来我们需要看的是磁盘操作,HDFS的数据操作是通过单独的线程来完成的,内存中的映射和磁盘数据之间异步。
接下来我们略过接收删除信号部分的代码(参考DN删除的源码分析),直接看FsDatasetImpl#invalidate方法。

private void invalidate(String bpid, Block[] invalidBlks, boolean async)
      throws IOException {
    // 略过非重点
    ......
      if (v.isTransientStorage()) {
        RamDiskReplica replicaInfo =
          ramDiskReplicaTracker.getReplica(bpid, invalidBlks[i].getBlockId());
        if (replicaInfo != null) {
          if (!replicaInfo.getIsPersisted()) {
            datanode.getMetrics().incrRamDiskBlocksDeletedBeforeLazyPersisted();
          }
          ramDiskReplicaTracker.discardReplica(replicaInfo.getBlockPoolId(),
            replicaInfo.getBlockId(), true);
        }
      }

      // If a DFSClient has the replica in its cache of short-circuit file
      // descriptors (and the client is using ShortCircuitShm), invalidate it.
      datanode.getShortCircuitRegistry().processBlockInvalidation(
                new ExtendedBlockId(invalidBlks[i].getBlockId(), bpid));

      // If the block is cached, start uncaching it.
      cacheManager.uncacheBlock(bpid, invalidBlks[i].getBlockId());

      try {
        // 异步磁盘删除操作,这里通过自己编写的方法为磁盘写提速。
        if (async) {
          // Delete the block asynchronously to make sure we can do it fast
          // enough.
          // It's ok to unlink the block file before the uncache operation
          // finishes.
          asyncDiskService.deleteAsync(v.obtainReference(), removing,
              new ExtendedBlock(bpid, invalidBlks[i]),
              // 获取当前DN上的Trash目录地址的String
              dataStorage.getTrashDirectoryForReplica(bpid, removing));
        } else {
          asyncDiskService.deleteSync(v.obtainReference(), removing,
              new ExtendedBlock(bpid, invalidBlks[i]),
              dataStorage.getTrashDirectoryForReplica(bpid, removing));
        }
      } catch (ClosedChannelException e) {
        LOG.warn("Volume {} is closed, ignore the deletion task for " +
            "block: {}", v, invalidBlks[i]);
      }
    }
    // 省略其他步骤
    ......
}

可以看到DN处理Trash删除的方案是通过传递应当出现Trash目录地址来实现的。我们继续向下追踪,来看看删除的具体逻辑。
FsDatasetAsyncDiskService#deleteAsync方法描述了这个过程。

/**
* Delete the block file and meta file from the disk asynchronously, adjust
* dfsUsed statistics accordingly.
*/
void deleteAsync(FsVolumeReference volumeRef, ReplicaInfo replicaToDelete,
      ExtendedBlock block, String trashDirectory) {
    LOG.info("Scheduling " + block.getLocalBlock()
        + " replica " + replicaToDelete + " on volume " +
        replicaToDelete.getVolume() + " for deletion");
    ReplicaFileDeleteTask deletionTask = new ReplicaFileDeleteTask(
        volumeRef, replicaToDelete, block, trashDirectory);
    execute(((FsVolumeImpl) volumeRef.getVolume()), deletionTask);
}

可以看到这里采用了命令模式来启动单独的线程处理磁盘的读写,这和该类的描述一致。这种磁盘操作,非常值得大家学习。也许日后做一些性能提升的需求就可以作为参考。
既然有execute方法那一定存在对应的run方法,我们找到FsDatasetAsyncDiskService#ReplicaFileDeleteTask#run方法,看看具体的实现。

public void run() {
      try {
        final long blockLength = replicaToDelete.getBlockDataLength();
        final long metaLength = replicaToDelete.getMetadataLength();
        boolean result;

        // 实际判断是否会放入trash要通过是否存在trashdir得到
        result = (trashDirectory == null) ? deleteFiles() : moveFiles();

        if (!result) {
          LOG.warn("Unexpected error trying to "
              + (trashDirectory == null ? "delete" : "move")
              + " block " + block.getBlockPoolId() + " " + block.getLocalBlock()
              + " at file " + replicaToDelete.getBlockURI() + ". Ignored.");
        } else {
          if (block.getLocalBlock().getNumBytes() != BlockCommand.NO_ACK) {
            datanode.notifyNamenodeDeletedBlock(block, volume.getStorageID());
          }
          volume.onBlockFileDeletion(block.getBlockPoolId(), blockLength);
          volume.onMetaFileDeletion(block.getBlockPoolId(), metaLength);
          LOG.info("Deleted " + block.getBlockPoolId() + " " +
              block.getLocalBlock() + " URI " + replicaToDelete.getBlockURI());
        }
        updateDeletedBlockId(block);
      } finally {
        IOUtils.cleanupWithLogger(null, this.volumeRef);
      }
    }
}

这里我们可以明确一点,实际上到这一步我们仍然没有在磁盘上创建对应Trash目录。只是把该目录的地址作为String传给了处理删除的方法。那么这个Trash目录到底什么时候生成我们继续向下看。FsDatasetAsyncDiskService#ReplicaFileDeleteTask#moveFiles

// 将块的删除改为放入DN Trash中
private boolean moveFiles() {
      if (trashDirectory == null) {
        LOG.error("Trash dir for replica " + replicaToDelete + " is null");
        return false;
      }

      File trashDirFile = new File(trashDirectory);
      try {
        // 创建目录的地方从BlockPoolSliceStorage传来的参数只是字符串
        // 如果第一次做moveFile操作就需要创建trah目录。
        volume.getFileIoProvider().mkdirsWithExistsCheck(
            volume, trashDirFile);
      } catch (IOException e) {
        return false;
      }

      if (LOG.isDebugEnabled()) {
        LOG.debug("Moving files " + replicaToDelete.getBlockURI() + " and " +
            replicaToDelete.getMetadataURI() + " to trash.");
      }

      final String blockName = replicaToDelete.getBlockName();
      final long genstamp = replicaToDelete.getGenerationStamp();
      File newBlockFile = new File(trashDirectory, blockName);
      File newMetaFile = new File(trashDirectory,
          DatanodeUtil.getMetaName(blockName, genstamp));
      try {
        return (replicaToDelete.renameData(newBlockFile.toURI()) &&
                replicaToDelete.renameMeta(newMetaFile.toURI()));
      } catch (IOException e) {
        LOG.error("Error moving files to trash: " + replicaToDelete, e);
      }
      return false;
}

在这里我们终于找到了创建Trash目录的地方,我们是否需要用到Trash目录取决于我们在集群滚动升级期间是否会删除数据。追到此处我们基本解答了一开始的问题,Trash目录的创建条件就分析清楚了。同样升级期间Trash的过程我们也完全搞清楚了。这里总结一下整个流程:
整个Trash分为两个动作,首先在内存中创建Trash标记,最后在磁盘删除操作中创建Trash目录改delete方法为move方法。
创建标记的过程已经小结过了,这里我们重点看一下磁盘操作的过程。

  1. DN心跳接收Invalid标志,并处理需要删除的块。
  2. 如果集群处于滚动升级,我们可以通过dataStorage.getTrashDirectoryForReplica(bpid, removing)方法拿到Trash目录的地址。
  3. 根据地址是否为空判断执行删除还是移动。
  4. 第一次执行移动操作的时刻创建Trash目录。

最后附上mkdir源码,供大家学习。(^_^)

  /**
* Create the target directory using {@link File#mkdirs()} only if
* it doesn't exist already.
*
* @param volume  target volume. null if unavailable.
* @param dir  directory to be created.
* @throws IOException  if the directory could not created
*/
public void mkdirsWithExistsCheck(
      @Nullable FsVolumeSpi volume, File dir) throws IOException {
    // 使用一个钩子计算磁盘性能
    final long begin = profilingEventHook.beforeMetadataOp(volume, MKDIRS);
    boolean succeeded = false;
    try {
      faultInjectorEventHook.beforeMetadataOp(volume, MKDIRS);
      // 通过File类的mkdir方法在磁盘上创建文件夹
      succeeded = dir.isDirectory() || dir.mkdirs();
      profilingEventHook.afterMetadataOp(volume, MKDIRS, begin);
    } catch(Exception e) {
      onFailure(volume, begin);
      throw e;
    }

    if (!succeeded) {
      throw new IOException("Mkdirs failed to create " + dir);
    }
}
posted @ 2022-12-05 19:59  默默Coding  阅读(170)  评论(0)    收藏  举报