PostgreSQL 主从复制与故障切换:从原理到落地
背景

我们有个项目上线后第一个月就经历了一次严重的数据库故障。主库所在服务器因为磁盘故障直接宕机,应用端报错一片,整个服务停了将近四十分钟才恢复。复盘的时候发现几个问题:
一是之前没做过高可用架构,单纯依赖单节点 PostgreSQL,没有任何容灾方案;二是即使后来补了主从复制,故障切换的过程也相当粗糙——手动切主的过程花了二十多分钟,期间业务完全不可用。
这次教训让我系统地研究了一遍 PostgreSQL 的复制机制和故障切换方案。今天把整个学习过程和落地经验整理出来,希望能帮到有同样需求的人。
核心原理
PostgreSQL 的流式复制基于 WALE(Write-Ahead Log)的传输机制。主库将 WAL 日志实时发送到从库,从库回放这些日志来保持数据一致。
这个过程分为三个关键阶段:
第一阶段是配置复制连接。主库需要开启 wal_level = replica(PostgreSQL 10 之后默认就是),并在 pg_hba.conf 中允许从库连接:
host replication replicator 192.168.1.0/24 md5
同时创建一个具有复制权限的用户:
CREATE USER replicator WITH REPLICATION ENCRYPTED PASSWORD 'your_password';
第二阶段是同步数据。从库启动后执行 pg_basebackup,获取主库的数据快照:
pg_basebackup -h primary_host -D /var/lib/postgresql/data -U replicator \
--checkpoint=fast --verbose --wal-method=stream
第三阶段是持续回放。从库启动后通过 recovery.signal 文件(或 standby.signal)进入恢复模式,连接主库订阅 WAL 流:
# postgresql.conf
hot_standby = on
primary_conninfo = 'host=primary_host port=5432 user=replicator password=your_password'
这里有一个重要的设计取舍:PostgreSQL 支持同步复制和异步复制两种模式。同步复制(synchronous_commit = on 配合 synchronous_standby_names)能确保数据不丢失,但会牺牲写入性能——主库必须等待至少一个从库确认收到 WAL 后才能提交事务。异步复制则相反,写入性能更高,但存在数据丢失风险。
我的项目选择了异步复制加定期备份的方案,原因是业务对强一致性要求不高,更看重写入吞吐。如果业务涉及资金交易,建议优先考虑同步复制或半同步复制。
故障切换的实现
纯手工搭建的主从架构有一个致命问题:没有自动故障切换机制。主库宕机后,需要从库提升为主库,这个过程需要处理多个关键点。
首先是时间窗口。从库提升过程中,旧的连接字符串需要更新,客户端需要重新连接。如果让业务自行处理,中间会有明显的空白期。
我调研过几种方案:
第一种是使用 pg_ctl promote 手动提升:
-- 在从库上执行
SELECT pg_ctl_promote();
或者:
pg_ctl promote -D /var/lib/postgresql/data
这种方式最简单,但完全依赖人工操作,不适合生产环境。
第二种是使用 Patroni,这是一个业界广泛使用的开源故障切换工具。Patroni 通过分布式配置存储(通常是 etcd 或 Consul)协调多个节点的状态,实现自动故障检测与切换:
# patroni.yml 示例
scope: postgresql-cluster
namespace: /service/postgresql/
name: pg-node-1
postgresql:
bin_dir: /usr/lib/postgresql/15/bin
data_dir: /var/lib/postgresql/15/main
config_dir: /etc/postgresql/15/main
authentication:
replication:
username: replicator
password: your_password
superuser:
username: postgres
password: your_superuser_password
parameters:
wal_level: replica
hot_standby: on
max_wal_senders: 10
max_replication_slots: 10
wal_keep_size: 1024
etcd:
hosts: etcd1:2379,etcd2:2379,etcd3:2379
Patroni 的核心优势在于它实现了 Raft 算法来选举 leader,避免了脑裂问题。同时它还提供了 REST API 供外部系统查询集群状态。
第三种是使用云厂商提供的托管方案,比如 AWS 的 RDS Multi-AZ、阿里云的 RDS 高可用版等。这类方案省事,但灵活性和成本都不如自建。
我们的项目最终选择了 Patroni + etcd 的方案,原因是在中小规模集群中,Patroni 的配置复杂度可控,且不需要引入额外的商业组件。
实践中的常见问题
实际部署过程中遇到几个比较典型的问题,记录下来作为参考。
第一个问题是复制延迟。异步复制场景下,从库的数据延迟可能是几秒甚至几分钟,这会导致故障切换时数据不一致。
排查方法:
-- 在主库查看复制状态
SELECT client_addr, state, sync_state, sent_lsn, write_lsn, flush_lsn, replay_lsn
FROM pg_stat_replication;
-- 在从库查看回放进度
SELECT * FROM pg_stat_wal_receiver;
解决思路包括:增加带宽、减少 WAL 文件压缩、或者将关键业务切换到同步复制模式。
第二个问题是「脑裂」。当网络分区导致两个节点都认为自己是主库时,会发生数据分裂。
Patroni 通过 etcd 的锁机制避免这个问题,但前提是 etcd 集群本身要可靠。部署时建议 etcd 节点数使用奇数(3、5),并确保 etcd 集群跨机房部署。
第三个问题是连接中断后的重连。PostgreSQL 客户端在断连后需要智能重连,否则会出现大量连接池耗尽的问题。
应用层面建议配置合理的重连参数:
# HikariCP 示例
spring.datasource.hikari.connection-timeout: 30000
spring.datasource.hikari.max-lifetime: 1800000
spring.datasource.hikari.keepalive-time: 30000
数据库层面可以配合 pg_hba.conf 的 hostssl 参数强制使用 TLS 连接,提高安全性。
监控与维护
一套成熟的主从架构需要配套的监控体系。以下指标建议持续关注:
复制延迟:使用 pg_stat_replication 的 write_lsn - replay_lsn 差值计算。延迟超过阈值时应告警。
复制槽状态:过期的复制槽会占用磁盘空间:
SELECT slot_name, active, restart_lsn FROM pg_replication_slots;
WAL 生成量:监控主库的 WAL 生成速率,异常情况往往意味着大事务或批量操作。
磁盘使用率:从库回放需要足够的磁盘空间,建议预留 20% 以上余量。
定期维护方面,有几个最佳实践:
一是定期执行 VACUUM 防止事务 ID 回卷。从库同样需要 VACUUM,否则可能导致复制中断。
二是监控 pg_stat_archiver 确保 WAL 归档正常进行,这是崩溃恢复的最后一道防线。
三是定期进行故障演练,验证切换流程是否顺畅。
总结
PostgreSQL 主从复制与故障切换是一个系统工程,涉及复制机制选择、故障检测、自动切换、数据一致性等多个维度。
如果是小规模集群或对成本敏感,Patroni + etcd 是值得考虑的方案;如果对一致性要求严格,同步复制或云托管方案更合适。
无论选择哪种方案,监控和维护都不能忽视。故障切换的成功与否,往往取决于平时的积累和演练。

浙公网安备 33010602011771号