HDFS 元信息管理的核心技术与实现

HDFS 简介
HDFS(Hadoop Distributed File System)是大数据领域中一种核心分布式文件系统,以高可靠性和高扩展性为特点,为海量数据存储提供了高效解决方案。具备高度容错性,通过分布式架构实现数据的高效存储与管理,HDFS 采用分块存储与冗余副本策略,确保数据的高可用性与持久性,同时支持高吞吐量的数据访问,满足大数据量处理对性能与稳定性的需求,适用于大规模数据存储和处理场景,特别是读多写少的场景。
架构简介
HDFS 是经典的 Master 和 Slave 架构,每一个 HDFS 集群包括两个 NameNode(Active/Standby)和多个 DataNode。

其中 NameNode 管理所有文件的元数据信息,并且负责与客户端交互。DataNode 负责管理存储在该节点上的文件块。每一个上传到 HDFS 的文件都会被划分为一个或多个数据块,这些数据块根据 HDFS 集群的数据备份策略被分配到不同的 DataNode 上,位置信息交由 NameNode 统一管理。
名词解释
NameNode:用于管理文件系统的命名空间、维护文件系统的目录结构树以及元数据信息,记录写入的每个数据块(Block)与其归属文件的对应关系。此信息以命名空间镜像(FSImage)和编辑日志(EditsLog)两种形式持久化在磁盘中。
DataNode :DataNode 是 Hadoop 分布式文件系统(HDFS)中实际存储数据块的核心组件。DataNode 会根据 NameNode 或 Client 的指令来存储或者提供数据块,并且定期地向 NameNode 汇报该 DataNode 存储的数据块信息。
Client :通过 Client 来访问文件系统,然后由 Client 与 NameNode 和 DataNode 进行通信。Client 对外作为文件系统的接口。
INode :用于描述 HDFS 文件系统对象,元信息重要组成部分。INodeFile 表示一个文件, INodeDirectory 文件目录, INodeAttributes 文件属性(group\owner\permission\accessTime\modifyTime\Acl\XAttr), INodeDirectoryWithQuota 有配额限制的目录。INodeFileUnderConstruction 处于构建状态的文件。
Blocks :HDFS 将文件拆分成 128 MB 大小的数据块进行存储,这些 Block 可能存储在不同的节点上。HDFS 可以存储更大的单个文件,甚至超过任何一个磁盘所能容纳的大小。一个 Block 默认存储 3 个副本,以 Block 为粒度将副本存储在多个节点上。此方式不仅提高了数据的安全性,而且对于分布式作业可以更好地利用本地的数据进行计算,减少网络传输。
Federation :HDFS 为扩展单 Namenode 管理元信息容量限制而引入的联邦机制,一个集群有多个命名空间,即多个 Namenode,提供元信息管理水平扩展能力及隔离能力。
Router :引入联邦机制后解决多 Namenode 管理问题,屏蔽 Federation 的实现细节;作为 Namenode 代理服务结合目录挂载表,将请求转发给正确的子集群,为用户提供了统一目录视图管理。


元信息管理
为加快元信息访问,服务运行时元信息存放在 Namenode 服务内存中,由两部分组成:
存在磁盘上的 fsimage 与 editLog,Namenode 启动时加载,生成文件与 Block 映射;
Datanode 服务启动后向 Namenode 汇报 Block 信息,并更新 BlockMap 完成 Block 与机器的映射更新,这部分是动态构建起来的;
简单说:Namenode 管理文件切分成了几个数据块以及这些数据块分别存储在哪些 datanode 上;


BlockInfo 非常核心的属性 triplets 的数组,这个数组的长度是 3*replication,replication 表示数据块的备份数。这个数组中存储了该数据块所有的备份数据块对应的 DataNode 信息,我们现在假设备份数是 3,那么这个数组的长度是 3 * 3,这个数组存储的数据如下:

