如果你只用过 hdfs dfs -put,那么在黑盒视角下,文件进入集群、hdfs fsck 查到三个副本,过程似乎很顺利。但在这条命令背后,客户端和整个集群协同完成了一次"多级流水线写入加逐级确认"的复杂流程:先向 NameNode 申请块,把数据切成 packet,沿着 DataNode 组成的流水线一级级传下去,每一级写完还要反向把 Ack 传回来,最后客户端才敢认为写入成功。而读到这条数据时,又要靠机架感知选出离你最近的副本。
这篇文章会把 HDFS 读写全链路拆开,重点讲清楚三件事:写入时的流水线怎么建立、Ack 怎么逐级回传、机架感知在副本放置和读取选点里到底做了什么。适合刚接触 HDFS 想搞懂原理的开发,也适合被面试题里"HDFS 读写流程"问倒的朋友,以及那些想排查写入慢、副本摆放不合理问题的运维。
1. 先把读写链路的地基打牢:NameNode、DataNode 和客户端各管哪一段
1.1 NameNode 不是文件数据库,它只记"户口"
很多人第一次接触 HDFS 时,会以为 NameNode 像 MySQL 一样存了所有数据。其实 NameNode 只维护文件系统的元数据:路径、权限、文件由哪些 block 组成、每个 block 的副本放在哪些 DataNode 上。真正存储数据的是 DataNode,而且 DataNode 是拿本地磁盘按块存储的,一个 block 对应一个文件,超过一定大小还会分成多个块段。
理解这个分工至关重要。因为后面讲的所有读写流程,本质上都是"客户端找 NameNode 要元数据,再去 DataNode 搬数据"。NameNode 不参与数据搬运,数据搬运是客户端和 DataNode 之间的事。这也解释了为什么 HDFS 适合大文件、流式访问:文件被拆成 128MB 甚至更大的块,读写路径上就能避免每次操作都把所有元数据加载到位,NameNode 内存压力也不会随文件数量线性爆炸。
1.2 DataNode 用"块"为单位存数据,配合心跳上报
DataNode 启动后会向 NameNode 注册并周期发送心跳,同时通过 BlockReport(块报告)把本节点上的所有 block 列表上报给 NameNode。这个设计让 NameNode 永远知道"每个 block 的每个副本在哪"。
但 DataNode 并不是写完数据就立刻上报。实际处理是:DataNode 在写入过程中只负责保存数据和校验和,等一个 block 完整写入且所有副本确认后,由流水线末端的 DataNode 向 NameNode 发起 blockReceivedAndDeleted 调用,通知"这个 block 已经落盘了"。这一步如果做错顺序,比如先上报再刷盘,宕机时就会出现"NameNode 认为有副本、实际磁盘没数据"的严重事故。
1.3 一个高频疑问:客户端写 HDFS 时,有没有"写一半"的中间状态
新手常问:客户端把文件写进 HDFS,如果我写到一半 Ctrl+C 退出,集群里会不会残留半个文件?
答案是会有,但名称节点会通过租约(lease)机制最终回收。客户端打开文件做写入时,会向 NameNode 申请一个租约,持有租约的客户端独占写权限。如果租约超时后客户端仍未续约,NameNode 会强制回收租约,关闭该文件并把未完整写入的 block 标记为不稳定状态,后台线程再把损坏副本清理掉。
这也是为什么 HDFS 写入流程里必须区分"flush"和"close":你不主动 close 文件,数据可能还在客户端缓冲区或 DataNode 的 OS page cache 里,并没有真正落盘。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 写入流程逐拍拆解:create、分包、流水线和 Ack
2.1 create() 阶段就决定了副本往哪摆
客户端调用 FileSystem.create(path) 时,DistributedFileSystem 会通过底层 RPC 向 NameNode 发起 create 操作。NameNode 要在这时做三件事:检查路径是否已存在、检查权限、检查目录配额。通过后,NameNode 会为文件分配一个 block ID,并按机架感知策略预选一组 DataNode(默认三个),作为这个 block 首个 pipeline 的目标列表,返
