很多人第一次听到“Ceph镜像快照”这个说法,第一反应是:快照就是镜像,镜像就是快照。我在生产环境里处理过上百次跟rbd快照、rbd镜像相关的故障,可以很明确地说,这两个东西在Ceph里不是一回事,但又是配合最紧密的一对。这篇文章基于我长期的存储运维经验,把Ceph镜像快照从概念、命令、方案设计到故障排查整个链路梳理一遍。适合正在用Ceph做虚拟机存储、云平台后端存储,或者准备搭建跨机房容灾的运维和架构师,也适合刚上手Ceph、想搞懂RBD数据保护机制的朋友。
1. 先分清快照与镜像:这两个概念在Ceph里不是一回事
1.1 我用一个踩坑案例说明白“rbd image”和“rbd snapshot”的关系
先讲个真实案例。之前有个同事在测试环境执行了这样一条命令:
rbd snap create rbd/myvm@backup_before_upgrade他以为这条命令创建了一个“镜像备份”,于是放心大胆地去升级虚拟机内部的应用。结果升级到一半发现版本不兼容,他想回滚,执行了:
rbd snap rollback rbd/myvm@backup_before_upgrade然后虚拟机照样启动失败,他当场就懵了:快照明明存在,为什么回滚了还是起不来?
这个案例暴露了新手对Ceph块存储最常见的误解。在Ceph RBD里,image是基础块设备,你可以把它理解成一块裸磁盘;snapshot是这块磁盘在某个时间点的只读状态。回滚操作做的事情,本质上是“把当前盘的内容覆盖成快照时的内容”,但如果你回滚的是一个正在被虚拟机使用的盘,虚拟机文件系统层面的缓存、日志、分区表信息大概率是残缺的,结果就是系统能启动但文件系统挂载失败,或者直接内核panic。
更关键的是,很多人以为快照是“拷贝了一份完整数据”,其实快照是基于COW的机制。Ceph RBD快照默认使用写时复制,创建快照的那一刻几乎占不了什么空间,真正的空间消耗发生在这个快照创建之后的新数据写入时。所以快照不能当成镜像备份来用,它在设计上就是为了“短时间点保护”和“快速回滚”服务的。
1.2 那“镜像”又是什么:RBD Mirroring是跨集群的数据复制
Ceph里的“镜像”指的是RBD Mirroring,即块设备镜像同步。它解决的是单集群故障时的数据可用性问题:如果一个机房整体断电、网络隔离甚至硬件损坏,你可以从另一个集群继续提供服务。
RBD Mirroring分两种模式:
| 模式 | 复制粒度 | 适用场景 |
|---|---|---|
| 池级镜像(Pool Mode) | 池内所有镜像自动复制 | 整池保护,运维成本低 |
| 镜像级镜像(Image Mode) | 只复制指定的镜像 | 只保护关键业务盘,节省网络带宽 |
这个机制是异步复制的,不是同步双写。RBD Mirroring的RPO取决于你的网络延迟和复制周期,极端情况下如果主集群瞬间崩掉,最近几秒的数据可能来不及同步到备集群。这和快照完全是两种定位:快照管的是“误操作回滚”,镜像管的是“数据中心级灾难恢复”。
1.3 三种能力定位对比:快照、镜像、克隆
再补充一个经常被搅在一起的:克隆。Ceph RBD支持从快照做COW克隆,也就是rbd clone。克隆出来的镜像可以独立使用,也可以继续打快照。三者的关系可以这么理解:
- 快照:给一个image做“存档点”,用于回滚。
- 克隆:基于某个快照创建新的image,用于快速交付新虚拟机或测试环境。
- 镜像:把整个image或者整个pool的数据持续同步到另一个Ceph集群。
我在设计存储方案的时候,通常会把三层的节奏定下来:快照做小时级保护,克隆做环境快速交付,镜像做机房级容灾。这样一套组合拳下来,单点故障和数据误删基本都能兜住。
2. RBD快照的实战套路:创建、回滚、克隆与分层
2.1 快照创建简单,但应用一致性的坑很多
直接命令创建快照很简单:
# 查看当前image列表 rbd ls mypool # 创建快照 rbd snap create mypool/mysql-data@snap_20250118_0300 # 列出快照 rbd snap ls mypool/mysql-data # 查看快照详情,包含创建时间、大小、保护状态 rbd info mypool/mysql-data@snap_20250118_0300但这里有个极其重要的问题:你到底在快照什么的一致性状态?
如果你的虚拟机里跑着MySQL,你直接用rbd snap create创建快照,得到的只是“磁盘在某毫秒的内容”,不代表MySQL的数据文件是一致的。MySQL可能正在写binlog,可能redo log刚好处于半提交状态,这个快照即使回滚过去,数据库也可能启动失败。
所以生产环境的正确姿势是:先通过应用层接口做一致化处理。比如MySQL用FLUSH TABLES WITH READ LOCK,或者启用XtraBackup的备份锁;如果用的是KVM虚拟机,可以先virsh snapshot-create --quiesce让QEMU Guest Agent帮忙冻结文件系统,再打Ceph快照。
我自己的经验是:Ceph快照做的是“存储层时间点”,应用层一致性必须由上层配合完成。不要指望存储工程师一个人能把所有事都干了。
2.2 回滚不是唯一的恢复手段,clone+flatten才是正经做法
遇到数据损坏,第一反应是rbd snap rollback。这个命令本身没问题,但如果你直接对正在运行的虚拟机镜像做rollback,大概率会出问题——虚拟机还在用这个盘,块设备突然被覆盖,文件系统直接进入不可预测状态。正确做法是:
# 停止使用该镜像的虚拟机 # 1. 先基于快照做保护,防止快照被错误删除 rbd snap protect mypool/mysql-data@snap_20250118_0300 # 2. 克隆出一个新镜像用于恢复 rbd clone mypool/mysql-data@snap_20250118_0300 mypool/mysql-data-restore # 3. 新镜像是COW类型,依赖父快照;如果需要独立使用,执行flatten rbd flatten mypool/mysql-data-restore # 4. 回滚原镜像 rbd snap rollback mypool/mysql-data@snap_20250118_0300这套流程有几个好处:
- 克隆出来的镜像和原镜像互不干扰,可以先挂载到新虚拟机做数据校验。
- flatten之后,新镜像完全脱离父快照依赖,成为独立的image,即使父快照被删除也不受影响。
- 回滚原镜像操作在虚拟机停机之后执行,避免在线回滚导致的数据错乱。
我在实际恢复中几乎都是这个顺序:先clone出来验证,再决定是直接启用克隆镜像,还是回滚原镜像。直接回滚往往是最危险的路径。
2.3 快照差异分析:用rbd diff做增量备份
快照的另一个高频用途是备份。很多人做Ceph备份用的还是全量导出,其实Ceph原生就支持差异导出,效率高很多:
# 导出快照与上一个快照的差异 # 格式:rbd export-diff <image>@<快照> <目标路径> rbd export-diff mypool/mysql-data@snap_20250118_0300 /backup/mysql-data_0300.diff # 如果需要从裸镜像导出全量初始数据,则可以这样 rbd export-diff mypool/mysql-data /backup/mysql-data_initial.diff差异导出基于快照链,第一次导出全量,之后每次都只导出上一个快照到当前快照之间的变更数据。恢复时按顺序导入即可:
rbd import-diff /backup/mysql-data_initial.diff mypool/mysql-data-restored rbd import-diff /backup/mysql-data_0300.diff mypool/mysql-data-restored这套机制对带宽和存储空间都非常友好。我用它做过一套备份方案:每天凌晨对数据库打快照,然后把差异数据同步到另一套Ceph集群的冷存储池里,保留30天。恢复时只需要按时间点导入即可,不需要一直开着RBD Mirroring。
3. 跨集群RBD Mirroring落地:从命令行到故障切换
3.1 环境准备:两个集群同步状态检查
RBD Mirroring需要两个Ceph集群,我以最常见的场景为例:主集群在业务机房,备集群在灾备机房。两个集群的Ceph版本必须一致或者差异极小,否则可能因为rbd协议版本不匹配导致复制失败。
部署前先检查两个集群的状态:
# 主集群 ceph -s ceph osd tree # 备集群 ceph -s ceph osd tree另外,需要确认两个集群的pool名称和pg_num设计保持一致。Mirroring是按pool名匹配的,如果你的pool在主集群叫rbd,在备集群叫rbd-bak,虽然可以手动指定remote pool,但会给自己增加很多配置复杂度。我建议一开始就让两个集群的池名、配额、pg分布尽量一致。
3.2 配置池级镜像的完整流程
以下是在主备集群之间启用RBD Mirroring的标准流程:
# 1. 在备集群创建同名的pool ceph osd pool create rbd 128 128 # 2. 在两个集群分别建立keyring,并且互相授权 # 这个步骤通常用cephx的client.rbd-mirror账户 ceph auth get-or-create client.rbd-mirror mon 'profile rbd-mirror' osd 'profile rbd' ceph auth export client.rbd-mirror # 3. 在备集群配置rbd-mirror守护进程 systemctl enable ceph-rbd-mirror@rbd-mirror systemctl start ceph-rbd-mirror@rbd-mirror # 4. 在主集群的pool上启用pool级镜像 rbd mirror pool enable rbd pool启用之后,可以查看同步状态:
rbd mirror pool status rbd # 输出示例 health: OK daemon health: OK ...如果看到WARNING,多半是备集群的rbd-mirror进程没有启动,或者网络不通,先查这两个。
这里有个关键point:启用pool级镜像之后,已经存在的image不会被自动复制。pool mirror mode只对后续新建的image生效,历史image需要手动逐个启用:
# 手动为已有镜像启用镜像同步 rbd mirror image enable rbd/mysql-data这个坑我踩过一次。当时以为pool级别开启镜像就万事大吉,结果备集群空转了好几天,主集群上面的旧数据根本没同步过去。后来写脚本扫了一遍所有image,逐个enable。
3.3 主备切换与故障恢复:promote、demote与split-brain
RBD Mirroring的故障切换流程,核心就两个命令:demote和promote。
正常情况下,主集群的镜像状态是primary,备集群是non-primary。当主集群挂掉,你需要把备集群提升为primary:
# 在备集群执行 rbd mirror image promote rbd/mysql-data恢复之后,备集群开始作为主提供读写,业务侧把虚拟机的存储指向改成备集群的Ceph即可。等到原主集群修好了,再把它作为新的备集群接入,反向同步数据。
但这里有一个非常严肃的坑:split-brain。如果在主集群没有降级(demote)的情况下,直接把备集群提升为primary,两个集群都认为自己有写入权,数据会各自发展,等网络恢复后重新同步时根本无法自动合并。这就是脑裂。
所以正确的切换顺序是:
- 如果主集群还能操作,先执行
rbd mirror image demote rbd/mysql-data。 - 再在备集群执行
rbd mirror image promote rbd/mysql-data。 - 如果主集群彻底不能操作,需要强制提升,并且标记主集群原数据为分离状态。
# 强制提升备集群 rbd mirror image promote --force rbd/mysql-data注意,使用--force之后,原主集群上的数据将被视为“过期数据”,后续恢复同步时,Ceph不会自动比对哪个数据新,而是以当前primary为准。这个行为是正确的,但前提是你必须确认原主的数据已经没用了,否则一同步就把新主的数据覆盖掉了。
4. 快照和Mirroring撞上掉盘故障:一次真实排错复盘
4.1 故障现象:OSD掉盘后快照“消失”了
有一次,业务反馈某台虚拟机的数据盘访问异常,我上Ceph集群看了一眼:ceph -s显示一个OSD down,PG状态里有不少degraded。然后同事试图回滚一个快照来恢复数据,结果发现rbd snap ls列出来的快照数量比预期少了很多。
“快照是不是丢了?”这是我当时的第一反应。
4.2 排查链路:从health状态到pg状态再到rbd映射
千万不要一上来就去查快照,先查Ceph集群整体健康状况:
ceph -s ceph health detail ceph osd tree ceph pg dump | grep -E "active|degraded|peered|incomplete"当时我看到的问题在于:某个OSD所在的主机硬盘彻底挂掉,导致该OSD上的PG进入degraded状态,部分PG甚至停留在peered,也就是只完成了部分数据对账,Ceph暂时还没有把它置为active。rbd快照元数据本身存储在monitor和相关的OSD上,当某个PG无法正常服务时,rbd snap ls就可能出现快照列表不完整的情况。
Ceph的底层机制是这样的:一个rbd image的数据被切分成多个对象,这些对象分布在不同的PG中。快照的元数据也是对象的一部分。如果掉盘的OSD上恰好有这些对象,PG没有完成恢复之前,客户端就无法读到完整的数据。也就是说,不是快照被删了,是暂时读不到。
4.3 快照在副本机制下的最终一致性保障
Ceph采用多副本机制,默认是3副本,掉一个盘并不会直接导致数据丢失。degraded状态意味着剩余副本还足够提供服务,但Ceph为了保证数据一致性,在某些操作上会等待PG状态恢复。这个时间取决于你的副本数、网络、磁盘IO,以及是否开启了noout、noscrub等集群标志。
我当时做的第一件事是确认没有开启误导性的集群标志:
ceph osd getcrushmap | ... ceph osd dump | grep flags然后等待集群自动恢复。如果掉盘的OSD短时间内无法拉起,要把故障OSD从集群中剔除,让Ceph启动数据重平衡:
# 确认该OSD确实没救了 ceph osd out <osd-id> ceph osd crush remove osd.<osd-id> ceph osd rm <osd-id>之后Ceph会在剩余的副本上重建对象数据,PG回到active+clean。这时候再看快照列表,之前“消失”的快照全部回来了。整个过程大概是两个小时,取决于数据量。
4.4 掉盘后恢复镜像数据的实操顺序
掉盘事件之后,如果你计划恢复某个镜像的数据,推荐顺序是:
- 先等集群状态恢复
HEALTH_OK,即所有PG都active+clean。 - 在恢复过程中不要盲目做快照回滚,否则会干扰后台数据恢复。
- 等集群健康后,用
rbd info确认镜像元数据完整。 - 用
rbd snap ls确认快照链完整。 - 用
rbd diff做一次快照校验,确认数据可读。 - 再执行clone或rollback。
这套顺序的核心逻辑是:先保证基础设施层面数据完整,再考虑应用层面恢复。如果你在PG还没恢复的时候就做回滚,可能会触发大量读丢失,反而拖慢Ceph自身的恢复进度。
5. 基于快照的备份与容量管理:别让快照反过来卡死集群
5.1 快照吃容量的机制:COW之外还有ROW
快照不是零成本。Ceph RBD快照默认是写时复制(COW),新写入数据时,Ceph会把原始数据复制一份保留到快照区域,然后在原位置写入新数据。所以快照越老,占用的空间可能越大,因为从快照点开始之后的数据变化都累积在这份快照引用里。
此外,Ceph还支持重定向写时复制(ROW),这种模式下,新数据写到新的位置,原始位置保持不变,快照引用的数据永远不会被覆盖。ROW的优点是快照性能好,不改变原数据地址;缺点是长时间不合并的话,数据位置碎片化严重,读取需要查找多个位置。
生产环境里,用户不会直接感知COW和ROW的区别,但你会看到rbd du输出的空间占用:
rbd du mypool/mysql-data多建几个快照之后,你会发现磁盘使用量暴涨。这不是Ceph的bug,而是快照的本质。
5.2 给存储池设置配额:防止快照洪峰打满数据池
快照增长不可控,是让很多集群翻车的元凶。尤其是备份脚本有bug,或者应用层每次升级都打快照但从不清理,几个月后整个存储池被快照数据占满,新数据写入直接失败。
我的建议是,从一开始就给pool设置配额:
# 限制pool的总容量为10TB ceph osd pool set-quota mypool max_bytes 10995116277760 # 限制pool的最大对象数 ceph osd pool set-quota mypool max_objects 100000000设置配额之后,当快照数据把pool塞满时,Ceph会拒绝新写入,虽然这个行为也会影响正常业务,但至少不会让整集群崩溃。更好的做法是配合告警监控,在容量达到50%、70%、85%时分别报警。
另外,我强烈建议在运维手册里写清楚快照保留策略:比如核心业务盘保留最近5个快照,每个快照保留24小时;普通开发盘保留最近3个快照。自动化清理脚本用rbd snap rm按时间戳删除旧快照即可。
5.3 定期清理过期快照:怎么删、删不动怎么办
删除快照本身很简单:
rbd snap rm mypool/mysql-data@snap_20250118_0300但如果这个快照被clone过,且clone镜像没有flatten,删除会失败,提示快照被保护或者是父快照。这时候有两个选择:
- 先解除保护再删除:
rbd snap unprotect mypool/mysql-data@snap_20250118_0300 rbd snap rm mypool/mysql-data@snap_20250118_0300- 或者先flatten所有依赖它的clone镜像,再执行删除。
我在生产环境里碰到过最麻烦的情况是:某个基础镜像打了快照,然后从这个快照clone了三十多台虚拟机。后续想清理这个快照,但clone镜像全部没有flatten,导致快照无法删除,而基础镜像又不敢动。最终只能让所有虚拟机逐个执行rbd flatten,再删除快照。这个过程非常耗时。所以我现在提倡:如果clone出去的数据需要长期使用,直接flatten;短期测试,才允许依赖父快照。
6. 设计一套可靠的快照与容灾方案时,我踩过的那些坑
6.1 快照保留策略不是越久越好
很多人觉得快照是个好东西,留得越多越安全。这是错误的。快照越久,对存储空间的占用越大,而且恢复性能会随着快照链的加深而下降。Ceph在读取一个长时间未写覆盖的数据块时,如果快照链很长,需要回溯多个快照层,虽然RBD内部做了优化,但性能依然有影响。
我的建议是:
| 数据等级 | 快照频率 | 保留时间 | 说明 |
|---|---|---|---|
| 核心数据库 | 每小时 | 24小时 | 快速回滚误操作 |
| 核心数据库 | 每天 | 7天 | 支持最近一周的恢复 |
| 一般业务 | 每天 | 3天 | 避免容量失控 |
| 测试环境 | 不保快照 | 0 | 用完即删 |
6.2 应用层一致性必须写进SOP
快照的创建本身不贵,但应用层一致性的配合经常被忽略。我见过很多团队,Ceph快照打了,但应用数据恢复后无法使用,于是得出结论:Ceph快照没用。其实问题出在快照的时间点不是应用一致的时间点。
运维团队必须和应用团队约定一套标准操作流程,比如:应用侧先进入维护状态,再触发快照,最后确认快照创建完成后再恢复业务。这个过程要自动化,不能靠人肉指挥。
6.3 容灾演练:平时不切,出事了不敢切
RBD Mirroring配置好之后,如果半年不做一次切换演练,真到灾备切换的时候大概率会出乱子。我的习惯是每季度做一次模拟切换:把主集群网络隔离,强制提升备集群,检查业务数据完整性和性能,然后再切回来。
这一步看起来没什么技术含量,但能暴露很多问题:备集群的rbd-mirror进程是不是挂了?备集群的容量够不够?备集群的存储池配置是否和主集群一致?网络高峰期同步是否跟得上?这些问题只有在演练中才会暴露。
6.4 最后再分享一个小技巧
如果你经常做快照清理,建议养成写脚本的习惯。Ceph本身不带“按时间自动清理”的功能,你需要用rbd命令配合shell脚本自己实现。一个简单的清理逻辑:
#!/bin/bash POOL=mypool IMAGE=mysql-data KEEP=5 rbd snap ls ${POOL}/${IMAGE} | tail -n +2 | awk '{print $1}' | sort > /tmp/snap_list.txt TOTAL=$(wc -l < /tmp/snap_list.txt) if [ "$TOTAL" -gt "$KEEP" ]; then REMOVE=$((TOTAL - KEEP)) head -n "$REMOVE" /tmp/snap_list.txt | while read snap; do rbd snap rm ${POOL}/${IMAGE}@${snap} echo "Removed ${snap}" done fi这个脚本只做最基本的按数量清理。生产环境建议加上快照时间戳解析,按天保留。另外,清理动作尽量放在业务低峰期,因为删除快照会触发元数据更新,如果快照数据量很大,也会产生不小的IO压力。
我在实际使用Ceph镜像快照的过程中,最大的感受是:快照和镜像本身都是很成熟的技术,真正的风险往往出在流程设计和运维习惯上。把概念理清,把命令跑熟,把容灾预案做扎实,这套存储体系才能真正可靠。