3、一致性保障:文件系统一致性(fsck)、应用层一致性(Quiesce)、分布式事务回滚

镜像回滚,说白了就是让系统「回到过去」。但问题来了——你回到的那个时间点,数据是干净的吗?

我见过太多人以为只要把磁盘快照恢复回去就万事大吉了。结果呢?数据库启动报错,文件系统挂载失败,业务数据对不上账。嗯,这就是没处理好一致性问题。

这一章,我们就来拆解三个层面的保障手段:文件系统、应用层、分布式事务。每个层面解决不同的问题,缺一不可。

3.1 文件系统一致性:fsck 是你的最后防线

先说说最底层的问题。你拍了一个磁盘快照,但快照那一瞬间,文件系统可能正在写数据。缓存里的数据还没落盘,元数据还没更新完。这时候你回滚回去,文件系统就处于「不一致」状态。

怎么解决?两个办法:

  • 回滚后自动执行 fsck——检查并修复文件系统结构
  • 回滚前强制 sync + 冻结文件系统——让快照点干净

我个人习惯在回滚脚本里加一段 fsck 检查。别指望用户手动执行,他们大概率会忘。

核心原则:回滚后的第一个动作,必须是文件系统一致性检查。否则后续所有操作都建立在沙子上。

fsck 能修什么?

  • 超级块损坏或校验失败
  • inode 引用计数错误
  • 目录项丢失或循环
  • 块位图与实际情况不符

但 fsck 不是万能的。遇到严重的元数据损坏,它可能直接放弃修复。所以更靠谱的做法是——在拍快照前就保证文件系统是干净的。

我的经验:在 XFS 文件系统上,我曾经遇到过回滚后 fsck 卡住 40 分钟的情况。后来发现是日志太大。从那以后,我要求所有关键系统的快照前必须执行 xfs_freeze 或 fsfreeze,把日志先刷干净。

3.2 应用层一致性:Quiesce 机制

文件系统一致了,应用层就一致了吗?不一定。

举个例子:MySQL 正在执行一个跨多张表的事务,刚写了一半,快照拍了。回滚后,这个事务只提交了一半——数据对不上。

这就是应用层一致性问题。解决思路叫 Quiesce(静默)。说白了就是:在拍快照前,让应用进入一个「暂停写操作」的状态。

Quiesce 的典型流程:

  1. 通知应用准备进入静默状态
  2. 应用完成当前正在执行的事务
  3. 应用暂停接受新的写请求
  4. 应用将内存中的脏数据刷到磁盘
  5. 通知存储层:可以拍快照了
  6. 快照完成后,恢复应用的写操作

不同应用的 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 是另一种思路。它把一个长事务拆成多个本地事务,每个本地事务都有对应的补偿操作(回滚操作)。

举个例子:

  1. 订单服务创建订单(正向操作)
  2. 库存服务扣减库存(正向操作)
  3. 支付服务扣款(正向操作)

如果第 3 步失败了,Saga 会依次执行补偿操作:

  1. 支付服务:退款(补偿操作)
  2. 库存服务:加回库存(补偿操作)
  3. 订单服务:取消订单(补偿操作)

Saga 的优点是高可用、低延迟。缺点是最终一致性,中间状态对外可见。

我的建议:在做镜像回滚时,如果底层是分布式系统,不要只依赖存储层的快照回滚。一定要在应用层配合 Saga 的补偿机制。我见过一个案例:快照回滚后,订单状态回到了「已支付」,但库存扣减的记录没回滚——因为库存服务用的是独立数据库,快照没覆盖到。最后靠 Saga 的补偿脚本手动修复了数据。

3.4 三层一致性保障的关系

这三层不是互斥的,而是层层递进的:

  • 文件系统一致性——保证磁盘数据结构的完整性
  • 应用层一致性——保证单个应用内部事务的完整性
  • 分布式事务一致性——保证跨服务、跨数据库的业务完整性

你想想看,如果只做了文件系统一致性,回滚后 MySQL 的表空间文件是完整的,但 binlog 和 relay log 可能对不上。如果只做了应用层一致性,但底层文件系统坏了,那应用层再干净也没用。

所以我的做法是:三层都要做,但每层的侧重点不同。

文件系统层靠 fsck 和 fsfreeze 兜底。应用层靠 Quiesce 机制保证快照点干净。分布式层靠 Saga 或 2PC 保证跨服务回滚的完整性。

下面这张图可以帮你理清整体逻辑:

镜像回滚一致性保障三层架构 第一层:文件系统一致性 核心手段:fsck、fsfreeze、sync 目标:保证磁盘元数据和数据块的完整性 典型工具:xfs_repair、fsck.ext4、btrfs check 第二层:应用层一致性 核心手段:Quiesce(静默机制) 目标:保证应用内部事务在快照点完整 典型实现:FLUSH TABLES、pg_start_backup、db.fsyncLock 第三层:分布式事务一致性 核心手段:2PC(两阶段提交)/ Saga(补偿事务) 目标:保证跨服务、跨数据库的业务状态一致 典型实现:Seata、TCC、本地消息表 关键原则 层层递进 缺一不可 下层是上层 的基础 回滚前 先检查 下层一致性 Quiesce 超时熔断 分布式回滚 要有补偿 脚本兜底

3.5 实战中的避坑指南

最后分享几个我踩过的坑:

坑一:fsck 不是万能的。遇到硬件层面的静默数据损坏(比如磁盘坏道导致的位翻转),fsck 检查不出来。这时候需要配合 checksum 校验,比如 ZFS 或 Btrfs 的自校验特性。

坑二:Quiesce 超时处理。我曾经遇到一个数据库,Quiesce 操作花了 15 分钟还没完成。原因是有一个大事务在回滚。后来我们加了超时机制——Quiesce 超过 60 秒就强制拍快照,并在日志里记录警告。

坑三:分布式事务回滚的幂等性。Saga 的补偿操作必须幂等。否则补偿执行两次,数据就乱了。我见过一个支付补偿接口,因为网络重试导致同一笔退款执行了两次,最后对账时发现多退了钱。

嗯,一致性保障这件事,说白了就是「宁可多检查一次,也不要相信运气」。三层都做到位了,回滚才能真的放心。