跳到正文

高可用与复制:用 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 = on

synchronous_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 start

pg_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 预估:
方案RPORTO(经验值)
仅每日逻辑备份24 小时小时级,取决于库大小
基础备份 + WAL 归档约一个 WAL 切换周期(archive_timeout 量级)分钟到小时
异步流复制 + Patroni秒级到分钟级(未传 WAL)数十秒
同步流复制 + Patroni0(同步备库确认后)数十秒,需 quorum 正常
  • 演练:每季度至少做一次 switchover 和一次旧主 rejoin;只验证「备库在跑」不够,要验证应用连接能切过去。

边界 ​

  • 异步复制不保证零丢失:主库崩溃时,最后一段未发送的 WAL 就是丢失窗口。
  • 同步复制不保证零停机:它保证已提交事务在备库有副本,不保证备库可读、不保证切换自动完成。
  • 逻辑复制不是高可用方案:DDL、序列、大对象不复制;主键冲突会让订阅停下。
  • 备库不是备份:误删、逻辑损坏会同步过去;需要独立的基础备份 + WAL 归档。
  • Patroni 不是数据库:它解决选主和配置,不解决存储、备份和脑裂的物理隔离;没有 fencing 的自动切换可能制造双主。
  • 跨版本、跨平台:物理复制要求同大版本、同架构;跨版本升级用逻辑复制或 pg_upgrade,不要混用。

讲清楚 ​

复制是把主库的 WAL 在另一台机器上持续重放。异步复制延迟低,但主库崩溃会丢最后一段 WAL;同步复制让主库等备库确认,RPO 可以做到 0,代价是写入延迟和「同步备库全挂就阻塞写入」。高可用不是只有复制:还需要仲裁、fencing、VIP/连接切换和演练。脑裂的本质是网络分区让两边都以为自己该做主库,数据库自己解决不了,必须靠 etcd quorum、watchdog 或 STONITH 先把一边彻底隔离,再用 pg_rewind 把旧主拉回新时间线。最后记住:备库不是备份,误删会同步,PITR 仍然依赖独立的 WAL 归档。

把一门技术讲透到既懂也会用