Ceph OSD EC(纠删码)实现过程与原理分析
1. 概述
EC(Erasure Code,纠删码)是 Ceph 中一种重要的数据冗余方式,相比传统的多副本复制,EC 可以提供更高的存储效率。例如,使用 k=4, m=2 的 EC 配置(即 4+2),可以将 6 个数据块编码为 4 个数据块和 2 个校验块,在保证可以容忍 2 个块丢失的同时,存储效率为 66.7%(相比 3 副本的 33.3%)。
1.1 核心概念
- k(数据块数):原始数据被分割成的数据块数量
- m(校验块数):通过编码生成的校验块数量
- 条带(Stripe):数据被分割和编码的基本单位
- 分片(Shard):每个条带块存储在不同的 OSD 上,称为一个分片
- 条带宽度(Stripe Width):一个条带的总大小 = k * chunk_size
1.2 架构设计
EC 实现采用了分层架构:
1 | PrimaryLogPG (上层) |
2. 核心组件
2.1 PGBackend 接口
PGBackend 是后端抽象接口,定义了统一的接口规范:
- Listener:由上层(PrimaryLogPG)实现,提供回调接口
- RecoveryHandle:恢复操作句柄
- 核心方法:
submit_transaction():提交事务objects_read_sync():同步读取对象objects_read_and_reconstruct():异步读取并重构对象recover_object():恢复对象
2.2 ECBackend
ECBackend 是 EC 实现的核心类,继承自 ECCommon:
1 | class ECBackend : public ECCommon { |
2.3 ECCommon
ECCommon 提供 EC 操作的通用接口和数据结构:
- ec_extent_t:扩展结构,包含错误码、扩展映射和分片扩展映射
- read_request_t:读取请求结构
- read_result_t:读取结果结构
- shard_read_t:分片读取结构
2.4 ECUtil
ECUtil 提供 EC 相关的工具函数和数据结构:
- stripe_info_t:条带信息,包含 k、m、chunk_size 等
- shard_extent_set_t:分片扩展集合
- shard_extent_map_t:分片扩展映射
- 对齐操作:EC_ALIGN_SIZE = 4KB,所有操作必须按 4KB 对齐
3. 写入流程
3.1 整体流程
1 | 客户端写入请求 |
3.2 写入计划(WritePlan)
ECTransaction::WritePlan 负责规划写入操作:
1 | struct WritePlan { |
写入计划生成逻辑:
- 分析写入范围:确定哪些条带需要更新
- 确定读取需求:
- 部分写入(Partial Write):需要读取现有数据
- 全条带写入:不需要读取
- 确定写入分片:
- 数据分片(0 到 k-1):写入更新的数据块
- 校验分片(k 到 k+m-1):写入重新计算的校验块
3.3 读-修改-写(RMW)流程
对于部分写入,需要执行 RMW 操作:
1 | 1. 读取现有数据 |
3.4 编码过程
编码使用 ErasureCodeInterface::encode():
1 | // 伪代码 |
3.5 分片写入
编码完成后,将数据块发送到对应的分片 OSD:
1 | // ECBackend::handle_sub_write() |
4. 读取流程
4.1 整体流程
1 | 客户端读取请求 |
4.2 读取策略
EC 读取有两种策略:
快速读取(Fast Read):
- 只从 k 个数据分片读取
- 不需要解码,直接拼接数据
- 适用于所有数据分片可用的情况
降级读取(Degraded Read):
- 某些数据分片不可用
- 需要从 k 个可用分片(可能包括校验分片)读取
- 使用
ErasureCodeInterface::decode()解码重构数据
4.3 解码过程
解码使用 ErasureCodeInterface::decode():
1 | // 伪代码 |
4.4 读取管道(ReadPipeline)
ReadPipeline 管理异步读取流程:
1 | class ReadPipeline { |
5. 恢复流程
5.1 恢复触发
恢复在以下情况触发:
- Peering 完成后:发现某些分片缺失数据
- OSD 上线:需要回填数据
- 手动触发:管理员命令
5.2 恢复流程
1 | 检测到缺失对象 |
5.3 恢复源选择
恢复源选择遵循以下原则:
- 优先使用数据分片:如果 k 个数据分片都可用,直接读取
- 使用校验分片:如果某些数据分片不可用,使用校验分片解码
- 最小化网络传输:优先选择本地或网络距离近的分片
5.4 恢复后端(ECRecoveryBackend)
ECRecoveryBackend 专门处理恢复操作:
1 | class ECRecoveryBackend : public RecoveryBackend { |
6. 条带对齐与扩展管理
6.1 对齐要求
EC 操作必须按 4KB(EC_ALIGN_SIZE)对齐:
- 原因:编码算法要求数据块大小固定
- 实现:所有读写操作都对齐到 4KB 边界
- 影响:部分写入可能需要读取整个条带
6.2 扩展(Extent)管理
EC 使用扩展(Extent)来管理数据范围:
1 | // 扩展集合:表示连续的偏移量区间 |
6.3 扩展缓存(ExtentCache)
ECExtentCache 缓存已读取的扩展,避免重复读取:
1 | class ECExtentCache { |
7. 部分写入优化
7.1 问题
传统 RMW 流程需要:
- 读取整个条带(k+m 个分片)
- 修改数据
- 重新编码
- 写入所有分片(k+m 个)
这会产生大量网络 I/O。
7.2 优化策略
Ceph 实现了多种优化:
校验增量写入(Parity Delta Write):
- 只读取受影响的数据分片
- 计算校验增量(delta)
- 只更新校验分片
子块(Sub-chunk)优化:
- 对于大对象,可以按子块处理
- 减少需要读取的数据量
零去重(Zero Deduplication):
- 检测零缓冲区
- 用共享的零缓冲区替换,减少内存使用
8. 消息类型
EC 使用专门的消息类型进行分片间通信:
8.1 ECSubWrite
分片写入消息:
1 | struct ECSubWrite { |
8.2 ECSubRead
分片读取消息:
1 | struct ECSubRead { |
8.3 ECSubWriteReply / ECSubReadReply
对应的回复消息。
9. 关键数据结构
9.1 stripe_info_t
条带信息:
1 | class stripe_info_t { |
9.2 shard_id_t
分片 ID 类型,用于标识不同的分片(0 到 k+m-1)。
9.3 ec_align_t
对齐区域:
1 | struct ec_align_t { |
10. 性能优化
10.1 并行处理
- 并行读取:同时从多个分片读取
- 并行写入:同时写入多个分片
- 异步操作:使用回调机制,避免阻塞
10.2 缓存策略
- 扩展缓存:缓存已读取的扩展
- 对象上下文缓存:缓存对象元数据
- 零缓冲区共享:共享零缓冲区,减少内存
10.3 网络优化
- 批量消息:合并多个小消息
- 压缩:对大数据进行压缩
- 优先级队列:区分恢复和客户端 I/O 的优先级
11. 错误处理
11.1 分片故障
当某些分片不可用时:
- 读取:使用可用分片解码重构数据
- 写入:如果写入分片不足,等待或使用临时分片
- 恢复:触发恢复流程,修复缺失数据
11.2 数据不一致
检测到数据不一致时:
- Scrub:定期检查数据完整性
- Repair:修复不一致的数据
- 日志记录:记录错误信息
12. 总结
EC 实现的核心要点:
- 分层架构:清晰的接口和实现分离
- 异步处理:使用管道和回调机制
- 对齐要求:所有操作按 4KB 对齐
- 优化策略:多种优化减少 I/O
- 容错能力:可以容忍 m 个分片故障
EC 相比多副本复制的优势:
- 存储效率高:例如 4+2 配置效率为 66.7%,而 3 副本为 33.3%
- 可配置性强:可以根据需求选择不同的 k 和 m
- 适合大对象:对于大对象,EC 的优势更明显
EC 的劣势:
- 计算开销:编码和解码需要 CPU 计算
- 部分写入性能:部分写入需要 RMW,性能较差
- 恢复复杂度:恢复流程比多副本复杂
13. 相关文件
核心文件
ECBackend.h/cc:EC 后端主实现ECCommon.h/cc:EC 通用功能ECUtil.h/cc:EC 工具函数ECTransaction.h/cc:EC 事务处理ECExtentCache.h/cc:扩展缓存
消息类型
ECMsgTypes.h/cc:EC 消息类型定义MOSDECSubOpWrite.h:分片写入消息MOSDECSubOpRead.h:分片读取消息
编码算法
erasure-code/ErasureCodeInterface.h:编码算法接口- 具体实现:jerasure、isa、shec 等插件
14. EC 与三副本(Replicated)详细对比
14.1 基本概念对比
| 特性 | EC(纠删码) | 三副本(Replicated) |
|---|---|---|
| 数据组织 | 数据被编码成 k 个数据块 + m 个校验块 | 数据完整复制 3 份 |
| 存储位置 | 每个块存储在不同的 OSD(分片) | 每个副本存储在不同的 OSD |
| 容错能力 | 可以容忍 m 个分片丢失 | 可以容忍 2 个副本丢失 |
| 存储效率 | 高(例如 4+2 为 66.7%) | 低(33.3%) |
| 典型配置 | k=4, m=2(4+2)或 k=8, m=3(8+3) | size=3 |
14.2 架构实现对比
EC 架构
1 | PrimaryLogPG |
三副本架构
1 | PrimaryLogPG |
14.3 写入流程对比
EC 写入流程
1 | 1. 客户端写入请求 |
特点:
- 需要编码计算(CPU 开销)
- 必须写入所有 k+m 个分片
- 部分写入需要 RMW,性能较差
- 所有操作必须 4KB 对齐
三副本写入流程
1 | 1. 客户端写入请求 |
特点:
- 无需编码计算
- 只需写入 3 个副本
- 部分写入直接覆盖,性能好
- 无对齐要求
14.4 读取流程对比
EC 读取流程
1 | 1. 客户端读取请求 |
特点:
- 快速读取:只需读取 k 个分片,无需解码
- 降级读取:需要解码计算
- 最小读取分片数:k 个
三副本读取流程
1 | 1. 客户端读取请求 |
特点:
- 直接读取,无需解码
- 只需读取 1 个副本
- 性能最优
14.5 恢复流程对比
EC 恢复流程
1 | 1. 检测到缺失分片 |
特点:
- 需要编码/解码计算
- 恢复源选择复杂
- 恢复流程复杂
三副本恢复流程
1 | 1. 检测到缺失副本 |
特点:
- 无需编码/解码
- 恢复源选择简单(任意可用副本)
- 恢复流程简单
14.6 性能对比
| 操作类型 | EC | 三副本 | 说明 |
|---|---|---|---|
| 全条带写入 | 中等 | 快 | EC 需要编码,但可以并行写入 |
| 部分写入 | 慢 | 快 | EC 需要 RMW,三副本直接覆盖 |
| 顺序读取 | 快 | 最快 | EC 快速读取只需 k 个分片 |
| 随机读取 | 中等 | 快 | EC 需要对齐,可能读取更多数据 |
| 降级读取 | 慢 | 快 | EC 需要解码计算 |
| 恢复速度 | 慢 | 快 | EC 需要编码/解码 |
| CPU 开销 | 高 | 低 | EC 需要编码/解码计算 |
| 网络开销 | 中等 | 高 | EC 写入 k+m 个分片,三副本写入 3 个副本 |
14.7 存储效率对比
EC 存储效率计算
1 | 配置:k=4, m=2 (4+2) |
三副本存储效率
1 | 配置:size=3 |
14.8 适用场景对比
EC 适用场景
✅ 适合的场景:
- 大对象存储:对象越大,EC 优势越明显
- 冷数据/归档数据:访问频率低,对性能要求不高
- 存储成本敏感:需要更高的存储效率
- 顺序读写为主:避免部分写入的 RMW 开销
- 数据量巨大:存储效率带来的成本节省显著
❌ 不适合的场景:
- 小对象频繁写入:RMW 开销大
- 随机写入为主:对齐要求导致额外 I/O
- 对延迟敏感:编码/解码增加延迟
- CPU 资源受限:编码/解码需要 CPU 计算
三副本适用场景
✅ 适合的场景:
- 高性能要求:需要低延迟、高吞吐
- 小对象频繁写入:直接覆盖,无额外开销
- 随机访问:无对齐要求,性能好
- 热数据:访问频率高,性能优先
- 简单运维:恢复流程简单
❌ 不适合的场景:
- 存储成本敏感:存储效率低
- 大容量冷数据:存储成本高
- 存储空间受限:需要更高的存储效率
14.9 代码实现对比
EC 关键代码路径
1 | // 写入 |
三副本关键代码路径
1 | // 写入 |
14.10 消息类型对比
EC 消息类型
ECSubWrite:分片写入消息ECSubRead:分片读取消息ECSubWriteReply:分片写入确认ECSubReadReply:分片读取响应PushOp:恢复推送操作
三副本消息类型
RepOp:复制操作消息RepOpReply:复制操作确认PushOp:恢复推送操作PullOp:恢复拉取操作
14.11 数据一致性对比
| 特性 | EC | 三副本 |
|---|---|---|
| 写入一致性 | 需要所有 k+m 个分片确认 | 需要所有 3 个副本确认 |
| 读取一致性 | 从 k 个分片读取,可能解码 | 从主副本读取 |
| 部分写入 | 需要 RMW,可能影响一致性 | 直接覆盖,一致性简单 |
| 对齐要求 | 必须 4KB 对齐 | 无对齐要求 |
14.12 运维复杂度对比
| 方面 | EC | 三副本 |
|---|---|---|
| 配置复杂度 | 需要选择 k 和 m | 只需设置 size=3 |
| 恢复复杂度 | 复杂(需要编码/解码) | 简单(直接复制) |
| 故障排查 | 复杂(涉及分片和解码) | 简单(直接查看副本) |
| 性能调优 | 复杂(涉及对齐、RMW 优化) | 相对简单 |
14.13 成本分析
存储成本(以 100TB 原始数据为例)
EC (4+2):
- 实际存储:100TB / 0.667 ≈ 150TB
- 存储成本:150TB × 单价
三副本:
- 实际存储:100TB × 3 = 300TB
- 存储成本:300TB × 单价
节省:EC 可以节省约 50% 的存储空间
计算成本
EC:
- CPU 开销:编码/解码需要 CPU 计算
- 网络开销:写入 k+m 个分片
三副本:
- CPU 开销:几乎无额外计算
- 网络开销:写入 3 个副本
14.14 总结对比表
| 维度 | EC | 三副本 | 胜者 |
|---|---|---|---|
| 存储效率 | 66.7% (4+2) | 33.3% | ✅ EC |
| 写入性能(全条带) | 中等 | 快 | ✅ 三副本 |
| 写入性能(部分) | 慢 | 快 | ✅ 三副本 |
| 读取性能 | 快 | 最快 | ✅ 三副本 |
| CPU 开销 | 高 | 低 | ✅ 三副本 |
| 恢复速度 | 慢 | 快 | ✅ 三副本 |
| 运维复杂度 | 复杂 | 简单 | ✅ 三副本 |
| 适用对象大小 | 大对象 | 任意 | - |
| 成本 | 低 | 高 | ✅ EC |
14.15 选择建议
选择 EC 的情况:
- 存储成本是主要考虑因素
- 数据主要是大对象
- 以顺序读写为主
- 对延迟不敏感
- 有足够的 CPU 资源
选择三副本的情况:
- 性能是主要考虑因素
- 数据主要是小对象
- 随机访问较多
- 对延迟敏感
- CPU 资源受限
- 需要简单的运维
混合使用:
- 热数据使用三副本(性能优先)
- 冷数据使用 EC(成本优先)
- 通过 Ceph 的 tiering 功能自动迁移
正在加载留言…