技术文章2025 年 10 月 4 日约 8.5 分钟

Ceph 分布式存储技术深度解析:CRUSH 算法、存储池与数据分片原理

CRUSH 真正替换掉的不是元数据服务器,而是"数据在哪"这件事的可协商性。这篇讲清一次写入要经过几次映射,以及为什么恢复时间是设计出来的而不是故障带来的。

一块 OSD 盘掉线的时候,你失去的其实不是那块盘。硬件的损失是确定的(一块盘、几 TB),真正不确定的是接下来几个小时集群在干什么:多少数据要搬家、搬家的流量从哪里挤、搬完之后有没有人发现别的盘也快扛不住了。

这块不确定性的源头,在 CRUSH 里就已经写好了。

#一、先看清 CRUSH 换掉的是什么

教科书会告诉你:Ceph 用 CRUSH 替代了中心化的元数据服务,所以没有单点、可以无限扩展。这句话没错,但它把重点放错了。真正被替换掉的是问题本身的形式:

传统做法CRUSH
数据位置从哪来查一张表(元数据服务器维护)算出来(客户端自己算)
谁决定数据在哪一个可以被人干预的调度器一份写死在拓扑里的规则
加减一块盘更新表、按策略选择迁移谁规则重新计算,迁移量由算法决定
能不能商量能,但要写调度逻辑不能,除非改 CRUSH Map

第三行和第四行才是运维真正要面对的东西。CRUSH 用「可计算的确定性」换掉了「可协商的调度」。这个交换在稳定期几乎无感,在故障期和扩容期会以非常具体的方式呈现出来:你没有"慢一点迁移"以外的调节手段。

CRUSH 本身出自 Weil 等人在 FAST '06 上的论文(2006),到今天快二十年,核心没有变。

#二、一次写入经过几次映射

理解 CRUSH 的性价比,最快的办法是数一遍映射次数。写一个对象,位置是这样被决定的:

  1. pool + 对象名 → PG:hash(对象名) mod pg_num。这一步是纯哈希,与集群拓扑无关。
  2. PG → 一组 OSD:CRUSH 按 crush_rule 从拓扑树上选 size 个 OSD,每个再选一个"扮演哪一份"(primary / replica)。
  3. OSD → 物理位置:BlueStore 决定它落在块的哪一段(这部分与 CRUSH 无关)。

关键在于只有第 2 步需要拓扑信息,而第 1 步把"对象数量"这个维度提前折叠掉了。这就是 PG 存在的全部理由:让几百万个对象在拓扑视角里退化成几千个 PG,再让这几千个 PG 去做分布。

所以 PG 不是"为了减少迁移"才有的中间层——它是把不可枚举的东西变成可枚举的。这个想法在工程上很常见(分段、分桶、分片),特殊之处是 Ceph 把它做成了一等公民,连状态机都单独定义了一套。

bash
# 看一个对象到底落在哪
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 -30

ceph 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。具体到你手上的版本,行为差别不小,动手前请以对应版本文档为准——这一点我不建议凭记忆操作。

bash
# 先看建议值和当前是否失衡
ceph osd pool autoscale-status

# 看单个池的 PG 分布是否均匀(每个 PG 的偏差)
ceph pg ls-by-pool <pool> | wc -l
ceph osd df tree

ceph 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

到这里可以回到开头那块不确定的盘了。集群的数据安全性由副本数决定,但业务可用性由恢复时间决定——因为恢复期间集群处于冗余下降状态,第二次故障的代价被放大。

而恢复时间是被这些参数直接调出来的:

bash
# 恢复并发与回填上限:调大 = 恢复快、业务抖动大
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_* 那批参数。

bash
# 看一个 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

留言 · remarks

00 条

还没有留言,来说点什么吧。

Ceph 分布式存储技术深度解析:CRUSH 算法、存储池与数据分片原理 · LXH·BLOG