FSDirectory
HDFS文件系统的命名空间是以“/”为根的整个目录树,是通过FSDirectory类来管理的。FSNamesystem也提供了管理目录树结构的方法,当FSNamesystem中的方法也是调用FSDirectory类的实现,FSNamesystem在FSDirectory类方法的基础上添加了editlog日志记录的功能。
HDFS引入FSDirectory是为了将文件系统目录树的所有操作抽象成一个统一的接口,这样其他子系统在调用目录树操作时,就不需要了解目录树内部实现的复杂逻辑了。
INode
在HDFS中,不管是目录还是文件,在文件系统目录树中都被看作是一个INode节点。如果是目录,则其对应的类为INodeDirectory;如果是文件,则是INodeFile类。这两个类都是INode的派生类。INodeDirectory中的变量children集合保存子目录或文件。
特性
HDFS2.6引入特性(Feature)概念,INode的所有特性都实现了这个接口,每个特性都对应一个Feature子类,包括:
FSImage
Namenode会定期将文件系统的命名空间(文件目录树、文件/目录元信息)保存到fsimage二进制文件中,以防止Namenode掉电或进程崩溃。但如果Namenode实时地将内存中的元数据同步到fsimage文件中,将会非常消耗资源且造成Namenode运行缓慢。所有Namenode会先将命名空间的修改操作保存在editlog文件中,然后定期合并fsimage和editlog文件。
管理fsimage文件的实现类是FSImage,FSImage类主要实现了以下功能:
FSEditLog
在HDFS源码中,使用FSEditLog类来管理editlog文件。和fsimage文件不同,editlog文件会随着Namenode的运行实时更新,所以FSEditLog类的实现依赖于底层的输入流和输出流。
Namenode维护着HDFS中两个最重要的关系:
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集群中所有数据块的元数据。
名字节点启动之后,会加载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在网络中的拓扑结构等信息。
HDFS文件是write-once-read-many,并且不支持客户端的并行写操作。HDFS提供了租约(Lease)机制来实现对HDFS文件的互斥操作。租约是Namenode给予租约持有者(LeaseHolder,一般是客户端)在规定时间内拥有文件权限(写文件)的合同。
在HDFS中,客户端写文件时需要先从租约管理器(LeaseManager)申请一个租约,成功申请租约之后,客户端就成为了租约持有者,也就拥有了对该HDFS文件的独占权限,其他客户端在该租约有效时无法打开这个HDFS文件进行操作。Namenode的租约管理器保存了HDFS文件与租约、租约与租约持有者的对应关系,租约管理器还会定期检查它维护的所有租约是否过期。租约管理器会强制收回过期的租约,所以租约持有者需要定期更新租约(renew),维护对该文件的独占锁定。当客户端完成了对文件的写操作,关闭文件时,必须在租约管理器中释放租约。
集中式缓存管理(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发送的缓存管理命令。
安全模式
安全模式是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)机制:
管理命令:
DFSHAAdmin [-ns <nameserviceId>]
[-transitionToActive <serviceId>]
[-transitionToStandby <serviceId>]
[-failover [--forcefence] [--forceactive] <serviceId><serviceId>]
[-getServiceState <serviceId>]
[-checkHealth <serviceId>]
[-help <command>]
使用Quorum Journal设计方案,基于Paxos算法实现HA的editlog数据共享存储。
基于Quorum Journal模式的HA提供了epoch number来解决互斥问题:
自动Failover机制依赖于两个新增的网元:ZooKeeper集群;ZKFailoverController(org.apache.hadoop.ha.ZKFailoverController)。
名字节点的启动
NameNodeRpcServer类用于接收和处理所有的RPC请求,FSNamesystem类负责Namenode的所有逻辑,而NameNode类则负责管理Namenode配置、RPC接口以及HTTP接口等。所以Namenode的启动操作是在NameNode类中执行的,具体方法是NameNode.main():
名字节点的停止
在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);
}
}