土法炼钢兴趣小组的算法知识备份

【分布式系统百科】主从复制:同步、异步与半同步的工程权衡

文章导航

分类入口
distributed
标签入口
#replication#leader-follower#failover#postgresql#mysql

目录

本文是分布式系统百科系列 Part IV:复制——一份数据,多个副本 的开篇。前面三个 Part 讨论了时间、一致性模型和共识协议;从这篇开始,我们把视角从”达成共识”转到”把数据复制到多个节点”。复制(Replication)是分布式存储的基本功,而主从复制(Leader-based Replication)是最常见的起点。

你启动了一个单节点的 PostgreSQL,跑了三年没出过事。某天凌晨,磁盘控制器烧了,数据库无法启动。备份是昨晚的,中间 8 小时的交易记录全部丢失。

解决办法只有一个:复制。把数据实时同步到另一台机器上,主节点挂了,从节点顶上。

但”实时同步”这四个字背后藏着一系列工程决策:复制是同步等确认还是异步不等?等几个从节点?Leader 挂了怎么选新的?选出来之后日志怎么对齐?

本文逐一拆解。

一、什么是主从复制

主从复制(Leader-based Replication),也叫主备复制、Primary-Standby Replication,核心模型极其简单:

  1. 一个 Leader(主节点) 接受所有写请求。
  2. 若干 Follower(从节点) 从 Leader 拉取写入日志(Write-Ahead Log,WAL),在本地重放,得到与 Leader 一致的数据副本。
  3. 读请求 可以发给 Leader 也可以发给 Follower(取决于一致性要求)。

这是关系数据库最主流的高可用方案——PostgreSQL Streaming Replication、MySQL 半同步复制、Oracle Data Guard、SQL Server Always On 都基于这个模型。

为什么是最常见的模式

代价也很明确:Leader 是单点。Leader 挂了,必须做故障转移(Failover)。这个过程可能丢数据、可能出现脑裂(Split-Brain)。后面会详细讨论。

WAL 日志运送

Leader 把每一笔写操作先写入 WAL,然后再更新数据页。Follower 接收 WAL 流,在本地重放。这种方式叫 物理复制(Physical Replication)——复制的是存储引擎级别的字节变更。

另一种方式是 逻辑复制(Logical Replication)——复制的是 SQL 语句或行级变更事件。逻辑复制更灵活(可以跨版本、跨引擎),但解析开销更大,也更容易出现不一致。

PostgreSQL 同时支持物理复制和逻辑复制;MySQL 的 binlog 复制本质上是逻辑复制(Row-Based / Statement-Based / Mixed)。

新 Follower 如何加入

一个常见的运维场景:集群已经运行了半年,现在要加一个新的 Follower。不可能让 Leader 从头重放半年的 WAL——WAL 早就被清理了。正确的流程是:

  1. 获取一致性快照:在 Leader 上执行 pg_basebackup(PostgreSQL)或 xtrabackup(MySQL),得到一个数据目录的物理拷贝和对应的 LSN 位点。
  2. 拷贝到新 Follower:把快照传输到新节点的数据目录。
  3. 启动并追赶:新 Follower 从快照对应的 LSN 开始拉取 WAL 流,逐步追上 Leader。
  4. 追赶完成:当复制延迟降到可接受范围内,新 Follower 正式上线接受读请求。
# PostgreSQL:使用 pg_basebackup 创建新 Follower
pg_basebackup -h leader-db -U replicator -D /var/lib/postgresql/data \
  --checkpoint=fast --wal-method=stream -P

# 创建 standby 信号文件
touch /var/lib/postgresql/data/standby.signal

# 配置连接信息后启动
pg_ctl start -D /var/lib/postgresql/data

整个过程中 Leader 不需要停机,也不需要加锁(快照通过 MVCC 保证一致性)。但传输快照的时间取决于数据量——一个 500GB 的数据库,在千兆网络下大约需要 70 分钟。

物理复制 vs 逻辑复制选型

维度 物理复制 逻辑复制
复制粒度 WAL 字节 行级变更
跨版本兼容 不支持 支持
选择性复制 整库 可选表/列
DDL 复制 自动 需手动处理
性能 高(无解析开销) 中(需解析和转换)
用途 高可用、灾备 数据集成、在线迁移

高可用场景首选物理复制,简单可靠。逻辑复制适合异构环境下的数据集成(如 PostgreSQL 到 Kafka、跨大版本在线升级)。

二、同步复制:强一致的代价

同步复制(Synchronous Replication)的流程:

  1. Client 发送写请求到 Leader。
  2. Leader 写入本地 WAL。
  3. Leader 把 WAL 发送给 所有 Follower。
  4. 等待所有(或指定数量的)Follower 确认已写入它们的本地 WAL。
  5. Leader 向 Client 返回成功。

