【Hadoop源码解读】【DataNode】DN trash功能解读
DN trash功能解读
理解trash功能需要我们先熟悉一下HDFS的删除操作,具体内容大家可以参考另外两篇博客(补充博客地址)
一、Trash原理介绍
整个Trash功能大家可以参考HDFS-12996阅读其设计文档了解详细的介绍。在这里我们只介绍一下背景。
HDFS滚动升级期间,为了保证回滚过程DN数据不会和NN元数据之间产生错乱和丢失(回滚时会恢复元数据到升级前的状态)。为此设计DN的Trash功能来保留升级前集群的数据。将对应的数据删除操作转为移动到Trash目录,以提供后续的数据恢复。
升级期间的目录结构(源自设计文档实际生产中rolling-upgrade-trash名为trash)

二、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这条线就追完了,这里做一个小结,盘点一下这条线都干了什么。
- 每一次心跳都去判断一下集群的升级状态
- 如果在滚动升级中就去开启Trash功能
- 通过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方法。
创建标记的过程已经小结过了,这里我们重点看一下磁盘操作的过程。
- DN心跳接收Invalid标志,并处理需要删除的块。
- 如果集群处于滚动升级,我们可以通过dataStorage.getTrashDirectoryForReplica(bpid, removing)方法拿到Trash目录的地址。
- 根据地址是否为空判断执行删除还是移动。
- 第一次执行移动操作的时刻创建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);
}
}
浙公网安备 33010602011771号