1. 文件系统目录树

FSDirectory

HDFS文件系统的命名空间是以“/”为根的整个目录树,是通过FSDirectory类来管理的。FSNamesystem也提供了管理目录树结构的方法,当FSNamesystem中的方法也是调用FSDirectory类的实现,FSNamesystem在FSDirectory类方法的基础上添加了editlog日志记录的功能。

HDFS引入FSDirectory是为了将文件系统目录树的所有操作抽象成一个统一的接口,这样其他子系统在调用目录树操作时,就不需要了解目录树内部实现的复杂逻辑了。

INode

在HDFS中,不管是目录还是文件,在文件系统目录树中都被看作是一个INode节点。如果是目录,则其对应的类为INodeDirectory;如果是文件,则是INodeFile类。这两个类都是INode的派生类。INodeDirectory中的变量children集合保存子目录或文件。

特性

HDFS2.6引入特性(Feature)概念,INode的所有特性都实现了这个接口,每个特性都对应一个Feature子类,包括:

  1. DirectoryWithSnapshotFeature:带有快照的目录特性。
  2. DirectorySnapshottableFeature:可以添加快照的目录特性。
  3. FileWithSnapshotFeature:带有快照的文件特性。
  4. DirectoryWithQuotaFeature:支持磁盘配额的目录特性。
  5. FileUnderConstructionFeature:正在构建的文件特性。
  6. XAttrFeature:支持文件系统扩展属性的特性。
  7. AclFeature:安全特性。

FSImage

Namenode会定期将文件系统的命名空间(文件目录树、文件/目录元信息)保存到fsimage二进制文件中,以防止Namenode掉电或进程崩溃。但如果Namenode实时地将内存中的元数据同步到fsimage文件中,将会非常消耗资源且造成Namenode运行缓慢。所有Namenode会先将命名空间的修改操作保存在editlog文件中,然后定期合并fsimage和editlog文件。

管理fsimage文件的实现类是FSImage,FSImage类主要实现了以下功能:

  1. 保存命名空
  2. 加载fsimage文件
  3. 加载editlog文件

FSEditLog

在HDFS源码中,使用FSEditLog类来管理editlog文件。和fsimage文件不同,editlog文件会随着Namenode的运行实时更新,所以FSEditLog类的实现依赖于底层的输入流和输出流。

2. 数据块管理

Namenode维护着HDFS中两个最重要的关系:

  1. HDFS文件系统的目录树以及文件的数据块索引。
  2. 数据块和数据节点的对应关系。

Block、Replica、BlocksMap

INodeFile.blocks字段记录了一个HDFS文件拥有的所有数据块。该字段是一个BlockInfo类型的数组,BlockInfo类是Block类的子类。

Block类用来唯一标识Namenode中的数据块。BlockInfo类定义了bc字段保存该数据块归属于哪个HDFS文件。

BlocksMap管理着Namenode上数据块的元数据,包括当前数据块属于哪个HDFS文件,以及当前数据块保存在哪些Datanode上。当Datanode启动时,会对Datanode的本地磁盘进行扫描,并将当前Datanode上保存的数据块信息汇报到Namenode。Namenode收到Datanode的汇报信息后,会建立数据块与保存这个数据块的数据节点的对应关系,并将这个信息保存在BlocksMap中。

Namenode中的数据块信息叫数据块(block),但Datanode中保存的数据块称为副本(replica)。在HDFS源码中使用Replica对象描述副本。

BlockManager类保存并管理了HDFS集群中所有数据块的元数据。

3. 数据节点管理

名字节点启动之后,会加载fsimage和editlog文件重建文件系统目录树,但是对于数据块与Datanode的映射关系却需要在Datanode上报后动态构建。Datanode在启动时除了与名字节点握手、注册以及上报数据块信息外,还会定时向Namenode发送心跳一级块汇报,并执行Namenode传回的指令。所以Namenode中会有很大一部分逻辑是与Datanode相关的,包括添加和删除Datanode、与Datanode启动过程的交互、处理Datanode发送的心跳。