tripletsi:Block 所在的 DataNode;tripletsi+1:该 DataNode 上前一个 Block;tripletsi+2:该 DataNode 上后一个 Block;其中 i 表示的是 Block 的第 i 个副本,i 取值[0,replication)。
1.1 Namenode 端
1.1.1 FsImage & EditLog(Namenode )
FsImage 保存了最新的元数据检查点,包含了整个 HDFS 文件系统的所有目录和文件的信息。对于文件来说包括了数据块描述信息、修改时间、访问时间等;对于目录来说包括修改时间、访问权限控制信息(目录所属用户,所在组)等。
Editlog 是记录 HDFS 元信息各种更新操作,HDFS 客户端执行所有的写操作都会被记录到 editlog 中。为了避免 editlog 不断增大,namenode(StandyBy)会周期性合并 fsimage 和 edits 成新的 fsimage,这个周期可以自己设置(editlog 到达一定大小或者定时)
Namenode 启动加载文件元信息过程:

fsimage 加载解析:
INodeMap:存储 INode 与 Block 映射
INodeMap 负责维护 INode(索引节点)到数据块(Block)之间的映射关系,采用 LightWeightGSet 结构存储,LightWeightGSet 是一个占用较低内存的集合的实现,它使用一个数组 array 存储元素,使用链表来解决冲突,它没有实现重新哈希分区,所以,内部的 array 不会改变大小; INodeMap 数组大小占 JVM 总内存 1%: 通过调用 LightWeightGSet.computeCapacity(1, "INodeMap")确保了内存资源的合理分配,还有效避免了因过度分配或不足导致的性能瓶颈。
1.1.2 FSNamesystem (Namenode)
NameNode 实际记录信息、核心功能处理地方,其中与写操作重要相关的文件租约管理 LeaseManager:
HDFS 特点支持 Write-Once-Read-Many,对文件写操作的互斥同步靠 Lease 实现。Lease 实际上是时间约束锁,其主要特点是排他性。客户端写文件时需要先申请一个 Lease,一旦有客户端持有了某个文件的 Lease,其它客户端就不可能再申请到该文件的 Lease,这就保证了同一时刻对一个文件的写操作只能发生在一个客户端。

由于 Lease 本身的时间约束特性,当 Lease 发生超时后需要强制回收,内存中与该 Lease 相关的内容要被及时清除。超时检查及超时后的处理逻辑由 LeaseManager.Monitor 统一执行。LeaseManager 中维护了两个与 Lease 相关的超时时间:软超时(softLimit)和硬超时(hardLimit),使用场景稍有不同。
正常情况下,客户端向集群写文件前需要向 NameNode 的 LeaseManager 申请 Lease;写文件过程中定期更新 Lease 时间,以防 Lease 过期,周期与 softLimit 相关;写完数据后申请释放 Lease。整个过程可能发生两类问题:
写文件过程中客户端没有及时更新 Lease 时间;写完文件后没有成功释放 Lease。
两个问题分别对应为 softLimit 和 hardLimit。两种场景都会触发 LeaseManager 对 Lease 超时强制回收。如果客户端写文件过程中没有及时更新 Lease 超过 softLimit 时间后,另一客户端尝试对同一文件进行写操作时触发 Lease 软超时强制回收;如果客户端写文件完成但是没有成功释放 Lease,则会由后台线程 LeaseManager.Monitor 检查是否硬超时后统一触发超时回收,保证后面其它客户端的写入能够正常申请到该文件的 Lease。
1.1.3 BlockManager(Namenode)
BlockManager 最重要的功能之一是维护 Namenode 内存中的数据块信息,通过对以下两部分内容进行修改实现的
■ 数据块与存储这个数据块的数据节点存储的对应关系, 这部分信息保存在数据块对应的 BlockInfo 对象的 triplets[]数组中, Namenode 内存中的所有 BlockInfo 对象则保存在 BlockManager.blocksMap 字段中。
■ 数据节点存储与这个数据节点存储上保存的所有数据块的对应关系, 这部分信息保存在 DatanodeStorageInfo.blocks 字段中, blockList 是 BlockInfo 类型的, 利用 BlockInfo.triplets[]字段的双向链表结构 ,DatanodeStorageInfo 可以通过 blockList 字段保存这个数据块存储上所有数据块对应的 BlockInfo 对象。
■ DatanodeManager: BlockManager 负责接收管理来自 DataNode 的消息,具体的管理操作由 DatanodeManager 接管,他负责监控 DataNode 节点的状态变化以及消费 Block 信息变化指令。
■ DocommissionManager: 管理需要退役或检修的节点信息,在确保这些节点上的数据都被成功转移后,才将节点置为退役和检修状态,避免直接设置导致的数据丢失。
待处理不同状态 Block:
由 ReplicationMonitor 周期默认 3s 执行 block 扫描, 如:
复制操作:从 blockManager 中的待复制数据块列表 neededReplications 中选出若干个数据块执行复制操作,为这些数据块的复制操作选出 source 源节点以及 target 目标节点,然后将其封装成 BlockTargetPair 对象添加到 DatanodeDescriptor.replicateBlocks 中,等待下次该 DataNode 心跳的时候将构造复制指令带到目标节点以执行副本的复制操作。
删除操作:从 blockManager 中的待删除数据块列表 invalidateBlocks 中选出若干个副本,然后构造删除指令,也即是将 blockManager.invalidateBlocks 中的待删除数据块添加到对应的 DatanodeDescriptor.invalidateBlocks 中,等待下次该 DataNode 心跳的时候将构造删除指令带到目标节点以执行副本的删除操作。
1.2 Datanode 端
1.2.1 BlockPoolMananger(Datanode )
Datanode 负责存储和管理数据 Block,启动向 Namenode 注册后,将存储的 Block 信息汇报给 Namenode;单台 Datanode 服务支持多磁盘挂载用于存储数据块(dfs.datanode.data.dir); 多个存储目录存储的数据块并不相同, 并且不同的存储目录可以是异构的, 这样的设计可以提高数据块 IO 的吞吐率;
datanode 磁盘目录结构


