主题
高可用与复制:用 WAL 换一份可接管的数据
一句话:复制是把主库的 WAL 持续送到另一台机器并重放;同步复制用延迟换 RPO,自动切换用复杂度换 RTO。
适用 PostgreSQL 16/17。示例主机名、路径按环境替换。
为什么需要
单机数据库的故障面很直接:磁盘坏、机器挂、机房断网、误删表。没有副本时,恢复只能依赖备份,RTO 通常是小时级;有了流复制,备库可以在几十秒内接管,RPO 取决于同步模式。复制还承担读扩展、版本升级演练、异地容灾;但它不是备份,误删表会同步到备库,逻辑错误需要 PITR。
核心机制
物理复制与逻辑复制
| 维度 | 物理流复制 | 逻辑复制 |
|---|---|---|
| 粒度 | 整个实例的块级变化 | 表级行变化 |
| 版本 | 主备大版本必须一致 | 支持跨大版本 |
| DDL | 自动复制 | 不复制,需要单独执行 |
| Sequence | 随实例复制 | 不同步 |
| 典型用途 | 高可用、只读副本 | 升级、迁移、按表分发 |
物理复制的主库由 walsender 发送 WAL,备库由 walreceiver 接收、startup 进程重放。备库通过 standby.signal 进入持续恢复模式,hot_standby=on(默认)允许只读查询。
逻辑复制用 CREATE PUBLICATION / CREATE SUBSCRIPTION 建立表级订阅,初始同步后持续解码 WAL。它不能替代物理复制做高可用:DDL、序列、大对象都不在复制范围内。
同步复制与 RPO
异步复制下,主库提交成功不等于备库收到 WAL;主库崩溃时,最后一段 WAL 可能丢失,RPO 大于 0。
同步复制让主库等待备库确认:
ini
# postgresql.conf(主库,PostgreSQL 16/17)
wal_level = replica
max_wal_senders = 10
wal_keep_size = 1GB
synchronous_standby_names = 'FIRST 1 (standby1, standby2)'
synchronous_commit = onsynchronous_commit 的级别决定了「确认」到哪一步:
| 级别 | 主库等待到 | RPO | 代价 |
|---|---|---|---|
off | 本地 WAL 写入不等刷盘 | 可能丢最近事务 | 最快,不建议用于关键数据 |
local | 本地 WAL 刷盘 | 本机不丢,备库可能丢 | 低延迟 |
remote_write | 备库收到并写入 OS 缓存 | 备库进程崩溃可能丢 | 中等 |
on(默认) | 备库刷盘 | 备库刷盘后不丢 | 等网络 + 备库 IO |
remote_apply | 备库已重放 | 备库可立即读到 | 延迟最高 |
FIRST 1 表示按列表优先选一个同步备库;ANY 1 (s1, s2) 表示任意一个确认即可,避免单个备库故障阻塞写入。代价是:同步备库全部不可用时,主库写入会被阻塞,除非你接受降级为异步。
切换与脑裂
- Switchover:计划内切换,先确认备库追平,再提升备库,原主降级。
- Failover:主库故障后的自动/手动提升,可能丢失未同步 WAL。
- 脑裂:网络分区时两边都认为自己该做主库。数据库本身无法解决,必须靠外部仲裁和 fencing:etcd/Consul 的 quorum、Patroni 的 leader lock、watchdog/STONITH 关机或断网旧主。
最小上手
主库准备复制用户和访问规则:
sql
CREATE ROLE replicator WITH REPLICATION LOGIN PASSWORD 'change-me';ini
# pg_hba.conf
host replication replicator 10.0.0.0/24 scram-sha-256从主库做基础备份,-R 会写好 standby.signal 和 primary_conninfo(PostgreSQL 12+):
bash
pg_basebackup -h 10.0.0.11 -U replicator \
-D /var/lib/pgsql/16/data -Fp -Xs -P -R
pg_ctl -D /var/lib/pgsql/16/data start验证:
sql
-- 主库:看备库状态和延迟
SELECT application_name, state, sync_state,
write_lag, flush_lag, replay_lag
FROM pg_stat_replication;
-- 备库:t 表示仍在恢复,只读
SELECT pg_is_in_recovery();
-- 备库:接收与重放的字节差
SELECT pg_wal_lsn_diff(pg_last_wal_receive_lsn(), pg_last_wal_replay_lsn()) AS lag_bytes;计划内切换(手动):
bash
# 1. 确认备库已追平:主库 pg_current_wal_lsn() 与备库 replay LSN 一致
# 2. 备库提升
pg_ctl promote -D /var/lib/pgsql/16/data
# 或在备库 SQL 执行:SELECT pg_promote();
# 3. 把原主改成备库:停库 -> pg_rewind -> 建 standby.signal -> 启动用 Patroni 时不要手工 promote:
bash
patronictl -c /etc/patroni.yml switchover \
--master pg-primary --candidate pg-standby1 --force原主重新加入前,先确认它已停止写入,再用 pg_rewind 丢弃分叉的 WAL:
bash
pg_ctl -D /var/lib/pgsql/16/data stop
pg_rewind --target-pgdata=/var/lib/pgsql/16/data \
--source-server="host=10.0.0.12 port=5432 user=postgres dbname=postgres"
touch /var/lib/pgsql/16/data/standby.signal
pg_ctl -D /var/lib/pgsql/16/data startpg_rewind 要求目标集群开启 wal_log_hints=on 或数据校验和,否则无法定位差异块。
生产动作
- 监控延迟:主库看
pg_stat_replication的write_lag/flush_lag/replay_lag;备库看pg_last_wal_receive_lsn()与pg_last_wal_replay_lsn()的字节差。字节差不是时间,要结合 WAL 生成速率换算。 - 同步备库故障:
FIRST 1下所有同步备库失联会阻塞写入;要么用ANY 1做 quorum,要么有降级为异步的预案。降级意味着 RPO 变大,必须让业务知情。 - WAL 归档:
archive_mode=on、archive_command、archive_timeout=60;归档断了,PITR 就会在缺口处停下。监控pg_stat_archiver.failed_count和last_archived_time。 - 脑裂处置:先 fencing,再恢复。确认旧主已关机/断网后才能提升新主;两边都写入过时,用
pg_rewind让旧主回退到新主时间线,不要直接把旧主挂回去。 - RPO/RTO 预估:
| 方案 | RPO | RTO(经验值) |
|---|---|---|
| 仅每日逻辑备份 | 24 小时 | 小时级,取决于库大小 |
| 基础备份 + WAL 归档 | 约一个 WAL 切换周期(archive_timeout 量级) | 分钟到小时 |
| 异步流复制 + Patroni | 秒级到分钟级(未传 WAL) | 数十秒 |
| 同步流复制 + Patroni | 0(同步备库确认后) | 数十秒,需 quorum 正常 |
- 演练:每季度至少做一次 switchover 和一次旧主 rejoin;只验证「备库在跑」不够,要验证应用连接能切过去。
边界
- 异步复制不保证零丢失:主库崩溃时,最后一段未发送的 WAL 就是丢失窗口。
- 同步复制不保证零停机:它保证已提交事务在备库有副本,不保证备库可读、不保证切换自动完成。
- 逻辑复制不是高可用方案:DDL、序列、大对象不复制;主键冲突会让订阅停下。
- 备库不是备份:误删、逻辑损坏会同步过去;需要独立的基础备份 + WAL 归档。
- Patroni 不是数据库:它解决选主和配置,不解决存储、备份和脑裂的物理隔离;没有 fencing 的自动切换可能制造双主。
- 跨版本、跨平台:物理复制要求同大版本、同架构;跨版本升级用逻辑复制或
pg_upgrade,不要混用。
讲清楚
复制是把主库的 WAL 在另一台机器上持续重放。异步复制延迟低,但主库崩溃会丢最后一段 WAL;同步复制让主库等备库确认,RPO 可以做到 0,代价是写入延迟和「同步备库全挂就阻塞写入」。高可用不是只有复制:还需要仲裁、fencing、VIP/连接切换和演练。脑裂的本质是网络分区让两边都以为自己该做主库,数据库自己解决不了,必须靠 etcd quorum、watchdog 或 STONITH 先把一边彻底隔离,再用 pg_rewind 把旧主拉回新时间线。最后记住:备库不是备份,误删会同步,PITR 仍然依赖独立的 WAL 归档。