关键点在第 4 步:Leader 不会在 Follower 确认之前返回成功。这意味着:

同步、异步与半同步复制模式对比

可用性链问题

代价是 可用性链(Chain of Availability):写入延迟取决于最慢的那个 Follower。

假设 Leader 在北京,Follower-A 在上海(RTT 30ms),Follower-B 在新加坡(RTT 80ms)。如果配置要求两个 Follower 都确认,每次写入至少等 80ms。更糟糕的是,如果 Follower-B 的磁盘 IO 出现毛刺,写入延迟可能飙到几秒。

如果某个 Follower 彻底挂了呢?Leader 会一直等,写入直接阻塞。整个集群的写可用性取决于最弱的那个 Follower——这就是可用性链。

-- PostgreSQL:配置全同步复制
-- postgresql.conf
synchronous_commit = on
synchronous_standby_names = 'FIRST 2 (standby_shanghai, standby_singapore)'
-- 含义:等待列表中前两个 standby 都确认才返回

在实际生产中,全同步复制极少使用。它把一个分布式系统的可用性拉低到了所有节点的交集。大多数团队会选择下面的异步或半同步方案。

同步复制的实际案例

尽管全同步罕见,但在特定场景下仍然有用:

关键观察:同步复制适合写入频率低、数据价值高的场景。高吞吐的 OLTP 系统通常不适用。

三、异步复制:延迟低,但可能丢数据

异步复制(Asynchronous Replication)的流程:

  1. Client 发送写请求到 Leader。
  2. Leader 写入本地 WAL。
  3. Leader 立刻 向 Client 返回成功。
  4. Leader 后台 把 WAL 发送给 Follower。
  5. Follower 收到后写入本地 WAL 并重放。

关键区别:第 3 步和第 4 步的顺序。Leader 不等 Follower 确认就返回,所以写延迟只取决于 Leader 本地磁盘 IO,与 Follower 无关。

优点:

致命缺点:Leader 崩溃时可能丢数据。如果 Leader 在把 WAL 发给 Follower 之前崩溃,那些已经向 Client 确认成功的写入就永久丢失了。

-- PostgreSQL:配置异步复制
-- postgresql.conf
synchronous_commit = off
-- Follower 连接后自动异步接收 WAL 流,无需额外配置

这有多危险?

假设 Leader 每秒处理 1000 笔写入,复制延迟 200ms。Leader 崩溃的瞬间,Follower 比 Leader 落后约 200 笔事务。这 200 笔事务的用户都收到了”写入成功”的响应,但数据其实已经丢了。

对于金融交易来说这不可接受。但对于社交媒体的点赞计数、日志收集系统来说,丢几百毫秒的数据是可以容忍的。选择复制模式的本质是在一致性、可用性、延迟之间做权衡

异步复制的复制延迟为什么会变大

正常情况下异步复制的延迟在毫秒级。但以下因素会导致延迟飙升:

  1. 写入突增:大批量数据导入、定时任务集中执行。Leader 的 WAL 产生速度超过 Follower 的消费速度。
  2. Follower 磁盘 IO 瓶颈:Follower 同时承担大量读查询,磁盘带宽被读 IO 和 WAL 重放 IO 争抢。
  3. 网络带宽不足:跨数据中心复制,带宽被其他业务占用。
  4. 长事务:MySQL 的 Statement-Based Replication 下,一个运行 30 分钟的 ALTER TABLE 在 Follower 上也要重放 30 分钟,期间后续事务全部排队。
  5. Follower 重启:Follower 重启后需要重新连接并追赶,期间延迟持续增大。

一个被频繁忽视的点:复制延迟不是恒定的,它会随负载波动。高峰期的延迟可能是低谷期的 100 倍。监控系统需要关注的不只是平均延迟,还有 P99 延迟和延迟的变化趋势。

四、半同步复制:折中的工程解

半同步复制(Semi-Synchronous Replication)是 MySQL 首先广泛推广的方案。核心思想:

这意味着:

MySQL 半同步协议细节

MySQL 的半同步复制分为两种模式:

模式 名称 等待时机 含义
AFTER_SYNC 增强半同步 引擎提交前等 ACK Follower 确认后才对其他会话可见
AFTER_COMMIT 传统半同步 引擎提交后等 ACK 其他会话可能先看到再回滚(幽灵读)

AFTER_SYNC(MySQL 5.7+ 的默认行为)更安全:Leader 在收到至少一个 Follower 的 ACK 后才在存储引擎层面提交事务,其他会话不可能读到”已提交但未复制”的数据。

