PostgreSQL 部署与高可用
单机之外要补的课
单机 PostgreSQL 能跑得很好,但生产环境绕不开三个问题:机器挂了业务停多久、误删数据能不能找回来、主库压力大了能不能分流读请求。对应的答案分别是高可用、备份与 PITR、流复制。
本篇按「先搭主备,再做备份,最后选高可用方案」的顺序走。环境假设两台 Ubuntu 22.04,主库 10.0.0.11、备库 10.0.0.12,PostgreSQL 16,数据目录 /data/pgdata。
下文 systemctl ... postgresql@16-main 的写法,假设 /data/pgdata 已经通过 pg_createcluster 16 main --datadir=/data/pgdata 注册成 main 集群。如果沿用 apt 默认目录,把路径换成 /var/lib/postgresql/16/main,启停命令不用变;手工 initdb 的实例则直接用 pg_ctl -D /data/pgdata 管理。
物理流复制原理
物理复制复制的是 WAL 字节流,备库重放后得到一个和主库「块级一致」的副本。整个过程只有两个角色:
- walsender:主库上的进程,负责把 WAL 发给备库。每个备库连接对应一个 walsender,可以在
pg_stat_replication里看到。 - walreceiver:备库上的进程,接收 WAL 写入本地
pg_wal/,再由 startup 进程重放。
几个关键特性:
- 异步是默认。主库提交不等备库确认,主库挂了可能丢最后一段 WAL。要强一致就配同步复制,代价是提交延迟变高。
- 备库只读。
hot_standby = on时备库可以接受只读查询,用于报表、备份、读分流。 - 级联复制。备库还能再挂备库,减轻主库的 walsender 压力。
- 复制槽。主库为备库保留 WAL,防止备库落后太多导致 WAL 被删。但槽不消费就会一直堆 WAL,必须配
max_slot_wal_keep_size兜底。
主库 备库
┌─────────────┐ WAL 流 ┌──────────────────┐
│ backend │ ─────────> │ walreceiver │
│ walsender │ │ ↓ 写 pg_wal │
│ pg_wal │ │ startup 重放 │
└─────────────┘ └──────────────────┘
主库配置
主库要允许备库连接、产生足够的 WAL、并保留一段历史:
# postgresql.conf —— 主库
listen_addresses = '10.0.0.11'
wal_level = replica # 流复制的最低要求
max_wal_senders = 10 # 允许的 walsender 数量,按备库数 + 余量
wal_keep_size = 1GB # 没有复制槽时,至少保留这么多 WAL 给备库
max_slot_wal_keep_size = 10GB # 有复制槽时,槽最多拖住多少 WAL,超了让备库重建
hot_standby = on # 备库需要(主库开着也无害)
建复制账号,并在 pg_hba.conf 里放行 replication 连接:
CREATE ROLE replicator WITH REPLICATION LOGIN PASSWORD 'Repl_2024';
# pg_hba.conf —— 主库
# TYPE DATABASE USER ADDRESS METHOD
host replication replicator 10.0.0.12/32 scram-sha-256
改完 reload。验证主库已经准备好:
SHOW wal_level; -- replica
SHOW max_wal_senders; -- 10
SELECT * FROM pg_stat_replication; -- 此时为空,备库还没连
wal_level 不是 reload 参数,改完必须重启实例。max_wal_senders 同样需要重启。重启前确认业务低峰,并检查 pg_stat_activity 里没有长事务。
用 pg_basebackup 搭备库
备库不需要 initdb,直接用 pg_basebackup 从主库拉一份基础备份,加上 -R 自动生成复制配置。
# 1. 停掉备库上可能存在的实例,清空数据目录
sudo systemctl stop postgresql@16-main
sudo rm -rf /data/pgdata/*
sudo chown postgres:postgres /data/pgdata
sudo chmod 700 /data/pgdata
# 2. 以 postgres 用户执行基础备份
sudo -u postgres pg_basebackup \
-h 10.0.0.11 \
-p 5432 \
-U replicator \
-D /data/pgdata \
-Fp \
-Xs \
-P \
-R
参数逐个说:
| 参数 | 作用 |
|---|---|
-D | 备库数据目录,必须为空 |
-Fp | 普通文件格式(-Ft 是 tar 包) |
-Xs | 备份过程中同时流式传输 WAL,保证备份一致 |
-P | 显示进度 |
-R | 自动写 standby.signal 和 primary_conninfo,省去手工配置 |
-R 做的事等价于:
# postgresql.auto.conf(备库,由 -R 自动生成)
primary_conninfo = 'host=10.0.0.11 port=5432 user=replicator password=Repl_2024'
# standby.signal 存在即表示这是备库,启动后进入恢复模式
sudo -u postgres touch /data/pgdata/standby.signal
启动备库并验证:
sudo systemctl start postgresql@16-main
-- 备库上执行
SELECT pg_is_in_recovery(); -- t,说明是备库
SELECT status, sender_host, sync_state FROM pg_stat_wal_receiver;
-- 主库上执行
SELECT application_name, client_addr, state, sent_lsn, replay_lsn,
pg_wal_lsn_diff(sent_lsn, replay_lsn) AS lag_bytes
FROM pg_stat_replication;
主库看到一条 streaming 记录、备库 pg_is_in_recovery() 返回 t,主备就通了。
备库是块级一致的副本,在备库上跑 pg_basebackup 或 pg_dump 可以避免备份压力打到主库。注意备份要记录对应的 LSN 或备份标签,恢复时才知道从哪接 WAL。
同步复制
异步复制下,主库宕机可能丢最后几个事务。金融、订单这类场景要开同步复制:
# 主库 postgresql.conf
synchronous_commit = on
synchronous_standby_names = 'FIRST 1 (pgstandby1)'
FIRST 1 表示「在列表里按顺序取第一个可用的备库确认」,ANY 1 表示任意一个确认即可。备库的连接 application_name 要对应上,可以在 primary_conninfo 里指定:
primary_conninfo = 'host=10.0.0.11 port=5432 user=replicator application_name=pgstandby1'
-- 主库确认同步状态
SELECT application_name, sync_state FROM pg_stat_replication;
-- sync_state = sync 表示当前是同步备库
同步备库挂了,主库的写会一直等——不会自动降级为异步,直到备库恢复或 DBA 调整 synchronous_standby_names。所以同步复制至少要配两个候选备库,或者接受「主库写阻塞」这个代价。别只配一个同步备库还指望高可用。
逻辑复制一句话
物理复制只能整实例复制、版本必须一致;逻辑复制基于发布/订阅,按表复制数据变更,可以跨版本、只复制部分表、在备库上写入。适合数据汇总、跨库同步、大版本升级过渡。它不复制 DDL 和序列,主键是硬要求,冲突处理也比物理复制麻烦。日常高可用还是物理复制,逻辑复制是补充手段。
备份:逻辑与物理
| 方式 | 命令 | 粒度 | 恢复方式 | 适用场景 |
|---|---|---|---|---|
| 逻辑备份 | pg_dump | 库/表/SQL | psql 或 pg_restore | 小库、单表恢复、跨版本迁移 |
| 逻辑备份(全实例) | pg_dumpall | 全实例含角色 | psql | 迁移整个实例、保留角色 |
| 物理备份 | pg_basebackup | 整个实例 | 直接启动 + 重放 WAL | 大库、PITR、搭备库 |
| 文件系统快照 | LVM/ZFS 快照 | 整个实例 | 挂回 + 重放 WAL | 大库快速备份(需配合 pg_backup_start) |
pg_dump 的常用姿势:
# 自定义格式,支持并行恢复和选择性恢复
pg_dump -h 10.0.0.11 -U app_user -d app_db -Fc -f /backup/app_db_$(date +%F).dump
# 并行恢复(-j 并行度,仅自定义格式支持)
pg_restore -h 10.0.0.11 -U app_user -d app_db_new -j 4 /backup/app_db_2024-06-01.dump
# 只恢复某张表
pg_restore -d app_db_new -t orders /backup/app_db_2024-06-01.dump
# 纯 SQL 格式,方便肉眼检查
pg_dump -h 10.0.0.11 -U app_user -d app_db -Fp -f /backup/app_db.sql
pg_dump 在单个事务里取快照,导出的数据是一致的,不会因为并发写入而错乱。但它不包含 WAL,只能恢复到备份那一刻,做不了「恢复到昨天下午 3 点」这种精细操作——那是 PITR 的活。
WAL 归档与 PITR
PITR(Point-In-Time Recovery)的原理是:一份基础备份 + 从备份开始到目标时间点的所有 WAL,重放到指定时间戳。
开启归档
# 主库 postgresql.conf
archive_mode = on
archive_command = 'test ! -f /backup/wal/%f && cp %p /backup/wal/%f'
archive_timeout = 60 # 最多 60 秒强制切一个 WAL 段,降低 RPO
%p是 WAL 文件的完整路径,%f是文件名。archive_command必须返回 0 才算归档成功,失败会不断重试并堆积 WAL。上面的test ! -f防止重复覆盖。- 归档目录要和数据目录分开,最好放独立磁盘或对象存储。
-- 确认归档状态
SELECT archived_count, failed_count, last_archived_wal, last_archived_time
FROM pg_stat_archiver;
恢复流程
假设要恢复到 2024-06-01 15:30:00:
# 1. 停库,清空数据目录
sudo systemctl stop postgresql@16-main
sudo rm -rf /data/pgdata/*
# 2. 还原「事前」的基础备份(不要从已经出问题的主库重新拉)
# 备份格式可能是目录、tar 包或快照,这里以目录形式为例
sudo -u postgres rsync -a /backup/base/2024-06-01/ /data/pgdata/
# 3. 写恢复配置
sudo -u postgres tee /data/pgdata/postgresql.auto.conf <<'EOF'
restore_command = 'cp /backup/wal/%f %p'
recovery_target_time = '2024-06-01 15:30:00+08'
recovery_target_action = 'promote'
EOF
# 4. 触发恢复
sudo -u postgres touch /data/pgdata/recovery.signal
# 5. 启动,观察日志里的 recovery 进度
sudo systemctl start postgresql@16-main
-- 恢复完成后确认
SELECT pg_is_in_recovery(); -- f,已提升为主库
SELECT pg_current_wal_lsn(); -- 当前写入位置,确认实例已正常接受写入
恢复期间可以盯日志:
tail -f /var/log/postgresql/postgresql-16-main.log
# 看到 "recovery stopping before ..." 和 "database system is ready to accept connections"
- 基础备份必须早于恢复目标时间,而且要和 WAL 归档配套——只有备份没有 WAL 只能恢复到备份时刻。
- 归档不能断。
pg_stat_archiver.failed_count持续增长说明归档失败,这时候的备份是不可靠的。 - 时间点要早于误操作。误删表后要恢复到删除之前,
recovery_target_time设成删除前的时刻,恢复完先验证数据再对外提供服务。
高可用方案选型
流复制解决了数据冗余,但主库挂了谁来做切换?手工切需要人盯着,生产上一般用现成方案:
| 方案 | 定位 | 优点 | 适合 |
|---|---|---|---|
| Patroni | 基于 DCS(etcd/Consul/ZooKeeper)的自动故障转移 | 生态成熟,支持自动切换、REST API、动态配置,K8s 上也有 operator | 中大型生产、需要自动切换 |
| Repmgr | 轻量级复制管理 | 部署简单,命令直观,自带 witness 防脑裂 | 中小规模、想少引入组件 |
| pg_auto_failover | 专用自动切换工具(原 Citus 团队) | 监控节点 + 自动晋升,配置简单 | 两节点/三节点小集群 |
| 云托管 | RDS/Cloud SQL 等 | 切换、备份、监控全托管 | 不想自己运维 |
选型建议:要自动切换、要 K8s、要 REST 管理接口,选 Patroni;只有两三台机器、希望组件少,选 Repmgr。两者都要有一个「仲裁者」来避免脑裂——Patroni 靠 etcd 的租约,Repmgr 可配 witness 节点投票。没有仲裁的自动切换,网络分区时会出现双主。
完整的 PostgreSQL 高可用不是只装个 Patroni:
- 数据层:流复制保证有副本,同步复制保证不丢数据。
- 仲裁层:etcd 或 witness 决定谁是主,避免脑裂。
- 接入层:HAProxy 或 VIP 把写流量指向主库,应用只连代理不直连 IP。
少任何一层,切换都可能出问题。应用侧的连接串要配多个地址和重试,别把主库 IP 写死。
小结
- 物理流复制靠 walsender / walreceiver 传 WAL,备库只读重放;异步默认可能丢数据,同步复制用
synchronous_standby_names控制。 - 主库三件套:
wal_level = replica、max_wal_senders、复制账号 +pg_hba.conf放行 replication。 - 搭备库一条命令:
pg_basebackup -Fp -Xs -P -R,-R自动写primary_conninfo和standby.signal。 - 逻辑复制按表、跨版本,适合汇总和迁移;高可用仍然用物理复制。
- 备份分逻辑(
pg_dump)和物理(pg_basebackup),前者灵活、后者能做 PITR。 - PITR = 基础备份 + WAL 归档 +
recovery_target_time,归档断了备份就不可靠。 - 高可用选型:Patroni 功能全、适合中大型;Repmgr 轻量、适合小集群;都别忘了仲裁和接入层。
有了主备和备份,下一步是把它们管起来。下一篇《运维与监控》讲 pg_stat 视图、慢查询定位和 Prometheus 监控。