current 目录包含 finalized、 rbw 以及 lazyPersisit 三个子目录。finalized 目录保存了所有 FINALIZED 状态的副本, rbw 目录保存了 RBW(正在写) 、 RWR(等待恢复) 、 RUR(恢复中) 状态的副本, tmp 目录保存了 TEMPORARY 状态的副本。
当集群数据量达到一定程度时, finalized 目录下的数据块将会非常多。 为了便于组织管理, 数据块将按照数据块 id 进行散列, 拥有相同散列值的数据块将处于同一个子目录下,不同版本目录层级限制不同(2.7.x -- 256 * 256,2.8.x -- 32 * 32)
以 Federation 架构为例,一个 Datanode 节点会同时向多个 Namenode 节点注册并提供数据块存储服务,HDFS 规定了一个目录能存放 Block 的数目,所以一个 Storage 上存在多个目录。管理工作划分:
■ BlockPoolSlice: 管理一个指定块池在一个指定存储目录下的所有数据块。 由于 Datanode 可以定义多个存储目录, 所以块池的数据块会分布在多个存储目录下。一个块池会拥有多个 BlockPoolSlice 对象, 这个块池对应的所有 BlockPoolSlice 对象共同管理块池的所有数据块。
■ FsVolumelmpl: 管理 Datanode 一个存储目录下的所有数据块。 由于一个存储目录可以存储多个块池的数据块, 所以 FsVolumelmpl 会持有这个存储目录中保存的所有块池的 BlockPoolSlice 对象。
■ FsVolumeList: Datanode 可以定义多个存储目录, 每个存储目录下的数据块是使用一个 FsVolumelmpl 对象管理的, 所以 Datanode 定义了 FsVolumeList 类保存 Datanode 上所有的 FsVolumelmpl 对象, FsVolumeList 对 FsDatasetlmpl 提供类似磁盘的服务。
datanode 启动过程

