上一篇讲了 HA 怎么解决 NameNode 单点问题。本篇讲 HDFS 在 Hadoop 3.x 时代的三个核心演进:纠删码(Erasure Coding)、路由联邦(Router-Based Federation)、Observer NameNode。这三个特性分别解决三个不同的瓶颈:存储成本、NameNode 元数据上限、读吞吐上限。

HDFS 3.x 常被描述成"HDFS 的现代化版本"。这个描述对应了发布节奏但没解释演进逻辑。准确的说法是:HDFS 在 2.x 解决了 HA 和 YARN 之后,3.x 时代面对的核心问题是 PB 级集群的存储成本和元数据瓶颈,所以引入纠删码降存储、引入路由联邦解决元数据上限、引入 Observer NameNode 拆分读写流量。三个特性的设计动机都是"假设集群规模已经超过单 NameNode 上限"。

本篇只抓一个问题:HDFS 3.x 引入的三个主要特性各自解决什么瓶颈、用什么代价、适合什么场景。

纠删码:用 CPU 换存储

HDFS 默认三副本意味着每存 1TB 用户数据要占 3TB 物理存储。这个开销在 PB 级集群里是巨大的成本——一个 100PB 用户数据的集群需要 300PB 物理存储。Hadoop 3.0 引入纠删码把存储开销从 3x 降到约 1.5x,节省 50% 物理存储。

HDFS 用的纠删码算法是 Reed-Solomon(RS),默认配置 RS-6-3:

1
2
3
4
6 个数据单元 + 3 个校验单元 = 9 个单元组成一个条带(stripe)
存 6 单元数据实际占 9 单元物理存储
存储效率 = 6/9 = 66.7%
可容忍任意 3 个单元丢失(数据不丢)

对比三副本:

1
2
3
4
5
6
7
3 副本存储:每 1 单元数据占 3 单元物理存储
存储效率 = 1/3 = 33.3%
可容忍任意 2 个节点丢失(同 block 的 3 副本不能全丢)

RS-6-3 纠删码:
存储效率 = 66.7%(约三副本的 2 倍)
可容忍任意 3 个单元丢失

注意 RS-6-3 的容错能力反而比三副本强——三副本里一个 block 的 3 个副本如果同时丢失(罕见但可能),数据就丢了。RS-6-3 任意 3 个单元丢失都能通过剩余 6 个单元恢复,容错维度更高。

代价是写入和重建时的 CPU 开销。写入时纠删码编码器要计算 3 个校验单元(基于 6 个数据单元做矩阵运算),读取完整数据时如果某个单元丢失,解码器要用剩余单元重建丢失部分。三副本没有这种计算开销。

HDFS 的纠删码默认用 RS-6-3,但 Hadoop 3.4 起也支持 RS-10-4(更省存储但写入开销更大)、XOR-2-1-2(轻量级,仅容忍 2 单元丢失但 CPU 开销最小)。选择哪种取决于集群的存储成本敏感度 vs CPU 资源充裕度。

纠删码适用于"冷数据"——访问频率低、对延迟不敏感的数据。HDFS 纠删码默认只支持顺序读(不支持随机修改),所以适合归档日志、历史快照、备份等。热数据(HBase、实时分析)仍然用三副本,因为纠删码的解码延迟和写入开销会拖慢随机访问。

HDFS 纠删码有两种布局:

条带布局(Striped Layout):每个文件被切成 cell(默认 1MB),6 个 cell 组成一个 stripe,分散写到 9 个 DataNode。这是默认布局,存储效率最高。

连续布局(Contiguous Layout):每个文件整体作为一段,编码后写到 9 个 DataNode。这种布局适合大文件随机读(cell 切换不频繁),但存储效率略低。

启用纠删码:

1
2
3
4
5
# 为某个目录启用 RS-6-3 纠删码
hdfs ec -setPolicy -path /cold-data -policy RS-6-3-64k

# 查看某目录的纠删码策略
hdfs ec -getPolicy -path /cold-data

启用后该目录下新写入的文件会自动按 RS-6-3 编码存储。已存在的旧文件不自动迁移,需要复制到新目录才能应用。

路由联邦:解决 NameNode 元数据上限

HDFS 单 NameNode 元数据上限受 JVM 堆内存约束。一个有 100GB 堆的 NameNode 大概能管 6-7 亿个文件(每个文件 + block 大约 150 字节元数据)。超过这个规模的集群——典型场景是大型互联网公司的日志归档、电商图片库、视频原始素材——单 NameNode 装不下。

