Wiki 深度科普:为什么应用软件难以实现“双活(Active-Active)”?底层机制剖析

要理解为什么双活如此困难,我们需要从单机文件 I/O 的“欺骗性”说起,进而推演到多机并发时的灾难。


1. 机制一:操作系统的“页缓存(Page Cache)”与丢失更新(Lost Update)

在单机时代,操作系统(OS)为了追求极致的性能,设计了一套“欺骗”应用程序的机制:

[ 应用程序 ] --(写入数据 "A")--> [ OS 内存页缓存 (Page Cache) ] --(立即返回 "成功")
                                       |
                                       +---(异步后台刷盘)---> [ 物理磁盘 ]

当程序调用 write() 写入文件时,操作系统并没有立即把数据写到磁盘上,而是写到了内存的 页缓存(Page Cache) 中,然后就对程序宣告“写入成功”。随后,操作系统在后台异步将缓存刷新到磁盘。

当这个机制放到“双活 + 共享文件系统(如 NFS)”中,灾难就发生了:

假设有两台双活的主机(Host A 和 Host B)挂载了同一个 NFS 共享存储,同时修改同一个配置文件 config.xml

                        [ NFS 共享存储 (config.xml) ]
                                 |  |
         +-----------------------+  +-----------------------+
         | (读取旧版本)                                       | (读取旧版本)
         v                                                   v
  [ Host A 内存 ]                                      [ Host B 正常 ]
  修改内容为 "A"                                        修改内容为 "B"
         |                                                   |
         v (写入 Page Cache)                                 v (写入 Page Cache)
  [ Host A 刷盘 ]                                      [ Host B 刷盘 ]
         |                                                   |
         +---------------------> [ 相互覆盖 ] <---------------+
  1. Host A 读取了 config.xml(内容为 0)。
  2. Host B 也读取了 config.xml(内容为 0)。
  3. Host A 将内存中的内容修改为 A,并写入本地 Page Cache,随后刷盘写入 NFS。此时 NFS 上文件内容变为 A
  4. Host B 此时完全不知道 Host A 做了修改。它将自己内存中的内容修改为 B,同样写入本地 Page Cache,随后刷盘写入 NFS。
  5. 结果:Host B 的写入完全覆盖了 Host A 的写入。Host A 的修改凭空消失了,这在数据库领域被称为“丢失更新”(Lost Update)“脏写”(Dirty Write)

2. 机制二:内存状态与磁盘状态的“脱节(Asynchrony)”

一个应用软件在运行时的“状态(State)”,是由【磁盘上的持久化文件】【JVM/内存中的活跃线程与数据结构】共同构成的。

像 Jenkins、Jira、GitLab 这样的应用,其底层设计是:内存是磁盘的绝对权威。

  • 当用户在 Jenkins 页面上创建一个新任务时,Jenkins 会在内存中生成这个任务对象,然后异步将其序列化为 XML 文件保存到磁盘。
  • 双活状态下的致命问题
    如果 Host A 修改了磁盘上的 XML,Host B 的 JVM 内存是完全感知不到的。Host B 内存中保存的依然是旧的配置。
    除非 Host B 每次操作前都去重新扫描一次磁盘(这会带来极其恐怖的磁盘 I/O 性能损耗),否则 Host B 就会用其内存中的旧数据去覆盖磁盘,导致数据回滚和逻辑错乱。

3. 机制三:分布式系统中的“共识协议(Consensus Protocol)”缺失

那么,为什么像 MySQL、Oracle、Elasticsearch、ZooKeeper 这样的软件就可以做多节点高可用,甚至部分实现多写呢?

因为它们底层拥有极其复杂的共识算法与分布式锁管理器(DLM),而普通应用软件没有。

比较维度 普通应用软件(如 Jenkins) 分布式数据库 / 共识引擎(如 ZooKeeper, MySQL)
写入介质 直接通过 OS 提供的 File I/O API(如 FileWriter)直接写文件。 通过特定的协议(如 Raft, Paxos, 二阶段提交 2PC)进行多节点写协商。
并发控制 。完全依赖操作系统和文件系统的锁,对应用层不透明。 强控制。拥有多版本并发控制(MVCC)、分布式意向锁、行级锁等。
数据同步 依赖外部共享存储的单方面写入,节点间不进行内存同步。 节点间通过 Binlog、Raft log 等在内存中实时广播和同步状态。

如果要让 Jenkins 实现真正的双活,其研发团队必须重构整套底层代码,引入像 Raft 这样的分布式共识算法,或者将所有的文件读写改成分布式数据库(如 Spanner)调用。这相当于重新写一个软件,开发成本和性能代价极大。


4. 机制四:CAP 定理的物理极限约束

分布式领域著名的 CAP 定理 证明:在一个分布式系统中,一致性(Consistency)可用性(Availability)分区容错性(Partition tolerance) 这三者不可兼得,最多只能同时满足两个。

                     Consistency (强一致性)
                             /\
                            /  \
                           /    \
                          /  *   \  <-- 开源 Jenkins 难以跨越的物理极限
                         /________\
            Availability            Partition tolerance
            (高可用性)               (分区容错性)

当网络发生抖动或分裂时(出现了 Partition):

  • 如果双活的两台机器都允许写入(保证了 Availability),那么它们在无法通信的情况下,数据必然会往不同的方向分叉,最终导致数据不一致(丧失了 Consistency)。
  • 如果要保证数据绝对一致(Consistency),在网络出现问题时,系统就必须拒绝其中一台主机的写入(丧失了 Availability),这就退化成了主备模式。

5. 总结:存储介质与并发控制矩阵

为了方便您在 Wiki 中进行对比,可以使用以下矩阵:

存储方案 允许多机并发写入? 底层后果 / 解决机制
本地文件系统 (Ext4/XFS) 绝对禁止(无法多机挂载) 物理隔离。
共享文件系统 (NFS/NAS) 允许挂载,但禁止应用级并发写 缺乏应用层分布式锁,并发写直接导致文件内容覆盖和损坏
分布式块存储 (SAN/GFS) 允许(需要配合 GFS2 等集群文件系统) 需要极其复杂的分布式锁管理器(DLM)协调块设备写入。
分布式共识数据库 (Raft/Paxos) 允许 通过共识算法在软件协议层解决冲突,确保强一致性(代价是写入延迟增加)。

因此,对于绝大多数非数据库类的企业级应用软件,“主备热备(Active-Passive)”是目前成本最低、技术最成熟、数据最安全的唯一高可用落地方案。

posted on 2026-05-24 12:32  LeeHang  阅读(12)  评论(0)    收藏  举报