纠删码(erasure coding,EC)允许 Ceph 以低于三副本的存储开销换取同等的数据持久性,但它为写路径引入了新的复杂度:当写请求不能填满整个条带时,必须先读出旧数据再计算校验,这就是读改写(Read-Modify-Write,RMW)代价。本篇拆解 ECBackend 的实现路径、整条带写与 RMW 的边界,以及 EC 修复路径;编码理论的基础(GF 域、MDS 码、XOR 纠错等)见 纠删码(storage/49),本篇不重写。
本文是「Ceph / RADOS 存储内核」系列第 11 篇(共 18 篇)。→ 系列目录
篇目 核心内容 第 10 篇 · Recovery / Backfill / Scrub reservation、async recovery、mClock 边界 第 11 篇 · EC 路径 ECBackend、整条带 vs RMW、EC 修复路径 第 12 篇 · RBD image→对象、krbd vs librbd、快照/克隆边界 上一篇:Recovery / Backfill / Scrub · 下一篇:RBD
版本锚定:Ceph Squid v19.2.5;
docs.ceph.com/en/squid/rados/operations/erasure-code/;docs.ceph.com/en/squid/dev/osd_internals/erasure_coding/;源码src/osd/ECBackend.cc@ v19.2.5。编码理论基础见 纠删码(storage/49)。
一、ECBackend 与 ReplicatedBackend 的分工
Ceph 的 PG 层通过 PGBackend
接口屏蔽了副本(replicated)和纠删码(erasure
coding)两种存储策略的差异。PrimaryLogPG 调用
pgbackend->submit_transaction()
时,并不关心底层是三副本还是 4+2 EC。
两个实现的核心差异:
| 维度 | ReplicatedBackend |
ECBackend |
|---|---|---|
| 对象粒度 | 整个对象发给每个副本 OSD | 对象被切成 \(k\) 个数据分片 + \(m\) 个校验分片,分片散布在 \(k+m\) 个 OSD 上 |
| 写语义 | 直接发 MOSDRepOp 给所有 Replica |
全条带写(direct)或 RMW(读→编码→写) |
| 读路径 | 从任意一个 acting OSD 读 | 至少读 \(k\) 个分片,解码后拼接 |
| 修复(recovery) | 从好的副本拷贝整个对象 | 从任意 \(k\) 个存活分片重建缺失分片 |
| 存储开销 | \(n\) 副本:\(n \times\) 原始大小 | \(k+m\) 分片:\(\frac{k+m}{k} \times\) 原始大小 |
以 k=4, m=2(4+2
EC)为例:总存储开销为原始大小的 1.5 倍,而三副本的开销是 3
倍。空间效率相差一倍,代价是写路径的 RMW
开销和读路径的解码开销。
二、条带(Stripe)的概念
EC 把对象数据按 条带(stripe)
组织。一个条带的大小是 \(k \times
\text{stripe\_unit}\),其中
stripe_unit(chunk 大小)由创建 EC pool 时的
stripe_unit 参数指定(默认 4
KiB,可配置)。
对于 4+2 EC,若 stripe_unit = 4 KiB: -
条带大小 = \(4 \times 4 =
16\) KiB; - 对象数据按 16 KiB 一组切分为条带; -
每个条带的 4 个 4 KiB 数据 chunk 分别存到 4 个数据 OSD; -
对应的 2 个 4 KiB 校验 chunk 存到 2 个校验 OSD。
RADOS 对象在 EC 池中的实际存储:
object "foo" (logical 100 KiB)
→ stripe 0 : shard 0..3 (data) + shard 4..5 (parity) → 6 个 OSD
→ stripe 1 : shard 0..3 + 4..5 → 同 6 个 OSD
→ ...
→ stripe 6 : shard 0..3(最后一个条带,可能不足 16 KiB)
每个 OSD 存储对象的一个 shard(包含所有条带的该 OSD
部分),shard 命名为
<pgid>/<oid>#<shard_id>。
三、整条带写(Whole Stripe Write)
开发者文档
ECBackend(dev/osd_internals/erasure_coding/ecbackend/)把单条带写分成数据流选择;WHOLE
STRIPE WRITE 是其中最简单的一支:Primary
已持有该条带全部 (k) 个数据块所需字节,本地算 (m)
个校验块,再发给各 shard。
flowchart LR
A["write 覆盖整条带\nW = K"] --> B["Primary EC encode"]
B --> C["MOSDECSubOpWrite\n→ 各 shard OSD"]
C --> D["各 OSD BlueStore 落盘"]
D --> E["Reply 收齐 → commit 路径"]
要点:
- 应用写可能跨多条带;ECBackend 先 tessellate 成「若干整条带 + 至多两个部分条带」。
- 整条带:无读旧分片;网络放大约为 ()(相对逻辑用户字节),低于三副本的 (3)。
- 编码库(Jerasure / ISA-L 等)只在 Primary(或文档指定角色)上算校验。
适用:对齐的大块顺序写、全新对象填充——RGW 大对象 PUT 常接近这条路径。编码理论(MDS、RS 距离)见 storage/49;本篇只钉 Ceph 数据流。
四、读改写(Read-Modify-Write)
当写只覆盖条带内 (W < k) 个数据块(不对齐、部分覆盖),文档 READ-MODIFY-WRITE 路径生效:
flowchart LR
A["部分条带写 W < K"] --> B["读未改动的数据 shard"]
B --> C["与新数据合并\n重算 parity"]
C --> D["写回被改动的 data/parity shard"]
步骤(与 ecbackend 文档一致):
- 识别本条带内哪些数据块不变;
- 从对应 shard 读出那些块;
- 与新写入合并,重算校验块;
- 把修改过的块写回各 OSD;
- 进入与整条带写相同的 commit / rollforward 一致性协议(见下)。
代价模型(符号,非实测 IOPS):记一次小写逻辑长度为 1 个条带内的部分更新,则粗略放大可写成「约读 (k-W)(或实现上读齐所需)+ 写涉及的数据/校验 shard」。文档用 (W K) 描述被写数据块数。对随机小写,额外读 RTT 往往主导延迟——这是 EC 不宜作为延迟敏感随机写主路径 的机制原因,而不是某张未复核的 IOPS 表。
4.1 Commit 与 rollforward
不论整条带还是
RMW,落盘是两阶段:commit(可能留下
rollback 旁路信息)与 rollforward(acting
成员都 commit 后丢掉回滚信息)。覆盖写时,rollback 信息曾以
sparse clone 等形式存在;BlueStore
提供更高效原语后,实现仍遵循「先可回滚提交,再前滚」的不变量(见
ecbackend OSD Object Write and
Consistency)。ExtentCache / Pipeline 防止同一对象上
RMW 与不可缓存 op(如 clone)
危险并发——细节见 ExtentCache.h 与
ECBackend::waiting_*。
4.2 与「伪造性能」的边界
可以说:RMW 比整条带多至少一轮读依赖。不可说:某 (k,m) 在「所有 NVMe 集群」上小写延迟等于副本的 (N) 倍——除非给出本机实验台账。本站无该台账。
五、EC overwrite 功能
Ceph Luminous(12.x)为 RBD 引入了
allow_ec_overwrites 功能,允许 EC
池支持覆盖写。在此之前,EC
池不支持对已存在对象的覆盖写(只支持全新对象写入),因为没有
RMW 机制。
allow_ec_overwrites 需要满足的前提:
- BlueStore 后端(FileStore 无法支持 EC overwrite 所需的原子性保证);
- 条带/shard
写入与本地分配语义兼容:官方要求与 BlueStore
能力对齐,避免不安全的中间状态;具体约束以 Squid
操作文档与当时源码检查为准(常与
min_alloc_size、checksum 路径相关)。
启用后,EC 池可作 RBD 后端并允许覆盖写,但 RMW 税仍在:小于条带对齐的随机写不会因为开关打开而变成整条带写。顺序大写 / 冷数据仍是 EC 主场;延迟敏感随机写仍宜副本池。
六、EC 修复路径
EC 池的修复(recovery)与副本池不同:不需要从别处拷贝整个 shard,而是利用编码冗余从 \(k\) 个存活分片重建缺失的分片。
6.1 修复触发
当一个 OSD 宕机,其上存储的所有对象的某个 shard
变为不可访问。PG 进入 active+recovering
状态,ECBackend 识别缺失的 shard,发起修复。
6.2 修复流程
flowchart LR
A["OSD down\nshard j 不可用"] --> B["Primary 收集\n任意 k 个存活 shard\n(MOSDECSubOpRead)"]
B --> C["解码:EC decode\n重建 shard j 数据"]
C --> D["将重建的 shard j 写到\n新 OSD 或恢复的 OSD\n(MOSDECSubOpWrite)"]
D --> E["PG 修复完成"]
修复只需读 \(k\) 个分片(而非 \(k+m\) 个),因此即使有 \(m\) 个 OSD 同时不可用,修复仍可进行。修复的网络 I/O:读 \(k\) 个分片 + 写 1 个分片(仅重建缺失的那个),比副本池修复(读整个对象 + 写整个对象)网络效率更好——特别是当 \(k\) 大、\(m\) 小时,每个分片只有原始对象大小的 \(\frac{1}{k}\)。
6.3 EC 降级读
当有分片不可用但数量不超过 \(m\) 时(即至少 \(k\) 个分片存活),EC 池仍可响应读请求,但需要执行降级读(degraded read):从 \(k\) 个存活分片读出数据,解码后返回客户端。降级读比正常读(只读 1 个分片不需解码)延迟更高,且对剩余 \(k\) 个 OSD 产生额外 I/O 负担。
七、编码插件:Jerasure、ISA-L 与 LRC
| 插件 | 库 | 说明 |
|---|---|---|
jerasure |
Jerasure | 常见默认;RS/Cauchy 等;评测见 Plank et al. FAST 2009 |
isa |
Intel ISA-L | 利用 SIMD 加速 GF 运算;相对 Jerasure 的加速比依赖 CPU 与块大小,本篇不引用未复核的「快 N 倍」营销句 |
lrc |
Ceph LRC plugin | 局部可修复码工程化;文档
erasure-code-lrc |
shec |
SHEC | 另一类局部修复思路 |
生产选择:在支持 AVX2 等的 x86 上常优先评估
isa;最终以本集群 encode/decode CPU
剖析为准。
7.1 LRC 文献必须对齐的两点
- 形式化 locality:Gopalan, Huang, Simitci, Yekhanin,《On the Locality of Codeword Symbols》,IEEE Transactions on Information Theory, 2012——定义码字符号的局部可修复性,是理论锚点。
- 工业 LRC:Huang et al.,《Erasure Coding in Windows Azure Storage》,USENIX ATC 2012(Best Paper)——引入 Local Reconstruction Codes,强调单故障重建少读分片;文中 LRC(12,2,2) 等参数对应 Azure 存储开销与重建代价的设计点。不是 FAST 2012。
Facebook f4 等系统有各自的局部修复/EC 变体;与 Azure
LRC、与 Ceph lrc
插件不是同一实现。Ceph
插件提供「局部修复」方向的可配置编码,生产采用率仍低于
jerasure/isa——这是部署观察,不是论文结论。
可证伪命题:「Ceph lrc 插件 ≡ Azure ATC’12
生产码」→ 假;「LRC 类码可降低单盘故障重建的分片读取数」→ 在
Huang/Gopalan 假设下真。
八、副本池 vs EC 池:边界与选择
| 维度 | 副本池 | EC 池 |
|---|---|---|
| 空间效率 | \(1 / n\)(3 副本时 33%) | \(k / (k+m)\)(4+2 时 67%) |
| 随机小写延迟 | 低(直写到所有副本,无 RMW) | 高(有 RMW:额外读 RTT + 编码计算) |
| 顺序大写延迟 | 中(\(n\) 份网络 I/O) | 低(\(\frac{k+m}{k}\) 份网络 I/O,少于 3 副本) |
| 读路径 | 从任意一副本读,无需解码 | 需读 \(k\) 分片解码(降级时)或 1 分片(正常时) |
| 快照/克隆 | 支持(COW 语义) | 支持(需 allow_ec_overwrites + BlueStore) |
| 对象覆盖 | 直接支持 | 需 allow_ec_overwrites(Luminous+);RMW 代价 |
| 适合场景 | RBD(随机 I/O)、CephFS(POSIX 工作负载)、对延迟敏感 | 冷数据归档、对象存储大文件(RGW)、顺序读写为主 |
选择边界的核心问题:
- 工作负载是否以顺序大写为主?如果是,EC 的整条带写路径高效,空间节省显著。
- 是否有频繁随机小写?如果有,RMW 代价使 EC 的写延迟不可接受。对于 RBD/CephFS,建议走副本池。
- 空间成本是否是主要约束?3 副本需要 3 倍存储,4+2 只需 1.5 倍;在 PB 级对象存储中,空间成本差距是主要决策因素。
RGW(Ceph 对象网关)是 EC 最常见的生产场景:S3/Swift 对象通常是一次性大文件写入(PUT 整个对象),读以完整读为主,几乎不做随机覆盖写,是 EC 整条带写路径的理想工作负载。
九、学术谱系与争论
9.1 谱系
RS / RAID-6 实践
→ 分布式 RS(GFS/Colossus、HDFS-RAID 等)
→ 局部可修复:Gopalan et al. TIT'12;Huang et al. ATC'12(Azure LRC)
→ Ceph:Jerasure/ISA-L 插件 + ECBackend 整条带/RMW(dev 文档)
→ 理论菜谱:[storage/49];本篇只钉 OSD 路径
Jerasure 生态的系统评测锚点:Plank et al.,《A Performance Evaluation and Examination of Open-Source Erasure Coding Libraries for Storage》,USENIX FAST 2009。Ceph 开发者文档中的 ECBackend 策略(whole stripe / RMW / commit-rollforward)是工程规范锚点,与编码数学分工明确。
9.2 争论(可证伪表述)
副本 vs EC:
- A:交互式随机写应留在副本池——RMW 读依赖使其尾延迟不可接受。
- B:NVMe 降低读 RTT 后,EC 可用于更广的写模式,空间收益值得。
检验:固定 (k,m,),对比对齐大写 vs 4 KiB 随机覆盖的延迟分布;本站无统一排行榜。
LRC vs RS:Azure/ATC’12
显示局部重建读更少;Ceph 默认生态仍以 RS
插件为主,因参数与工具链更简单。检验:同存储开销下单 OSD
故障重建的网络字节是否下降——需在具体 plugin 配置下测,不能用
Azure 论文数字直接标到 Ceph lrc。
十、开放问题
1. EC 池上 RMW 的 I/O 放大定量分析。对于给定的 \((k, m, \text{stripe\_unit})\) 配置,RMW 场景下单次小写产生的实际 I/O 放大倍数(读 \(k\) 分片 + 写 \(k+m\) 分片,共 \((2k+m) / 1\) 倍逻辑写量)在 HDD vs NVMe 上的实际延迟差异,是选择 EC 还是副本池的关键依据,但需要在特定集群上实测。本文不编造未实测的具体数字。
2. EC 与 mClock 的交互。EC
修复(recovery)也使用 mClock 调度(第 10 篇),但 EC
修复的一次操作实际上涉及 \(k\) 个 OSD 的读和 1 个 OSD
的写,比副本修复(1 个 OSD 读 + 1 个 OSD 写)的 I/O
放大更大。mClock 的 recovery_op
带宽配额是按”ops 数量”限速,而非按”实际字节 I/O”限速,因此
EC 修复在相同 recovery_op
配额下会消耗更多实际带宽。这个不对等性是否需要在 mClock
配置中手动补偿,目前官方文档没有明确建议。
3. EC 元数据一致性与 split brain。EC
池的 PG 在 min_size(通常等于 \(k\))个 OSD
存活时可以响应读,但写需要 \(k+m\)
全部存活(否则无法保证校验一致性)。当网络分区导致 Primary
与部分校验 OSD 断开时,EC
池的写可用性低于副本池(副本池只需超过半数副本存活即可写),这是
EC 在可用性上的固有劣势,设计分布策略时需要考虑故障域覆盖
\(m+1\) 个不同机架。
十一、参考资料
规范 / 官方文档(A 级)
- Ceph Squid,Erasure
Code,
docs.ceph.com/en/squid/rados/operations/erasure-code/。 - Ceph Squid,Erasure Coding Internals(Developer
Guide),
docs.ceph.com/en/squid/dev/osd_internals/erasure_coding/。 - Ceph Squid,Erasure Code LRC
Plugin,
docs.ceph.com/en/squid/rados/operations/erasure-code-lrc/。
源码(A 级)
ceph/cephtag v19.2.5:src/osd/ECBackend.cc、src/osd/ECBackend.h(RMW、full-stripe write、EC recovery);src/erasure-code/jerasure/、src/erasure-code/isa/(编码插件);src/erasure-code/lrc/(LRC 插件)。
论文(A 级)
- Plank, J. S., Luo, J., Schuman, C. D., Xu, L., & Wilcox-O’Hearn, Z. (2009). A Performance Evaluation and Examination of Open-Source Erasure Coding Libraries for Storage. USENIX FAST 2009. Jerasure 等开源库评测锚点。
- Huang, C., Simitci, H., Xu, Y., Ogus, A., Calder, B., Gopalan, P., Li, J., & Yekhanin, S. (2012). Erasure Coding in Windows Azure Storage. USENIX ATC 2012. Azure LRC 工业实践(非 FAST)。
- Gopalan, P., Huang, C., Simitci, H., & Yekhanin, S. (2012). On the Locality of Codeword Symbols. IEEE Transactions on Information Theory. 局部可修复性的形式化基础。
- Ceph Squid,ECBackend Implementation
Strategy,
docs.ceph.com/en/squid/dev/osd_internals/erasure_coding/ecbackend/(whole stripe / RMW / commit-rollforward)。
站内
- 纠删码(storage/49)(GF 域、Reed-Solomon、MDS 码等编码理论;本篇不重写)
- 第 10 篇:Recovery / Backfill / Scrub(mClock 与修复路径)
- 第 12 篇:RBD · 系列目录
- Ceph 与
CRUSH(EC 池的 CRUSH rule
配置:
step chooseleaf覆盖 \(m+1\) 故障域)
→ 系列目录 · 上一篇:Recovery / Backfill / Scrub · 下一篇:RBD
读完这篇,下一步读什么
优先读同系列或同问题的下一篇,把单篇消费变成主题集群。
【Ceph RADOS】RADOS 内核全景:五轴坐标系与 18 篇路线
补齐站内 distributed/38 之上的 RADOS 内核层缺口;定义 Map 传播、通信、一致性、本地存储、接入代价五条坐标系,给出 18 篇阅读路线与系列边界。
【Ceph RADOS】MON/MGR 与 Map:quorum、epoch 传播与客户端可见性
拆解 Ceph MON Paxos quorum、OSDMap/PGMap 结构与 epoch 传播机制;说明 MGR 主备模型与模块框架;分析客户端 epoch 缓存与可见性窗口的边界条件。
【Ceph RADOS】OSD 与 Messenger:daemon 结构、msgr2 协议与 Op 入队路径
拆解 ceph-osd daemon 内部结构、msgr2 Frame 格式与握手阶段,追踪 Op 从 TCP socket 经 AsyncMessenger、OSD::dispatch_context 到 ShardedOpWQ 的完整入队路径。
【Ceph RADOS】CRUSH 加深:rule/bucket/weight 机制与失败域语义
相对 distributed/38 的 CRUSH 算法概览,深入 bucket 类型的算法复杂度差异、rule step 语义、weight 体系与 primary_affinity,分析 chooseleaf 与失败域的精确定义及 CRUSH 与一致性哈希的本质区别。