在datanode启动的主流程中,启动了多种工作线程,包括InfoServer、JVMPauseMonitor、BPServiceActor等。其中,最重要的是BPServiceActor线程,真正代表datanode与namenode通信的正是BPServiceActor线程。
BPServiceActor#run():
此处说的“通信”包括与握手、注册(BPServiceActor#connectToNNAndHandshake)和后期循环提供服务(BPServiceActor#offerService(),本文暂不讨论)。
启动过程中主要关注BPServiceActor#connectToNNAndHandshake():
通过两次握手完成了datanode的注册,比较简单,不讨论。
重点是BPOfferService#verifyAndSetNamespaceInfo():
尽管是在BPServiceActor线程中,却试图以BPOfferService为单位初始化blockpool(包括内存与磁盘上的存储结构)。如果初始化成功,万事大吉,以后同BPOfferService的其他BPServiceActor线程发现BPOfferService#bpNSInfo !=null就不再初始化;而如果一个BPServiceActor线程初始化blockpool失败了,还可以由同BPOfferService的其他BPServiceActor线程重新尝试初始化。
DataNode#initBlockPool():
此时可知,blockpool是按照namespace逐个初始化的。这很必要,因为要支持Federation的话,就必须让多个namespace既能共用BlockManager提供的数据块存储服务,又能独立启动、关闭、升级、回滚等。
在逐个初始化blockpool之前,先以datanode整体进行初始化。这一阶段操作的主要对象是DataStorage、StorageDirectory、FsDatasetImpl、FsVolumeList、FsVolumeImpl等;后面的FsDatasetImpl#addBlockPool操作的主要对象才会具体到各blockpool。具有1-5工作经验的,面对目前流行的技术不知从何下手,需要突破技术瓶颈的可以加群。在公司待久了,过得很安逸,但跳槽时面试碰壁。需要在短时间内进修、跳槽拿高薪的可以加群。如果没有工作经验,但基础非常扎实,对java工作机制,常用设计思想,常用java开发框架掌握熟练的可以加群。java架构群:582505643一起交流。
DataNode#initStorage():
初始化DataStorage:DataStorage#recoverTransitionRead()
DataStorage#recoverTransitionRead():
根据Javadoc,BlockPoolSliceStorage管理着该datanode上相同bpid的所有BlockPoolSlice。然而,猴子暂时没有发现这个类与升级外的操作有关(当然,启动也可能是由于升级重启),暂不深入。
BlockPoolSlice详见后文FsVolumeImpl#addBlockPool。
DataStorage#recoverTransitionRead()、BlockPoolSliceStorage#recoverTransitionRead()与数据节点恢复的关系非常大
初始化FsDatasetImpl:FsDatasetFactory#newInstance()
FsDatasetFactory#newInstance():
FsDatasetImpl.()
初始化DataStorage的过程中,将各${dfs.data.dir}放入了storage(即DataNode#storage)。对于datanode来说,${dfs.data.dir}/current目录就是要添加的卷FsVolumeImpl。
FsDatasetImpl#initPeriodicScanners()(名为初始化,实为启动):
初始化并启动DataBlockScanner、DirectoryScanners。
命名为init或许是考虑到有可能禁用了数据块和目录的扫描器,导致经过FsDatasetImpl#initPeriodicScanners方法后,扫描器并没有启动。但仍然给人造成了误解。
FsDatasetImpl#addBlockPool()操作的主要对象具体到了各blockpool,完成blockpool、current、rbw、tmp等目录的检查、恢复或初始化:
FsVolumeList#addBlockPool(),并发向FsVolumeList中的所有卷添加blockpool(所有namespace共享所有卷):
正如FsVolumeList#addBlockPool(),FsVolumeList封装了很多面向所有卷的操作。
FsVolumeImpl#addBlockPool():
BlockPoolSlice是blockpool在每个卷上的实际存在形式。所有卷上相同bpid的BlockPoolSlice组合成小blockpool(概念上即为BlockPoolSliceStorage),再将相关datanode(向同一个namespace汇报的datanode)上相同bpid的小blockpool组合起来,就构成了该namespace的blockpool。
而FsVolumeImpl#bpSlices维护了bpid到BlockPoolSlice的映射。FsVolumeImpl通过该映射获取bpid对应的BlockPoolSlice,而BlockPoolSlice再反向借助FsDatasetImpl中的静态方法完成实际的文件操作(见后续文章中的写数据块过程)。
回到BlockPoolSlice.:
BlockPoolSlice(String bpid, FsVolumeImpl volume, File bpDir, Configuration conf) throws IOException { this.bpid=bpid; this.volume=volume; this.currentDir=new File(bpDir,
DataStorage.STORAGE_DIR_CURRENT); this.finalizedDir=new File( currentDir,
DataStorage.STORAGE_DIR_FINALIZED); this.lazypersistDir=new File(currentDir,
DataStorage.STORAGE_DIR_LAZY_PERSIST); // 检查并创建finalized目录 if (!this.finalizedDir.exists()) { if (!this.finalizedDir.mkdirs()) { throw new IOException("Failed to mkdirs " + this.finalizedDir); } }
this.deleteDuplicateReplicas=conf.getBoolean(
DFSConfigKeys.DFS_DATANODE_DUPLICATE_REPLICA_DELETION,
DFSConfigKeys.DFS_DATANODE_DUPLICATE_REPLICA_DELETION_DEFAULT); // 删除tmp目录。每次启动datanode都会删除tmp目录(并重建),重新协调数据块的一致性。 this.tmpDir=new File(bpDir,
DataStorage.STORAGE_DIR_TMP); if (tmpDir.exists()) { FileUtil.fullyDelete(tmpDir); } // 检查并创建rbw目录 this.rbwDir=new File(currentDir,
DataStorage.STORAGE_DIR_RBW); final boolean supportAppends=conf.getBoolean(
DFSConfigKeys.DFS_SUPPORT_APPEND_KEY,
DFSConfigKeys.DFS_SUPPORT_APPEND_DEFAULT); // 如果不支持append,那么同tmp一样,rbw里保存的必然是新写入的数据,可以在每次启动datanode时删除rbw目录,重新协调 if (rbwDir.exists() && !supportAppends) { FileUtil.fullyDelete(rbwDir); } // 如果支持append,待datanode启动后,有可能继续append数据,因此不能删除,等待进一步确定或恢复 if (!rbwDir.mkdirs()) { if (!rbwDir.isDirectory()) { throw new IOException("Mkdirs failed to create " + rbwDir.toString()); } } if (!tmpDir.mkdirs()) { if (!tmpDir.isDirectory()) { throw new IOException("Mkdirs failed to create " + tmpDir.toString()); } } // 启动dfsUsage的监控线程(详见对hadoop fs shell中df、du区别的总结) this.dfsUsage=new DU(bpDir, conf, loadDfsUsed()); this.dfsUsage.start(); ShutdownHookManager.get().addShutdownHook( new Runnable() { @Override public void run() { if (!dfsUsedSaved) { saveDfsUsed(); } } }, SHUTDOWN_HOOK_PRIORITY); }
可知,每个blockpool目录下的存储结构是在构造BlockPoolSlice时初始化的。
关于du的作用及优化:
在linux系统上,该线程将定期通过du -sk命令统计各blockpool目录的占用情况,随着心跳汇报给namenode。
执行linux命令需要从JVM继承fork出子进程,成本较高(尽管linux使用COW策略避免了对内存空间的完全copy)。为了加快datanode启动速度,此处允许使用之前缓存的dfsUsage值,该值保存在current目录下的dfsUsed文件中;缓存的dfsUsage会定期持久化到磁盘中;在虚拟机关闭时,也会将当前的dfsUsage值持久化。
ReplicaMap#initBlockPool()
ReplicaMap#initBlockPool(),初始化ReplicaMap中blockpool的映射