1.2.2 FsDataSet
与块相关的操作由 Dataset 相关的类处理,存储结构由大到小是卷(FSVolume)、目录(FSDir)和文件(Block 和元数据等

ReplicaMap
维护 Datanode 上所有数据块副本, 副本包含 5 种状态,Datanode 会将不同状态的副本存储到磁盘的不同目录下, 也就是 Datanode 上副本的状态是会被持久化的。
■ FINALIZED:Datanode 上已经完成写操作的副本。 Datanode 定义了 FinalizedReplica 类来保存 FINALIZED 状态副本的信息。
■ RBW(Replica Being Written):刚刚由 HDFS 客户端创建的副本或者进行追加写(append) 操作的副本, 副本的数据正在被写入, 部分数据对于客户端是可见的。 Datanode 定义了 ReplicaBeingWritten 类来保存 RBW 状态副本的信息。
■ RUR(Relica Under Recovery):进行块恢复时的副本。 Datanode 定义了 ReplicaUnderRecovery 类来保存 RUR 状态副本的信息。
■ RWR(ReplicaWaitingToBeRecovered):如果 Datanode 重启或者宕机, 所有 RBW 状态的副本在 Datanode 重启后都将被加载为 RWR 状态, RWR 会等待块恢复操作。 Datanode 定义了 ReplicaWaitingToBeRecovered 类来保存 RWR 状态副本的信息。
■ TEMPORARY:Datanode 之间复制数据块, 或者进行集群数据块平衡操作(cluster balance) 时, 正在写入副本的状态就是 TEMPORARY 状态。 和 RBW 不同的是, TEMPORARY 状态的副本对于客户端是不可见的, 同时 Datanode 重启时将会直接删除处于 TEMPORARY 状态的副本。 Datanode 定义了 ReplicaInPipeline 类来保存 TEMPORARY 状态副本的信息。
FSVolumeList
FSVolumeList 对所有的 FSVolume 对象进行管理,实际上就是对所有的存储路径进行管理。FSVolumeList 主要为上层(DataNode 进程)提供存储数据块选择一个的存储路径(分区),就是为该数据块创建一个对应的本地磁盘文件,同时也负载统计它的存储空间的状态信息和收集所有的数据块信息。其中一个 FSVolume 对应一个 Storage,内部由 FSDir 对应一个目录;
2.数据读写
2.1 文件删除过程
通过 Hadoop 客户端执行删除命令 rm -skipTrash 过程如下:

注意:避免短时间内删除大文件,降低 NN-RPC 处理性能及 DN 大量数据删除 IO 升高,对集群吞吐造成影响;
2.2 文件写入过程
通过 DFSClient 创建文件写入数据过程如下:

如果写满了一个数据块, 客户端会调用 ClientProtocol.addBlock()方法向 Namenode 申请一个新的数据块。 这个请求到达 Namenode 后会由 FSNamesystem.getAdditionalBlock()方法响应,getAdditionalBlock()方法首先会检查文件系统状态, 然后为新添加的数据块选择存放副本的 Datanode, 最后构造 Block 对象并调用 FSDirWriteFileOp.storeAllocatedBlock()方法将 Block 对象加入文件对应的 INode 对象中。
更新并持久化内存中.最后向 client 返回存储信息;当 Datanode 上写入了一个新的数据块副本或者完成了一次数据块副本复制操作后, 会通过 DatanodeProtocol.blockReport()或 DatanodeProtocol.blockReceivedAndDeleted()方法向 Namenode 汇报该 Datanode 上添加了一个新的数据块副本。
Datanode 视角看 pipeline 写的流程:
每个 datanode 会创建一个线程 DataXceiverServer,接收上游过来的 TCP 连接,对于每个新建的 TCP 连接,都会创建一个叫做 DataXceiver 的线程处理这个连接. 这个线程不断的从 TCP 连接中读 op,然后调用 processOp(op)处理这个 op,这里以 write block 这个 op 为例,由 DataXceiver 的 writeBlock 函数实现:
大体步骤如下:
1. new 一个 BlockReceiver 对象,随后用于接收上游(client 或者 datanode)的 block 数据;
2. 根据传进来的 DatanodeInfo 数组,向数组的第一个元素代表的 datanode 建立 TCP 连接;
2.1. 调用 Sender 的 writeBlock 方法给下游 datanode 发送 write block 相关元信息,包括 DatanodeInfo 数组(刨去第一个元素),clientname,block 的当前 gs,minBytesRcvd,maxBytesRcvd(对于 append,recovery 操作有用)等;
2.2. 读取下游的回复,封装在 BlockOpResponseProto 对象中,可以通过内部成员 firstBadLink 知道建 pipeline 中第一个失败的 datanode 节点。接着将 BlockOpResponseProto 回复给上游(datanode 或者 client)
2.3. 最后调用第一步 new 的 BlockReceiver 的 receiveBlock 方法用于接收一个完整的 block. receiveBlock 内部根据 clientname 发现是一个客户端在写 block,创建一个 PacketResponder 线程用于处理下游 datanode 对 packet 的 ack.PacketResponder 后面分析。
2.4 不断的调用 receivePacket()方法从上游(datanode 或者 client)接收一个个的 packet,接收一个完整的 packet 的逻辑是由内部的 PacketReceiver 来处理的。对于一个接收到的 packet,写入 block file 文件,同时 checksum 信息写 meta 文件,然后放入 PacketResponder 的 ack queue 队列,然后将 packet 写给下游的 datanode。
2.5 最后调用 PacketResponder 的 close 方法,这个方法会等到 ack queue 为空,即所有 packet 都已经从下游收到,并且已经给上游 ack.
3.receiveBlock()结束后,关掉和上下游的连接.
清空 ack queue 的逻辑由专门处理下游 ack 包的 PacketResponder 线程处理,逻辑如下:
1. 如果 datanode 是 pipeline 的中间 node(通过 PacketResponder 的 type 属性来决定,LAST_IN_PIPELINE 和 HAS_DOWNSTREAM_IN_PIPELINE),从下游读一个 PipelineAck 并拿到 seqno,然后从 ack queue 中 get(不删除)第一个 packet,拿出 seqno,记作 expected_seq_no,然后比较是否相等,如果不相等,说明写出错。
2. 如果从 ack queue 中 get 的 packet 是 block 的最后一个 packet,说明一个 block 接收完成.那么调用 finalizeBlock 方法。关闭 block file 和 meta file 文件,调用 FsDatasetImpl 的 finalizeBlock(block)将 block 文件以及对应的 meta 文件移动到对应的 block pool 下的 finalized 目录下,然后生成一个 FinalizedReplica 对象,将 bpid->FinalizedReplica 的映射关系记录在内存中的 volumnMap 中,对象位于 FsDatasetImpl 下的 ReplicaMap volumnMap(从 ReplicaMap 中定位一个 ReplicaInfo,需要拿着 bpid 和 block id 去找)最后调用 datanode 的 closeBlock()方法, ,最后调用 DatanodeProtocolClientSideTranslatorPB bpNamenode 的 blockReceivedAndDeleted()将 block 信息汇报上给 namenode,实现数据块更新.
3. 给从下游接收的 ack 回复给上游。
4.将 packet 从 ack queue 的头部删除。
在 HDFS 中,数据块(block)的写操作依赖于两个关键线程的协同工作,这两个线程分别是 DataXceiver 和 PacketResponder,它们通过明确的职责分工和紧密的协作机制,以确保数据传输的高效性、可靠性和一致性。
DataXceiver 负责从上游节点接收数据流,并高效地写入本地存储介质。它不仅需要处理网络层的通信细节,还需确保数据包(packet)在本地节点的正确存储与完整性校验,从而为后续的数据持久化奠定基础。
PacketResponder 线程则专注于数据传输的可靠性保障。其主要职责是处理来自下游节点的确认消息(ACK),并对整个数据传输链路进行状态同步和一致性校验。通过对接收到的 ACK 消息进行解析和验证,PacketResponder 能够确保每个数据块在下游节点的写入操作已成功完成,并据此向上游节点发送相应的确认信号。这种基于 ACK 的反馈机制构确保了数据传输可靠性。
尚未收到下游节点确认响应且未向上游节点回复确认的数据包会被暂存于一个特殊的队列——ACK queue。该队列充当了数据传输过程中的缓冲区,用于管理数据包的状态流转和重传逻辑。ACK queue 为系统的容错能力提供了重要支撑。
从架构设计层面来看,DataXceiver 和 PacketResponder 的协同工作不仅是分布式存储系统中数据流管理的核心,也是现代分布式系统设计理念的典范,为更广泛的分布式应用场景提供了重要的参考价值。
版权声明: 本文为 InfoQ 作者【童子龙】的原创文章。
原文链接:【http://xie.infoq.cn/article/3303ab7dcb6d0c5e609c8e7b0】。文章转载请联系作者。
评论