文章摘要
飞宇 GPT

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
2
文件 → block0[ d0 d1 d2 d3 d4 d5 | p0 p1 p2 ]  block1[ d0 d1 d2 d3 d4 d5 | p0 p1 p2 ] ...
└──── 这 9 个 cell 分布到 9 个 DN ────┘

特点:编码在一个 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
2
3
4
stripe 0:  DN0  DN1  DN2  DN3  DN4  DN5 | DN6  DN7  DN8
d00 d01 d02 d03 d04 d05 p00 p01 p02
stripe 1: d10 d11 d12 d13 d14 d15 p10 p11 p12
...

关键好处:一个文件只需凑满一个 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 不可用后:

  1. NN 判断该 cell 所属的 EC group 是否还有 k 个存活单元;
  2. 若存活单元数 ≥ k 且总单元数 < k+m(即冗余度下降),NN 调度一个 reconstruction(重建)任务到某个 DN;
  3. 该 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-1024kRS-3-2-1024kRS-10-4-1024k 等策略(1024k 是 cell 大小),状态分 ENABLED / DISABLED

5.2 给目录设置策略

1
2
# 给冷数据目录启用 RS-6-3
hdfs ec -setPolicy -path /data/cold/archive -policy RS-6-3-1024k

注意几点:

  • -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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 1. 确认集群 DN 数与机架数满足 RS-6-3 的最低要求(至少 9 个 DN,理想跨 3+ 机架)
hdfs dfsadmin -report | grep -c "Name:"

# 2. 查看当前 EC 策略状态,确认 RS-6-3-1024k 是 ENABLED
hdfs ec -listPolicies

# 3. 先在小目录上灰度(关键!不要一上来全量)
hdfs ec -setPolicy -path /data/cold/archive_test -policy RS-6-3-1024k

# 4. 写入测试文件验证
hdfs dfs -put /tmp/test.parquet /data/cold/archive_test/
hdfs ec -getPolicy -path /data/cold/archive_test/test.parquet
# 期望输出:RS-6-3-1024k

# 5. 灰度观察 1~2 周(重建、读写性能、作业影响)后,正式目录上策略
hdfs ec -setPolicy -path /data/cold/archive -policy RS-6-3-1024k

# 6. 把目录下已有副本数据迁移成 EC(后台跑,重 IO,建议业务低峰)
nohup hdfs mover /data/cold/archive > mover.log 2>&1 &

# 7. 监控迁移进度(mover 是幂等的,可反复跑)
hdfs fsck /data/cold/archive | grep -E "Erasure Coded|Replicated"

关键经验:第 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 倍网络)

几个结论:

  1. 顺序读写 EC 几乎不亏,因为 striping 把负载分摊到多个 DN,反而能利用并发。冷数据仓库的批扫作业(Hive/Spark 大表 scan)影响很小。
  2. 随机读是 EC 的软肋,P99 飙升明显。任何依赖随机 lookup 的场景(HBase on HDFS、Parquet 点查、向量检索底座)都不适合 EC。
  3. 重建慢是结构性问题,不是调参能解决的,必须靠监控兜底。
  4. 写入吞吐接近腰斩,所以 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
2
3
# 目录设为 COLD + RS-6-3
hdfs storagepolicies -setStoragePolicy -path /data/cold/archive -policy COLD
hdfs ec -setPolicy -path /data/cold/archive -policy RS-6-3-1024k

如果集群接了对象存储(如 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% 的存储。这笔交易对冷数据划算,对热数据血亏。落到工程上,关键就三件事:

  1. 数据先分层——没有清晰的冷热画像,就不知道该给谁上 EC;
  2. 灰度先于放量——先在测试目录验证读写链路、作业 split、重建行为,再扩量;
  3. 监控先于故障——EC 重建慢、容错域敏感、append 受限,任何一个失控都会变成线上事故。

把 EC 当成”冷数据专用的高密度存储模式”,和 3 副本的热存储明确分工,这套组合在 PB 级集群上省下的钱是实打实的。但记住它有边界——边界以内是红利,边界以外是雷区。这篇复盘想传递的,就是怎么划这条边界。