【前言】
在dn整体架构一文中提到了逻辑业务层包括BP管理、块扫描和目录扫描,文本就来聊聊块扫描和目录扫描的大概原理。
【块扫描】
块扫描主要是对dn上存储的所有block进行数据完整性校验。进一步来讲,就是读取dn存储的所有block文件,以及对应的元数据(meta)文件,然后进行数据完整性的校验。
在dn的具体实现中,块扫描对应的类BlockScanner只是一个壳,具体扫描由VolumeScanner负责。每个VolumeScanner负责一个目录,同时也是一个独立的线程。在该线程中,扫描并读取各自目录中的block数据。
每个配置的目录都有一个唯一的ID,BlockScanner中就以该ID为key,VolumeScanner为value,保存所有的VolumeScanner。
由于扫描需要读取具体block文件中的数据和meta文件中的数据,为了避免扫描产生的IO对正常读写IO性能产生影响,因此,块扫描会进行一定的限速处理。
又因为有了限速,一次完整的块扫描耗时是非常长的,为了避免扫描过程中,dn意外的重启,导致重新开始扫描,因此扫描过程中会定期将扫描的信息记录到磁盘文件中(游标文件),dn重启后读取该文件继续本次扫描直到扫描完所有的block。
另外,VolumeScanner并不是在dn启动后就创建运行的,而是当dn成功向nn获取到命名空间信息后,才会在每个VolumeScanner中添加对应的BP,并启动扫描。也就是说只有获取到命名空间(包括BP信息)的BP,才会扫描对应目录下的block,而未被添加的BP,则不会扫描。
相关的配置:
- dfs.block.scanner.volume.bytes.per.second 每秒扫描的字节数,默认为1M,设置为0表示不启用块扫描功能。
- dfs.datanode.scan.period.hours 块扫描的时间间隔,默认为504(小时)。
- dfs.block.scanner.cursor.save.interval.ms block扫描游标文件保存的时间间隔。
【目录扫描】
目录扫描的主要目的判断是磁盘中实际存储的block文件和内存中记录的是否一致。具体包括检查block是否存在,block的大小、时间戳等和内存中记录的是否一致。如果有不一致的情况,会以实际磁盘中的文件为准,修正内存中记录的信息。
与块扫描不同,目录扫描不会进行实际文件的读取,仅仅是扫描目录下的文件,从文件名去判断内存中是否存在对应的block。
相关的配置:
- dfs.datanode.directoryscan.threads 扫描的最大并发线程数,默认值为1,如果dn配置了多个目录,可以考虑设置与目录数一致(用于加速对目录的扫描)
- dfs.datanode.directoryscan.interval 目录扫描的时间间隔,默认时间为21600s,即6小时。
有几点需要注意:
- 与块扫描一样,dn启动时并不会立即启动目录扫描,而是等成功向nn请求到命名空间信息后,才初始化目录扫描。并且是计算出一个随机的时间(小于配置的时间),在这个时间点开始首次扫描,而后就是按照配置的时间周期性的进行扫描。
- 在目录扫描的过程中,会在内存中构造所有block的完整信息,类似于《DN的存储数据结构》一文中提到的ReplicaMap,然后再与内存中记录的信息进行比较。 对于存储了较多block的dn而言,可能会出现短暂的内存飙升,扫描结束后内存自然也就降下来了。但是如果在扫描过程中,dn所在的节点没有足够多的内存容纳对应的数据(尤其是dn以容器的方式部署运行时,容器没有设置足够大的内存),那么就会导致dn频繁的进行full GC,甚至导致dn进程异常退出。
【总结】
dn的块扫描和目录扫描一定程度上保证了数据的完整性。