-- MySQL:启用增强半同步复制
-- Leader 端
INSTALL PLUGIN rpl_semi_sync_master SONAME 'semisync_master.so';
SET GLOBAL rpl_semi_sync_master_enabled = 1;
SET GLOBAL rpl_semi_sync_master_timeout = 3000;  -- 3 秒超时
SET GLOBAL rpl_semi_sync_master_wait_for_slave_count = 1;
SET GLOBAL rpl_semi_sync_master_wait_point = 'AFTER_SYNC';

-- Follower 端
INSTALL PLUGIN rpl_semi_sync_slave SONAME 'semisync_slave.so';
SET GLOBAL rpl_semi_sync_slave_enabled = 1;

超时退化

rpl_semi_sync_master_timeout 控制等待超时。如果在 3 秒内没有任何 Follower 回复 ACK,Leader 自动 退化为异步模式,继续处理写入。这是一个关键的工程决策:

退化为异步后,Leader 会持续尝试恢复半同步。一旦 Follower 重新跟上,自动切回半同步模式。

半同步在生产中的注意事项

半同步看起来是完美的折中方案,但实际使用中有几个容易踩的坑:

坑一:所有 Follower 都挂了。如果配置了 wait_for_slave_count = 1,但唯一在线的 Follower 也挂了,Leader 会在超时后退化为异步。此时如果 Leader 也崩溃,数据就丢了。解决方案:至少部署 3 个 Follower,配置 wait_for_slave_count = 1,确保至少有 2 个备用。

坑二:超时后写入了大量异步数据。退化为异步后,Leader 可能在短时间内写入大量数据。如果随后 Leader 崩溃,这些数据全部丢失。有些团队在检测到退化后会主动降低写入速率或触发告警。

坑三:Follower 的 ACK 含义不清楚。MySQL 的 Semi-Sync ACK 表示 Follower 已经收到并写入 relay log,但不代表已经重放(Apply)。如果 Follower 在重放前崩溃,relay log 中的数据可能因为格式损坏而无法恢复(虽然概率极低)。PostgreSQL 的 synchronous_commit = on 则保证 Follower WAL 刷盘,更安全。

五、复制延迟深度分析

无论使用哪种异步模式,Follower 的数据总是比 Leader “旧”一些。这个时间差叫做 复制延迟(Replication Lag)。正常情况下延迟在毫秒级,但在负载高峰、网络抖动、Follower 硬件慢时可能飙到秒级甚至分钟级。

复制延迟会导致三类经典的一致性问题。

5.1 读己之写(Read-Your-Writes)

用户写了一条评论,刷新页面,评论不见了。原因:写入走 Leader,读取走了一个延迟较大的 Follower,那条评论还没复制过来。

# 模拟读己之写问题
import time
import psycopg2

# 写入走 Leader
leader = psycopg2.connect("host=leader-db dbname=app")
with leader.cursor() as cur:
    cur.execute("INSERT INTO comments (user_id, body) VALUES (%s, %s)",
                (42, '这是一条新评论'))
    leader.commit()
    comment_id = cur.fetchone()[0] if cur.description else None

# 立刻从 Follower 读
follower = psycopg2.connect("host=follower-db dbname=app")
with follower.cursor() as cur:
    cur.execute("SELECT body FROM comments WHERE user_id = %s "
                "ORDER BY created_at DESC LIMIT 1", (42,))
    row = cur.fetchone()
    print(row)  # 可能是 None!评论还没复制过来

# 解决方案一:写后读走 Leader
# 解决方案二:记录写入时间戳,Follower 滞后时路由到 Leader
# 解决方案三:等待 Follower 追上指定 LSN

解决思路

方案 实现 代价
写后读 Leader 应用层标记最近写入的用户,短时间内路由读到 Leader Leader 读负载增加
记录写入 LSN 写入时记录 Leader 的 LSN,读时确认 Follower 已追上 需要传递 LSN;Follower 未追上时阻塞或回退到 Leader
因果一致会话 使用会话级因果一致性保证 需要数据库原生支持(如 MongoDB causal consistency)

5.2 单调读(Monotonic Reads)

用户第一次请求读到了新数据(走了延迟小的 Follower-A),第二次请求读到了旧数据(走了延迟大的 Follower-B)。数据”穿越时空”回退了。

# 模拟单调读问题
import random

followers = ["follower-a", "follower-b", "follower-c"]

def read_balance(user_id: int) -> float:
    # 负载均衡随机选一个 Follower
    host = random.choice(followers)
    conn = psycopg2.connect(f"host={host} dbname=app")
    with conn.cursor() as cur:
        cur.execute("SELECT balance FROM accounts WHERE id = %s", (user_id,))
        return cur.fetchone()[0]

