3、一致性保障:文件系统一致性(fsck)、应用层一致性(Quiesce)、分布式事务回滚
镜像回滚,说白了就是让系统「回到过去」。但问题来了——你回到的那个时间点,数据是干净的吗?
我见过太多人以为只要把磁盘快照恢复回去就万事大吉了。结果呢?数据库启动报错,文件系统挂载失败,业务数据对不上账。嗯,这就是没处理好一致性问题。
这一章,我们就来拆解三个层面的保障手段:文件系统、应用层、分布式事务。每个层面解决不同的问题,缺一不可。
3.1 文件系统一致性:fsck 是你的最后防线
先说说最底层的问题。你拍了一个磁盘快照,但快照那一瞬间,文件系统可能正在写数据。缓存里的数据还没落盘,元数据还没更新完。这时候你回滚回去,文件系统就处于「不一致」状态。
怎么解决?两个办法:
- 回滚后自动执行 fsck——检查并修复文件系统结构
- 回滚前强制 sync + 冻结文件系统——让快照点干净
我个人习惯在回滚脚本里加一段 fsck 检查。别指望用户手动执行,他们大概率会忘。
核心原则:回滚后的第一个动作,必须是文件系统一致性检查。否则后续所有操作都建立在沙子上。
fsck 能修什么?
- 超级块损坏或校验失败
- inode 引用计数错误
- 目录项丢失或循环
- 块位图与实际情况不符
但 fsck 不是万能的。遇到严重的元数据损坏,它可能直接放弃修复。所以更靠谱的做法是——在拍快照前就保证文件系统是干净的。
我的经验:在 XFS 文件系统上,我曾经遇到过回滚后 fsck 卡住 40 分钟的情况。后来发现是日志太大。从那以后,我要求所有关键系统的快照前必须执行 xfs_freeze 或 fsfreeze,把日志先刷干净。
3.2 应用层一致性:Quiesce 机制
文件系统一致了,应用层就一致了吗?不一定。
举个例子:MySQL 正在执行一个跨多张表的事务,刚写了一半,快照拍了。回滚后,这个事务只提交了一半——数据对不上。
这就是应用层一致性问题。解决思路叫 Quiesce(静默)。说白了就是:在拍快照前,让应用进入一个「暂停写操作」的状态。
Quiesce 的典型流程:
- 通知应用准备进入静默状态
- 应用完成当前正在执行的事务
- 应用暂停接受新的写请求
- 应用将内存中的脏数据刷到磁盘
- 通知存储层:可以拍快照了
- 快照完成后,恢复应用的写操作
不同应用的 Quiesce 方式不一样:
| 应用类型 | Quiesce 方式 | 关键命令/接口 |
|---|---|---|
| MySQL | FLUSH TABLES WITH READ LOCK | FLUSH TABLES WITH READ LOCK; SHOW MASTER STATUS; |
| PostgreSQL | pg_start_backup() | SELECT pg_start_backup('label'); |
| MongoDB | db.fsyncLock() | db.fsyncLock(); db.fsyncUnlock(); |
| VMware vSphere | VMware Tools quiesce | 快照时勾选「Quiesce guest file system」 |
| 自定义应用 | 自定义 API 回调 | 应用暴露 /quiesce 和 /resume 端点 |
注意:Quiesce 不是银弹。如果应用本身有 bug,或者 Quiesce 超时,快照可能还是不一致。我曾经在项目中遇到一个 Java 应用,它的 Quiesce 回调里有个死锁,导致快照一直卡住。最后我们加了超时熔断——超过 30 秒就强制拍快照,宁可丢一点数据,也不能让整个备份流程卡死。
3.3 分布式事务回滚:两阶段提交与 Saga
到了分布式系统层面,事情就更复杂了。一个操作可能涉及多个服务、多个数据库、多个消息队列。回滚一个镜像,可能意味着要把整个分布式事务的状态都回退到某个一致点。
这里有两个主流方案:
3.3.1 两阶段提交(2PC)
2PC 的思路很直接:
- 第一阶段(准备阶段):协调者问所有参与者:「能提交吗?」参与者记录日志,锁住资源,回复「准备好了」。
- 第二阶段(提交/回滚阶段):如果所有人都说准备好了,协调者通知大家提交。如果有人说不,或者超时没回复,协调者通知大家回滚。
2PC 的优点是强一致性。缺点是性能差、阻塞时间长、协调者是单点。
适用场景:对一致性要求极高、并发量不大的系统。比如金融核心账务系统。
3.3.2 Saga 模式
Saga 是另一种思路。它把一个长事务拆成多个本地事务,每个本地事务都有对应的补偿操作(回滚操作)。
举个例子:
- 订单服务创建订单(正向操作)
- 库存服务扣减库存(正向操作)
- 支付服务扣款(正向操作)
如果第 3 步失败了,Saga 会依次执行补偿操作:
- 支付服务:退款(补偿操作)
- 库存服务:加回库存(补偿操作)
- 订单服务:取消订单(补偿操作)
Saga 的优点是高可用、低延迟。缺点是最终一致性,中间状态对外可见。
我的建议:在做镜像回滚时,如果底层是分布式系统,不要只依赖存储层的快照回滚。一定要在应用层配合 Saga 的补偿机制。我见过一个案例:快照回滚后,订单状态回到了「已支付」,但库存扣减的记录没回滚——因为库存服务用的是独立数据库,快照没覆盖到。最后靠 Saga 的补偿脚本手动修复了数据。
3.4 三层一致性保障的关系
这三层不是互斥的,而是层层递进的:
- 文件系统一致性——保证磁盘数据结构的完整性
- 应用层一致性——保证单个应用内部事务的完整性
- 分布式事务一致性——保证跨服务、跨数据库的业务完整性
你想想看,如果只做了文件系统一致性,回滚后 MySQL 的表空间文件是完整的,但 binlog 和 relay log 可能对不上。如果只做了应用层一致性,但底层文件系统坏了,那应用层再干净也没用。
所以我的做法是:三层都要做,但每层的侧重点不同。
文件系统层靠 fsck 和 fsfreeze 兜底。应用层靠 Quiesce 机制保证快照点干净。分布式层靠 Saga 或 2PC 保证跨服务回滚的完整性。
下面这张图可以帮你理清整体逻辑:
3.5 实战中的避坑指南
最后分享几个我踩过的坑:
坑一:fsck 不是万能的。遇到硬件层面的静默数据损坏(比如磁盘坏道导致的位翻转),fsck 检查不出来。这时候需要配合 checksum 校验,比如 ZFS 或 Btrfs 的自校验特性。
坑二:Quiesce 超时处理。我曾经遇到一个数据库,Quiesce 操作花了 15 分钟还没完成。原因是有一个大事务在回滚。后来我们加了超时机制——Quiesce 超过 60 秒就强制拍快照,并在日志里记录警告。
坑三:分布式事务回滚的幂等性。Saga 的补偿操作必须幂等。否则补偿执行两次,数据就乱了。我见过一个支付补偿接口,因为网络重试导致同一笔退款执行了两次,最后对账时发现多退了钱。
嗯,一致性保障这件事,说白了就是「宁可多检查一次,也不要相信运气」。三层都做到位了,回滚才能真的放心。