继承自DatanodeInfo的DatanodeDescriptor是Namenode中用于描述一个Datanode信息的类。这个类只用在Namenode侧,对于Client是不可见的。

DatanodeStorageInfo类描述了Datanode上的一个存储(storage),一个Datanode可以定义多个存储(在dfs.datanode.data.dir中配置多个Datanode的存储目录)来保存数据块,这些存储还可以是异构的,例如可以是磁盘、内存、SSD等。

DatanodeManager类中记录了在Namenode上注册的Datanode,以及这些Datanode在网络中的拓扑结构等信息。

4. 租约管理

HDFS文件是write-once-read-many,并且不支持客户端的并行写操作。HDFS提供了租约(Lease)机制来实现对HDFS文件的互斥操作。租约是Namenode给予租约持有者(LeaseHolder,一般是客户端)在规定时间内拥有文件权限(写文件)的合同。

在HDFS中,客户端写文件时需要先从租约管理器(LeaseManager)申请一个租约,成功申请租约之后,客户端就成为了租约持有者,也就拥有了对该HDFS文件的独占权限,其他客户端在该租约有效时无法打开这个HDFS文件进行操作。Namenode的租约管理器保存了HDFS文件与租约、租约与租约持有者的对应关系,租约管理器还会定期检查它维护的所有租约是否过期。租约管理器会强制收回过期的租约,所以租约持有者需要定期更新租约(renew),维护对该文件的独占锁定。当客户端完成了对文件的写操作,关闭文件时,必须在租约管理器中释放租约。

5. 缓存管理

集中式缓存管理(Centralized Cache Management)功能,允许用户将一些文件和目录保存到HDFS缓存中。HDFS集中式缓存是由分布在Datanode上的堆外内存组成,由Namenode统一管理。

通过“hdfs cacheadmin”命令管理集中式缓存。

缓存指令(Cache Directive):一条缓存指令定义了一个要被缓存的路径。

命令

描述

hdfs cacheadmin -addDirective

缓存指定路径

hdfs cacheadmin -removeDirective

删除指定id对应的缓存

hdfs cacheadmin -removeDirectives

删除指定路径的缓存

hdfs cacheadmin -listDirectives

显示当前所有缓存

缓存池(Cache Pool):是一个管理单元,是管理缓存指令的组。

命令

描述

hdfs cacheadmin -addPool

创建一个缓存池

hdfs cacheadmin -modifyPool

修改一个缓存池的配置

hdfs cacheadmin -removePool

删除一个缓存池

CacheManager类是Namenode管理集中式缓存的核心组件,它管理着分布在HDFS集群中Datanode上的所有缓存数据块,同事负责响应“hdfs cacheadmin”命令或HDFS API发送的缓存管理命令。

6. ClientProtocol实现

  1. Client首先调用ClientProtocol.create()方法创建一个新的HDFS文件。
  2. 然后调用ClientProtocol.addBlock()方法获取一个新的数据块的位置信息,或者通过调用ClientProtocol.append()追加写接口获取已有HDFS文件中最后一个未写满数据块的位置信息。
  3. 有了数据块位置信息,Client就可以建立数据流管道并进行写操作了。
  4. 当Client完成一个数据块的写操作后,会调用addBlock()申请新的数据块并提交上一个完成写操作的数据块。
  5. Datanode成功写入了数据块的副本之后,会调用DatanodeProtocol.blockReceivedAndDeleted()向Namenode汇报。
  6. 当Client完成了整个文件的写操作之后,会调用ClientProtocol.complete()方法向Namenode提交最后一个数据块,将文件从构建中状态改为正常状态并释放租约。

7. Namenode的启动和停止

安全模式

安全模式是Namenode的一种状态,此时不接受任何对命名空间的修改,同时也不触发任何复制和删除数据块的操作。

Namenode启动时会自动进入安全模式状态,可以通过“dfsadmin -safemode value”命令来操作安全模式。

配置名

类型

默认值

描述

dfs.namenode.replication.min

int

1