# 第一次读:follower-a(延迟 5ms),余额 1000
# 第二次读:follower-b(延迟 500ms),余额 800(更旧的值)
# 用户看到余额从 1000 "变回" 800,但实际没有扣款

解决思路:将同一个用户的所有读请求路由到同一个 Follower。最简单的实现是按 user_id 哈希选 Follower:

def get_follower(user_id: int) -> str:
    return followers[user_id % len(followers)]

5.3 一致前缀读(Consistent Prefix Reads)

因果相关的两次写入,在 Follower 上以相反的顺序被读到。例如:

这在分区(Sharding)+ 异步复制的架构中尤其常见。每个分区独立复制,没有全局顺序保证。

解决方案通常需要引入 因果序(Causal Order) 或确保因果相关的写入落到同一个分区。

复制延迟的量化思考

复制延迟不是一个抽象概念。我们可以用具体数字来感受它的影响:

复制延迟 影响 用户感知
<10ms 几乎无感知 正常体验
10~100ms 写后立刻读可能看不到 偶尔困惑
100ms~1s 读己之写频繁失败 明显异常
1~10s 多种一致性问题同时出现 严重体验问题
>10s Follower 数据严重过时 应停止从该 Follower 读取

实践中常见的策略是设置一个 最大可容忍延迟(Max Tolerable Lag)。当某个 Follower 的延迟超过阈值时,负载均衡器自动将其从读请求池中移除,直到延迟恢复到正常范围。

监控复制延迟

无论选择哪种模式,监控复制延迟都是必须的。

# PostgreSQL:查看复制延迟
psql -c "SELECT client_addr,
         state,
         sent_lsn,
         write_lsn,
         flush_lsn,
         replay_lsn,
         write_lag,
         flush_lag,
         replay_lag
  FROM pg_stat_replication;"

# MySQL:查看 Seconds_Behind_Master
mysql -e "SHOW SLAVE STATUS\G" | grep -E "Seconds_Behind|Slave_IO|Slave_SQL"

# 关键指标:
# PostgreSQL: replay_lag(重放延迟)
# MySQL: Seconds_Behind_Master(注意:这个值不准确,只是估算)

MySQL 的 Seconds_Behind_Master 陷阱:这个值是 Follower 当前重放的事件时间戳与当前时间的差值。如果 Follower 的 IO 线程已经拉取了所有 binlog 但 SQL 线程还在排队重放,Seconds_Behind_Master 显示的是 SQL 线程的延迟,而不是 IO 线程的延迟。更准确的方式是对比 Leader 和 Follower 的 GTID 位置。

六、故障转移:最危险的时刻

Leader 挂了之后的处理流程叫 故障转移(Failover)。这是主从复制架构中最复杂、最危险的环节。

故障转移流程

6.1 故障检测

怎么判断 Leader 挂了?最常见的方式是 心跳超时:Follower 周期性向 Leader 发送心跳请求,如果连续 N 次超时(如 3 次,每次 10 秒),判定 Leader 故障。

问题在于:网络分区和 Leader 真挂在 Follower 看来是一样的。无法区分”Leader 死了”和”Leader 和我之间的网络断了但 Leader 还活着”。这是脑裂的根源。

6.2 选举新 Leader

Leader 被判定故障后,需要从 Follower 中选一个新 Leader。选择标准通常是:

  1. 数据最新:选择 LSN(Log Sequence Number)最大的 Follower,即拥有最多已复制数据的节点。
  2. 优先级:管理员可以为每个 Follower 设置优先级(如 PostgreSQL 的 recovery_target_timeline)。
  3. 共识:如果有多个 Follower 候选,需要某种共识机制(如 Raft)来避免多个 Follower 同时自认为新 Leader。

6.3 日志对齐

新 Leader 选出来后,其它 Follower 需要把自己的 WAL 对齐到新 Leader。如果某个 Follower 的日志比新 Leader 多(它收到了一些旧 Leader 发出但未被多数确认的 WAL),那些多出来的日志必须 截断(Truncate)

这意味着 已经写入但未完成复制的数据会丢失。这是异步复制的固有风险。

6.4 脑裂(Split-Brain)

最恐怖的场景:旧 Leader 没有真死,只是网络分区。新 Leader 被选出来后,旧 Leader 恢复了网络连接,认为自己还是 Leader。此时两个 Leader 同时接受写入——脑裂。

脑裂的后果:两个 Leader 各自接受了不同的写入,数据产生分叉(Divergence),合并极其困难甚至不可能。

Fencing 实战案例:一次脑裂事故的完整过程

以下是一个在未配置 Fencing 机制的 PostgreSQL 主从集群中发生的脑裂场景还原:

