跳到主要内容

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 修改要重启

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库/表/SQLpsql 或 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 的一致性

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"
PITR 的三个前提
  1. 基础备份必须早于恢复目标时间,而且要和 WAL 归档配套——只有备份没有 WAL 只能恢复到备份时刻。
  2. 归档不能断。pg_stat_archiver.failed_count 持续增长说明归档失败,这时候的备份是不可靠的。
  3. 时间点要早于误操作。误删表后要恢复到删除之前,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:

  1. 数据层:流复制保证有副本,同步复制保证不丢数据。
  2. 仲裁层:etcd 或 witness 决定谁是主,避免脑裂。
  3. 接入层: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 监控。