本文是分布式系统百科系列 Part IV:复制——一份数据,多个副本 的开篇。前面三个 Part 讨论了时间、一致性模型和共识协议;从这篇开始,我们把视角从”达成共识”转到”把数据复制到多个节点”。复制(Replication)是分布式存储的基本功,而主从复制(Leader-based Replication)是最常见的起点。
你启动了一个单节点的 PostgreSQL,跑了三年没出过事。某天凌晨,磁盘控制器烧了,数据库无法启动。备份是昨晚的,中间 8 小时的交易记录全部丢失。
解决办法只有一个:复制。把数据实时同步到另一台机器上,主节点挂了,从节点顶上。
但”实时同步”这四个字背后藏着一系列工程决策:复制是同步等确认还是异步不等?等几个从节点?Leader 挂了怎么选新的?选出来之后日志怎么对齐?
本文逐一拆解。
一、什么是主从复制
主从复制(Leader-based Replication),也叫主备复制、Primary-Standby Replication,核心模型极其简单:
- 一个 Leader(主节点) 接受所有写请求。
- 若干 Follower(从节点) 从 Leader 拉取写入日志(Write-Ahead Log,WAL),在本地重放,得到与 Leader 一致的数据副本。
- 读请求 可以发给 Leader 也可以发给 Follower(取决于一致性要求)。
这是关系数据库最主流的高可用方案——PostgreSQL Streaming Replication、MySQL 半同步复制、Oracle Data Guard、SQL Server Always On 都基于这个模型。
为什么是最常见的模式
- 简单:写入路径只有一个入口,不需要处理写冲突。
- 成熟:几乎所有生产级数据库都内置支持。
- 可预测:Leader 决定写入顺序,Follower 按序回放,状态机复制(State Machine Replication)的经典实例。
代价也很明确: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 早就被清理了。正确的流程是:
- 获取一致性快照:在 Leader 上执行
pg_basebackup(PostgreSQL)或xtrabackup(MySQL),得到一个数据目录的物理拷贝和对应的 LSN 位点。 - 拷贝到新 Follower:把快照传输到新节点的数据目录。
- 启动并追赶:新 Follower 从快照对应的 LSN 开始拉取 WAL 流,逐步追上 Leader。
- 追赶完成:当复制延迟降到可接受范围内,新 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)的流程:
- Client 发送写请求到 Leader。
- Leader 写入本地 WAL。
- Leader 把 WAL 发送给 所有 Follower。
- 等待所有(或指定数量的)Follower 确认已写入它们的本地 WAL。
- Leader 向 Client 返回成功。
关键点在第 4 步:Leader 不会在 Follower 确认之前返回成功。这意味着:
- 强一致:任何一个 Follower 上都能读到最新的已提交数据。
- 零数据丢失:即使 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 都确认才返回在实际生产中,全同步复制极少使用。它把一个分布式系统的可用性拉低到了所有节点的交集。大多数团队会选择下面的异步或半同步方案。
同步复制的实际案例
尽管全同步罕见,但在特定场景下仍然有用:
- 金融核心账务系统:银行的核心交易数据库往往配置为同步复制。一笔转账涉及资金安全,即使延迟高一些也不能丢数据。典型配置是同城双中心同步复制 + 异地异步灾备。
- 分布式元数据存储:etcd、ZooKeeper 这类协调服务本质上是同步复制(通过 Raft / ZAB 协议)。因为元数据量小、写入频率低,同步的延迟开销可以接受。
- 配置中心:配置变更频率极低,但一旦丢失会导致大面积故障。同步复制的成本在这个场景下可以忽略。
关键观察:同步复制适合写入频率低、数据价值高的场景。高吞吐的 OLTP 系统通常不适用。
三、异步复制:延迟低,但可能丢数据
异步复制(Asynchronous Replication)的流程:
- Client 发送写请求到 Leader。
- Leader 写入本地 WAL。
- Leader 立刻 向 Client 返回成功。
- Leader 后台 把 WAL 发送给 Follower。
- Follower 收到后写入本地 WAL 并重放。
关键区别:第 3 步和第 4 步的顺序。Leader 不等 Follower 确认就返回,所以写延迟只取决于 Leader 本地磁盘 IO,与 Follower 无关。
优点:
- 低延迟:写入不等网络往返。
- 高可用:Follower 挂了不影响 Leader 写入。
致命缺点:Leader 崩溃时可能丢数据。如果 Leader 在把 WAL 发给 Follower 之前崩溃,那些已经向 Client 确认成功的写入就永久丢失了。
-- PostgreSQL:配置异步复制
-- postgresql.conf
synchronous_commit = off
-- Follower 连接后自动异步接收 WAL 流,无需额外配置这有多危险?
假设 Leader 每秒处理 1000 笔写入,复制延迟 200ms。Leader 崩溃的瞬间,Follower 比 Leader 落后约 200 笔事务。这 200 笔事务的用户都收到了”写入成功”的响应,但数据其实已经丢了。
对于金融交易来说这不可接受。但对于社交媒体的点赞计数、日志收集系统来说,丢几百毫秒的数据是可以容忍的。选择复制模式的本质是在一致性、可用性、延迟之间做权衡。
异步复制的复制延迟为什么会变大
正常情况下异步复制的延迟在毫秒级。但以下因素会导致延迟飙升:
- 写入突增:大批量数据导入、定时任务集中执行。Leader 的 WAL 产生速度超过 Follower 的消费速度。
- Follower 磁盘 IO 瓶颈:Follower 同时承担大量读查询,磁盘带宽被读 IO 和 WAL 重放 IO 争抢。
- 网络带宽不足:跨数据中心复制,带宽被其他业务占用。
- 长事务:MySQL 的 Statement-Based
Replication 下,一个运行 30 分钟的
ALTER TABLE在 Follower 上也要重放 30 分钟,期间后续事务全部排队。 - Follower 重启:Follower 重启后需要重新连接并追赶,期间延迟持续增大。
一个被频繁忽视的点:复制延迟不是恒定的,它会随负载波动。高峰期的延迟可能是低谷期的 100 倍。监控系统需要关注的不只是平均延迟,还有 P99 延迟和延迟的变化趋势。
四、半同步复制:折中的工程解
半同步复制(Semi-Synchronous Replication)是 MySQL 首先广泛推广的方案。核心思想:
- Leader 等待 至少一个 Follower 确认写入成功后,才向 Client 返回。
- 其它 Follower 异步复制。
这意味着:
- 至少有一份完整副本:如果 Leader 崩溃,至少有一个 Follower 拥有所有已确认的数据。
- 延迟可控:只等最快的那个 Follower,而不是最慢的。
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 自动
退化为异步模式,继续处理写入。这是一个关键的工程决策:
- 超时太短(如 200ms):网络抖动就退化,半同步形同虚设。
- 超时太长(如 30s):Follower 故障时写入阻塞 30 秒,业务不可接受。
- 典型配置:1~5 秒。
退化为异步后,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 上以相反的顺序被读到。例如:
- 用户 A 发消息:“你几点到?”(写入分区 1)
- 用户 B 回复:“下午三点”(写入分区 2)
- 观察者从 Follower 读时先收到分区 2 的回复,再收到分区 1 的问题——看起来 B 在 A 说话之前就回复了。
这在分区(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。选择标准通常是:
- 数据最新:选择 LSN(Log Sequence Number)最大的 Follower,即拥有最多已复制数据的节点。
- 优先级:管理员可以为每个 Follower
设置优先级(如 PostgreSQL 的
recovery_target_timeline)。 - 共识:如果有多个 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 没有的数据(那些在故障前已写入但未复制的事务)。
处理方式有两种:
- 重建:把旧 Leader 当作全新的 Follower,从新 Leader 做一次全量快照复制。简单粗暴但数据量大时很慢。
- 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.signalsynchronous_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 |
off:最快,但 Leader 本地崩溃也可能丢数据(WAL 在操作系统缓存中还没刷盘)。local:Leader 本地 WAL 刷盘后返回,异步复制。最常用的异步模式。remote_write:等 Follower 收到 WAL 并写入操作系统缓存(但不刷盘)。Follower OS 崩溃可能丢。on:等 Follower WAL 刷盘。Leader 和 Follower 同时丢了才会丢数据。remote_apply:等 Follower 把 WAL 重放到数据页。之后在 Follower 上读到的一定是最新数据。
Quorum Commit
PostgreSQL 9.6+ 支持 Quorum 模式:
synchronous_standby_names = 'ANY 2 (standby1, standby2, standby3)'含义:3 个 Standby 中任意 2 个确认就返回。这比
FIRST 2 更灵活——FIRST 2
固定等前两个,如果第一个慢了全部阻塞;ANY 2
等最快的两个,第三个慢了无所谓。
这在跨数据中心部署中非常有用。假设三个 Standby 分别在北京、上海、新加坡:
FIRST 2:如果北京列在第一个,北京 Standby 挂了,即使上海和新加坡都在线,也要等北京恢复。ANY 2:北京挂了,上海和新加坡两个确认就行。
八、MySQL Group Replication 与 Galera Cluster
当单纯的主从复制不够用时,MySQL 生态有两个更高级的方案:Group Replication(官方)和 Galera Cluster(第三方)。
MySQL Group Replication
Group Replication(GR)是 MySQL 5.7.17 引入的官方多节点复制方案。它在传统主从复制之上增加了:
- 基于 Paxos 的组通信:节点间通过 Paxos 协议对事务顺序达成共识。
- 认证式冲突检测(Certification-based Conflict Detection):每个节点独立执行事务,提交前广播到组内所有节点,通过对比写集(Write Set)检测冲突。如果两个事务修改了同一行,后提交的事务被回滚。
- 自动成员管理:节点加入和离开自动处理,不需要手动重配。
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 = ONGalera Cluster
Galera Cluster 是 Codership 开发的同步多主复制方案,被集成到 MariaDB(MariaDB Galera Cluster)和 Percona XtraDB Cluster 中。
核心机制:
- 虚拟同步复制(Virtually Synchronous Replication):事务提交时广播写集,所有节点认证(检测冲突)后本地提交。看起来是同步的(所有节点数据一致),但实际上各节点的应用顺序可以不同。
- 流控(Flow Control):如果某个节点的应用队列积压过长,它会通知其他节点减速,防止节点间差距过大。
- 全局事务排序:使用全局序列号(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 两种模式 |
选择建议:
- 对一致性要求不极端、架构简单:Semi-Sync 足够。
- 需要自动故障转移和成员管理:Group Replication(配合 MySQL Router 或 ProxySQL)。
- 已经在用 MariaDB / Percona,需要多主写入:Galera Cluster。
九、工程建议:何时用哪种模式
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 N 比 FIRST 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 的承载能力,或者需要跨数据中心低延迟写入时,主从复制不再适用。多主带来了写冲突这个全新的挑战。
参考资料
- Kleppmann, Martin. Designing Data-Intensive Applications. O’Reilly, 2017. Chapter 5: Replication.
- PostgreSQL Documentation. High Availability, Load Balancing, and Replication.
- PostgreSQL Documentation. synchronous_commit.
- MySQL Documentation. Semisynchronous Replication.
- MySQL Documentation. Group Replication.
- Galera Cluster Documentation. Certification-Based Replication.
- Ongaro, Diego, and John Ousterhout. “In Search of an Understandable Consensus Algorithm.” USENIX ATC, 2014.
- Kingsbury, Kyle. Jepsen: MySQL. 分布式系统正确性测试。
- Percona. Percona XtraDB Cluster Documentation.
- Zalando. Patroni: A Template for High Availability PostgreSQL.
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【分布式系统百科】多主复制:冲突检测与解决策略的深水区
单主复制只有一个节点能写入,跨数据中心延迟高、写入吞吐有上限。多主复制(Multi-Leader Replication)让每个数据中心都有自己的 Leader,写入延迟降到本地网络级别——但代价是并发写入可能产生冲突。本文深入拆解向量时钟的冲突检测机制、五种主流冲突解决策略(LWW、自定义合并函数、CRDT、OT、无冲突 Schema 设计)以及 CouchDB 的多主实战案例,帮你判断什么场景值得趟这趟浑水。
【分布式系统百科】ZooKeeper 内核:从 ZAB 协议到分布式协调实践
深入拆解 ZooKeeper 的核心机制:ZAB 协议的三阶段流程、ZNode 数据模型、Watch 一次性通知、会话管理,以及分布式锁、Leader 选举、配置管理等典型用法。分析惊群效应等已知问题,并梳理 ZooKeeper 在 Kafka、HBase、Hadoop 生态中的角色。
【分布式系统百科】分布式锁的真相:从 Redlock 争论到 Fencing Token
完整还原 Kleppmann 与 Antirez 关于 Redlock 的技术争论,拆解 Fencing Token 方案的原理与实现,对比基于 etcd 和 ZooKeeper 的分布式锁正确实现,讨论锁粒度、Advisory Lock 与 Mandatory Lock 的区别,以及用版本号代替锁的替代思路。
【分布式系统百科】成员协议:SWIM 与 Gossip 的工程实现
从 Gossip 协议的 SI 传播模型出发,深入拆解 SWIM 故障检测协议的直接探测、间接探测和怀疑机制,分析 HashiCorp Memberlist 的源码实现,对比 Serf 与 Consul 的成员管理策略,并提供基于 Memberlist 构建集群成员管理的完整 Go 代码示例。