数据块最低副本系数,也用于写操作时判断是否可以complete一个数据块

dfs.safemode.threshold.pct

float

0.999

离开安全模式时,系统需要满足的阈值比例,也就是满足最低副本系数的数据块与系统内所有数据块的比例

dfs.safemode.extension

int

30000

安全模式等待时间,也就是满足了最低系数之后,离开安全模式的时间,用于等待剩余的数据节点上报数据块

HDFS Hign Availability

在HA集群中,会配置两个独立的Namenode。在任何时刻,只有一个节点会作为活动的节点,另一个节点则处于备份状态。

为解决出现两个Namenode同时修改命名空间的问题,HDFS提供了三个级别的隔离(fencing)机制:

  1. 共享存储隔离:同一时间只允许一个Namenode向JournalNames写入editlog数据。
  2. 客户端隔离:同一时间只允许一个Namenode响应客户端请求。
  3. Datanode隔离:同一时间只允许一个Namenode向Datanode下发名字节点指令。

管理命令:

DFSHAAdmin [-ns <nameserviceId>]
  [-transitionToActive <serviceId>]
  [-transitionToStandby <serviceId>]
  [-failover [--forcefence] [--forceactive] <serviceId><serviceId>]
  [-getServiceState <serviceId>]
  [-checkHealth <serviceId>]
  [-help <command>]

使用Quorum Journal设计方案,基于Paxos算法实现HA的editlog数据共享存储。

  1. JournalNode(JN):运行在N台独立的物理机器上,它将editlog文件保存在JournalNode的本地磁盘上,同时JournalNode还对外提供RPC接口QJournalProtocol以执行远程读写editlog文件的功能。
  2. QuorumJournalManager(QJM):运行在Namenode上,通过调用RPC接口QJournalProtocol中的方法向JournalNode发送写入、互斥、同步editlog。

基于Quorum Journal模式的HA提供了epoch number来解决互斥问题:

  1. 当一个Namenode变成活动状态时,会分配给它一个epoch number。
  2. 每个epoch number都是唯一的,没有任意两个Namenode有相同的epoch number。
  3. epoch number定义了Namenode写editlog文件的顺序。对于任意两个Namenode,拥有更大epoch number的Namenode被认为是活动节点。

自动Failover机制依赖于两个新增的网元:ZooKeeper集群;ZKFailoverController(org.apache.hadoop.ha.ZKFailoverController)。

名字节点的启动

NameNodeRpcServer类用于接收和处理所有的RPC请求,FSNamesystem类负责Namenode的所有逻辑,而NameNode类则负责管理Namenode配置、RPC接口以及HTTP接口等。所以Namenode的启动操作是在NameNode类中执行的,具体方法是NameNode.main():

  1. main()方法首先调用createNameNode()创建一个NameNode对象
  2. 创建成功后调用NameNode.join()方法等待RPC服务结束
  3. NameNode启动成功后,NameNode将对文件系统的管理都委托给FSNamesystem对象,NameNode会调用FSNamesystem.loadFromDisk()创建FSNamesystem对象
  4. 构建FSNamesystem对象,然后加载fsimage以及editlog文件到命名空间中
  5. 成功构造FSNamesystem对象后,NameNode的构造方法会调用StandbyState.enterState()进入Standby状态。至此,NameNode也就成功地启动了。

名字节点的停止

在NameNode.main()方法中调用StringUtils.startupShutdownMessage()添加一个Hook,这个Hook会在Namenode结束运行并且对应的JVM退出时,在日志中输出退出信息。

public static void main(String argv[]) throws Exception {
  if (DFSUtil.parseHelpArgument(argv, NameNode.USAGE, System.out, true)) {
    System.exit(0);
  }

  try {
    StringUtils.startupShutdownMessage(NameNode.class, argv, LOG);
    NameNode namenode = createNameNode(argv, null);
    if (namenode != null) {
      namenode.join();
    }
  } catch (Throwable e) {
    LOG.error("Failed to start namenode.", e);
    terminate(1, e);
  }
}