Ceph 分布式存储技术深度解析:CRUSH 算法、存储池与数据分片原理
CRUSH 真正替换掉的不是元数据服务器,而是"数据在哪"这件事的可协商性。这篇讲清一次写入要经过几次映射,以及为什么恢复时间是设计出来的而不是故障带来的。
一块 OSD 盘掉线的时候,你失去的其实不是那块盘。硬件的损失是确定的(一块盘、几 TB),真正不确定的是接下来几个小时集群在干什么:多少数据要搬家、搬家的流量从哪里挤、搬完之后有没有人发现别的盘也快扛不住了。
这块不确定性的源头,在 CRUSH 里就已经写好了。
#一、先看清 CRUSH 换掉的是什么
教科书会告诉你:Ceph 用 CRUSH 替代了中心化的元数据服务,所以没有单点、可以无限扩展。这句话没错,但它把重点放错了。真正被替换掉的是问题本身的形式:
| 传统做法 | CRUSH | |
|---|---|---|
| 数据位置从哪来 | 查一张表(元数据服务器维护) | 算出来(客户端自己算) |
| 谁决定数据在哪 | 一个可以被人干预的调度器 | 一份写死在拓扑里的规则 |
| 加减一块盘 | 更新表、按策略选择迁移谁 | 规则重新计算,迁移量由算法决定 |
| 能不能商量 | 能,但要写调度逻辑 | 不能,除非改 CRUSH Map |
第三行和第四行才是运维真正要面对的东西。CRUSH 用「可计算的确定性」换掉了「可协商的调度」。这个交换在稳定期几乎无感,在故障期和扩容期会以非常具体的方式呈现出来:你没有"慢一点迁移"以外的调节手段。
CRUSH 本身出自 Weil 等人在 FAST '06 上的论文(2006),到今天快二十年,核心没有变。
#二、一次写入经过几次映射
理解 CRUSH 的性价比,最快的办法是数一遍映射次数。写一个对象,位置是这样被决定的:
- pool + 对象名 → PG:
hash(对象名) mod pg_num。这一步是纯哈希,与集群拓扑无关。 - PG → 一组 OSD:CRUSH 按
crush_rule从拓扑树上选size个 OSD,每个再选一个"扮演哪一份"(primary / replica)。 - OSD → 物理位置:BlueStore 决定它落在块的哪一段(这部分与 CRUSH 无关)。
关键在于只有第 2 步需要拓扑信息,而第 1 步把"对象数量"这个维度提前折叠掉了。这就是 PG 存在的全部理由:让几百万个对象在拓扑视角里退化成几千个 PG,再让这几千个 PG 去做分布。
所以 PG 不是"为了减少迁移"才有的中间层——它是把不可枚举的东西变成可枚举的。这个想法在工程上很常见(分段、分桶、分片),特殊之处是 Ceph 把它做成了一等公民,连状态机都单独定义了一套。
# 看一个对象到底落在哪
ceph osd map <pool> <object>
# osdmap e412 pool 'rbd' (1) object 'foo' -> pg 1.abcd1234 (1.4) -> up ([3,7,5], p3) acting ([3,7,5], p3)
# 看某一个 PG 的映射是怎么算出来的
ceph pg 1.4 query | head -30ceph osd map 的输出值得逐字段读一遍:up 是"应该在哪",acting 是"实际在哪"。平时这两个集合必须完全相同;一旦出现差异,说明有 OSD 掉了或者正在做 peering,这时候看 ceph -s 里的 degraded 才有意义。
#三、"算出来"的代价:没有中间商,也就没有缓冲
元数据中心的架构里,迁移是一个可以被调度器排序的队列。你可以让重要的池先迁、让不重要的池等夜间再迁。CRUSH 没有这个位置——迁移量由拓扑变化和 crush_rule 直接决定。
这带来两个后果,运维必须提前接受:
- 加减一块 OSD 的迁移量是可预估的,但不是可谈判的。 它大致与"受影响的 PG 数 × 数据量 ÷ 集群规模"成正比。想要少迁,唯一干净的做法是一次加一批而不是一块一块加,或者用
crush reweight做渐进式引入。 noout/norebalance不是"取消迁移",只是"推迟迁移"。 它们把系统的平衡状态挂起,代价是集群长期运行在非平衡状态:某些 OSD 明显更满、某些 PG 副本数不足。挂起期间每多坏一块盘,离数据不可用就更近一步。
这是典型的工程选择被当成开关来用。set noout 看起来是个开关,实际是一次风险转移:把"现在抖动"换成"未来某一刻的不可用概率"。这一类决定没有技术上的正确答案,只有"你更怕哪一个"——所以它其实是价值选择。
#四、PG 数:一个几乎改不动、又必须在第一天定下的数
pg_num 的痛苦在于它的两个性质同时成立:
- 它必须在建池时定下来,因为
pg_num影响 PGHash 的取模结果,改了就意味着大量对象的位置会变; - 它对恢复时间的影响是超线性的:PG 太大会让单个 PG 的恢复变成一根很长的尾巴,PG 太小又会让每块 OSD 承载的 PG 数暴涨、监控和 peering 开销压不住。
业界流传的"每 OSD 100 个 PG"是个粗糙的起点,Ceph 自带的 pg_autoscaler 会给你一组更贴合的建议值。但要注意 autoscaler 建议的是均衡值,不是你的恢复目标——它不知道你能忍受多长的恢复窗口。
关于调整:早期版本增加 pg_num 的代价很高;Luminous(12.x,2017)之后 PG split 的开销大幅降低(大致变成元数据操作),Nautilus(14.2,2019)又补上了 PG merge。具体到你手上的版本,行为差别不小,动手前请以对应版本文档为准——这一点我不建议凭记忆操作。
# 先看建议值和当前是否失衡
ceph osd pool autoscale-status
# 看单个池的 PG 分布是否均匀(每个 PG 的偏差)
ceph pg ls-by-pool <pool> | wc -l
ceph osd df treeceph osd df tree 是这里最有用的一条命令,因为它同时回答了"容量均衡"和"PG 数均衡"两个问题。两者不均衡的修法完全不同:容量不均要调 crush weight,PG 数不均要看 pg_num 和 autoscaler 的 profile。把这两种失衡混在一起调,是很多"越调越乱"的起点。
#五、故障域:把"什么叫同时坏"写进拓扑
CRUSH Map 最容易被低估的部分是它的层次结构。host / rack / row / room 这些层级不是描述性的标签,它们直接定义了"同时坏掉"的判定。
crush_rule 里那条 chooseleaf firstn 3 type host,翻译成人话是:"三份副本,任何两份不在同一台机器上"。它保证的是"一台机器整机掉电后数据仍可用",而不是"不怕坏盘"。这两个是不同的强度,取决于你把 type 写成什么。
实际设计时值得逐层问一遍:
| 你写的 type | 你实际声明的容错能力 | 代价 |
|---|---|---|
osd | 不怕坏盘 | 基本没有容错意义,一般只用于测试 |
host | 不怕单机整机故障 | 最常见;但同机架掉电会击穿 |
rack | 不怕机架级故障 | 跨机架带宽成为恢复瓶颈 |
room / 机房 | 不怕一个机房失联 | 跨机房延迟写进每一次写入路径 |
越往下写,容错越强,恢复越慢。 这个权衡里没有"更安全"的选项,只有"安全在哪一层"。现实中大部分集群停在 host,不是因为 rack 不好,而是因为跨机架的网络带宽常常撑不起一次恢复。
#六、恢复时间才是真正的 SLA
到这里可以回到开头那块不确定的盘了。集群的数据安全性由副本数决定,但业务可用性由恢复时间决定——因为恢复期间集群处于冗余下降状态,第二次故障的代价被放大。
而恢复时间是被这些参数直接调出来的:
# 恢复并发与回填上限:调大 = 恢复快、业务抖动大
ceph config get osd osd_recovery_max_active
ceph config get osd osd_max_backfills
ceph config get osd osd_recovery_sleep
# 看当前恢复进度与剩余量
ceph -s | grep -E "recovery|degraded"
ceph pg stat这几个参数之间的关系是纯价值选择:你是在用前台业务的延迟,换集群回到冗余状态的速度。把它们调高,恢复窗口缩短、用户投诉变多;调低,用户无感、但集群裸奔的时间拉长。任何一个"推荐值"都隐含了一个"我更怕哪边"的立场。
更新的版本(Quincy 17.x,2022)引入了 mClock 调度器,试图把这件事从"几个互相牵制的阈值"变成"按客户端类别分配份额"。这部分我没有在生产上跑过,属于读文档得到的理解,不作为结论。 如果你在规划新版集群,值得单独验证它在你负载下的行为。
#七、BlueStore 之后,边界变了但没消失
老的 FileStore 时代,写路径上有一层 Journal:先写日志盘再写数据盘,小 IO 的性能瓶颈和"journal 盘挂了怎么办"都是经典题。BlueStore(Luminous 起成为默认后端)直接把对象写进裸块设备,自己管元数据(RocksDB),Journal 这个独立故障点消失了。
但这不是"少了一个组件"那么简单,而是故障边界平移了:现在要在意的变成了 WAL/DB 所在设备与数据设备是否同盘、RocksDB 的延迟会不会拖住整个 OSD、以及 bluestore_* 那批参数。
# 看一个 OSD 的 BlueStore 配置与延迟贡献
ceph osd metadata <id> | grep -i bluestore
ceph osd perf # 每块 OSD 的 apply/commit 延迟,排查"慢盘"的第一站ceph osd perf 是 BlueStore 时代最值钱的一条命令之一:它把"哪块盘的 commit 延迟拖后腿"直接摊开。当集群整体变慢而 ceph -s 一切正常时,先看这里。
#八、这篇没写什么
按这个站的习惯,把刻意省掉的东西交代清楚:
- 没写完整的 CRUSH Map 语法。 那种内容抄一遍不如直接看官方
crush.txt,而且要跟着版本走。这里只保留"层次结构 = 故障域声明"这一个判断。 - 没写容量规划的数字。 任何一个"每 TB 需要多少 OSD"的经验值,脱离你的盘型、网络和副本策略都是错的。
- 没写纠删码池。 纠删码把这里的每一个权衡都改写了一遍(恢复变成网络密集、
k+m取代size),塞进一篇只会让两边都讲不清,单独写。 - 没写性能调优清单。 理由见上:没有负载画像的调参建议,本质是让别人替你承担试错成本。
一句话收束:CRUSH 把"数据在哪"做成了计算,于是恢复时间从运维手感变成了设计参数。你没法再和它商量,只能在一开始就把话说明白。
写于 2025 年 10 月 4 日
- 栏目
- 技术文章
- 约
- 8.5 分钟
- 字数
- 4.4K
- 阅读
- 2
本文为原创记录,转载请注明出处。如果这篇替你省了时间,欢迎留言说说你踩到的坑。
同题 · related
- H3C UIS 超融合架构深度剖析:从原理到企业落地实践超融合真正卖的不是"融合",是把采购单位从设备换成了节点。代价是故障爆炸半径从一台机器扩大到整个集群的恢复时间,这笔账买之前就得算。
- Ceph 集群运维实战:常见命令、故障诊断与性能优化命令表最大的问题不是太长,而是不告诉你哪一条会后悔。这篇把 Ceph 命令分三类,只留真正改变判断的十几条,并说清哪些动作是不可逆的。
- 把博客从 docker-compose 搬到 k3s:单机迁移与上线全过程在 4C3.6G 的云主机上把博客从 docker-compose 迁到单机 k3s。附完整 YAML 清单(Deployment、Service、PVC、Ingress、Certificate 等)与全部配置命令,记录部署、数据迁移与切换上线的踩坑过程。
留言 · remarks
00 条还没有留言,来说点什么吧。