HDFS 读写流程:客户端、NameNode、DataNode 之间怎么配合
HDFS 的读写流程,本质上是三个角色配合完成一件事:
- 客户端:我要读/写数据
- NameNode:告诉我”数据在哪”或”数据该存哪”
- DataNode:真正干活的——存数据或给数据
整个流程的核心逻辑是:NameNode 管”位置”,DataNode 管”数据”,客户端不经过 NameNode 直接跟 DataNode 传数据。
写入流程:数据怎么进 HDFS
写入的核心机制叫 流水线复制(Pipeline Replication)——客户端把数据发给第一个 DataNode,第一个转发给第二个,第二个转发给第三个,像流水线一样。
写入流程时序图
sequenceDiagram
participant Client
participant NN[NameNode]
participant DN1[DataNode 1]
participant DN2[DataNode 2]
participant DN3[DataNode 3]
Client->>NN: 创建文件请求(/user/data.txt)
NN->>Client: 确认创建,返回文件句柄
Client->>NN: 请求分配第一个数据块
NN->>Client: 分配块 ID(blk_123),返回 DataNode 列表(DN1→DN2→DN3)
Client->>DN1: 建立写入流水线(blk_123)
DN1->>DN2: 建立连接
DN2->>DN3: 建立连接
Client->>DN1: 流式写入数据(64KB 数据包)
DN1->>DN2: 转发数据
DN2->>DN3: 转发数据
DN3->>DN2: 确认接收
DN2->>DN1: 确认接收
DN1->>Client: 确认接收
loop 直至所有数据写入
Client->>DN1: 发送下一个数据包
DN1->>DN2: 转发
DN2->>DN3: 转发
DN3->>DN2: 确认
DN2->>DN1: 确认
DN1->>Client: 确认
end
Client->>NN: 关闭文件请求
NN->>Client: 确认关闭,持久化元数据
分步拆解:
第一步:客户端问 NameNode——“我要写文件”
客户端发送创建文件请求,NameNode 检查权限和路径合法性,在元数据中创建文件节点。此时不分配块,只是占个位置。
第二步:客户端申请块——NameNode 分配位置
客户端开始写数据时,向 NameNode 申请一个新的数据块。NameNode 按机架感知策略选出一组 DataNode(默认 3 个),返回给客户端。
第三步:建立流水线——客户端连 DN1,DN1 连 DN2,DN2 连 DN3
客户端收到 DataNode 列表后,依次建立连接形成一条链。这保证了数据按固定顺序流动。
第四步:数据流式写入——客户端 → DN1 → DN2 → DN3
数据以 64KB 数据包的形式从客户端流向 DN1,DN1 边收边转给 DN2,DN2 边收边转给 DN3。每个数据包发完后,确认信息从 DN3 反向传回客户端。
第五步:关闭文件——NameNode 记下块的位置
客户端写完所有数据后,通知 NameNode 关闭文件。NameNode 把块的 ID 和位置信息持久化到 Edits。
关键设计点:
- 数据流和确认流分开:数据正向流,确认反向流,提高吞吐
- NameNode 不参与数据传输:只负责分配位置,避免成为瓶颈
- 管道中任何节点失败:客户端会关闭当前管道,用剩下的节点重建管道继续写
代码示例:HDFS 文件写入
以下是使用 Java API 写入 HDFS 文件的示例代码:
1 | import org.apache.hadoop.conf.Configuration; |
读取流程:数据怎么从 HDFS 出来
读取的核心原则是 就近读取(Data Locality)——哪个 DataNode 离客户端最近就读哪个。
读取流程时序图
sequenceDiagram
participant Client
participant NN[NameNode]
participant DN1[DataNode 1]
participant DN2[DataNode 2]
participant DN3[DataNode 3]
Client->>NN: 请求文件元数据(/user/data.txt)
NN->>Client: 返回文件块信息(blk_123→DN1, blk_124→DN2)
Client->>DN1: 读取块 blk_123
DN1->>Client: 返回数据
Client->>DN2: 读取块 blk_124
DN2->>Client: 返回数据
loop 直至所有块读取完成
Client->>DataNode: 请求下一个块
DataNode->>Client: 返回数据
end
Client->>NN: 关闭文件(可选)
分步拆解:
第一步:客户端问 NameNode——“我要读文件”
客户端发送读取请求,NameNode 返回文件的块列表,每个块包含块 ID 和存储该块的 DataNode 地址列表。
第二步:客户端选 DataNode——“挑最近的那个”
客户端拿到块位置列表后,按”就近原则”选择 DataNode:
- 同一节点优先(如果是本地任务)
- 同一机架次之
- 不同机架最后
第三步:客户端直连 DataNode 读数据
客户端选好 DataNode 后,直接建立连接读取数据,不走 NameNode。读取时自动验证校验和,确保数据完整。
第四步:多块并行读取
如果文件有多个块,客户端可以同时连接多个 DataNode 并行读取,提升吞吐。
关键设计点:
- NameNode 只给位置,不传数据:数据流不经过 NameNode
- 就近读取:减少网络传输,MapReduce 任务跑在数据所在的节点上
- 读取失败自动切换副本:一个 DataNode 读不了,自动换另一个副本
代码示例:HDFS 文件读取
以下是使用 Java API 读取 HDFS 文件的示例代码:
1 | import org.apache.hadoop.conf.Configuration; |
读写的共性原则
1. NameNode 不传数据,只传”在哪”
读写过程中,NameNode 只负责返回块的位置信息,实际数据传输都在客户端和 DataNode 之间直接进行。这是 HDFS 高吞吐的核心。
2. 机架感知决定副本分布和读取优先级
- 写入时:第一个副本放本地机架,第二、三个放不同机架
- 读取时:优先读同机架的数据,减少跨机架网络开销
3. 校验和保护数据完整性
- 写入时:客户端计算校验和,存为
.meta文件 - 读取时:客户端验证校验和,不一致则换副本
几个常见问题
1. 写文件时,管道中的一个 DataNode 挂了怎么办?
客户端收不到确认,关闭当前管道,用剩下的节点重建管道继续写。NameNode 之后会补副本。
2. 读文件时,某个 DataNode 读不了怎么办?
客户端自动换到其他副本节点重试(最多 3 次)。如果所有副本都读不了,报错。
3. 写入过程中客户端挂了怎么办?
NameNode 不会收到关闭文件请求,文件处于”未完成”状态。NameNode 有后台线程定期清理未完成的文件(根据 fs.trash.interval 或 dfs.namenode.replication.work.multiplier.per.iteration 等配置)。
4. 为什么 NameNode 不参与数据读写?
让 NameNode 只管元数据,数据读写走 DataNode。如果 NameNode 也走数据,读写的网络压力会压垮它。