HDFS 读写流程与副本放置策略¶
作者:JiangChong | 撰写时间:2026年08月
适用场景:想搞懂 HDFS 一份数据如何变成三份(写流程)、读的时候如何选副本(读流程)、三副本为什么这么放(放置策略),或排查 HDFS 写入失败、副本分布不均、读性能瓶颈时参考本文。零基础读者建议先读 HDFS 架构原理。
阅读前回顾(30 秒)¶
上一篇 HDFS 架构原理 的核心画面是一座仓库:NameNode 是只记台账的管理员,DataNode 是搬运工,Client 是先问路再自己取货的取货人。本文把这个画面往前推一步,讲「取货人和搬运工之间到底怎么交货」:
- 写:一份文件怎么被切成块、像传水接力一样在三个仓库各落一份;
- 读:要读的时候,从三份副本里挑哪一份;
- 放:三份副本为什么是「两个机架 2+1」这个格局。
读完本文,你就明白 HDFS 官方文档里 Pipeline、Ack、Rack Awareness 这些词到底在讲什么了。
关联文章¶
- HDFS 架构原理 — 前置基础:NameNode/DataNode 分工、块与元数据模型
- Hadoop 调优之 HDFS 调优 — 本文涉及参数的既有调优口径(
dfs.replication、块大小等) - Vertica 读取 HDFS 文件的原理分析 — Vertica 按 block 粒度读取 HDFS 的实测分析
- 系列其他文章:HDFS 高可用(QJM、ZKFC、Federation)、HDFS 小文件问题与治理、纠删码 vs 三副本存储策略、HDFS 数据湖存储底座(与 Iceberg 的关系)、Vertica Eon Communal Storage 运维实战
理解全文脉络¶
本文按"数据的一生"组织:先讲写(文件如何切块并逐块随机落点、经 pipeline 落成三份、节点失败如何自愈),再讲读(就近选副本、短路读的加速原理),然后解释副本放置策略每条规则背后的带宽与容错权衡,最后落到 dfs.replication 参数与日常命令。只想查命令的读者可直接跳到第五节;遇到"写入失败 / 副本不足"类问题的读者重点看 1.5 与 5.3;对「为什么是 2+1 格局」好奇的读者直奔第三节。
1 写流程:切块 → 分配 → Pipeline 接力复制¶
1.1 文件先切成 Block,NameNode 只负责"指路"¶
写文件的起点是客户端向 NameNode 发起 create 请求:NameNode 做权限与配额检查、在目录树中登记文件元数据,然后返回一个输出流;真正的数据从头到尾不经过 NameNode。文件被按固定大小切成若干 block(数据块),每个 block 默认 128 MB(dfs.blocksize,Hadoop 2.0 起;1.x 时代为 64 MB,旧文档与部分发行版写作 dfs.block.size,本库 Hadoop 调优之 HDFS 调优 的示例即旧写法)。块是 HDFS 的"最小寻址单元":NameNode 的元数据账本按"block → 副本位置"记明细,副本数固定时,块越小账目条目越多。
用一个具体例子建立直觉:一个 300MB 的文件会被切成 3 个块(128 + 128 + 44MB),最后一个块不满 128MB 也没关系——物理磁盘只占 44MB,不会补齐到 128MB(块大小是逻辑上的分配单位)。这 3 个块各自复制 3 份,集群里最终出现 9 份块文件,散落在不同 DataNode 上(散落的规则见 1.2 节)。
块为什么取 128 MB 这么大?因为每个块都要付出固定的元数据与调度开销(一条记录、一次网络往返),块太小则这些开销占比过高;太大则并行度与重平衡粒度变粗。块大小本质上是"元数据开销"与"并行度"之间的杠杆。
用一个比喻建立整体画面:NameNode 像调度台,只保管"货单"(元数据),DataNode 像仓库,客户端是运输车。写文件 = 先到调度台领"每箱货放哪个仓库"的指令(装满一箱再领下一箱),然后运输车自己在仓库之间跑数据,调度台不参与搬货。数据面与控制面分离,是 HDFS 能支撑大流量而不把 NameNode 压成瓶颈的根本设计。
1.2 块间分布:逐块随机,天然打散¶
「散落在不同 DataNode 上」的规则是什么?这里要分清两层放置,新手最容易混淆:
| 层 | 要回答的问题 | 答案 |
|---|---|---|
| 块间分布(本节) | 同一个文件的多个 block 各落哪些节点? | 逐块独立随机,块与块之间没有任何位置约束 |
| 副本放置(第三节) | 每个 block 的 3 份副本各落哪里? | 机架感知的「2 机架 2+1」格局 |
先看块间分布。NameNode 不会为整个文件预先规划布局:客户端写满一个 block 后,才向 NameNode 申请下一个 block 的目标(addBlock 请求),每个块在各自申请的时刻独立选点。选点规则——客户端跑在 DataNode 上,该块的第一份放本机(本地写,省一次网络);否则随机挑一个节点(Hadoop 3.3.0 起优先客户端同机架,2.x 是全集群随机),同时避开忙节点和剩余空间不足的节点。块与块之间没有「相邻、连续、同节点」的任何约束,所以 300MB 文件的 3 个块,第一份通常落在 3 个不同节点(下图是「客户端不在 DataNode 上」的典型场景):
flowchart LR
F["300 MB 文件"] -->|"写满一块,申请下一块"| NN["NameNode 逐块随机选点"]
NN --> D1["DN-A:block 1"]
NN --> D2["DN-D:block 2"]
NN --> D3["DN-B:block 3(44 MB)"]
两个容易误判的细节:
- 副本数 = 1 且客户端就在 DataNode 上:每个块唯一的一份都优先放本机,整个文件的所有块会全部落在客户端所在的那一台节点,并不会打散——「单副本文件一定均匀散布」是常见误解。3 副本时没有这个问题:第一份在本机,其余两份仍按 2+1 格局撒开。
- 打散只是「大概率均匀」:随机选点没有硬性均匀保证,写久了集群仍会失衡,HDFS 靠 Balancer(均衡器)工具事后把块从拥挤节点搬到空闲节点。
生产默认 3 副本时同理:每块的 3 份按第三节的 2+1 格局摆放,块与块之间依然独立随机,1GB 文件的 24 份块副本散布全集群。读侧因此还有个隐藏收益:多块文件的不同块天然分布在不同节点,读整文件时并发流量自动摊到多个节点(见 2.1)。
1.3 Pipeline:三份副本用"一份流量"写完(传水接力)¶
客户端写每个 block 时,先向 NameNode 申请分配 DataNode(addBlock 请求),NameNode 按放置策略(见第三节)返回一个有序的节点列表,例如 [dn1, dn2, dn3]——这个顺序就是写入管线(pipeline)的顺序。随后客户端把 block 切成更小的数据包(packet,默认 64 KB),逐包发出:
graph LR
C["Client<br/>运输车"] -->|"packet(64KB)"| DN1["dn1<br/>仓库①"]
DN1 -->|"边落盘边转发"| DN2["dn2<br/>仓库②"]
DN2 -->|"边落盘边转发"| DN3["dn3<br/>仓库③"]
DN3 -.->|"ack 签收"| DN2
DN2 -.->|"ack 签收"| DN1
DN1 -.->|"三份全部签收"| C
每个 DataNode 收到 64 KB 的 packet 后一边落盘、一边将这 64 KB 数据转发给下一个节点。这就是 pipeline 写入,像传水接力:第一个人只需把水桶递给旁边的人,桶沿队伍传一遍,三个人就都接到了水。关键收益在于——客户端只需把每份数据发送一次,出站带宽 = 数据量本身,而不是副本数倍。如果改成"客户端并行给三个仓库各发一份",客户端出站带宽立刻变成 3 倍,客户端自己成了瓶颈。
pipeline 用"端到端确认的串行延迟"换来了"带宽节省 + 高写吞吐",这是三副本 HDFS 写路径的核心设计。另一个细节:dn2 到 dn3 的转发发生在 dn2 落盘的同时(边收边转),所以数据到达第三跳的延迟只比第一跳多一跳;若是"dn1 收完再整体复制给 dn2",写入延迟会成倍放大。写三副本的代价不是"写三遍",而是"传一遍、每级接力"——这是理解 HDFS 写性能的关键。
1.4 逐级 Ack:每个 Packet 都要"签收"¶
光发不算成功。每个 packet 沿 pipeline 反向回传确认(ack,acknowledgement「确认回执」的缩写,源自网络协议术语):dn3 落盘成功后回 ack 给 dn2,dn2 确认后回给 dn1,dn1 汇总后回给客户端。客户端只有在收到"从 dn3 一路签收到 dn1"的完整 ack 后,才确认这个 packet 真正写入了三份。
为什么 ack 要逐级回传,而不是 dn3 直接回给客户端? 因为逐级回传让每一级都知道下游是否收到——断点发生在哪一段,谁就负责重传哪一段,客户端只需要盯住离自己最近的 dn1。就像接力赛中每一棒都看着自己递出的接力棒有没有被接稳,而不是队长一个人盯着终点。
实现上,客户端输出流(DFSOutputStream)维护一个待确认队列:packet 发出后留在队列里,收到 ack 才移除;若超时未确认或确认顺序错乱,即判定该 packet 写入失败,进入重传流程。
写成功的定义是"三个节点都落盘了",而不是"客户端发完了"。为什么需要逐级 ack 而不是"发了就算"?因为没有 ack 就无从知道哪个节点丢了数据,节点宕机时也就无法精确地从断点重传——ack 机制是后续容错的前提。
1.5 写失败:坏 DataNode 如何被替换,写入如何继续¶
pipeline 中某个节点写入失败(磁盘故障、节点宕机、网络中断)时,处理分四步:
- 客户端关闭当前 pipeline,并向 NameNode 汇报坏节点;
- NameNode 把坏节点从该 block 的副本位置列表里移除,其上的不完整副本会被废弃;
- 客户端从"剩余健康节点 + NameNode 新分配的一个节点"组成新 pipeline;
- 客户端从未收到 ack 的那个 packet 开始重传——已确认过的数据不用重发。
这就是接力赛中"有人摔倒,换人接棒,从交接失败的那一棒重新跑"——已经跑完的路程不用重跑。写失败不等于文件写失败:只要还能凑齐 pipeline 所需节点,客户端会"换人继续传水"。
这一实时替换行为由两个参数控制:dfs.client.block.write.replace-datanode-on-failure.enable(默认 true)和 dfs.client.block.write.replace-datanode-on-failure.policy(取值 DEFAULT / ALWAYS / NEVER,默认 DEFAULT)。DEFAULT 由 Hadoop 自行判断是否值得替换,其中有个实际生产中最常见的坑:当集群 DataNode 总数恰好等于副本数时(如 3 台机器、副本 3),一台宕机后没有"多余"的好节点可换,替换失败,写入报错:
java.io.IOException: Failed to replace a bad datanode on the existing pipeline due to no more good datanodes being available to try
本库就有真实案例踩中此坑:Vertica Eon 以 HDFS 为公共存储的小集群(3 个 DataNode、副本 3),任意一个 DataNode 故障后写入持续失败,解法是把 replace-datanode-on-failure.policy 设为 NEVER(不尝试换节点,按剩余节点继续写)。详见 Eon报错Failed to replace a bad datanode on the existing pipeline due to no more good datanodes being available to try。DEFAULT 的更精确判定条件随版本演进,以 Apache Hadoop 官方文档为准。
最后要区分两套易混淆的机制:pipeline 替换是写入时的实时处理(客户端侧,保证本次写入成功);后台补副本是 NameNode 侧的异步任务(把 under-replicated 的 block 复制到目标副本数,保证长期容错)。前者负责"当下不失败",后者负责"未来不掉级"。
本节要点:块与块之间逐块随机落点、天然打散(客户端在 DataNode 上时第一份落本机);写三副本 = 传一遍 + 每级接力(pipeline),不是写三遍;每个 packet 逐级签收(ack)才算成功;中间有节点挂了就"换人继续传",从没签收的 packet 重传。记住一句话:数据不经过 NameNode,失败从断点续传。
2 读流程:就近原则与短路读¶
2.1 就近读取:按网络距离选副本¶
读文件的流程与写对称:客户端向 NameNode 询问文件各 block 的位置(getBlockLocations),NameNode 返回每个 block 的副本所在 DataNode 列表,并按与客户端的网络距离由近到远排序;客户端从最近的副本开始读,读取失败则透明地切换到下一个副本(对上层应用无感)。
就像取快递选最近的驿站:同样一个包裹在三个驿站都有,系统永远指给你离你最近的那个;这个驿站临时关门,才去第二近的。
"网络距离"是机架感知(rack awareness)下的定义,距离计算的是两个节点到最近公共祖先(拓扑树上的机架/机房节点)的路径长度之和:
| 拓扑关系 | 距离值 | 含义 |
|---|---|---|
| 同一节点 | 0 | 本机,最理想 |
| 同一机架不同节点 | 2 | 走机架内交换机 |
| 同机房不同机架 | 4 | 走核心交换机 |
| 不同机房 | 6 | 最远,尽量避免 |
读取永远选距离最近的副本,这样同机架命中时,跨机架链路和核心交换机完全不被占用——读流量通常是 HDFS 的主要流量,就近读把跨机架流量压到最低。
两个常被误解的点:
- 单个客户端读一个 block,只从一个副本读(三副本模式没有"分片并行拉同一个块"的能力,条带化并行读是纠删码模式才有的特性,见 纠删码 vs 三副本存储策略)。注意这只限「同一个块」:多块文件的不同块,客户端会并行/预取地同时从多个节点读——300MB 文件(3 块)确实会产生 3 个节点的并发读流量。
- 读并发来自"多个客户端各读各的副本":同一份数据能同时服务多少路读请求,上限就是副本数。副本越多,读热点越不容易成为瓶颈——就像一个包裹存了三个驿站,三个取件人可以去三个驿站同时取,而不是在一个窗口排队。
Vertica 侧有现成的印证:Vertica 通过 WebHDFS(HDFS 的 HTTP REST 接口)读 HDFS 文件时同样按 block 粒度逐个读取,每读一个 block 向 NameNode 请求一次位置信息,访问 NameNode 的次数与 block 数成正比(1.5 GB 文件、128 MB 块 ≈ 12 次),详见 Vertica 读取 HDFS 文件的原理分析。这解释了为什么块越大,大文件加载对 NameNode 的请求压力越小。
2.2 Short-Circuit Local Read:钥匙直接交给客户端¶
就近读的一个特例:当计算进程(MapReduce Task、Spark Executor)恰好运行在持有该副本的 DataNode 同一台机器上时,普通读路径仍然要走"客户端 → DataNode 的 TCP 连接 → DataNode 读盘 → 网络回传",把"数据就在本机"的优势完全浪费了。短路读(short-circuit local read)正是为这个场景设计的。
原理:DataNode 通过 Unix domain socket(本地进程间通信通道)把 block 文件的文件句柄直接交给同机客户端进程,客户端绕开 TCP 栈和 DataNode 进程,直接从本地磁盘或 OS 页缓存读取。比喻:仓库管理员把库房钥匙直接交给你,你自己进库房拿货,不再需要管理员搬进搬出。收益是省掉一次网络传输和 DataNode 进程内的数据搬运,且能命中页缓存。
短路读的前提(缺一不可,以 Apache Hadoop 官方文档为准):
| 前提 | 说明 |
|---|---|
| 客户端与 DataNode 同节点 | 异机客户端永远无法短路,这是硬约束 |
dfs.client.read.shortcircuit = true |
客户端侧开启,默认 false |
配置 dfs.domain.socket.path |
如 /var/lib/hadoop-hdfs/dn_socket,DataNode 创建该 socket,客户端进程须有访问权限 |
| 客户端具备 Hadoop 原生库 | 底层依赖 JNI(Java Native Interface,Java 本地接口,Hadoop 中称 NativeIO),缺少原生库时短路读不生效 |
| 校验和校验默认仍执行 | dfs.client.read.shortcircuit.skip.checksum 默认 false,数据完整性不因短路而放弃 |
实操含义:短路读依赖任务调度亲和性(YARN——Hadoop 的资源调度器——的 node-local 调度让计算任务落在持有副本的节点上)。如果计算与存储分离部署(客户端全部在远端),短路读永远不会触发,配置了也白配——排查"为什么本地读还是慢"时先确认这一点,而不是盲目调大各种缓冲参数。
本节要点:读永远选离客户端最近的副本(同机 0 < 同机架 2 < 跨机架 4 < 跨机房 6);单客户端单块只读一个副本,并发靠多客户端×多副本;本机读本机数据走短路读,绕过网络与 DataNode 进程。
3 副本放置策略:为什么是"同机架 2 副本 + 跨机架 1 副本"¶
默认副本数为 3(见第四节),NameNode 给每个 block 分配三个副本时遵循固定规则(HDFS Architecture 文档的 Block Placement Policy):
| 副本 | 放置位置 | 为什么这么放 |
|---|---|---|
| 第 1 副本 | 客户端所在节点;若客户端不在 DataNode 上,3.3.0 起放客户端同机架的节点(2.x 是随机选节点) | 写路径最短、不占跨机架带宽;计算进程写完数据后大概率要本地读它(数据本地性) |
| 第 2 副本 | 不同机架的某个随机节点 | 机架级容错:一个机架整体故障(断电、交换机挂掉)时,仍有其他机架的副本可用 |
| 第 3 副本 | 与第 2 副本同机架、但不同节点的另一台机器 | 第 2、3 副本共享机架,pipeline 跨机架的流量只有 1/3(dn2→dn3 同机架,不走跨机架链路);两副本分属不同节点,单节点故障仍可容忍 |
画成图就是「两栋楼放三份」:
graph LR
subgraph rack1["机架 1(客户端所在机架)"]
C["Client"] --> N1["副本①"]
end
subgraph rack2["机架 2(另一个故障域)"]
N2["副本②"]
N3["副本③"]
end
N1 -->|"pipeline 唯一一次跨机架"| N2
N2 -->|"同机架,不走核心交换机"| N3
三条规则合起来的最终格局是 2 + 1:一个机架放 2 份、另一个机架放 1 份。逐一解释背后的权衡:
- 为什么不是"三个机架各放一份"? 那样写数据时 dn2→dn3 也要跨机架,跨机架流量从 1/3 变成 2/3,日常写带宽成本翻倍。机架整体故障本就是小概率事件,2+1 布局等于用"单机架故障时丢 2 份副本"的极端风险,换取每一天的跨机架流量减半。
- 为什么第 3 副本不与第 1 副本同机架? 那样写路径要跨机架两次(dn1→dn2、dn2→dn3),跨机架流量同样变成 2/3。
- 为什么第 2、3 副本要分属不同节点? 单节点故障(最常见的故障形态)必须做到 100% 无损,若第 2、3 副本同节点,一个节点挂掉就丢两份。
- 读侧同样受益:第 2、3 副本所在机架的读者有 2/3 概率就近命中,跨机架读取成为兜底;对称的代价是第 1 副本所在机架的读者只有 1/3 概率就近命中。
版本差异(3.3.0,HDFS-14333):上面第 1 副本的「同机架优先」规则是 Hadoop 3.3.0 起的新行为,2.x 时代客户端不在 DataNode 上时是随机选节点。这个改动把「集群外客户端写入」的跨机架流量从约 2/3 降到约 1/3(与 HDFS 架构原理 中机架感知一节的表述一致)。客户端就在集群节点上写入时,新旧策略的 pipeline 跨机架流量都是 1/3。
这个策略的本质是显式的风险定价:对"单节点故障"要求零损失,对"机架故障"只承诺"至少剩 1 份可读",用后者换前者在日常写入上的带宽收益。就像把重要文件放两栋楼:一栋楼里的柜子坏了(单节点)无所谓,一栋楼整个失火(机架故障)时另一栋还有一份兜底——而"三栋楼各放一份"虽然更保险,日常搬运的成本却翻倍。
两个重要的前提条件:
- 机架感知必须真实配置。集群通过
topology.script.file.name(core-site.xml,映射节点 IP → 机架)获得拓扑。未配置拓扑脚本时,所有节点都被视为同一个机架(/default-rack),三副本只能"跨节点"而不能"跨机架",机架级容错形同虚设——这是生产环境最常见的放置策略失效原因。 - 机架数不足 2 的极端集群(如单机架测试环境),Hadoop 提供可选的
BlockPlacementPolicyRackFaultTolerant策略允许同节点多副本,属高级配置,以官方文档为准。
本节要点:2+1 格局 = 用「机架故障时只保 1 份」的极端风险,换「每次写入只有 1/3 流量跨机架」的实惠;前提是机架感知配置真实,否则一切容错假设作废。
4 dfs.replication:副本数的取舍与调整¶
dfs.replication 是新建文件时的默认副本数,默认 3。它是一个"创建时生效"的参数:hdfs-site.xml 中的配置是集群默认值,客户端可用 -D dfs.replication=N 覆盖,每个文件创建时的副本数会作为文件元数据持久化——所以同一目录下不同文件可以有不同副本数。
客户端配置与集群默认冲突时,以客户端为准。 这个参数本质是客户端侧参数:文件创建时,由客户端把「期望副本数」发给 NameNode 写入文件元数据,NameNode 只照单登记。优先级从高到低:-D 命令行参数 > 客户端机器上的 hdfs-site.xml > Hadoop 内置默认 3。NameNode 侧的同名配置管的是 NameNode 自身,不会反过来覆盖客户端请求——唯一例外是边界夹取:超出 dfs.replication.min~max(默认 1~512)的值会被夹到边界内。这解释了常见的运维困惑「改了集群配置怎么不生效」:只改了 NameNode 侧并重启,但客户端进程用的仍是自己机器上的旧配置;要全局改默认副本数,两侧都要更新。
副本数是一个显式的多维取舍:
| 维度 | 副本多(如 5) | 副本少(如 2) |
|---|---|---|
| 磁盘占用 | N 倍(5 倍),翻倍立竿见影 | 省一半磁盘 |
| 读并发 | 同一数据可同时服务更多读请求 | 读热点容易成为瓶颈 |
| 容错 | 可同时容忍 N−1 个节点宕机 | 只能容忍 1 个 |
| 写成本 | pipeline 更长、确认链更长 | 更短 |
| 后台复制/均衡代价 | 更大 | 更轻 |
3 是默认平衡点:以 3 倍磁盘占用,换"任意单点故障不丢数据 + 3 路读并发"。对读热点场景,临时提高副本数是立竿见影的手段——本库 Hadoop 调优之 HDFS 调优 的既有口径正是如此:多个任务同时读一个文件时增大副本数可缓解读瓶颈,代价是磁盘占用增加;只修改客户端配置,就能让之后上传的文件按新副本数生效,不动集群全局。
调整副本数的三种方式:
| 方式 | 命令/配置 | 生效范围 |
|---|---|---|
| 上传时指定 | hdfs dfs -D dfs.replication=2 -put ... |
仅本次上传的文件 |
| 全局默认 | 修改客户端与集群共同引用的 hdfs-site.xml,并重启客户端进程 | 之后新建的文件(已有文件不变) |
| 事后修改 | hdfs dfs -setrep -R N /path |
指定文件/目录(异步生效) |
副本不足的修复是自动的:NameNode 的后台复制线程周期扫描,按"缺的越多优先级越高"的顺序把 under-replicated block 补齐到目标副本数。因此看到 fsck 报告 under-replicated 不必恐慌,多数时候等后台复制完成即可;但如果长期存在(例如有节点持续离线),说明容错窗口被拉长,随时可能面临"最后一个副本也丢失"的风险。
补充一条路线:Hadoop 3.x 的纠删码(Erasure Coding)是"三副本"的替代方案——以 1.33~1.5 倍存储开销达到近似容错级别,比 3 副本省一半以上磁盘,代价是读延迟与部分 API 兼容性,详见 纠删码 vs 三副本存储策略。
5 hdfs dfs 命令实操¶
以下命令在配置了 Hadoop 客户端的机器上执行(hdfs dfs 即新版写法,老版本为 hadoop fs)。
5.1 上传(put)¶
# 按客户端配置(或集群默认)的副本数上传
hdfs dfs -put /data/local.csv /user/ods/
# 本次上传强制 2 副本(适合临时、可重建的数据)
hdfs dfs -D dfs.replication=2 -put /data/local.csv /user/ods/
第一行把本地文件拷入 HDFS,块大小与副本数按客户端生效的配置决定。第二行演示了按文件控制副本数的标准做法:-D 参数必须放在子命令之前,只对本次命令生效。命令执行期间,客户端在后台完成第一节描述的 pipeline 写入——命令返回即表示所有块都已收到完整 ack(三份都落盘了),这一点常用于脚本化的"写入成功"判定。
5.2 下载(get)¶
hdfs dfs -get /user/ods/local.csv /data/backup/local.csv
# 等价别名:hdfs dfs -copyToLocal /user/ods/local.csv /data/backup/
下载时客户端对每个 block 向 NameNode 要位置,按就近原则选最近的副本读(见 2.1)。注意:文件即使有 3 个副本,-get 也只从一个副本读取,不会做"三副本合并"——副本只是容错与并发的冗余,不是数据来源的叠加。
5.3 查看副本分布¶
# ls 输出的第二列就是每个文件的副本数
hdfs dfs -ls /user/ods/
# 细看单个文件的副本数(%r)、块大小(%b)、块数(%o)
hdfs dfs -stat '%r 副本 / %b 块大小 / %o 块数' /user/ods/local.csv
# 查看每个 block 的副本具体落在哪些节点
hdfs fsck /user/ods/local.csv -files -blocks -locations
-ls 第二列的数值即该文件的 replication factor——只靠 -ls 就能发现"这个文件只有 1 个副本"之类的异常,这是最便宜的巡检手段。fsck(file system check,文件系统检查)是排查副本问题的主工具:-files -blocks -locations 会逐块列出副本所在节点(示意输出):
当某个 block 的存活副本数少于目标数时,fsck 逐块标注 Under replicated,并在文末汇总 Total blocks 与 Under-replicated blocks 两个计数;对全集群做体检用 hdfs fsck / -files -blocks。
5.4 修改副本数¶
# 把整个目录的副本数改为 3(-R 递归生效)
hdfs dfs -setrep -R 3 /user/ods/
# 阻塞等待补副本完成(-w 等待达到目标副本数)
hdfs dfs -setrep -w 3 /user/ods/local.csv
-setrep 是异步命令:它只把"期望副本数"写进文件元数据,实际复制由 NameNode 后台线程完成,-w 参数可阻塞等待复制结束。副本数修改不影响已写好的数据内容,只影响后续的复制与容错水平——在副本不足期间(复制进行中),该文件的容错能力是打折扣的,对关键文件建议用 -w 确认补完。
命令速记表:
| 目的 | 命令 | 一句话提示 |
|---|---|---|
| 上传 | hdfs dfs -put <本地> <HDFS路径> |
返回即三份落盘 |
| 下载 | hdfs dfs -get <HDFS路径> <本地> |
只从最近副本读 |
| 看副本数 | hdfs dfs -ls <路径> |
第二列 |
| 查副本健康 | hdfs fsck <路径> -files -blocks -locations |
看 Under replicated 计数 |
| 改副本数 | hdfs dfs -setrep [-w] -R N <路径> |
异步,-w 等补完 |
6 最佳实践清单¶
- 集群上线前先配置机架拓扑脚本(
topology.script.file.name)→ 为什么:不配置则所有节点视为同一机架,三副本的机架级容错与就近读全部失效,这是放置策略发挥作用的先决条件。 - DataNode 数恰好等于副本数的小集群,把
replace-datanode-on-failure.policy设为 NEVER → 为什么:DEFAULT 策略在无多余节点可换时写操作必然失败(本库真实案例:Eon报错Failed to replace a bad datanode on the existing pipeline due to no more good datanodes being available to try),NEVER 让写入按剩余节点继续。 - 读热点文件按需
setrep提高副本,热点过去后降回 → 为什么:读并发上限 = 副本数,热点期临时 +1~2 副本立竿见影;长期保留则白占磁盘,成本完全可预期地失控。 - 全局
dfs.replication保持 3 不动,按文件用-D或setrep调节 → 为什么:全局调高会让之后所有新文件都背上成倍的磁盘成本,影响面不可控;按文件调节是外科手术式操作。 - 定期
hdfs fsck / -files -blocks检查 Under-replicated blocks 计数 → 为什么:该计数长期不为 0 说明存在离线节点且容错窗口被拉长,随时可能从"少一份"恶化成"丢最后一份"。 - 计算与存储混合部署时,确认短路读四个前提全部满足(socket 路径、权限、
dfs.client.read.shortcircuit、原生库)→ 为什么:前提不满足时短路读静默不生效,排查"本地读仍慢"时先验证前提,而不是盲目调缓冲参数。 - 批量写入小文件前先评估块大小与文件合并 → 为什么:每个文件都是一条独立元数据记录,小文件让 NameNode 账本膨胀且读写无法并行,治理手段见 HDFS 小文件问题与治理。
- 不要手动向 DataNode 数据目录复制 block 文件来"补副本" → 为什么:副本由 NameNode 统一调度记账,手动拷贝不会进入副本账本,反而可能在块校验中制造坏块;正确的做法是等后台补副本或先排查节点离线原因。
- 磁盘占用敏感的大数据量目录评估纠删码(Hadoop 3.x) → 为什么:三副本 3 倍磁盘,EC 约 1.33~1.5 倍即可达到相近容错级别,权衡细节见 纠删码 vs 三副本存储策略。
- 写入报错时先区分"pipeline 替换失败"与"副本不足"两类信号 → 为什么:前者是写入实时的客户端侧问题(看报错文本),后者是 NameNode 后台的复制缺口(看 fsck 计数),混淆两者会误判故障等级、选错处置手段。
附录:新手常见疑问(FAQ)¶
Q1:hdfs dfs -put 命令返回了,是不是三个副本都写好了?
是的。命令返回意味着所有 packet 都收到了完整 ack——三份都落盘了(5.1 节)。如果中途有 DataNode 失败且无法替换,命令会报错而不是假装成功。
Q2:客户端写到一半宕机了,数据会怎样? 文件会停留在未完成状态,由 NameNode 的租约(lease)机制收尾:租约超时后触发恢复,最后一个 block 被截断到最后一次确认的位置,文件保留但末尾可能不完整。应用侧通常把这种情况当作「写入异常」整体重写(具体行为以 Apache Hadoop 官方文档为准)。
Q3:副本数可以超过 3 吗?有什么代价?
可以(-D dfs.replication=5 -put 或 setrep)。收益是读并发提升、容错增强;代价是磁盘占用 N 倍、pipeline 更长、确认链更长(第四节表格)。默认 3 是行业公认的平衡点。
Q4:setrep 执行后副本立刻变吗?
不会。它是异步命令,只改「期望副本数」元数据,实际复制由 NameNode 后台线程慢慢补。关键文件用 -w 阻塞等待补完,否则补副本期间容错能力是打折扣的。
Q5:三个副本都落在同一个机架会怎样? 机架级容错失效:该机架断电/交换机故障时三份副本全部不可读。这通常意味着机架感知没配置或配置错误(所有节点都在 /default-rack),见第三节的前提条件。
Q6:读一个 300MB 的文件(3 个块:128 + 128 + 44MB),客户端会并行从多个节点拉数据吗? 分两层看。块级:会——3 个块各自从「离客户端最近的副本」读,客户端会同时向多个 DataNode 发起读请求(至少是流水线预取:读当前块的同时预取下一块),所以一个 3 块的文件确实会带来 3 个节点的并发读流量。副本级:不会——同一个块只从一个副本读,不会同时拉它的 3 份副本(2.1 节)。副本的价值是容错与多客户端并发,不是单客户端的加速;把同一块的 3 份副本当成 3 条并行通道是常见误解。
Q7:单副本(dfs.replication=1)上传大文件,块会均匀撒遍集群吗?
不一定。客户端本身跑在 DataNode 上时,每块唯一的一份都优先放本机——整个文件集中在一台节点;客户端不在集群内时(3.3.0 起优先客户端同机架,2.x 全集群随机),块才随机打散。而且打散只是「大概率均匀」,没有硬保证,写久了要靠 Balancer(均衡器)工具事后均衡(见 1.2 节)。