t=0s      Leader-A 正常运行,Follower-B 和 Follower-C 从 Leader-A 复制
t=5s      Leader-A 所在机房网络交换机故障,Leader-A 与 Follower-B/C 断开连接
          但 Leader-A 仍在运行,且部分应用服务器仍可连接 Leader-A
t=35s     Follower-B 判定 Leader-A 故障,自动提升为新 Leader
          应用层 VIP 切换指向 Leader-B
t=35~120s 脑裂窗口:
          - 新连接走 Leader-B,写入订单 #1001~#1050
          - 旧连接仍走 Leader-A,写入订单 #1051~#1060
          - 两个 Leader 各自维护独立的 WAL 流
t=120s    网络恢复,Leader-A 重新可见
          Leader-A 的 WAL 与 Leader-B 已分叉
          订单 #1051~#1060 只存在于 Leader-A 上,与 Leader-B 的数据不一致

这个场景中,10 笔订单的数据丢失或需要人工比对合并。如果使用了 Fencing Token 机制,可以从根源上防止旧 Leader 继续写入:

改进方案(Fencing Token):
t=0s      Leader-A 持有 Fencing Token = 7
t=5s      网络分区发生
t=35s     Leader-B 提升,获取 Fencing Token = 8
t=35s+    Leader-B 的写入携带 Token=8,存储层接受
          Leader-A 的写入仍携带 Token=7
          存储层检测到 Token=7 < 当前最大 Token=8
          拒绝 Leader-A 的所有写入 -> 脑裂被阻止

Fencing Token 的核心思想是将 Leader 身份与单调递增的令牌绑定,存储层(或中间件层)只接受令牌值不低于已见最大值的写入请求。这要求存储层具备令牌检查能力——etcd 的 Lease 机制和 ZooKeeper 的临时节点天然支持这种模式。

下面的状态机图展示了故障转移的完整状态转换:

stateDiagram-v2
    [*] --> Normal: 集群启动

    Normal --> LeaderFailed: Leader心跳超时<br/>连续N次未响应
    Normal --> Normal: 心跳正常

    LeaderFailed --> Electing: 触发选举<br/>选择LSN最大的Follower

    Electing --> Electing: 选举冲突<br/>随机退避后重试
    Electing --> Fencing: 选出候选Leader<br/>获取新Fencing Token

    Fencing --> NewLeader: Fencing完成<br/>旧Leader被隔离/断电
    Fencing --> Electing: Fencing失败<br/>无法确认旧Leader停止

    NewLeader --> LogAlign: 新Leader就绪<br/>其他Follower开始日志对齐

    LogAlign --> Normal: 日志对齐完成<br/>VIP/DNS切换<br/>服务恢复

    LogAlign --> LogAlign: Follower日志截断<br/>追赶新Leader

该状态机展示了从正常运行到故障恢复的完整流程。关键观察点:Fencing 阶段是防止脑裂的核心防线;日志对齐阶段可能需要截断旧 Leader 未提交的日志条目,这是异步复制可能丢数据的根源。任何故障转移方案都应该在 Fencing 成功之后才允许新 Leader 接受写入。

防护措施(Fencing)

机制 原理 实现
Epoch / Term 编号 每次选举递增编号,旧 Leader 的写入因编号过期被 Follower 拒绝 Raft 的 Term、PostgreSQL 的 Timeline ID
STONITH Shoot The Other Node In The Head——在选出新 Leader 后,强制关闭(或隔离)旧 Leader 的机器 通过 IPMI / BMC 远程断电
Fencing Token 分布式锁的 token 随选举递增,存储层拒绝旧 token 的写入 需要存储层配合检查 token
共享存储仲裁 使用外部存储(如 ZooKeeper、etcd)记录谁是当前 Leader,节点写入前先检查 增加外部依赖

6.5 手动 vs 自动故障转移

维度 手动故障转移 自动故障转移
速度 慢(分钟到小时级,等人处理) 快(秒到十秒级)
安全性 高(人工判断确认后操作) 有风险(误判导致不必要切换)
适用场景 金融核心系统、对数据丢失零容忍 互联网业务、对可用性要求极高
脑裂风险 低(人工确认旧 Leader 已停止) 较高(需要可靠的 fencing 机制)

实践中很多团队选择 半自动:自动检测故障、准备好新 Leader、但需要运维人员一键确认才真正切换。

6.6 故障转移的完整时间线

一次典型的自动故障转移经历以下阶段:

