PostgreSQL的流复制机制从9.0版本引入后,已经成为构建高可用架构的绝对核心。很多人一上来就急着配参数、搭集群,结果遇到主备切换就抓瞎,根本原因在于没吃透WAL(Write-Ahead Log)的生成、传输和应用这三个阶段的底层逻辑。流复制本质上就是把主库不断产生的WAL记录,通过网络连接近乎实时地传输到备库,然后备库把这些记录“重放”一遍,从而保持与主库的数据一致性。你不需要纠结那些花里胡哨的高可用方案,先把异步流复制和同步流复制这两个基础模式吃透,后面再上Patroni、repmgr这些工具就是水到渠成的事。
WAL机制与流复制的核心原理要理解流复制,必须先理解WAL。PostgreSQL在执行任何修改数据的操作之前,会先把变更记录写入WAL日志,然后才去修改数据文件。这个“先写日志”的规则保证了即使数据库崩溃,也能通过重放WAL日志恢复到一致状态。流复制巧妙地利用了这个机制:主库不需要等WAL文件写完再传,而是通过WAL Sender进程,把刚产生的WAL记录流式地发送给备库的WAL Receiver进程。备库收到后写入自己的WAL存储,再由Startup进程进行恢复重放。这个过程是持续不断的,延迟可以控制在毫秒级。
这里有一个容易被忽略的关键点:备库在重放WAL期间,数据库是处于“恢复模式”的,这时候备库只能接受只读查询,不能写入。很多人以为备库开着就是能查的,结果连接上去发现报错,就是因为备库还没完全追平或者配置了hot_standby参数。只有把hot_standby设为on,备库才能在恢复的同时接受只读连接,这就是所谓的“热备”状态。
异步流复制的实战配置异步流复制是默认模式,也是性能开销最小的方案。主库提交事务时,只要WAL记录写入本地磁盘就算完成,不会等待备库确认。这意味着如果主库突然宕机,那些还没来得及传输到备库的WAL记录就会丢失,可能导致少量数据损失。但对于读多写少、能容忍秒级数据丢失的业务场景,异步复制完全够用。
配置异步流复制需要修改几个核心参数。在主库的postgresql.conf中,首先设置wal_level为replica,这确保WAL中包含了备库恢复所需的全部信息。然后设置max_wal_senders,这个值决定了最多允许多少个备库同时连接,一般根据实际备库数量加一些余量,比如设成5。wal_keep_size参数控制主库保留多少旧的WAL文件,防止备库还没追完主库就把WAL清理了,建议根据网络状况和备库追赶速度来设定,生产环境通常给个几GB起步。
接下来是创建复制专用的数据库角色。这个角色需要REPLICATION权限和LOGIN权限,主库的pg_hba.conf里也要允许这个用户通过replication协议连接。具体操作如下:
-- 在主库创建复制用户 CREATE ROLE replicator WITH REPLICATION LOGIN PASSWORD 'your_strong_password'; -- pg_hba.conf中添加 host replication replicator 192.168.1.0/24 md5
备库的配置相对简单,但有一个步骤绝对不能出错:使用pg_basebackup从主库拉取基础备份。这个工具会通过流复制协议把整个数据目录拷贝过来,同时自动生成standby.signal文件,告诉PostgreSQL这个实例是备库。执行命令时要注意,如果主库数据量很大,最好加上压缩选项,并且用-P显示进度:
pg_basebackup -h 192.168.1.100 -p 5432 -U replicator -D /var/lib/postgresql/data -Fp -Xs -P -R
这里的-R参数至关重要,它会在备库的数据目录下自动生成postgresql.auto.conf文件,里面包含了连接主库所需的primary_conninfo信息。备库启动后,还需要在postgresql.conf中设置hot_standby为on,然后重启,备库就会进入持续恢复模式,同时对外提供只读服务。
同步流复制的配置与权衡同步流复制解决了异步模式下的数据丢失风险,但代价是增加了事务提交的延迟。原理很简单:主库提交事务时,必须等待至少一个同步备库把WAL写入磁盘并返回确认,事务才算真正完成。这个“同步备库”的选择由synchronous_standby_names参数控制,它支持灵活的优先级和数量配置。
配置同步复制只需要在主库的postgresql.conf中加上一行:
synchronous_standby_names = 'FIRST 1 (sync_standby1, sync_standby2)'
这个配置表示优先选择sync_standby1作为同步备库,如果它挂了就切换到sync_standby2。备库那边需要在primary_conninfo中声明自己的application_name,与主库配置中的名称对应:
primary_conninfo = 'host=192.168.1.100 port=5432 user=replicator password=your_strong_password application_name=sync_standby1'
同步复制的一个常见误区是认为设了同步就绝对不丢数据。实际上,如果所有同步备库都宕机了,主库会一直挂起等待,所有写操作都会被阻塞。这时候要么手动把synchronous_standby_names清空让主库降级为异步模式继续工作,要么就得接受服务中断。生产环境中更稳妥的做法是设置synchronous_commit参数为remote_write,这样主库只要确认备库收到了WAL记录并写入操作系统缓存就算完成,不用等备库刷盘,在性能和数据安全之间取得一个平衡点。
流复制延迟监控与故障排查流复制搭起来只是第一步,持续监控复制延迟才是运维的重头戏。PostgreSQL提供了几个系统视图来查看复制状态,最常用的是pg_stat_replication。在主库上执行查询可以看到每个备库的当前状态、WAL位置差异和延迟时间:
SELECT
application_name,
state,
sync_state,
pg_wal_lsn_diff(pg_current_wal_lsn(), sent_lsn) AS send_lag_bytes,
pg_wal_lsn_diff(pg_current_wal_lsn(), write_lsn) AS write_lag_bytes,
pg_wal_lsn_diff(pg_current_wal_lsn(), flush_lsn) AS flush_lag_bytes,
pg_wal_lsn_diff(pg_current_wal_lsn(), replay_lsn) AS replay_lag_bytes,
write_lag,
flush_lag,
replay_lag
FROM pg_stat_replication;
这几个延迟指标分别代表不同的含义。sent_lsn是主库已经发送给备库的最后一个WAL位置,write_lsn是备库已写入操作系统缓存的位置,flush_lsn是备库已刷盘的位置,replay_lsn是备库已重放完成的位置。如果replay_lag持续增长,说明备库的重放速度跟不上主库的写入速度,可能是备库硬件性能不足,或者有长时间运行的查询占用了资源。
还有一种情况是复制槽导致的WAL堆积。复制槽是PostgreSQL 9.4引入的机制,它保证主库在备库确认收到WAL之前不会删除这些日志文件。但如果备库长时间离线,复制槽会阻止WAL清理,最终撑爆主库磁盘。检查pg_replication_slots视图可以看到每个槽的WAL保留量,如果发现某个槽的restart_lsn远远落后于当前WAL位置,就要考虑是否手动删除这个槽或者催促备库恢复连接。
高可用架构的构建思路单靠流复制本身并不算完整的高可用方案,因为它缺少自动故障检测和切换机制。一个实用的高可用架构需要在流复制基础上叠加至少三层能力:健康检测、自动切换和客户端路由。
健康检测可以用简单的keepalived心跳,也可以上更专业的Patroni配合etcd或Consul。Patroni的思路很清晰:每个PostgreSQL节点上跑一个Patroni代理,这些代理通过分布式共识存储来选举主库,并自动管理备库的primary_conninfo指向。当主库发生故障时,Patroni会在几秒内完成检测、选举新主库、提升备库、更新所有其他备库的复制源这一系列操作。这种自动化程度远高于手动执行pg_ctl promote,是生产环境的首选。
自动切换中最危险的操作是“脑裂”,即同时出现两个可写的主库。避免脑裂的关键在于STONITH(Shoot The Other Node In The Head)机制,也就是在提升新主库之前,必须确保旧主库已经被彻底隔离。Patroni通过分布式锁和Leader Key的TTL机制来实现这一点,旧主库如果失去锁,会自动降级为只读状态。
客户端路由层面,传统的做法是用VIP(虚拟IP)漂移,主库在哪台机器上VIP就跟到哪。更现代的做法是用HAProxy或Pgpool-II作为数据库代理,它们能自动检测主备角色变化,把写请求路由到主库、读请求分发到备库。Pgpool-II还能做到连接池和查询缓存,但配置复杂度也更高。对于大多数场景,HAProxy加一个简单的健康检查脚本就足够用了,脚本逻辑就是连上数据库执行pg_is_in_recovery()函数,返回true的是备库,false的是主库。
流复制的性能调优细节流复制在高并发写入场景下可能成为性能瓶颈,因为WAL Sender进程需要把每一条WAL记录都发送给所有备库。如果备库数量多或者网络带宽有限,主库的CPU和网络资源会被大量消耗。一个有效的优化手段是启用WAL压缩,在primary_conninfo中加上compression参数:
primary_conninfo = 'host=192.168.1.100 port=5432 user=replicator compression=zstd'
PostgreSQL 15开始支持Zstandard和LZ4压缩算法,压缩比和速度都远好于之前的pglz。实际测试中,zstd能在消耗很少CPU的情况下把WAL传输量减少60%以上,对跨机房复制的场景尤其有价值。
另一个容易被忽视的调优点是把wal_sender_timeout设置得合理一些。默认值是60秒,如果备库因为网络抖动超过60秒没有响应,主库会主动断开连接。在网络不稳定的环境下,可以适当调大这个值,比如300秒,避免频繁的重连导致备库反复做基础备份。
最后,不要忘记对备库的查询性能进行优化。备库在重放WAL时,如果遇到与只读查询冲突的情况,比如查询正在扫描的表恰好要被重放的VACUUM操作清理,PostgreSQL默认会等待一段时间后强制终止查询。这个行为由max_standby_streaming_delay参数控制,默认30秒。如果备库上跑着报表类长查询,建议把这个值设大一些,或者干脆设成-1让查询永远不被终止,但要承担复制延迟增大的风险。
备份策略与流复制的配合有了流复制不代表可以不做备份,这是很多人的认知盲区。流复制只能防止硬件故障导致的数据丢失,对于误删表、误改数据这类逻辑错误,备库会忠实地把错误操作也重放一遍,起不到任何保护作用。正确的做法是在备库上执行定期的基础备份和WAL归档,这样既能减轻主库的备份压力,又能利用备库的物理副本实现快速恢复。
具体操作上,可以在备库的postgresql.conf中设置archive_mode为always,这样备库也会生成归档WAL文件。然后用pg_basebackup或pgBackRest这类专业工具定期做全量备份,配合持续归档的WAL文件,就能实现任意时间点的恢复。这种“主库负责业务,备库负责备份”的架构,把资源利用率和数据安全性都做到了最优。
流复制从配置到调优,再到融入完整的高可用体系,每一步都有很多细节需要打磨。但只要把WAL传输的链路、同步策略的选择、故障切换的自动化这三个核心问题想清楚,PostgreSQL的高可用架构就能稳定地支撑起关键业务。数据库运维没有银弹,持续监控、定期演练切换流程、保持对复制状态的敏感度,才是长久之道。
