Ceph 与 CRUSH:去中心化存储的工程实现
一个 200 节点的 HDFS 集群,NameNode 进程 OOM,整个集群立即不可读写。运维团队花了 40 分钟从备份恢复元数据镜像,重放 EditLog,等待所有 DataNode 上报块信息。在这 40 分钟里,依赖该集群的数据管线全部停滞。
这不是假设场景。Google 内部在 GFS 时代就反复遭遇单 Master 的可用性瓶颈,最终在 Colossus 中改为分布式元数据架构。而在开源世界,2006 年 Sage Weil 在 OSDI 上发表的 Ceph 论文,从一开始就选择了另一条路——彻底消除中心化元数据服务器。
Ceph 的核心思路:不维护”对象在哪里”的映射表,而是用一个确定性算法(CRUSH)在客户端本地计算出对象的存储位置。只要集群拓扑信息(CRUSH Map)一致,任何节点都能独立计算出相同结果,不需要查询中心服务器。
这个设计使得 Ceph 从 2006 年 Sage Weil 博士论文中的研究原型,发展为 Red Hat、SUSE 商业支持的生产级存储系统,在 OpenStack 生态中几乎成为默认的后端存储方案。本文深入分析 CRUSH 算法的工作原理、Ceph 的工程架构,以及实际运维中的关键问题。
一、去中心化架构:为什么不要元数据服务器
1.1 中心化元数据的代价
上一篇讨论的 GFS/HDFS 使用单 Master(NameNode)管理所有文件到块的映射。这个设计简洁有效,但有三个固有限制:
元数据瓶颈。所有文件的打开、创建、删除操作都必须经过 NameNode。当文件数量达到数亿级别时,NameNode 的内存和 CPU 成为整个系统的天花板。HDFS Federation 通过多个 NameNode 分管不同命名空间来缓解,但增加了运维复杂度,且跨命名空间操作仍然受限。
单点恢复时间。NameNode 故障后,Standby NameNode 需要加载 fsimage、重放 EditLog、等待 DataNode 块汇报。在大规模集群中,这个恢复过程可以持续数十分钟。HDFS HA 通过热备和 Journal Node 把切换时间缩短到秒级,但增加了架构复杂度和运维成本。
扩展非线性。添加存储节点时,元数据量随之增长,但元数据服务器的容量并不随存储节点线性扩展。100PB 的集群和 10PB 的集群对 NameNode 的内存需求差异巨大。
1.2 Ceph 的思路:计算代替查询
Ceph 的核心洞察是:如果数据的存储位置可以用算法计算出来,就不需要维护位置映射表。
在传统架构中,客户端读取数据的流程是:
Client -> MetadataServer: "文件 X 的第 3 个块在哪?"
MetadataServer -> Client: "在 Node-7, Node-12, Node-23"
Client -> Node-7: "读取块数据"
Ceph 的流程是:
Client: hash("pool_id.object_name") mod pg_num -> PG_id
Client: CRUSH(PG_id, CRUSH_Map) -> [OSD.7, OSD.12, OSD.23]
Client -> OSD.7: "读取对象数据"
客户端只需要知道两件事:存储池(Pool)的 PG 数量,以及集群拓扑信息(CRUSH Map)。这两个信息变化不频繁,可以缓存在客户端。绝大多数 I/O 操作不需要和任何中心服务器通信。
1.3 RADOS:Ceph 的基础层
Ceph 的一切建立在 RADOS(Reliable Autonomic Distributed Object Store,可靠自律分布式对象存储)之上。RADOS 层提供:
- 对象的可靠存储和复制
- 数据一致性保证
- 故障检测和自动恢复
- 数据再平衡
RADOS 不是文件系统,而是扁平的对象存储:每个对象有一个名称和一段二进制数据,没有目录层次。文件系统语义(CephFS)、块设备语义(RBD)、S3/Swift 接口(RadosGW)都是建立在 RADOS 之上的适配层。
上图展示了 Ceph 的分层架构。最上层是三种客户端接口,通过 LIBRADOS 库与底层 RADOS 集群通信。RADOS 内部由 Monitor(MON)、Manager(MGR)、Metadata Server(MDS)和 OSD(Object Storage Daemon)四种守护进程组成。CRUSH 算法在客户端执行,将对象映射到具体的 OSD 集合。
二、CRUSH 算法:可控的伪随机放置
2.1 设计目标
CRUSH(Controlled Replication Under Scalable Hashing,可扩展哈希下的可控复制)发表于 2006 年 SC 会议。算法的设计目标是:
- 去中心化:任何节点都能独立计算数据位置,不需要中央查询
- 确定性:相同输入产生相同输出,所有节点的计算结果一致
- 伪随机均匀:数据在存储设备间近似均匀分布
- 拓扑感知:副本分散在不同故障域(Failure Domain),例如不同机架、不同机房
- 最小迁移:添加或移除设备时,只迁移必要的数据,而不是全局重新分布
目标 5 是 CRUSH 相对于简单一致性哈希的关键优势。一致性哈希在节点变更时也只迁移少量数据,但它不支持故障域感知。CRUSH 同时实现了拓扑感知和最小迁移。
2.2 CRUSH Map:集群的拓扑描述
CRUSH 算法的输入之一是 CRUSH Map,它用树形结构描述集群的物理拓扑:
root default
├── datacenter dc-beijing
│ ├── rack rack-01
│ │ ├── host node-001
│ │ │ ├── osd.0 (weight: 3.6)
│ │ │ ├── osd.1 (weight: 3.6)
│ │ │ └── osd.2 (weight: 1.8)
│ │ └── host node-002
│ │ ├── osd.3 (weight: 3.6)
│ │ └── osd.4 (weight: 3.6)
│ └── rack rack-02
│ └── host node-003
│ ├── osd.5 (weight: 7.2)
│ └── osd.6 (weight: 7.2)
└── datacenter dc-shanghai
└── rack rack-03
└── host node-004
├── osd.7 (weight: 3.6)
└── osd.8 (weight: 3.6)
树的叶子节点是 OSD(对应物理磁盘),每个 OSD 有一个权重值(weight),通常设为磁盘容量(TB)。中间节点是 Bucket,代表故障域层级:主机(host)、机架(rack)、数据中心(datacenter)。根节点是 root。
权重决定了该设备被选中的概率。一块 3.6TB 的磁盘和一块 7.2TB 的磁盘,后者被选中的概率大约是前者的两倍,从而实现按容量比例分配数据。
用 ceph osd tree 可以查看当前集群的 CRUSH
层级:
ceph osd tree输出示例:
ID CLASS WEIGHT TYPE NAME STATUS REWEIGHT
-1 43.20000 root default
-2 21.60000 datacenter dc-beijing
-3 16.20000 rack rack-01
-4 9.00000 host node-001
0 hdd 3.60000 osd.0 up 1.00000
1 hdd 3.60000 osd.1 up 1.00000
2 ssd 1.80000 osd.2 up 1.00000
-5 7.20000 host node-002
3 hdd 3.60000 osd.3 up 1.00000
4 hdd 3.60000 osd.4 up 1.00000
2.3 CRUSH 放置规则
CRUSH Map 还包含放置规则(Placement Rule),定义如何从拓扑树中选择 OSD。一条典型规则:
rule replicated_rule {
id 0
type replicated
step take default
step chooseleaf firstn 0 type rack
step emit
}
含义:从根节点 default
出发,选择若干个不同的 rack,在每个 rack
中向下选到叶子节点(OSD)。firstn 0
表示选择数量等于存储池的副本数(通常是 3)。
这条规则保证了三个副本分布在三个不同机架上。如果把
type rack 改为
type host,则保证副本分布在不同主机上(同一机架的不同主机也可以)。
2.4 算法核心流程
CRUSH 的核心是一个递归的 Bucket 选择算法。给定一个输入值
x(通常是 PG ID)和需要选择的副本数
r,算法在 CRUSH Map
的树形结构中自顶向下选择节点。
伪代码如下:
def crush_select(bucket, x, num_replicas, replica_type):
"""
从 bucket 中选择 num_replicas 个类型为 replica_type 的子节点
"""
result = []
for r in range(num_replicas):
retry = 0
while True:
# 在 bucket 中做伪随机选择
item = bucket.choose(x, r + retry)
# 如果选中的是目标类型,检查冲突
if item.type == replica_type or item.is_leaf():
if item in result: # 已选过,重试
retry += 1
continue
if item.is_failed() or item.is_overloaded():
retry += 1
continue
result.append(item)
break
else:
# 还没到目标层级,递归向下
sub = crush_select(item, x, 1, replica_type)
if sub[0] in result:
retry += 1
continue
result.append(sub[0])
break
return result关键在 bucket.choose(x, r) 的实现。Ceph
支持多种 Bucket 选择算法,最常用的是 Straw2(稻草抽签算法第
2 版):
def straw2_choose(bucket, x, r):
"""
Straw2 算法:为每个子节点计算一个"签长",选最长签的节点
"""
max_draw = -1
selected = None
for item in bucket.children:
# 用输入值 x、副本编号 r 和子节点 ID 做哈希
draw = crush_hash(x, item.id, r)
# 按权重调整:权重越大,签越长
draw = draw / log(1.0 / (draw_as_uniform / MAX_INT))
draw = draw * item.weight # 简化表示
if draw > max_draw:
max_draw = draw
selected = item
return selectedStraw2 的核心性质:当添加或移除一个 OSD 时,只有该 OSD 相关的数据需要迁移。其他 OSD 之间不发生数据搬迁。这是因为每个 OSD 的”签长”独立计算,一个 OSD 的加入或退出不影响其他 OSD 之间的相对排序。
早期版本使用的 Straw(v1)算法在某些场景下存在不必要的数据迁移——移除一个 OSD 可能导致其他 OSD 之间也发生数据搬迁。Straw2 修复了这个问题,是目前的默认选择。
2.5 与一致性哈希的对比
| 维度 | 一致性哈希 | CRUSH |
|---|---|---|
| 拓扑感知 | 不支持 | 支持多层故障域 |
| 权重分配 | 通过虚拟节点近似 | 原生支持精确权重 |
| 最小迁移 | 是 | 是 |
| 副本放置控制 | 无 | 完全可控(放置规则) |
| 计算复杂度 | O(log N) | O(N) 每层 Bucket |
| 适用场景 | 缓存、负载均衡 | 持久存储、多副本 |
一致性哈希用虚拟节点近似均匀分布,但虚拟节点数量需要精心调参。CRUSH 通过 Straw2 算法和权重直接实现按容量分配,且天然支持”三副本必须在三个不同机架”这类约束。
2.6 CRUSH 放置流程详解
为了具体理解 CRUSH
的工作过程,以一个实际对象的放置为例。假设客户端需要存储对象
photo.jpg 到存储池 images(pool
id=3,pg_num=256,3 副本,放置规则要求不同机架)。
完整的映射过程如下:
第一步:对象到 PG
的映射。对对象名计算哈希值:hash("photo.jpg") = 0x7A3F...,取模得到
PG 编号:0x7A3F... mod 256 = 161。PG
的完整标识为 3.a1(池 ID.PG
编号的十六进制表示)。
第二步:PG 到 OSD 集合的映射。以 PG ID
3.a1 作为输入,执行 CRUSH 算法。算法从 CRUSH
Map 的根节点 default 开始,按照放置规则
chooseleaf firstn 3 type rack
进行三轮选择:
- 第一轮(r=0):
crush_hash(3.a1, r=0)在根节点的子 Bucket 中做 Straw2 抽签。假设rack-01中签最长,被选中。然后在rack-01内递归向下,经过host node-002,最终选中osd.5(权重 3.6)。 - 第二轮(r=1):
crush_hash(3.a1, r=1)再次在根节点做 Straw2 抽签,要求选择与第一轮不同的 rack。假设选中rack-02,递归向下选中osd.12(权重 7.2)。 - 第三轮(r=2):
crush_hash(3.a1, r=2)选中rack-03,递归向下选中osd.17(权重 3.6)。
最终结果:对象 photo.jpg
的三个副本分别存储在
OSD.5、OSD.12、OSD.17
上,分布在三个不同机架,满足故障域隔离要求。第一个
OSD(OSD.5)是 Primary,负责协调该 PG 内的读写操作。
flowchart TD
A["对象: photo.jpg<br/>池: images (id=3)"] --> B["hash(photo.jpg) mod 256"]
B --> C["PG 3.a1"]
C --> D["CRUSH 算法输入:<br/>PG ID + CRUSH Map + 放置规则"]
D --> E{"从根节点选择<br/>3 个不同 rack"}
E -->|"r=0: Straw2 抽签"| F["rack-01 → node-002 → OSD.5"]
E -->|"r=1: Straw2 抽签"| G["rack-02 → node-003 → OSD.12"]
E -->|"r=2: Straw2 抽签"| H["rack-03 → node-004 → OSD.17"]
F --> I["Acting Set: [OSD.5, OSD.12, OSD.17]<br/>Primary: OSD.5"]
G --> I
H --> I
该流程图展示了 CRUSH 放置算法的两阶段映射过程。第一阶段通过简单的哈希取模将对象映射到 PG,将数十亿对象的放置问题降维为数千个 PG 的放置问题。第二阶段是 CRUSH 算法的核心——在拓扑树中自顶向下执行多轮 Straw2 抽签,每轮选择一个不同的故障域,确保副本在物理拓扑上充分隔离。整个过程是纯计算的,任何持有相同 CRUSH Map 的节点都能独立得出相同结果。
三、CRUSH Map 层级与故障域
3.1 故障域的工程意义
分布式存储的可靠性建立在一个前提上:副本存储在独立故障域中。如果三个副本都在同一台机器的三块磁盘上,机器故障就意味着三副本全部丢失。如果三个副本在同一个机架的三台机器上,机架交换机故障或电源故障就导致三副本同时不可达。
CRUSH Map 的层级结构和放置规则精确控制副本的分散程度。实际部署中常见的层级配置:
| 部署规模 | 放置规则中的故障域 | 含义 |
|---|---|---|
| 单机房少量节点 | type host |
每个副本在不同主机 |
| 单机房多机架 | type rack |
每个副本在不同机架 |
| 多机房 | type datacenter |
每个副本在不同数据中心 |
| 混合 SSD/HDD | type host,按 device class 分离 |
SSD 池和 HDD 池独立 |
3.2 设备类(Device Class)
Ceph Luminous(12.x)引入了设备类特性。同一集群中可以混合 HDD 和 SSD,通过设备类将它们归入不同的逻辑子树:
# 查看设备类
ceph osd crush tree --show-shadow
# 创建基于 SSD 的存储规则
ceph osd crush rule create-replicated ssd_rule default host ssd这在生产环境中非常实用。典型做法是:RBD 卷使用 SSD 池获得低延迟,RadosGW 的对象数据使用 HDD 池获得大容量。
3.3 CRUSH Map 修改的风险
修改 CRUSH Map 会触发数据再平衡。如果修改不当,可能导致大量数据迁移,占满网络带宽和磁盘 I/O,影响前台业务。
安全操作要点:
# 修改前先导出 CRUSH Map
ceph osd getcrushmap -o crushmap.bin
crushtool -d crushmap.bin -o crushmap.txt
# 编辑后先测试
crushtool -c crushmap.txt -o crushmap-new.bin
crushtool --test -i crushmap-new.bin --show-mappings --rule 0 --num-rep 3
# 确认迁移量可接受后再注入
ceph osd setcrushmap -i crushmap-new.bincrushtool --test 可以模拟新 CRUSH Map
下的数据分布,估算迁移量。生产环境中,任何 CRUSH Map
变更都应该先在测试环境验证。
四、三种存储接口
Ceph 通过三种接口满足不同的存储需求,它们都建立在 RADOS 之上:
4.1 RBD:RADOS 块设备
RBD(RADOS Block Device)提供虚拟块设备接口,主要用于虚拟化和容器场景。
工作原理。一个 RBD
卷(image)被切分为固定大小的对象(默认 4MB),每个对象以
{pool_name}.rbd_data.{image_id}.{offset_hex}
的命名规则存入 RADOS。客户端通过 Linux
内核模块(krbd)或 librbd 库访问
RBD
卷,操作系统看到的是一个标准块设备(/dev/rbd0)。
# 创建一个 100GB 的 RBD 卷
rbd create mypool/myimage --size 102400
# 映射到内核块设备
rbd map mypool/myimage
# /dev/rbd0
# 格式化并挂载
mkfs.xfs /dev/rbd0
mount /dev/rbd0 /mnt/ceph-block快照与克隆。RBD 支持 COW(Copy-on-Write)快照。创建快照是瞬时操作,不拷贝数据。基于快照可以创建克隆卷,多个克隆共享快照的数据,写入时才分配独立存储。这个特性在 OpenStack 中被大量用于虚拟机镜像管理——一个基础镜像可以快速克隆出数百个虚拟机磁盘。
主要使用场景:OpenStack Cinder 卷后端、Kubernetes Persistent Volume(通过 CSI 驱动)、虚拟机磁盘。
4.2 CephFS:POSIX 文件系统
CephFS 在 RADOS 之上实现了符合 POSIX 语义的分布式文件系统。
架构。CephFS 引入了 MDS(Metadata Server,元数据服务器)守护进程来管理文件系统的目录树、inode、权限等元数据。MDS 本身是无状态的——所有元数据持久化在 RADOS 的专用元数据池中。MDS 故障后,另一个 MDS 实例可以从元数据池恢复状态并接管服务。
# 创建 CephFS
ceph fs new myfs metadata_pool data_pool
# 内核挂载
mount -t ceph mon1:6789:/ /mnt/cephfs -o name=admin,secret=<key>
# FUSE 挂载(用户态,不需要内核模块)
ceph-fuse /mnt/cephfs动态子树分区(Dynamic Subtree Partitioning)。这是 CephFS 的独特设计。当存在多个活跃 MDS 时,目录树被自动分区到不同的 MDS 实例上。热点目录会被迁移到负载较轻的 MDS。这个过程完全自动,不需要管理员手动规划命名空间(对比 HDFS Federation 需要手动分配命名空间)。
但 CephFS 的 POSIX
兼容性也带来性能代价。ls -la
一个大目录需要获取所有文件的 stat
信息,在分布式环境中意味着大量元数据请求。在 HDFS
的典型使用场景(大文件顺序 I/O)下,CephFS 的 POSIX
语义是多余的开销。
主要使用场景:共享文件存储、HPC 高性能计算环境、需要 POSIX 语义的传统应用迁移。
4.3 RadosGW:对象存储网关
RadosGW(RADOS Gateway)提供兼容 Amazon S3 和 OpenStack Swift 的 RESTful 对象存储接口。
架构。RadosGW 是一个无状态的 HTTP 网关进程(基于 Civetweb 或 Beast),接收客户端的 HTTP 请求,转换为 RADOS 操作。由于无状态,可以通过部署多个 RadosGW 实例并用负载均衡器分发流量来实现水平扩展。
# 使用 S3 API 上传对象
aws s3 cp myfile.dat s3://mybucket/myfile.dat \
--endpoint-url http://radosgw.example.com:7480
# 列举对象
aws s3 ls s3://mybucket/ \
--endpoint-url http://radosgw.example.com:7480多站点同步。RadosGW 支持多集群之间的异步数据同步,实现跨地域的对象存储。同步基于变更日志(changelog),支持 Active-Active 模式(两个站点都可以读写)和 Active-Passive 模式。
主要使用场景:私有云对象存储(替代 S3)、备份归档、日志存储、静态资源托管。
4.4 三种接口的选型
| 维度 | RBD | CephFS | RadosGW |
|---|---|---|---|
| 访问语义 | 块设备 | POSIX 文件系统 | HTTP REST |
| 客户端形态 | 内核模块 / librbd | 内核模块 / FUSE | HTTP 客户端 |
| 多客户端共享 | 单客户端(除非只读) | 多客户端 | 多客户端 |
| 典型延迟 | 亚毫秒 | 毫秒级 | 毫秒到数十毫秒 |
| 适合场景 | 数据库、虚拟机 | 共享存储、HPC | 备份、归档、媒体 |
五、PG:对象到 OSD 的中间层
5.1 为什么需要 PG
一个 Ceph 集群可能存储数十亿个对象,而 OSD 数量通常在数百到数千。如果直接把每个对象映射到一组 OSD,CRUSH 需要为每个对象独立计算并维护映射关系,开销不可接受。
PG(Placement Group,放置组)是对象和 OSD 之间的中间抽象层。映射过程分两步:
Step 1: object_name -> PG
pg_id = hash(object_name) mod pg_num
Step 2: PG -> OSD set
osd_set = CRUSH(pg_id, crush_map, placement_rule)
第一步是简单的哈希取模,代价极低。第二步是 CRUSH 算法,但输入数量从数十亿个对象降低为数千个 PG,计算量大幅减少。
每个 PG 对应一组 OSD(例如 3 个,对应 3 副本)。PG 中的第一个 OSD 是 Primary,负责协调该 PG 内所有对象的读写和复制。
5.2 PG 数量的选择
PG 数量是 Ceph 集群最关键的配置参数之一。Red Hat 官方文档给出的推荐公式:
Total PGs = (OSD 数量 × 100) / 副本数
然后向上取 2 的幂次。例如,100 个 OSD、3 副本:
Total PGs = (100 × 100) / 3 ≈ 3333
向上取 2 的幂 → 4096
PG 太少的问题:数据分布不均匀,某些 OSD 的负载显著高于其他 OSD。PG 太多的问题:每个 OSD 需要处理和跟踪更多的 PG,内存开销和 Peering 时间增加。Ceph Nautilus(14.x)引入了 PG Autoscaler,可以自动调整存储池的 PG 数量,减少了手动计算的负担。
# 查看 PG 自动调整建议
ceph osd pool autoscale-status
# 启用自动调整
ceph osd pool set mypool pg_autoscale_mode on5.3 PG 状态机
PG 是 Ceph 内部状态管理的核心单元。每个 PG 维护一个复杂的状态机,反映其健康状况和当前操作。常见的状态组合:
| 状态 | 含义 |
|---|---|
active+clean |
正常。所有副本就位,可以正常读写 |
active+undersized |
副本数不足,但仍可读写 |
peering |
OSD 之间正在协商 PG 的权威日志和数据版本 |
recovering |
正在从其他 OSD 恢复缺失的对象 |
backfilling |
正在向新 OSD 回填完整数据 |
remapped |
PG 映射到了新的 OSD 集合,但数据还没完全迁移 |
stale |
Primary OSD 未在规定时间内上报状态 |
incomplete |
无法找到足够的 OSD 来重建 PG 的完整数据 |
# 查看 PG 状态概览
ceph pg stat
# 查看异常 PG
ceph pg dump_stuck stale
ceph pg dump_stuck inactive
ceph pg dump_stuck uncleanactive+clean
是唯一的健康状态。生产环境中,运维的核心目标是让所有 PG
尽快回到 active+clean。
5.4 Peering 过程
当 OSD 变更(加入、退出、故障)导致 PG 的 OSD 集合发生变化时,PG 进入 Peering 阶段。Peering 的目标是让 PG 中所有 OSD 就以下问题达成一致:
- 哪些对象是最新版本
- 哪些对象需要恢复
- 谁持有权威的操作日志
Peering 过程中,Primary OSD 收集所有参与 OSD 的 PG
日志(pg_log),合并出最新状态。如果某个 OSD
的数据落后,它会被标记为需要恢复。Peering 完成后,PG 进入
active 状态,可以处理客户端请求。
这个过程的耗时取决于 PG 日志的长度和需要比对的 OSD 数量。大规模集群中,OSD 故障可能导致数千个 PG 同时 Peering,成为性能瓶颈。
5.5 PG 状态机全景
PG 的状态转移是 Ceph 数据可靠性的核心机制。以下状态图覆盖了 PG 从创建到故障恢复的完整生命周期:
stateDiagram-v2
[*] --> Creating : 存储池创建,PG 初始化
Creating --> Peering : OSD 分配完成,开始协商
Peering --> Active_Clean : 所有副本数据一致,就绪
Active_Clean --> Degraded : OSD 故障,副本数不足
Degraded --> Recovering : 从存活副本恢复数据
Recovering --> Active_Clean : 数据恢复完成,副本数恢复
Active_Clean --> Remapped : OSD 拓扑变更,PG 迁移到新 OSD
Remapped --> Backfilling : 向新 OSD 回填完整数据
Backfilling --> Active_Clean : 回填完成
Degraded --> Incomplete : 无法找到足够副本重建数据
Peering --> Stale : Primary OSD 长时间无响应
Stale --> Peering : Primary OSD 恢复,重新协商
该状态图揭示了 PG
状态转移的两条主要路径。正常恢复路径为:Active+Clean → Degraded → Recovering → Active+Clean,对应单个
OSD 故障后 Ceph
自动从存活副本恢复数据的过程。拓扑变更路径为:Active+Clean → Remapped → Backfilling → Active+Clean,对应新
OSD 加入或 CRUSH Map 调整导致 PG
迁移的过程。Incomplete
状态是数据安全的最后警告——它表示所有持有该 PG 数据的 OSD
均不可用,可能面临数据永久丢失。
5.6 OSD 故障恢复时序
当一个 OSD 发生故障时,Ceph 的自动恢复流程涉及 Monitor
检测、PG 状态转移和数据回填等多个阶段。以下时序图展示了从
OSD 故障到 PG 恢复 Active+Clean
的完整过程:
sequenceDiagram
participant MON as Monitor 集群
participant OSD_F as OSD.5(故障)
participant OSD_P as OSD.12(Primary)
participant OSD_S as OSD.17(Secondary)
participant OSD_N as OSD.8(新副本)
Note over OSD_F: OSD.5 进程崩溃
OSD_P->>MON: 心跳超时,报告 OSD.5 不可达
MON->>MON: 等待 mon_osd_down_out_interval(默认 600s)
MON-->>OSD_P: 更新 OSD Map:OSD.5 标记为 down+out
Note over OSD_P,OSD_S: PG 进入 Degraded 状态
OSD_P->>OSD_P: 重新计算 CRUSH:Acting Set 变更<br/>[OSD.12, OSD.17, OSD.8]
OSD_P->>OSD_S: Peering:交换 PG 日志,确定权威版本
OSD_P->>OSD_N: Peering:通知参与新的 Acting Set
OSD_S-->>OSD_P: PG 日志确认
OSD_N-->>OSD_P: 确认加入(数据为空)
Note over OSD_P,OSD_N: Backfill 阶段:向新 OSD 回填数据
OSD_P->>OSD_N: 逐对象发送数据(受限于 osd_max_backfills)
OSD_N-->>OSD_P: 对象写入确认
OSD_P->>OSD_N: 全部对象回填完成
Note over OSD_P,OSD_N: PG 恢复 Active+Clean
OSD_P->>MON: 报告 PG 状态:Active+Clean
该时序图展示了故障恢复的三个关键阶段。检测阶段由 Monitor
通过心跳超时判定 OSD 故障,并在
mon_osd_down_out_interval 等待窗口后将其标记为
out,这个延迟是为了避免短暂网络抖动触发不必要的数据迁移。Peering
阶段由 Primary OSD 主导,通过交换 PG
日志确定数据的权威版本,确保恢复的正确性。Backfill
阶段将数据逐对象复制到新的 OSD,受
osd_max_backfills 参数限速,在数据安全和前台
I/O 性能之间取得平衡。
六、强一致性保证
6.1 写入路径
Ceph 的写入采用 Primary-Copy 协议,保证强一致性(线性一致性)。写入流程:
1. Client 计算对象所属 PG,确定 Primary OSD
2. Client 发送写请求到 Primary OSD
3. Primary 为该写操作分配版本号(epoch + version)
4. Primary 将数据写入本地存储(BlueStore)
5. Primary 将数据转发给所有 Replica OSD
6. 每个 Replica 写入本地后,回复 Primary ACK
7. Primary 收到所有 Replica 的 ACK 后,向 Client 返回成功
注意第 7 步:只有所有副本都确认写入后,客户端才收到成功响应。这意味着任何一个副本 OSD 的延迟都会影响写入延迟。但好处是读取任何一个副本都能得到最新数据。
默认情况下 Ceph 的读操作只发送到 Primary
OSD。这保证了读取一致性(Primary
总是持有最新数据)。但如果需要更高的读取吞吐,可以开启
read_from_replica
策略,代价是可能读到稍旧的数据(在副本还没完成同步的短暂窗口内)。
6.2 min_size 与数据安全
存储池的 min_size
参数控制最小可用副本数。如果可用副本数低于
min_size,PG 停止接受写入。
# 3 副本池,至少 2 个副本可用才允许写入
ceph osd pool set mypool min_size 2min_size = 1
意味着即使只剩一个副本也能写入,这在单一 OSD
故障时可以维持服务,但如果这个 OSD
也故障了,数据就永久丢失。生产环境通常设置
min_size = 2,在可用性和数据安全之间取折中。
6.3 Erasure Coding 与副本
除了多副本复制,Ceph 也支持纠删码(Erasure Coding,EC)。EC 将数据分成 k 个数据块和 m 个校验块,分布在 k+m 个 OSD 上。只要任意 k 个块可用,就能恢复完整数据。
# 创建 EC 配置:4 数据块 + 2 校验块
ceph osd erasure-code-profile set myprofile k=4 m=2
# 创建 EC 存储池
ceph osd pool create ecpool erasure myprofileEC 的优势是存储效率。3 副本的存储开销是 300%,而 4+2 EC 的存储开销是 150%(6 块存储 4 块数据量)。劣势是写入延迟更高(需要计算校验块)且部分写(partial write)需要 read-modify-write 流程。EC 池通常用于冷数据、归档数据等写少读少的场景。
七、BlueStore 存储后端
7.1 FileStore 的问题
Ceph 早期使用 FileStore 作为 OSD 的本地存储后端:将 RADOS 对象存储为本地文件系统(通常是 XFS)上的文件,元数据存在 LevelDB。这个方案有几个根本性问题:
双重写入(Double Write)。FileStore 需要先将数据写入 XFS 的日志(Journal),再写入数据区。而 XFS 本身也有自己的日志。一次写入实际在磁盘上写了两次甚至三次。
元数据开销。POSIX 文件系统的 inode、目录项、扩展属性等元数据管理不是为对象存储设计的。对象的属性(XATTR)存储在文件系统的扩展属性中,受限于文件系统的 XATTR 大小限制。
无法利用裸设备性能。FileStore 需要先
mkfs
格式化磁盘,通过文件系统间接访问块设备。对于 SSD 和
NVMe,文件系统层成为不必要的性能瓶颈。
7.2 BlueStore 的设计
BlueStore 在 Ceph Luminous(12.x)引入,Nautilus(14.x)成为默认后端。核心思想是绕过文件系统,直接管理裸块设备。
整体架构:
BlueStore OSD
├── BlockDevice(裸块设备 /dev/sda)
│ ├── 数据区:直接存储对象数据
│ └── 预留区:BlueFS 使用
├── BlueFS(轻量级文件系统)
│ └── 仅用于存储 RocksDB 的 SST 文件
├── RocksDB(元数据存储)
│ ├── 对象名 → 数据位置映射
│ ├── 对象属性(omap)
│ ├── 分配位图(allocator state)
│ └── 集合元数据(collection metadata)
└── Allocator(空间分配器)
└── 位图方式管理块设备空间
关键设计决策:
数据直接写入裸设备。对象数据绕过任何文件系统,直接写入块设备的指定偏移量。空间分配器(Allocator)用位图管理块设备上的空闲和已用区域。
元数据存储在 RocksDB。对象名到磁盘偏移的映射、对象属性、分配信息等元数据存储在嵌入式 KV 数据库 RocksDB 中。RocksDB 的 SST 文件存储在 BlueFS 上——这是一个极其轻量的日志结构文件系统,只为 RocksDB 服务。
消除双重写入。对于大写入(大于
min_alloc_size,HDD 默认 64KB,SSD 默认 4KB),数据直接写入新分配的空间,然后原子性地更新 RocksDB 中的元数据指针。对于小写入或覆盖写入,数据先写入 RocksDB 的 WAL(Write-Ahead Log),再异步搬移到数据区。校验和(Checksum)。BlueStore 为每个数据块计算校验和(默认 crc32c),存储在 RocksDB 中。读取数据时验证校验和,检测静默数据损坏(silent data corruption)。FileStore 依赖底层文件系统的校验机制,而 XFS 默认不提供数据校验和。
内联压缩。BlueStore 原生支持数据压缩(snappy、zstd、lz4 等),压缩在写入路径中完成。FileStore 时代需要依赖文件系统层面的压缩支持(如 ZFS),可选项有限。
7.3 性能对比
在 Ceph 社区的基准测试中,BlueStore 相对 FileStore 的性能改进:
| 指标 | FileStore(XFS) | BlueStore | 提升 |
|---|---|---|---|
| 4K 随机写 IOPS | ~15,000 | ~25,000 | ~67% |
| 4K 随机读 IOPS | ~18,000 | ~22,000 | ~22% |
| 顺序写吞吐 | ~350 MB/s | ~420 MB/s | ~20% |
| 写放大 | 2-3x | ~1.0-1.5x | 显著降低 |
| 空间利用率 | 受 XFS 碎片影响 | 直接管理,碎片可控 | 提高 |
以上数据为单 OSD 在 NVMe SSD 上的参考值,来自 Ceph 社区在 Luminous 版本发布时的测试报告。实际性能受硬件配置、工作负载和调优参数影响。
7.4 BlueStore 调优要点
# 查看 BlueStore 配置
ceph config get osd.0 bluestore_min_alloc_size_hdd
ceph config get osd.0 bluestore_min_alloc_size_ssd
# 对于 SSD/NVMe,调整最小分配粒度
ceph config set osd bluestore_min_alloc_size_ssd 4096
# WAL 和 DB 分离到快速设备
# 在 OSD 部署时指定
ceph-volume lvm create \
--data /dev/sdb \
--block.wal /dev/nvme0n1p1 \
--block.db /dev/nvme0n1p2WAL/DB 分离是 BlueStore 调优的核心手段。将 RocksDB 的 WAL 和 SST 文件放在 NVMe SSD 上,数据放在 HDD 上,可以用少量 SSD 容量显著提升 HDD OSD 的写入性能。
八、实际运维经验与常见问题
8.1 OSD Flapping(OSD 抖动)
OSD Flapping 是指 OSD 在短时间内反复标记为
down 和 up。每次状态变更都触发 PG
的 Peering
和数据迁移,可能引发连锁反应,导致整个集群性能严重下降。
常见原因:
- 网络不稳定,心跳超时
- OSD 所在机器负载过高,心跳线程得不到 CPU 时间
- 磁盘 I/O 延迟过高,OSD 无法及时回复心跳
mon_osd_down_out_interval设置过短
处理方法:
# 临时关闭自动标记 out,防止大规模数据迁移
ceph osd set noout
# 排查根因(网络、磁盘、CPU)
# 调整心跳超时参数
ceph config set osd osd_heartbeat_grace 40
ceph config set osd osd_heartbeat_interval 10
# 确认问题解决后取消 noout
ceph osd unset nooutnoout 标志是 Ceph
运维中最常用的”急刹车”。设置后,OSD 即使被标记为
down,也不会触发数据迁移。这给运维人员留出排查和修复的时间窗口。
8.2 Recovery Thundering Herd(恢复风暴)
一个 OSD 故障后,它上面的所有 PG 都需要恢复。如果该 OSD 承载数千个 PG,所有 PG 同时启动恢复会产生巨大的 I/O 和网络开销,可能把其他 OSD 也拖慢,引发更多超时和故障。
控制恢复速度的关键参数:
# 单个 OSD 同时恢复的 PG 数上限
ceph config set osd osd_max_backfills 1
# 恢复操作的优先级(越低越不影响前台 I/O)
ceph config set osd osd_recovery_max_active 3
# 恢复操作的睡眠间隔
ceph config set osd osd_recovery_sleep_hdd 0.1
ceph config set osd osd_recovery_sleep_ssd 0.0实际操作中,建议在业务高峰期将恢复速度调低(osd_recovery_sleep_hdd = 0.5),在低谷期调高(osd_recovery_sleep_hdd = 0),加速恢复完成。
8.3 PG Stuck 状态处理
PG 长时间停留在非 active+clean
状态(stuck),是生产环境中最常见的告警。排查流程:
# 查看 stuck PG 详情
ceph health detail
# 查看特定 PG 的信息
ceph pg 2.3a query
# 常见 stuck 场景及处理
# 1. stuck inactive:PG 无法找到足够 OSD 来 Peering
# 通常是多个 OSD 同时故障
ceph pg dump_stuck inactive
# 2. stuck stale:Primary OSD 长时间未上报
# 检查 Primary OSD 是否正常运行
ceph pg dump_stuck stale
# 3. stuck unclean:副本数不足,正在恢复但未完成
# 通常等待恢复完成即可
ceph pg dump_stuck unclean
# 4. unfound objects:对象丢失,所有持有该对象的 OSD 都不可用
ceph pg 2.3a list_unfound
# 如果确认数据不可恢复
ceph pg 2.3a mark_unfound_lost deleteunfound
是最危险的状态——意味着数据可能永久丢失。在 3
副本配置下,只有 3 个 OSD 同时不可用(且无法恢复)才会出现
unfound。这是选择故障域隔离级别的核心考量。
8.4 容量规划
Ceph 集群不应让任何 OSD 的使用率超过 85%。原因:
- BlueStore 的空间分配器在高使用率下效率降低,碎片增加
- OSD 满载后,该 OSD 上的 PG 需要迁移到其他 OSD,触发再平衡,进一步增加负载
mon_osd_full_ratio(默认 0.95)达到后,集群进入 FULL 状态,拒绝所有写入
# 查看关键阈值
ceph osd dump | grep -E "full_ratio|nearfull_ratio|backfillfull_ratio"
# 默认值
# nearfull_ratio: 0.85 — 告警
# backfillfull_ratio: 0.90 — 停止回填
# full_ratio: 0.95 — 停止写入
# 查看每个 OSD 的使用率
ceph osd df tree容量规划的经验法则:
可用容量 = 原始容量 × (1 / 副本数) × 0.80
例如,100TB 原始容量、3 副本:可用容量 = 100 × (1/3) × 0.80 ≈ 26.7TB。0.80 的安全系数预留了再平衡和碎片的空间。
8.5 Monitor 的部署与维护
Monitor 集群使用 Paxos 协议维护集群状态的一致性。部署要点:
- 数量必须为奇数(3 或 5),保证多数派存活即可工作
- Monitor 节点应分布在不同故障域
- Monitor 的存储(RocksDB)应使用 SSD,否则在集群状态变更频繁时响应延迟过高
- Monitor
数据库(
/var/lib/ceph/mon)应定期备份
# 查看 Monitor 状态
ceph mon stat
# 查看 quorum
ceph quorum_status --format json-pretty
# 压缩 Monitor 数据库(在线操作)
ceph tell mon.* compactMonitor 数据库过大会导致选举超时和集群不稳定。定期执行
compact
可以清理已删除的历史记录,减少数据库体积。
8.6 版本升级策略
Ceph 的滚动升级(Rolling Upgrade)是其去中心化架构的直接收益。升级流程:
- 升级 Monitor 节点(逐一重启,保持 quorum)
- 升级 Manager 节点
- 升级 MDS 节点(如果使用 CephFS)
- 升级 OSD 节点(逐一重启,等待 PG 恢复
active+clean) - 升级客户端
# 升级前设置 noout,防止 OSD 重启时触发迁移
ceph osd set noout
# 逐个升级 OSD
systemctl stop ceph-osd@0
# 安装新版本
systemctl start ceph-osd@0
# 等待 PG 恢复
ceph -s # 确认所有 PG active+clean
# 全部完成后取消 noout
ceph osd unset noout跨大版本升级(如 Nautilus → Octopus → Pacific)必须逐版本进行,不能跳版本。每个版本的升级文档都会列出不兼容的配置变更和需要关注的 deprecation。
九、Ceph 在技术栈中的位置
Ceph 解决的核心问题是:用通用硬件构建统一的、可扩展的存储基础设施。与其他存储方案的定位差异:
| 方案 | 定位 | 与 Ceph 的关系 |
|---|---|---|
| HDFS | 大数据分析的批处理存储 | Ceph 不适合 MapReduce 数据本地性 |
| GlusterFS | 无元数据服务器的文件存储 | 类似思路,但 CRUSH 更灵活 |
| MinIO | 轻量级 S3 兼容对象存储 | MinIO 更简单,Ceph 更全面 |
| 商业 SAN/NAS | 专用硬件存储 | Ceph 是软件定义存储的替代方案 |
Ceph 的优势是”三合一”——一套集群同时提供块、文件和对象存储,减少运维负担。劣势同样源于此:通用性意味着在特定场景下不如专用方案。例如,对延迟敏感的数据库工作负载,专用 NVMe 阵列的表现通常优于 Ceph RBD。
从 CRUSH 算法的角度看,Ceph 对分布式系统设计的核心贡献在于证明了:通过精心设计的伪随机算法,可以在保持确定性和拓扑感知的前提下,完全消除数据放置的中央协调开销。这个思路影响了后续许多分布式存储系统的设计。
参考文献
- Weil, S. A., Brandt, S. A., Miller, E. L., & Maltzahn, C. (2006). CRUSH: Controlled, Scalable, Decentralized Placement of Replicated Data. Proceedings of the 2006 ACM/IEEE Conference on Supercomputing (SC ’06). https://ceph.com/assets/pdfs/weil-crush-sc06.pdf
- Weil, S. A., Brandt, S. A., Miller, E. L., Long, D. D. E., & Maltzahn, C. (2006). Ceph: A Scalable, High-Performance Distributed File System. Proceedings of the 7th Symposium on Operating Systems Design and Implementation (OSDI ’06). https://www.usenix.org/legacy/events/osdi06/tech/weil.html
- Weil, S. A. (2007). Ceph: Reliable, Scalable, and High-Performance Distributed Storage. Ph.D. Dissertation, University of California, Santa Cruz. https://ceph.com/assets/pdfs/weil-thesis.pdf
- Aghayev, A., Weil, S., Kuchnik, M., Nelson, M., Ganger, G. R., & Amvrosiadis, G. (2019). File Systems Unfit as Distributed Storage Backends: Lessons from 10 Years of Ceph Evolution. Proceedings of the 27th ACM Symposium on Operating Systems Principles (SOSP ’19). https://doi.org/10.1145/3341301.3359656
- Red Hat Ceph Storage Documentation. https://docs.ceph.com/en/latest/
- Weil, S. A., Brandt, S. A., Miller, E. L., & Maltzahn, C. (2004). Dynamic Metadata Management for Petabyte-Scale File Systems. Proceedings of the 2004 ACM/IEEE Conference on Supercomputing (SC ’04). https://doi.org/10.1109/SC.2004.22
- Ceph BlueStore: A New, Faster Storage Backend for Ceph. https://ceph.io/en/news/blog/2017/new-luminous-bluestore/
| 上一篇 | 下一篇 |
|---|---|
| 从 GFS 到 HDFS | 分布式 KV 存储对比 |