Hadoop 2.x 引入了 HDFS Federation,把命名空间切成多个独立 NameNode,每个 NameNode 管一个 Block Pool。客户端通过 ViewFS 配置路径映射,例如:

1
2
3
/viewfs://cluster-x/user  → hdfs://nn-1/user
/viewfs://cluster-x/data → hdfs://nn-2/data
/viewfs://cluster-x/log → hdfs://nn-3/log

ViewFS 的缺陷是路径映射配置在客户端——每个客户端的 core-site.xml 都要维护这份映射,集群调整路径时所有客户端配置都要更新。这在大规模多租户集群里运维成本很高。

Hadoop 3.x 引入 Router-Based Federation(RBF)解决客户端配置问题。RBF 引入一个无状态的 Router 进程,客户端只配置 Router 地址,Router 内部维护路径 → NameNode 映射,把请求路由到对应 NameNode:

1
2
3
4
Client → Router → 路径表查找 → 对应 NameNode

State Store (ZooKeeper / LevelDB)
存路径 → NameNode 映射

Router 是无状态的——可以部署多个 Router 实例做负载均衡,任何一个 Router 挂掉客户端自动切换到另一个。映射表存在 State Store(ZooKeeper 或 LevelDB 后端),所有 Router 共享这份映射。

RBF 的核心收益是路径映射集中管理。集群调整路径时,运维只更新 State Store,所有 Router 立刻看到新映射,客户端配置不需要变化。

RBF 还提供 Mount Table 管理 API,可以动态挂载/卸载子树到不同 NameNode。这种动态挂载让 HDFS 集群可以按需扩展——某个 NameNode 元数据满了,把它的一部分子树迁移到新 NameNode,更新 Mount Table 即可,客户端无感。

RBF 在 Hadoop 3.0 引入,3.1-3.2 期间持续优化,到 Hadoop 3.3 进入生产可用状态。大型互联网公司(Facebook、腾讯、阿里)的 PB 级 HDFS 集群普遍使用 RBF 管理 10+ 个 NameNode。

Observer NameNode:拆分读写流量

HDFS HA 部署里 Standby NameNode 不接受客户端 RPC,只做 tail 和 Checkpoint。这让 Standby 的资源(CPU、内存)大部分时间闲置。

观察生产集群的 NameNode 流量分布:读 RPC(listStatus、getBlockLocations、getFileInfo 等元数据查询)通常占 80%+ 流量,写 RPC(create、delete、append 等元数据修改)占 20% 以下。Active NameNode 同时承担读和写,瓶颈往往在读侧——特别是 MapReduce 作业启动时大量 Task 同时 getBlockLocations,把 Active NameNode CPU 打满。

Hadoop 3.2 引入 Observer NameNode(ONN),让 Standby NameNode 也能承担读 RPC(但是只读)。部署形态变成三 NameNode:一个 Active + 一个 Standby + 一个 Observer。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Active NameNode
- 接受所有写 RPC
- 接受部分读 RPC(强一致读)
- EditLog 写 JournalNode quorum

Standby NameNode
- 热备 Active,等待故障切换
- tail EditLog 保持内存同步
- 不接受客户端 RPC

Observer NameNode
- tail JournalNode 上的 EditLog
- 接受读 RPC(最终一致读)
- 不能切换为 Active(除非特殊配置)

Observer NameNode 的"最终一致"指的是:客户端在 Observer 上读到的元数据可能比 Active 慢几十毫秒(EditLog tail 的延迟)。对于读操作来说,这种延迟通常可接受——查 getBlockLocations 慢 50ms 不影响读文件本身的吞吐。

客户端通过 dfs.client.failover.observer.reads 配置启用 Observer 读。开启后客户端的读 RPC 会路由到 Observer,写 RPC 仍然路由到 Active。客户端通过 DFS Observer Read Provider 选择最近的 Observer(基于距离排序)。

Observer NameNode 的收益是读吞吐线性扩展——可以部署多个 Observer(Hadoop 3.x 支持任意数量),读吞吐随 Observer 数量线性增长。这对元数据读密集型负载(HBase RegionServer 频繁 getBlockLocations、Spark Driver 大量 listStatus)效果显著。

Observer NameNode 不替代 HA——它依赖 JournalNode 和 Active NameNode,不能在 Active 宕机时接管写服务。HA 切换仍然在 Active 和 Standby 之间进行,Observer 不参与切换。

三个特性的应用场景

这三个特性各自解决不同瓶颈。生产集群是否启用、怎么组合,取决于集群的具体瓶颈:

1
2
3
4
5
6
7
8
9
10
集群瓶颈                  推荐特性                    代价
───────────────────── ──────────────────────── ─────────────────────
物理存储成本高 纠删码(RS-6-3) CPU 开销 + 仅顺序读
(数据量大、访问频率低) 或 RS-10-4(更省存储) 写入延迟变长

NameNode 元数据上限 路由联邦(RBF) 增加运维复杂度
(文件数超 6-7 亿) 部署多个 NameNode 跨 NameNode 操作不支持

NameNode 读吞吐瓶颈 Observer NameNode 读延迟略增(最终一致)
(读 RPC 频繁,CPU 满) 部署 1-2 个 Observer 增加 JVM 实例数

大型集群的典型部署:纠删码 + RBF + Observer 同时启用,按子树(subtree)分配不同 NameNode,每个 NameNode 配 1-2 个 Observer,冷数据走纠删码目录、热数据走三副本目录。

小型集群(< 1 亿文件、< 100TB 数据)通常不需要任何这些特性。三副本 + 单 NameNode HA 足够。

实验:观察 HDFS 3.x 特性

观察纠删码(如果集群启用):

1
hdfs ec -listPolicies

输出当前集群支持的纠删码策略:

1
2
3
4
5
6
Erasure Coding Policies:
RS-6-3-64k [Codec:rs, numDataUnits:6, numParityUnits:3, ...]
RS-3-2-64k [Codec:rs, numDataUnits:3, numParityUnits:2, ...]
RS-LEGACY-6-3-256k [Codec:rs-legacy, numDataUnits:6, numParityUnits:3, ...]
XOR-2-1-64k [Codec:xor, numDataUnits:2, numParityUnits:1, ...]
RS-10-4-64k [Codec:rs, numDataUnits:10, numParityUnits:4, ...]

为某个目录启用纠删码:

1
2
hdfs ec -setPolicy -path /user/me/cold -policy RS-6-3-64k
hdfs ec -getPolicy -path /user/me/cold

上传一个文件到这个目录,用 hdfs fsck 观察块分布——会看到 9 个 block 分布到 9 个 DataNode(6 data + 3 parity),而不是 3 副本。

观察 RBF(如果集群启用):

1
hdfs dfsrouteradmin -ls /

输出 Mount Table:

1
2
3
4
5
Source             Destination                  Owner  Group  ...
/ hdfs://nn-cluster-root/ ... ... ...
/user hdfs://nn-user/ ... ... ...
/data hdfs://nn-data/ ... ... ...
/log hdfs://nn-log/ ... ... ...

每条记录展示一个路径子树对应的 NameNode 集群。

观察 Observer NameNode(如果集群启用):

1
hdfs haadmin -getAllServiceStates

输出所有 NameNode 的角色:

1
2
3
nn-1: active
nn-2: standby
nn-3: observer

如果集群没有启用这些特性,输出会显示 HA 部署的 active + standby 两个,没有 observer。

模式提炼

HDFS 3.x 的三个特性可以提炼成一组分布式系统扩展性模式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
模式 A:用 CPU 换存储(纠删码)
- 用计算密集型编码替代简单复制
- 适合存储成本敏感、CPU 资源充裕的场景
- 经典权衡:备份 vs 计算

模式 B:无状态路由层 + 共享状态存储(路由联邦)
- 把单一控制节点拆成多个独立控制节点
- 上层加无状态路由层对客户端屏蔽内部切分
- 状态存储用 ZooKeeper / etcd 等共识系统保证一致
- 经典权衡:单点简化 vs 多点扩展

模式 C:读写流量分离(Observer NameNode)
- 主节点承担写,从节点承担读
- 主从通过 tail journal 保持最终一致
- 读延迟略高于主节点,但读吞吐随从节点数线性扩展
- 经典权衡:强一致 vs 读吞吐

这三个模式不只是 HDFS。

纠删码模式出现在 Ceph(RADOS 默认 EC pool 用于冷数据)、对象存储(S3 后端用 RS 编码)、分布式数据库(Cassandra 的 EC 后端)。

无状态路由层模式出现在 Kubernetes Ingress、Service Mesh(Istio Pilot 无状态 + etcd 状态)、数据库代理(Vitess、Sharding-Sphere)。任何一个"单控制节点瓶颈"场景都会演化出这种结构。

读写流量分离模式出现在 MySQL 读写分离、PostgreSQL 流复制 + 读副本、Redis 主从、Kafka follower fetch。任何读多写少的场景都可以用这种结构扩展读吞吐。

