启动流程
- 检查配置项${dfs.data.dir}对应的存储目录是否创建,是否具有读写的权限检查;
- 获取本节点的主机名称和NameNode的地址,以及其他一些运行时需要的配置项;
- 构造向NameNode注册需要的DataNodeRegistration对象,并在接下来进行一些属性的初始化;
- 建立到NameNode的IPC连接,并调用
handshake()
方法与NameNode进行握手,得到了NamespaceInfo对象,包含了layoutVersion、namespaceID、cTime等信息; - 构造数据节点存储DataStorage对象,执行可能的存储空间状态恢复,接着构造数据节点使用的FSDataset对象;
- 创建流式接口服务器DataXceiverServer对象;
- 创建数据块扫描器DataBlockScanner对象;
- 创建数据节点上的HTTP信息服务器,该服务器提供了数据节点上的运行状态信息;
- 创建该数据节点IPC服务器,对外提供ClientDatanodeProtocol和InterDatanodeProtocol远程接口服务;
- 最后,调用NameNode的远程接口
register()
进行注册,并启动DataNode线程开始对外提供服务。
存储目录的文件结构以及特点
blocksBeingWritten:保存了客户端发起的,当前正在写的数据块;
detach:用于配合数据节点升级,==数据块分离==操作时的临时文件夹;
tmp:保存了用于数据块复制时,当前正在写的数据块;
in_user.lock:表示当前目录已经被使用,实现了一种锁机制,这样DataNode可以独自使用该目录;
current:保存了已写入HDFS文件系统的数据块和一些系统工作时需要的文件;
- blk_开头的文件:HDFS数据块,用来保存HDFS文件内容;
- .meta后缀文件:CRC32校验文件,保存数据块的校验信息;
当current目录达到一定规模时(由配置项${dfs.datanode.numblocks}指定),DataNode会在current目录下新创建一个子目录subdir*,用于保存新的数据块和元数据。
默认配置下,current目录下最多只有64个数据块(128个文件)和64个子目录。通过这种手段,DataNode既保证目录不会太深,影响文件检索性能,同时也避免了某个目录保存大量的数据块。
对存储目录的管理
数据节点的业务逻辑主要由文件结构对象提供的服务进行管理,数据节点的文件结构管理包括两部分内容:数据节点存储DataStorage和文件系统数据集FSDataSet。
DataStorage
数据节点存储DataStorage是抽象类Storage的子类,而抽象类Storage又继承自StorageInfo。和DataStorage同级的FSImage类主要用于组织NameNode的磁盘数据,FSImage的子类CheckpointStorage,则管理SecondaryNameNode使用的文件结构。
StorageInfo包含了三个重要的共有的属性,包括HDFS存储系统信息结构版本号layoutVersion、存储系统标识namespaceID和存储系统创建时间cTime。这些信息都保存在current目录下的VERSION文件中,典型的VERSION文件内容为:
namespaceID=1301932004
storageID=DS-1056522850-172.31.207.102-50010-1498861743747
cTime=0
storageType=DATA_NODE
layoutVersion=-32
在StorageInfo的基础上,抽象类Storage可以管理多个目录,存储目录的实现类为StorageDirectory,它是Storage的内部类,提供了在存储目录上的一些通用操作。
DataStorage扩展了Storage,专注于数据节点存储空间的生命周期管理,其代码可以分为两个部分:升级相关和升级无关的。在数据节点第一次启动时,会调用DataStorage.format()
创建存储目录结构,通过删除原有目录及数据,并重新创建目录,然后将VERSION文件的属性赋值,并持久化到磁盘中。
DataStorage的升级操作
HDFS升级时需要复制以前版本的元数据(对NameNode)和数据(对DataNode)。在DataNode上,升级并不需要两倍的集群存储空间,DataNode使用了Linux文件系统的硬链接技术,可以保留对同一个数据块的两个引用,即当前版本和以前版本。通过这样的技术,就可以在需要的时候,轻松回滚到以前版本的文件系统。
硬链接是一种特殊的文件系统机制,它允许一个文件可以有多个名称。当一个文件有多个名称时,删除其中的一个名称,并不会删除文件数据,只有所有的文件名被删除后,文件系统才会真正删除文件数据。
好比在Java中,一个对象的多个引用。将其中一个引用置为null,垃圾收集器并不会去回收该对象,只有所有对该对象的引用都断开时,该对象才会被垃圾回收器回收。
升级操作
HDFS升级最多保留前一版本的文件系统,没有多版本的升级、回滚机制。同时引入升级提交机制,该机制用于删除以前的版本,所以在升级提交后,就不能回滚到以前版本了。综上所述,DataNode和NameNode升级过程采用了下图的状态机:
数据节点的升级主要由
DataStorage.doUpgrade()
方法实现,其中升级过程主要涉及如下几个目录:- curDir:当前版本目录,通过
StorageDirectory.getCurrentDir()
获得,目录名为current; - prevDir:上一版本目录,目录名为previous;
- tmpDir:上一版本的临时目录,目录名为${dfs.data.dir}/previous.tmp;
DataStorage.doUpgrade()
升级流程如下:- 确保上述涉及的目录处于正常状态:current存在、previous存在则删除、previous.tmp存在则抛出异常;
- 将current目录改名为previous.tmp;
- 使用HardLink,在新创建的current目录下,建立到previous.tmp目录中数据块和数据块校验文件的硬链接;
- 在新的current目录下,创建新版本的VERSION文件。
- 最后,将previous.tmp目录改名为previous目录,完成升级。
这时,DataNode上存储着previous和current两个目录,而他们包含了同样的数据块和数据块校验文件,但他们有各自的VERSION文件。
- curDir:当前版本目录,通过
升级回滚
- 检查previous目录是否存在,如果不存在,则无法回滚;
- 检查previous目录下的VERSION文件中的layoutVersion和cTime,如果preLayoutVersion小于当前系统的HDFS存储系统信息结构版本号或者preCTime小于当期存储系统的创建时间cTime,则无法进行回滚。
- 将current目录重命名为removed.tmp;
- 将previous目录重命名为current;
- 删除removed.tmp目录;
升级提交
- 将previous重命名为finalized.tmp;
- 启动一个线程删除finalized.tmp;
升级过程中临时文件的用途
因为升级、升级提交或升级回滚都需要进行一定的耗时操作。在系统升级过程中,doUpgrade()
需要建立数据块文件的硬链接,在这一过程中,如果突然出现故障,那么存储空间就有可能处于某一中间状态。因此,引入上述的目录,系统就能够判断目前doUpgrade()
处于什么阶段,并采取相应的措施。
数据节点Storage状态机
Storage和DataStorage提供了一个完美的HDFS数据节点升级机制,Storage状态机、以及配合状态机工作的临时文件,提供了一个完备的升级方法。在升级过程中或者升级回滚过程中的任意步骤出现错误,都可以通过状态机恢复到正常状态。
FSDataset
FSDatasetInterface接口
FSDatasetInterface接口的方法主要分为三类:
- 数据块相关的方法:FSDataset管理了DataNode上的数据块,大量的FSDataset方法和数据块相关,如创建数据块、打开数据块的输入、输出流、提交数据块等;
- 数据块校验信息文件相关的方法:包括维护数据块和校验信息文件的关系,如校验信息文件输入流等方法。
- 其他方法:包括FSDataset健康检查、关闭FSDataset的
shutDown()
等方法。
FSDir、FSVolume和FSVolumeSet
FSDataset借鉴了LVM的一些概念,可以管理多个数据目录,文件数据集将它管理的存储空间分为三个级别,分别用FSDir