t=0s     Leader 进程崩溃
t=0~10s  Follower 尚未察觉(心跳间隔内)
t=10s    第一次心跳超时
t=20s    第二次心跳超时
t=30s    第三次心跳超时 → 判定 Leader 故障
t=30~32s 选举新 Leader(选择 LSN 最大的 Follower)
t=32~35s 新 Leader 提升为可写状态,其他 Follower 对齐日志
t=35~40s DNS / VIP 更新,客户端开始连接新 Leader
t=40s    服务恢复

整个过程约 30~60 秒。这段时间内 写入完全不可用。读请求如果可以走 Follower,影响较小;如果读也走 Leader,则读写都不可用。

对于互联网业务来说,30 秒的写不可用通常可以接受。但对于交易系统来说,30 秒意味着几万笔交易无法处理。这就是为什么金融系统往往采用同城双活 + 手动切换的方案——宁可切换慢一些,也不能自动切换出脑裂。

6.7 故障转移后的数据修复

故障转移完成后,旧 Leader 恢复了怎么办?它不能直接加入集群——它的 WAL 可能包含新 Leader 没有的数据(那些在故障前已写入但未复制的事务)。

处理方式有两种:

  1. 重建:把旧 Leader 当作全新的 Follower,从新 Leader 做一次全量快照复制。简单粗暴但数据量大时很慢。
  2. pg_rewind(PostgreSQL 专用):比较旧 Leader 和新 Leader 的 WAL 分叉点,只回滚分叉之后的变更,然后从新 Leader 追赶。速度比全量重建快很多,但需要开启 wal_log_hints 或 data checksums。
# PostgreSQL:使用 pg_rewind 修复旧 Leader
pg_rewind --target-pgdata=/var/lib/postgresql/data \
          --source-server="host=new-leader port=5432 user=replicator" \
          --progress
# 然后配置为 standby 模式启动

七、PostgreSQL 流复制实战

PostgreSQL 的流复制(Streaming Replication)是物理复制方案,配置相对简单。

Leader(Primary)配置

# postgresql.conf (Leader)

# 基础复制参数
wal_level = replica                    # 必须设为 replica 或 logical
max_wal_senders = 5                    # 允许的最大 WAL 发送进程数
wal_keep_size = 1GB                    # 保留的 WAL 大小,防止 Follower 断连后追不上

# 同步模式控制
synchronous_commit = on                # on=同步, off=异步, remote_apply=等到 Follower 重放
synchronous_standby_names = 'FIRST 1 (standby1, standby2)'
# FIRST 1:等待列表中第一个可用的 standby 确认
# ANY 2 (s1, s2, s3):等待任意两个 standby 确认(quorum commit)

# 归档(可选但推荐)
archive_mode = on
archive_command = 'cp %p /archive/%f'

Follower(Standby)配置

PostgreSQL 12+ 使用 standby.signal 文件 + postgresql.conf 中的 primary_conninfo

# postgresql.conf (Follower)
primary_conninfo = 'host=leader-db port=5432 user=replicator password=secret application_name=standby1'
hot_standby = on                       # 允许 Follower 接受只读查询
# 在 Follower 的数据目录下创建信号文件
touch $PGDATA/standby.signal

synchronous_commit 详解

PostgreSQL 的 synchronous_commit 提供了五个级别,精确控制一致性和性能的权衡:

Leader WAL 写入 Leader WAL 刷盘 Follower WAL 写入 Follower WAL 刷盘 Follower 重放
off - - - - -
local Y Y - - -
remote_write Y Y Y - -
on Y Y Y Y -
remote_apply Y Y Y Y Y

Quorum Commit

PostgreSQL 9.6+ 支持 Quorum 模式:

synchronous_standby_names = 'ANY 2 (standby1, standby2, standby3)'

含义:3 个 Standby 中任意 2 个确认就返回。这比 FIRST 2 更灵活——FIRST 2 固定等前两个,如果第一个慢了全部阻塞;ANY 2 等最快的两个,第三个慢了无所谓。

这在跨数据中心部署中非常有用。假设三个 Standby 分别在北京、上海、新加坡:

八、MySQL Group Replication 与 Galera Cluster

当单纯的主从复制不够用时,MySQL 生态有两个更高级的方案:Group Replication(官方)和 Galera Cluster(第三方)。

MySQL Group Replication

Group Replication(GR)是 MySQL 5.7.17 引入的官方多节点复制方案。它在传统主从复制之上增加了:

GR 有两种模式:

模式 写入节点 冲突可能性 适用场景
Single-Primary 只有一个 Primary 接受写入 无(与传统主从相同) 大多数场景
Multi-Primary 所有节点都接受写入 有(通过 Certification 检测) 需要多点写入的场景
-- MySQL Group Replication 基础配置
-- my.cnf
[mysqld]
server_id = 1
gtid_mode = ON
enforce_gtid_consistency = ON
binlog_checksum = NONE