工程迁移表

HDFS 3.x 特性 Ceph 对象存储 Kubernetes 数据库
纠删码(EC) RADOS 默认 S3 / OSS 后端 - Cassandra EC 后端
路由联邦(RBF) CRUSH 算法 bucket 路由 Ingress controller sharding proxy
Observer NameNode OSD read replica 多 AZ 端点 多副本 API Server MySQL read replica
Mount Table 动态挂载 crush map bucket service + endpoints sharding config

注意对象存储这一列。S3、OSS 这类对象存储在底层都用了纠删码(不是 HDFS 的三副本),所以对象存储的"标准存储"价格通常只有 HDFS 三副本的 1/3。这是为什么大数据栈从 HDFS 迁移到对象存储的成本收益巨大——同样的存储成本下,对象存储能存 3 倍数据。代价是对象存储的元数据接口(PUT / GET 单个 object)延迟显著高于 HDFS 的本地元数据 RPC,所以热数据场景 HDFS 仍然占优。第二十一篇会展开这个对比。

常见误解

误解一:“纠删码总是比三副本好”。纠删码节省存储但增加 CPU 开销和写入延迟。冷数据(归档、备份)适合纠删码,热数据(数据库、实时分析)适合三副本。把 HBase RegionServer 的数据放到纠删码目录会让随机读延迟显著上升,因为每次读都要解码。

误解二:“Federation 之后集群规模无上限”。Federation 让集群可以横向扩展 NameNode,但每个 NameNode 仍然是单点瓶颈——单个 NameNode 的元数据上限仍然是 JVM 堆内存决定的。集群规模无上限只意味着"可以加 NameNode",不意味着"一个 NameNode 可以管任意多文件"。RBF 也引入了新的运维复杂度——Mount Table 管理、跨 NameNode 操作不支持、客户端配置兼容性。

误解三:“Observer NameNode 可以替代 Active”。Observer 只接受读 RPC,不能接受写。Active 宕机时 Observer 不能接管写服务(即使配置成"Observer 也可以转 Active",仍然需要 Standby 在场)。Observer 的价值是读吞吐扩展,不是 HA 替代品。

误解四:“启用纠删码会立刻节省存储”。已存在的旧文件不会自动迁移到纠删码布局,需要复制到启用纠删码策略的目录才生效。这一步通常是离线进行的——先把冷数据迁移到纠删码目录,原三副本文件删除,物理存储才会真正下降。

误解五:“RBF 让 HDFS 像对象存储一样易扩展”。RBF 解决的是 NameNode 元数据上限,但 HDFS 仍然是"文件系统"语义——每个文件有路径、有元数据、有 block 切分。对象存储的"object + key"语义比 HDFS 的"file + path"更简洁,扩展性也更好。RBF 让 HDFS 接近对象存储的扩展能力,但没有完全达到。这是为什么云原生时代对象存储逐步替代 HDFS(第二十一篇展开)。

练习

  1. 在 Hadoop 3.x 集群上运行 hdfs ec -listPolicies,查看支持的纠删码策略。为某个测试目录启用 RS-6-3,上传一个 100MB 文件,用 hdfs fsck 观察块分布(应该是 9 个 block 而不是 3 副本)。

  2. apache/hadoop 源码里找到 ErasureCodingPolicy.javaRBFRouter.javaObserverNameNode.java(hadoop-hdfs-project 模块),观察三个特性的核心实现类。

  3. 思考题:如果一个集群同时启用纠删码 + RBF + Observer NameNode,单个子树(subtree)的工作流程是怎样的?读、写、元数据查询分别走哪些组件?

  4. 思考题:为什么 HDFS 选择 RS-6-3 作为默认纠删码策略而不是 RS-3-2 或 RS-10-4?存储效率、容错能力、CPU 开销三者是怎么权衡的?

系列导航

序号 主题 状态
00 导读:Hadoop 的核心前提是节点总会失败
01 HDFS 架构:NameNode、DataNode 与元数据的三层切分
02 文件写入路径:从客户端到 DataNode 的流水线
03 文件读取路径:副本选择与短路读
04 NameNode 内存模型:FSImage、EditLog 与启动恢复
05 HDFS HA:Quorum Journal Manager、ZKFC 与脑裂防御 上一篇
06 HDFS 3.x 演进:纠删码、路由联邦与 Observer NameNode 本篇(HDFS 篇完结)
07-12 YARN 资源管理层(架构 / 资源模型 / 调度器 / 生命周期 / HA / Timeline v2) 第二阶段,下一篇开始

参考资料