流复制:从库拿到的是一堆字节,不是 SQL
这是 PG 和 MySQL 主从最大的区别。MySQL 传 binlog(逻辑变更),从库用 SQL 线程重放;PG 传 WAL(物理日志),从库按页级别把主库的改动「照抄」一遍。所以 PG 的从库必然和主库物理一致——一模一样,一个字节都不差。
「物理一致」意味着什么
| 能力 | PG 物理流复制 | MySQL binlog 复制 |
|---|---|---|
| 从库能不能有不同索引 | 不能。数据文件必须逐字节一致 | 能,binlog 重放不关心索引 |
| 能不能跨大版本 | 不能。页格式变了就重放不了 | 能,5.7 可以复制到 8.0(有坑) |
| 能不能只复制某几张表 | 不能,要整库 | 能,replicate-do-table |
| 从库能不能写 | 不能,read_only 且物理上会冲突 | 能(但不该) |
| 主从数据会不会逻辑漂移 | 不会,物理一致 | 会,非确定性函数、触发器、版本差异都可能 |
| 延迟的度量 | LSN 差(字节数) | Seconds_Behind_Master(秒) |
Seconds_Behind_Master。要看的是 pg_stat_replication 里的 write_lag / flush_lag / replay_lag——分别是「从库收到」「从库刷盘」「从库重放完」三个阶段的延迟。真正影响读到旧数据的是 replay_lag。同步复制:五个档位,不是一个开关
MySQL 的半同步是一个插件,PG 的同步是 synchronous_commit 的五个取值——它决定的是「主库什么时候才告诉客户端提交成功」,不是一个「开/关」。
| 取值 | 主库等谁 | 典型用途 |
|---|---|---|
off | 谁都不等,WAL 还在内存就返回 | 批量导入、可重跑的任务 |
local | 等本地 WAL 刷盘 | 单机场景,没有从库时等价于 on |
remote_write | 等从库收到并写进自己的 WAL buffer | 要「主库挂了数据还在」,但不介意从库也挂 |
on(默认) | 等从库刷到自己的磁盘 | 绝大多数生产环境 |
remote_apply | 等从库重放完成,能被查询看到 | 读己之写(read-your-writes)要求强的场景 |
synchronous_commit 只有在配了 synchronous_standby_names 时才有意义。没配同步备库时,这个参数实际上就是本地的 local——看着是 on,其实一个备库都没等。很多人以为「我开了同步复制」,其实只是改了 synchronous_commit。怎么指定「必须等几个备库」
# 等 1 个备库确认即可(性能优先)
synchronous_standby_names = '1 (standby1, standby2)'
# 等 2 个里全部确认(金融级)
synchronous_standby_names = '2 (standby1, standby2)'
# 按应用名,支持 quorum(PG 9.6+)
synchronous_standby_names = 'FIRST 2 (s1, s2, s3)' -- 前 2 个可达的
synchronous_standby_names = 'ANY 2 (s1, s2, s3)' -- 任意 2 个
'1 (s1)',s1 掉线之后主库的每次提交都无限等待,业务直接挂死。所以要设 wal_sender_timeout,并且理解 PG 的行为:同步备库失联时,主库的写入会阻塞,而不是自动降级为异步(这一点和 MySQL 半同步超时自动降级不一样)。要降级只能手工把
synchronous_standby_names 改空并 reload——所以这个操作必须写进应急预案,并且演练过。ANY 2 (s1,s2,s3) 这种 quorum 写法,避免单点失联就全站写不进去。」复制槽:保命的机制,也是撑爆磁盘的元凶
WAL 段文件会被循环回收——「这个位置之前的所有备库都确认收到了」。可如果备库掉线很久,主库凭什么知道它还没收到?复制槽就是主库给每个备库记的一个书签:这个位置之前的 WAL 一律不许删。
两种槽,用途完全不同
| 物理复制槽(physical) | 逻辑复制槽(logical) | |
|---|---|---|
| 用在哪 | 流复制、pg_basebackup | 逻辑复制、CDC(Debezium / wal2json) |
| 保留什么 | WAL 段文件 | WAL + 系统表的旧版本(catalog xmin) |
| 危害 | 磁盘被 WAL 填满 | 磁盘 + 阻止 VACUUM 清理系统表 |
| 查 | SELECT slot_name, slot_type, active, restart_lsn, pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) AS retained FROM pg_replication_slots; | |
三个必须进监控的指标
-- 1. 每个槽卡住了多少 WAL(超过几十 GB 就该报警)
SELECT slot_name, active,
pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) AS retained_wal,
age(xmin) AS xmin_age, age(catalog_xmin) AS catalog_age
FROM pg_replication_slots ORDER BY 3 DESC NULLS LAST;
-- 2. pg_wal 目录多大(这个涨起来就是磁盘告警)
SELECT pg_size_pretty(sum(size)) FROM pg_ls_waldir();
-- 3. 备库到底跟到哪了
SELECT client_addr, state, sync_state,
write_lag, flush_lag, replay_lag
FROM pg_stat_replication;
active = false 的槽是最危险的。活跃的槽说明备库在追,WAL 最终会被消费掉;不活跃的槽说明没人在消费,但主库仍然不许删——它在无限累积。所以监控要盯「不活跃 + 保留量大」这个组合,而不是只看总量。pg_basebackup 流程里明确『重建成功就删旧槽』。」逻辑复制:什么时候必须用它
物理复制是「整实例照抄」,逻辑复制是「按表解析出行变更再重放」。它慢、限制多,但只有它能做到跨版本、跨平台、只同步部分表、以及从库可写。
-- 主库
ALTER SYSTEM SET wal_level = 'logical'; -- 需要重启
CREATE PUBLICATION pub_orders FOR TABLE orders, order_items;
-- 目标库
CREATE SUBSCRIPTION sub_orders
CONNECTION 'host=primary dbname=app user=repl'
PUBLICATION pub_orders;
和物理复制的取舍
| 维度 | 物理流复制 | 逻辑复制 |
|---|---|---|
| 粒度 | 整个实例 | 按表(可以只同步几张) |
| 跨大版本 | 不行 | 行(16 → 18 可以) |
| 跨平台 / 跨云 | 要求架构、页大小一致 | 不要求 |
| 目标库可写 | 不行(只读) | 可以,不同表可以各自写 |
| DDL | 自动同步 | 不同步,要手工在两边都执行 |
| 序列(serial) | 自动同步 | 不同步,要单独处理 |
| 大事务 | 按字节流,影响小 | 要在内存里拼完整事务,可能 OOM |
| 延迟 | 毫秒级 | 通常更高,取决于解码 |
① DDL 不同步。主库加了字段,目标库没加,复制立刻报错并停下——之后所有变更都堆积。必须约定「DDL 双写」流程,或者用工具统一执行。
② 序列不同步。
SERIAL / IDENTITY 的当前值不会复制。目标库如果也要插入(比如双写或切主),序列落后会导致主键冲突。切换前要手工 setval。备份与 PITR:全备 + 无限重放 WAL
PG 的恢复模型比 MySQL 干净得多:基础备份(pg_basebackup)+ 归档的 WAL = 可以恢复到任意一个时间点。不需要「先恢复全备再算 binlog 位点」,因为 WAL 是连续的字节流,天然接得上。
基础备份
archive_command / pgBackRest
# 主库:开启归档(改完 reload 即可,不用重启)
archive_mode = on
archive_command = 'pgbackrest --stanza=main archive-push %p'
wal_level = replica -- 至少要 replica
# 做一次基础备份
pg_basebackup -h primary -D /backup/base -Ft -z -Xs -P
# 恢复:在目标实例上放一个 recovery.signal + 配置
restore_command = 'pgbackrest --stanza=main archive-get %f "%p"'
recovery_target_time = '2026-11-12 14:31:59+08'
recovery_target_action = 'promote' -- 到了就停在那儿并变成可写
三个必须理解的行为
① 恢复点之后的数据是「被丢弃」,不是「回滚」。PG 到 recovery_target_time 就停止重放并 promote,之后的 WAL 全部不要。所以要恢复到新实例上验证,确认无误再切流量——别在原实例上直接恢复。
② recovery_target_action 默认是 pause,不是 promote。恢复完会停在只读状态等你确认。这是好事(可以检查数据),但自动化脚本里必须显式设置,否则实例一直不可写,看起来像「恢复失败」。
③ 主备切换后的分叉要靠 pg_rewind。旧主库被提升过的备库甩开了一段 WAL,直接当备库连上去会失败(时间线不一致)。pg_rewind 会把旧主库「倒带」到分叉点,再让它作为备库追上去——不需要重新做全量基础备份。
① 归档必须有人监控失败。
archive_command 失败只会不断重试,pg_stat_archiver 里的 failed_count 一直涨,而 WAL 会在 pg_wal 里堆积直到磁盘满。你必须监控这个数。② 必须定期做恢复演练。备份存在 ≠ 能恢复。版本不匹配、归档缺一个段、权限不对,都会让你在事故现场才发现。
工具怎么选
| 工具 | 类型 | 适用 |
|---|---|---|
pg_dump / pg_dumpall | 逻辑备份 | 小库、单表迁移、跨版本升级、导出给分析 |
pg_basebackup | 物理基础备份 | 标准做法,但不支持增量 |
| pgBackRest | 物理备份套件 | 支持增量、并行、压缩、保留策略、校验。生产首选 |
| Barman | 物理备份套件 | 多实例集中管理,云上常见 |
pg_rewind | 不是备份 | 切换后让旧主库重新加入,避免重做全备 |
面试里被问「PG 怎么做高可用」,答「流复制 + Patroni + etcd,配 VIP 或 HAProxy 做流量切换」,再说一句「要配 fencing 防止脑裂」,基本就完整了。