-- Group Replication 参数
plugin_load_add = 'group_replication.so'
group_replication_group_name = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"
group_replication_start_on_boot = OFF
group_replication_local_address = "node1:33061"
group_replication_group_seeds = "node1:33061,node2:33061,node3:33061"
group_replication_single_primary_mode = ON

Galera Cluster

Galera Cluster 是 Codership 开发的同步多主复制方案,被集成到 MariaDB(MariaDB Galera Cluster)和 Percona XtraDB Cluster 中。

核心机制:

  1. 虚拟同步复制(Virtually Synchronous Replication):事务提交时广播写集,所有节点认证(检测冲突)后本地提交。看起来是同步的(所有节点数据一致),但实际上各节点的应用顺序可以不同。
  2. 流控(Flow Control):如果某个节点的应用队列积压过长,它会通知其他节点减速,防止节点间差距过大。
  3. 全局事务排序:使用全局序列号(Global Transaction ID, GTID)保证所有节点以相同的逻辑顺序认证事务。

对比表

维度 MySQL Semi-Sync Group Replication Galera Cluster
复制方式 异步 + 至少一个同步 ACK 基于 Paxos 的共识 虚拟同步(Certification)
写入节点 单 Leader 单 / 多 Primary 所有节点可写
冲突检测 无(单写入点) Write Set 认证 Write Set 认证
最小节点数 2 3 3
网络分区处理 手动处理 自动(多数派继续服务) 自动(多数派继续服务)
延迟 低(只等一个 ACK) 中(Paxos 一轮广播) 中(认证广播)
流控 有(group_replication_flow_control_mode 有(gcs.fc_limit
跨数据中心 可以但延迟高 官方不推荐 可以但需要调参
DDL 处理 正常复制 需要谨慎(Rolling Schema Upgrade) TOI / RSU 两种模式

选择建议:

九、工程建议:何时用哪种模式

RPO/RTO 对比与复制模式时序分析

在做复制模式选型之前,必须明确两个关键指标:RPO(Recovery Point Objective,恢复点目标) 即能容忍丢失多少数据;RTO(Recovery Time Objective,恢复时间目标) 即故障后多久恢复服务。

复制模式 RPO RTO 写延迟 适用场景 代价
全同步 0(零数据丢失) 高(分钟级,需等所有Follower) 最高(等最慢Follower) 金融核心账务、审计日志 任一Follower故障阻塞写入
半同步(AFTER_SYNC) 0~极小(至少一副本) 中(秒~十秒级) 中(等最快Follower) 订单系统、支付系统 超时退化为异步时RPO不为0
异步 数百毫秒~数秒 低(秒级,快速切换) 最低(不等Follower) 日志、缓存、社交动态 Leader崩溃必丢数据
Quorum同步(ANY 2/3) 0~极小 中低(秒级) 中(等第N快的Follower) 跨数据中心部署 需要至少3个Follower

下面的时序图对比了三种复制模式在一次写入操作中的行为差异,特别标注了 Leader 崩溃时的恢复点:

sequenceDiagram
    participant C as 客户端
    participant L as Leader
    participant F1 as Follower-1
    participant F2 as Follower-2

    rect rgb(232, 245, 233)
        Note over C,F2: 同步复制 -- RPO=0
        C->>L: 写入请求
        L->>L: 写入本地WAL
        par 并行发送
            L->>F1: WAL数据
            L->>F2: WAL数据
        end
        F1-->>L: ACK(已刷盘)
        F2-->>L: ACK(已刷盘)
        L-->>C: 写入成功
        Note over L: 此处崩溃: 数据丢失=0
    end

    rect rgb(255, 243, 224)
        Note over C,F2: 半同步复制 -- RPO接近0
        C->>L: 写入请求
        L->>L: 写入本地WAL
        par 并行发送
            L->>F1: WAL数据
            L->>F2: WAL数据
        end
        F1-->>L: ACK(已刷盘)
        Note over L: 收到1个ACK即返回
        L-->>C: 写入成功
        Note over L: 此处崩溃: F1有数据, 可恢复
        F2-->>L: ACK(异步到达)
    end

    rect rgb(252, 228, 236)
        Note over C,F2: 异步复制 -- RPO=复制延迟
        C->>L: 写入请求
        L->>L: 写入本地WAL
        L-->>C: 写入成功(立即返回)
        Note over L: 此处崩溃: 数据丢失!
        L->>F1: WAL数据(后台发送)
        L->>F2: WAL数据(后台发送)
    end

该时序图清晰展示了三种模式的核心区别:同步复制在所有 Follower 确认后才返回,RPO 为零但延迟最高;半同步只等一个 Follower,平衡了安全性和延迟;异步复制立即返回,延迟最低但 Leader 崩溃时必然丢失未复制的数据。红色区域标注的”此处崩溃”是理解 RPO 差异的关键。

决策框架

能容忍丢数据吗?
├── 不能 → 同步或半同步
│   ├── 能容忍写延迟增加?
│   │   ├── 能 → 全同步(synchronous_commit = on)
│   │   └── 不能 → 半同步(MySQL Semi-Sync 或 PG 的 ANY N quorum)
│   └── 需要零数据丢失 + 自动切换?
│       └── Group Replication / Galera / etcd+Patroni
└── 能 → 异步
    ├── 可接受秒级丢失 → 异步 + 短复制延迟监控
    └── 可接受分钟级丢失 → 异步 + 备份兜底

实践清单

序号 建议 说明
1 监控复制延迟 设置告警阈值(如 >1s 告警、>10s 断开读流量)
2 读写分离要考虑一致性 写后读走 Leader 或等待 LSN 追上
3 故障转移要有 fencing 没有 fencing 的自动切换等于定时炸弹
4 定期演练故障转移 不演练就不知道切换脚本能不能跑通
5 半同步超时不要太短 太短导致频繁退化为异步,失去半同步的意义
6 跨数据中心用 quorum ANY NFIRST N 更抗单点故障
7 升级 Follower 前先检查延迟 延迟大的 Follower 升为 Leader 意味着丢更多数据
8 考虑使用 Patroni / Orchestrator PostgreSQL 用 Patroni + etcd,MySQL 用 Orchestrator,成熟的故障转移管理工具

延迟 vs 一致性 vs 可用性总结

模式 写延迟 数据安全 可用性 典型场景
全同步 最高(RPO=0) 最低 金融核心账务
半同步 高(至少一副本) 交易系统、订单系统
异步 低(可能丢数据) 最高 日志、缓存、社交媒体
Quorum 同步 较高 跨数据中心部署

十、总结

主从复制是分布式存储最基础也最实用的技术。它的工程权衡可以用一句话概括:

等 Follower 确认就安全但慢,不等就快但可能丢。

同步、异步、半同步不是三个割裂的选择,而是一个连续谱上的三个标记点。PostgreSQL 的 synchronous_commit 五个级别就是这个连续谱的具体实现。

故障转移是整个体系中最危险的环节——不是因为技术上难,而是因为在极端场景下做出正确判断很难。自动化的故障转移需要可靠的 fencing 机制保障,否则脑裂比宕机更可怕。

下一篇我们讨论 多主复制(Multi-Leader Replication)——当写入压力超过单个 Leader 的承载能力,或者需要跨数据中心低延迟写入时,主从复制不再适用。多主带来了写冲突这个全新的挑战。

参考资料

  1. Kleppmann, Martin. Designing Data-Intensive Applications. O’Reilly, 2017. Chapter 5: Replication.
  2. PostgreSQL Documentation. High Availability, Load Balancing, and Replication.
  3. PostgreSQL Documentation. synchronous_commit.
  4. MySQL Documentation. Semisynchronous Replication.
  5. MySQL Documentation. Group Replication.
  6. Galera Cluster Documentation. Certification-Based Replication.
  7. Ongaro, Diego, and John Ousterhout. “In Search of an Understandable Consensus Algorithm.” USENIX ATC, 2014.
  8. Kingsbury, Kyle. Jepsen: MySQL. 分布式系统正确性测试。
  9. Percona. Percona XtraDB Cluster Documentation.
  10. Zalando. Patroni: A Template for High Availability PostgreSQL.

Prev: 共识协议的工程权衡 | Next: 多主复制

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。

2026-04-13 · distributed

【分布式系统百科】多主复制:冲突检测与解决策略的深水区

单主复制只有一个节点能写入,跨数据中心延迟高、写入吞吐有上限。多主复制(Multi-Leader Replication)让每个数据中心都有自己的 Leader,写入延迟降到本地网络级别——但代价是并发写入可能产生冲突。本文深入拆解向量时钟的冲突检测机制、五种主流冲突解决策略(LWW、自定义合并函数、CRDT、OT、无冲突 Schema 设计)以及 CouchDB 的多主实战案例,帮你判断什么场景值得趟这趟浑水。

2026-04-13 · distributed

【分布式系统百科】成员协议:SWIM 与 Gossip 的工程实现

从 Gossip 协议的 SI 传播模型出发,深入拆解 SWIM 故障检测协议的直接探测、间接探测和怀疑机制,分析 HashiCorp Memberlist 的源码实现,对比 Serf 与 Consul 的成员管理策略,并提供基于 Memberlist 构建集群成员管理的完整 Go 代码示例。


By .