HDFS 纠删码实战:RS 编码、Striping 与落地选型
HDFS 纠删码实战:RS 编码、Striping 与落地选型
一、引言:3 副本是冷数据的成本黑洞
我做这个集群的第一次存储盘点时,数据量已经到了 80 PB 量级。其中真正每天被读的”热”数据大概只有 18%,剩下都是数仓历史分区、原始日志归档、模型训练快照这类”写一次、偶尔查”的冷数据。问题是,HDFS 默认 3 副本,意味着这 80 PB 里接近 65 PB 的冷数据,每份都白白存了 2 份冗余——也就是约 43 PB 的纯浪费。
3 副本的冗余率是 200%(实际占用 = 逻辑大小 × 3)。它换来的好处是吞吐和容错,但对冷数据而言,这两个好处都用不上:冷数据几乎不读,吞吐不重要;冷数据变更慢,副本修复的紧迫性也低。这笔账算下来非常亏。
纠删码(Erasure Coding,EC)就是为这种场景准备的武器。Hadoop 3.0 起正式支持 EC(HDFS-EC,HDFS-7285),它用数学编码(而非整盘复制)来提供容错,典型策略下存储开销能从 3x 砍到 1.5x,等于把冷数据的存储成本直接腰斩。这篇复盘就讲我们是怎么把 RS-6-3 落地到冷数据目录的,以及它带来的、文档里没明说的那些代价。
区别于本博客的《Hadoop 3.x 新特性》那篇只做了概念性介绍,这篇专攻 EC 的落地:编码原理、两种布局的差异、基准数据、踩坑。
二、纠删码原理:从”复制”到”算”
2.1 Reed-Solomon RS(k, m)
HDFS 默认用的是 Reed-Solomon 码(RS 码),记作 RS(k, m):
- 把原始数据切成 k 个等长的数据单元(data cell);
- 通过有限域 GF(2^8) 上的矩阵运算,算出 m 个校验单元(parity cell);
- 这 k+m 个单元打散到不同 DataNode 上;
- 只要这 k+m 个单元里任意存活 k 个,就能完整还原原始数据——也就是可容忍最多 m 个单元丢失。
这个”任意 k 个即可还原”的特性,是 EC 比副本省的根源。副本靠”整份多存几份”来抗丢失,EC 靠”算出来的冗余信息”来抗丢失,冗余信息只占 m/k 的额外开销。
HDFS 内置的三个策略:
| 策略 | k : m | 存储开销 | 可容忍丢失 | 典型用途 |
|---|---|---|---|---|
RS-6-3 |
6 : 3 | (6+3)/6 = 1.5x | 3 个单元 | 默认,通用冷数据 |
RS-3-2 |
3 : 2 | (3+2)/3 = 1.67x | 2 个单元 | 中小文件较多的冷目录 |
RS-10-4 |
10 : 4 | (10+4)/10 = 1.4x | 4 个单元 | 超大文件、对容错要求高 |
对比 3 副本的 3.0x,RS-6-3 直接省了 50% 的物理存储。
2.2 编码与解码的计算代价
天下没有免费的存储。EC 把”复制磁盘开销”换成了”编码 CPU 开销”:
- 编码(写路径):写入 k 个数据单元时,NN 侧协调、DN 侧用 Intel ISA-L(默认 Native 库,HDFS-11932 起)跑矩阵乘法生成 m 个校验单元。RS-6-3 的编码吞吐在 ~500 MB/s/核量级(ISA-L 开启时),不开 ISA-L 会掉到几十 MB/s。
- 解码(重建路径):当某个单元丢失需要重建时,需要读 k 个存活单元,做一次逆矩阵运算。计算量比编码略高,且要拉起网络传输。
理解这一点很重要:EC 的”省”是用 CPU 换的。冷数据写入量小、读频次低,CPU 换得动;热数据频繁读写,CPU 换不起。
三、两种布局:Contiguous vs Striping
EC 落地最大的认知门槛,是搞清楚两种 cell 布局。这是 HDFS-EC 设计的核心,也是后面所有性能差异的根因。
3.1 Contiguous(连续布局)
文件被切成一个一个的 block(默认 64 MB,可配),每个 block 内部按 cell 大小(默认 1 MB)切成 k 个数据 cell,加上 m 个校验 cell,这 k+m 个 cell 分布在 k+m 个 DN 上。下一个 block 重新开始一轮编码。
1 | 文件 → block0[ d0 d1 d2 d3 d4 d5 | p0 p1 p2 ] block1[ d0 d1 d2 d3 d4 d5 | p0 p1 p2 ] ... |
特点:编码在一个 block 内部完成,顺序读友好,append 友好(虽然 EC 文件整体不完整支持 append,但 contiguous 在语义上更接近)。问题是对小文件不友好——一个 5 MB 的小文件在 RS-6-3 下也要凑齐 6 个数据 cell 才能编码,凑不满就要补零,浪费算力和空间。
3.2 Striping(条带化布局,HDFS 默认)
HDFS 默认用的是 Striping。它不再以 block 为编码单位,而是把整个文件按 stripe 横向条带化:第一个 stripe 取 k 个数据 cell(每 cell 1 MB),算出 m 个校验 cell,分布在 k+m 个 DN 上;然后第二个 stripe,第三个……
1 | stripe 0: DN0 DN1 DN2 DN3 DN4 DN5 | DN6 DN7 DN8 |
关键好处:一个文件只需凑满一个 cell(1 MB)就能开始编码,小文件也能享受 EC。这就是为什么 HDFS 把 Striping 设为默认——大多数冷数据目录里小文件占比不低。
代价是:
- 顺序读放大:读一个 stripe 的 k 个数据 cell 要并发从 k 个 DN 拉,客户端要做 striping 重组;
- 随机读放大更严重:读文件中间某 1 字节,理论上要拉一整个 cell(1 MB),但相比 contiguous 的 block(64 MB)放大已经小很多;
- append 不支持:striping 布局下文件写完即定型,无法追加(这是 EC 文件的硬限制,下一节踩坑会讲)。
3.3 两者对比
| 维度 | Contiguous | Striping(默认) |
|---|---|---|
| 编码单位 | 单个 block | 跨 block 的 stripe |
| 小文件友好 | 差(凑不够 k 个 cell) | 好(1 cell 起编) |
| 顺序读 | 单 DN 流式,放大小 | 多 DN 并发重组 |
| 随机读 | 放大到 block 级(64 MB) | 放大到 cell 级(1 MB) |
| Append | 部分支持 | 不支持 |
| HDFS 默认 | 否 | 是 |
我们落地时,冷数据目录里有大量 GB 级的 Parquet 文件,也有不少几十 MB 的小日志归档,所以整体选 Striping(即默认的 RS-6-3),不折腾 Contiguous。
四、容错与重建
EC 的容错逻辑和副本不同:副本丢一个就少一个副本,EC 丢一个 cell 不影响数据完整性,丢到第 m+1 个才真正丢数据。
当 NN 通过心跳发现某个 DN 上的 EC cell 不可用后:
- NN 判断该 cell 所属的 EC group 是否还有 k 个存活单元;
- 若存活单元数 ≥ k 且总单元数 < k+m(即冗余度下降),NN 调度一个 reconstruction(重建)任务到某个 DN;
- 该 DN 拉取其余存活单元(可能跨机架),用逆矩阵运算解码出丢失的 cell,写到本地或新 DN。
重建的代价明显高于副本复制:
- 副本修复 = 直接复制一份,纯 IO;
- EC 重建 = 读 k 个单元 + 一次矩阵运算 + 写 1 个单元,网络拉取量是副本的 k 倍。
所以 EC 集群对 DN 故障的”自愈速度”比副本集群慢,必须监控重建积压。我们有一次一个机架抖动导致几十万个 cell 进入重建队列,重建跑了 6 个多小时才追平,期间该 EC group 的冗余度一直是降级的。
五、EC Policy 管理
EC 在 HDFS 里是目录级(dir-level)策略,和副本是互斥关系——同一个目录要么 3 副本要么 EC,不能混。
5.1 内置策略查看
1 | hdfs ec -listPolicies |
输出会列出 RS-6-3-1024k、RS-3-2-1024k、RS-10-4-1024k 等策略(1024k 是 cell 大小),状态分 ENABLED / DISABLED。
5.2 给目录设置策略
1 | # 给冷数据目录启用 RS-6-3 |
注意几点:
-setPolicy只对设置之后新写入的文件生效,已有文件不会自动转换;- 已有文件要转,得用
hdfs mover(数据迁移工具)主动搬,或在目录上反复 mover; - 要取消策略用
-unsetPolicy,但同样不转换已有 EC 文件; - 目录下写新文件时,客户端会感知策略,写入直接按 EC 编码,不需要应用层改动。
5.3 查询策略
1 | hdfs ec -getPolicy -path /data/cold/archive/some_file.parquet |
返回该文件实际采用的 EC 策略(或返回 “Replicated” 表示还是副本)。
5.4 自定义策略
如果内置三个不够用(比如想要 RS-7-2),可以用 schema XML 注册:
1 | hdfs ec -addPolicy -policyFile my-ec-policy.xml |
不过自定义要谨慎:k+m 越大,需要的 DN 数 / 机架数越多,放置算法的约束越紧。RS-10-4 要求集群至少有 14 个 DN 且最好跨足够多机架,否则放不下。
六、为冷数据目录启用 RS-6-3 的命令序列
下面是我们实际给一个冷数据目录上 EC 的完整序列(已脱敏):
1 | # 1. 确认集群 DN 数与机架数满足 RS-6-3 的最低要求(至少 9 个 DN,理想跨 3+ 机架) |
关键经验:第 3 步的灰度一定不能省。我们一开始图快直接在主目录 setPolicy,结果当天就有几个 Spark 作业 split 性能劣化报警(后面踩坑会讲)。后来改成先在一个测试子目录验证一周,再灰度放量。
七、3 副本 vs RS-6-3 对比
| 维度 | 3 副本 | RS-6-3 (Striping) |
|---|---|---|
| 存储开销 | 3.0x | 1.5x(省 50%) |
| 容错能力 | 丢 2 个副本仍可用 | 丢任意 3 个 cell 仍可用 |
| 写入开销 | 纯复制,CPU 几乎无负担 | 编码 CPU 开销,依赖 ISA-L |
| 顺序读吞吐 | 高(本地副本优先) | 高(多 DN 并发) |
| 随机读 | 直接定位,放大 1x | 放大到 cell(1 MB) |
| 写延迟 | 低 | 略高(编码 + 多 DN 写) |
| Append | 支持 | 不支持 |
| 随机写 | 支持(hflush) | 不支持 |
| 故障重建 | 复制 1 份 | 读 6 份 + 解码,慢 6 倍网络 |
| 作业 split | 1 split = 1 block,单 DN | 1 split 跨 6 DN,元数据放大 |
| 适用场景 | 热数据、频繁写 | 冷数据、写一次读多次 |
一句话总结:热数据用副本,冷数据用 EC,二者分工不混用。
八、性能权衡:省下的 50% 存储花在哪了
这是落地前最该想清楚的部分。我们做了一组基准测试(集群规模 ~200 个 DN,单盘 12×8TB HDD),对比同一个 Parquet 文件(~30 GB)在 3 副本和 RS-6-3 下的表现:
| 操作 | 3 副本 | RS-6-3 | 差异 |
|---|---|---|---|
| 顺序写吞吐(单客户端) | ~180 MB/s | ~95 MB/s | -47%(编码 + 9 DN 写入) |
| 顺序读吞吐 | ~1.6 GB/s(含本地副本) | ~1.4 GB/s | -12%(并发优势弥补) |
| 随机读 P99 延迟 | 8 ms | 35 ms | +337%(cell 放大 + 多跳) |
| Spark 全表扫描 | 12 min | 13 min | +8%(可接受) |
| Spark 点查(小 split) | 40 s | 95 s | +137%(split 跨 DN 放大) |
| DN 单盘故障后重建时间 | 25 min | 3.5 h | +740%(k 倍网络) |
几个结论:
- 顺序读写 EC 几乎不亏,因为 striping 把负载分摊到多个 DN,反而能利用并发。冷数据仓库的批扫作业(Hive/Spark 大表 scan)影响很小。
- 随机读是 EC 的软肋,P99 飙升明显。任何依赖随机 lookup 的场景(HBase on HDFS、Parquet 点查、向量检索底座)都不适合 EC。
- 重建慢是结构性问题,不是调参能解决的,必须靠监控兜底。
- 写入吞吐接近腰斩,所以 EC 不能给还在持续大量写入的”温”数据用,只能给已经稳定不再增长的真冷数据。
九、冷热分层落地
9.1 三层数据画像
我们把数据按访问频率分了三层:
- 热层(近 7 天,频繁读):3 副本,存 SSD / 高性能 HDD;
- 温层(7~90 天,偶尔读):3 副本,普通 HDD;
- 冷层(>90 天,几乎不读):RS-6-3 EC,普通 HDD 或对象存储后端。
配合 HDFS 存储策略(HOT / WARM / COLD)和 mover,能形成一套自动分层:数仓分区按时间衰减,定期 mover 把超过 90 天的分区从温层搬到冷层并转 EC。
9.2 与存储策略、对象存储归档配合
HDFS 的存储策略(Storage Policy)和 EC 是正交的两个维度,可以组合:
1 | # 目录设为 COLD + RS-6-3 |
如果集群接了对象存储(如 S3 / OSS / 自建对象存储)作为归档后端,更激进的做法是 冷数据直接走对象存储生命周期归档,本地 HDFS 只保留温热层。但对象存储的 EC 是黑盒的,本篇聚焦 HDFS-EC。
9.3 迁移的处理细节
-setPolicy 只对新文件生效,老数据转 EC 必须用 mover。两个坑:
- mover 是重 IO 操作,全量迁移时会和在线作业抢磁盘带宽。我们用
hdfs balancer同款带宽限流(dfs.datanode.balance.max.concurrent.moves等),并在业务低峰跑。 - mover 不删源,是先写到目标再删源。中间会临时占用额外空间(最坏 2x),目录磁盘水位高的要预留空间,别把 DN 写爆。
十、生产踩坑实录
坑 1:EC 文件不支持 append 和随机写
EC 文件一旦写完就是只读的,hdfs dfs -appendToFile 会直接报错。我们有一个日志归档流程,原来用 append 追加日志,改成 EC 后写入全失败。
解决:把 append 流程拆成”临时副本文件 + 定期 roll 成 EC 文件”两段。临时文件用 3 副本在温层 append,攒到一个 chunk(如 1 GB 或 1 小时)后,原子 mv 到冷层 EC 目录,由 NN 触发重新编码写入。
坑 2:跨机架放置要求
RS-6-3 需要 9 个 cell 分布在足够多的 DN 上,HDFS 的 EC block placement 默认要求 k+m 个单元尽量跨机架。我们有一次扩容前集群只有 2 个机架,RS-6-3 直接放不下(要求至少 3 机架才能保证容错域隔离),写文件一直失败。
经验:上 EC 前先确认机架数 ≥ m(RS-6-3 至少 3 机架,RS-10-4 至少 4 机架),否则容错域无法隔离,一个机架挂了可能同时丢多个 cell。
坑 3:Spark / MapReduce split 性能劣化
Striping 布局下,一个逻辑 block 的数据实际打散在 k 个 DN 上。计算框架做 input split 时,一个 split 会引用跨多个 DN 的 cell,导致:
- split 元数据膨胀(一个 split 引用 6 个 block location);
- 客户端到 NN 的 RPC 往返增多;
- 小文件多时 split 阶段延迟显著上升(我们的点查作业慢了 1.4 倍)。
缓解:
- 冷数据尽量合并成大文件(我们用 compaction 任务把小 Parquet 合成 GB 级);
- 调大
dfs.blocksize(EC 文件也可调,减小 split 数); - 对延迟敏感的查询走副本层的缓存表,不直查 EC 层。
坑 4:重建任务积压无告警
DN 故障后 EC 重建默认走 dfs.namenode.reconstruction.threads(每 DN 默认 2 个线程)。一次机架抖动后积压了 40 万+ 待重建 cell,但因为没告警,直到下一次盘点才发现冗余度长期降级。
解决:在 Prometheus + NameNode JMX 上加了三个告警:
- 待重建块数 > 阈值(我们设 1 万)持续 10 分钟;
- 单 EC group 存活单元 < k+m 持续 1 小时;
- 重建速率(blocks/min)连续走低于均值 50%。
坑 5:客户端版本必须 ≥ 服务端 EC 版本
老版本 Hadoop 客户端(< 3.0)读 EC 文件会报 “Unsupported encryption zone or EC” 类错误。我们有些遗留 ETL 节点还是 2.7 客户端,统一升级后才能正常读。
十一、决策矩阵:哪些目录适合 EC
| 目录特征 | 是否适合 EC | 理由 |
|---|---|---|
| 数仓历史分区(>90 天,只读) | ✅ 强烈推荐 | 冷、大文件、只读,EC 收益最大 |
| 原始日志归档 | ✅ 推荐 | 冷、写一次不再改,注意 append 流程改造 |
| 模型训练 checkpoint / 快照 | ✅ 推荐 | 大文件、只读、量大 |
| Kafka 历史数据归档 | ✅ 推荐 | 同日志归档 |
| 实时数仓热分区(近 7 天) | ❌ 不推荐 | 频繁随机读,EC 延迟敏感 |
| HBase on HDFS 底层 | ❌ 不推荐 | 强随机读写,EC 完全不兼容 |
| 小文件极多的临时区 | ⚠️ 谨慎 | Striping 能扛但 NN 内存压力大,先治理小文件 |
| 需要频繁 append 的流式写入目录 | ❌ 不推荐 | EC 不支持 append |
| 跨机房同步的源目录 | ⚠️ 谨慎 | 重建网络放大可能跨机房,评估带宽 |
实操上我们最终把 约 65 PB 冷数据中的 52 PB转成了 RS-6-3,物理存储从 ~195 PB 降到 ~143 PB,净省 ~52 PB,按我们的硬件成本折算大约省了 150 万元/年的扩容压力。剩下的 13 PB 因为有 append 需求或随机读敏感,保留 3 副本。
十二、小结
HDFS 纠删码不是”开开关就省钱”的银弹,它是一笔明确的交易:用 CPU 和读尾延迟,换 50% 的存储。这笔交易对冷数据划算,对热数据血亏。落到工程上,关键就三件事:
- 数据先分层——没有清晰的冷热画像,就不知道该给谁上 EC;
- 灰度先于放量——先在测试目录验证读写链路、作业 split、重建行为,再扩量;
- 监控先于故障——EC 重建慢、容错域敏感、append 受限,任何一个失控都会变成线上事故。
把 EC 当成”冷数据专用的高密度存储模式”,和 3 副本的热存储明确分工,这套组合在 PB 级集群上省下的钱是实打实的。但记住它有边界——边界以内是红利,边界以外是雷区。这篇复盘想传递的,就是怎么划这条边界。
