☰
PostgreSQL流复制高可用实战:主从实时同步配置与切换指南
2026/10/22 11:17:45 网站建设 项目流程

1. 为什么流复制是 PostgreSQL 高可用方案里最值得先啃的硬骨头

很多团队第一次接触 PostgreSQL 高可用,脑子里蹦出来的往往是各种分布式中间件、共享存储集群或者逻辑订阅方案,结果折腾一圈发现,最稳、最省事、延迟最低的,反而是数据库自带的流复制(Streaming Replication)。它不是什么外挂组件,而是 PostgreSQL 内核原生支持的一套物理复制机制,主库把预写日志(WAL)源源不断地推给备库,备库一边接收一边重放,最终达到和主库几乎一模一样的数据状态。

我先把结论摆在这儿:如果你要的是主从实时同步数据,也就是备库能近乎实时地跟上主库的写入,流复制是首选。它解决的核心问题是——当主库因为硬件故障、误操作或者机房问题挂掉时,你手里还有一个数据几乎不丢的备库可以顶上来,同时备库还能分担一部分只读查询压力。这套方案适合谁?适合所有用 PostgreSQL 承载核心业务、又不想引入过重外部依赖的开发和运维同学。哪怕你之前没碰过复制,只要会改配置文件、会用psql,跟着走一遍就能跑通。

但我要先泼一盆冷水:流复制看着简单,真正落地时坑不少。同步还是异步、wal_level设成什么、复制槽要不要建、备库能不能被提升、主库崩了之后怎么切回来,这些细节决定了你的方案是"能跑"还是"敢上生产"。这篇内容我会把原理、配置、验证、排错、切换这几块掰开揉碎讲清楚,重点放在为什么这么配,而不是甩给你一堆参数让你抄。你照着做,能搭出一套可用的主从实时同步环境;你理解了背后的逻辑,遇到线上抖动才知道往哪儿查。

在正式开始之前,先明确一个概念边界:流复制是物理复制,复制的是 WAL 记录的字节流,所以主备两边的 PostgreSQL 大版本必须一致,操作系统架构最好也一致。它和逻辑复制(Logical Replication)不是一回事,逻辑复制复制的是行级变更、可以跨版本、可以只复制部分表,但延迟和稳定性通常不如物理流复制。我们要做的是前者。

2. 流复制的底层机制:WAL、复制槽与同步提交到底在干什么

2.1 WAL 是整套复制的地基

要理解流复制,绕不开 WAL(Write-Ahead Logging,预写式日志)。PostgreSQL 修改任何数据之前,会先把这次修改的意图写进 WAL 日志,然后再去改实际的数据文件。这个顺序不能反,因为一旦中途崩溃,数据库可以靠重放 WAL 把数据恢复到一致状态。你可以把 WAL 想象成一本"操作流水账",数据文件是"最终账本",流水账先记,账本后改。

流复制正是复用了这套机制。主库产生的 WAL 不只是本地写完就完事,它还会被发送到备库。备库收到 WAL 后,先写到自己的 WAL 文件里(这叫接收),然后再按顺序重放这些记录到数据文件(这叫重放或应用)。所以备库天然处于一个"持续恢复"的状态,它本质上是一台一直在做崩溃恢复的机器。

这里有个关键参数wal_level,它决定了 WAL 里记录多少信息。默认的replica级别就足够支撑流复制,它记录了重建备库所需的所有信息。如果你设成minimal,WAL 记录会精简,但流复制就跑不起来。所以第一步配置里,wal_level = replica是硬性前提。

2.2 复制槽:防止主库把还没传完的 WAL 删掉

早期做流复制,最怕的一种情况是:备库临时断线,主库这边 WAL 文件被 checkpoint 回收了,等备库重新连上来,发现自己需要的 WAL 已经没了,复制直接断裂,只能重建备库。复制槽(Replication Slot)就是来解决这个问题的。

复制槽相当于主库给备库开的一个"预留位"。主库会记住这个备库当前消费到哪个 WAL 位置,只要这个槽还在,主库就不会删除备库还没消费的 WAL 文件。这样即使备库断线一段时间,只要 WAL 没被撑爆磁盘,重连后还能接着传。

但复制槽是把双刃剑。如果备库长期下线,主库的 WAL 会一直堆积,磁盘迟早被写满,主库自己反而先挂了。所以生产环境里,复制槽一定要配合监控——监控槽的restart_lsn和主库pg_wal目录的增长速度,发现备库长时间不消费就得告警。

2.3 同步提交与异步提交:实时性和可用性的取舍

"实时同步"这四个字,落到配置上其实是一个选择题。PostgreSQL 的synchronous_commit参数控制着主库在什么时机向客户端返回"提交成功"。

  • 异步(off或local):主库本地写完 WAL 就返回成功,不管备库收没收到。性能最好,但主库崩溃时可能丢最后一段数据。
  • 同步(on):主库要等到synchronous_standby_names里指定的备库确认收到 WAL,才返回成功。数据最安全,但主库的提交延迟会被备库的网络和磁盘拖累。

很多人一听"实时同步"就无脑上同步提交,结果发现写入性能掉了一大截。我的建议是:先想清楚你的 RPO(恢复点目标)。如果业务能容忍主库故障时丢几秒数据,异步就够了,延迟低、可用性高;如果一分钱数据都不能丢,再上同步,同时要接受性能损耗,并且至少配两个备库做仲裁,避免单个备库拖垮主库。

synchronous_standby_names的写法也有讲究,比如'FIRST 1 (standby1, standby2)'表示在括号里列出的备库中,只要有 1 个确认就行。这种"法定人数"式的配置,能在保证安全的同时避免单点备库故障导致主库卡死。

3. 从零搭一套主从环境:配置、备份与启动的完整链路

3.1 环境规划与前置检查

假设我们有两台机器,主库叫node-primary,备库叫node-standby,都装好了同一个大版本的 PostgreSQL(这里以 14 为例,其他版本思路一致)。两台机器要能互相通过数据库端口通信,防火墙放行 5432。

动手前先确认三件事:第一,主备的 PostgreSQL 大版本完全一致,psql --version对一下;第二,主库的data目录位置和配置文件名心里有数,不同发行版可能放在/var/lib/pgsql/data或/etc/postgresql/14/main;第三,准备好一个用于复制的数据库账号,别直接用超级用户裸奔。

3.2 主库侧:打开复制开关

主库要改的核心参数集中在postgresql.conf里:

# 必须:开启足够级别的 WAL wal_level = replica # 必须:允许备库连接的数量,至少留 2~3 个余量 max_wal_senders = 5 # 建议:保留足够的 WAL 文件,给备库断线留缓冲 wal_keep_size = 512MB # 可选:同步提交相关,先注释掉,等异步跑通再开 # synchronous_commit = on # synchronous_standby_names = 'FIRST 1 (standby1)'

max_wal_senders是允许同时连接的 WAL 发送进程数,每个备库连接会占用一个。设成 5 是给未来扩容和临时连接留余地,别卡着 1 设,否则备库重连时可能因为槽位不够连不上。

wal_keep_size是 PostgreSQL 13 之后替代wal_keep_segments的参数,单位直接写 MB/GB。它的作用是即使没有复制槽,主库也至少保留这么多 WAL。我一般会同时开复制槽和设wal_keep_size,双保险。

接着改pg_hba.conf,放行备库的复制连接:

# 允许备库所在网段做物理复制 host replication repl_user 192.168.1.0/24 scram-sha-256

注意这里是replication这个特殊数据库名,不是某个具体库。认证方式用scram-sha-256比md5更稳妥。

然后创建复制账号:

CREATE ROLE repl_user WITH REPLICATION LOGIN PASSWORD 'your_strong_password';

改完配置重启主库,或者用pg_ctl reload让部分参数生效——但wal_level和max_wal_senders这类参数需要重启才能生效,别偷懒。

3.3 用 pg_basebackup 做基础备份

备库要跟上主库,第一步得有一份主库的"全量快照",这个动作交给pg_basebackup。它会连上主库,把整个数据目录拷过来,同时自动处理好复制起点。

在备库上执行(确保备库的 data 目录是空的):

pg_basebackup \ -h node-primary \ -p 5432 \ -U repl_user \ -D /var/lib/pgsql/14/data \ -Fp \ -Xs \ -P \ -R

逐个解释这些参数为什么这么选:

  • -Fp表示 plain 格式,直接输出成和主库一样的目录结构,方便直接启动,不用再解压。
  • -Xs表示用流式方式传输 WAL,备份过程中产生的 WAL 会一并传过来,保证备份的一致性。
  • -P显示进度,大库备份时心里有底。
  • -R是关键,它会自动在备库数据目录里生成standby.signal文件,并把主库连接信息写进postgresql.auto.conf。没有这个文件,备库启动后会以为自己是主库。

3.4 备库侧:确认复制配置

pg_basebackup -R之后,检查备库的postgresql.auto.conf,应该能看到类似内容:

primary_conninfo = 'host=node-primary port=5432 user=repl_user password=your_strong_password'

如果你用了复制槽,还要加上:

primary_slot_name = 'standby1_slot'

复制槽需要在主库提前创建:

SELECT pg_create_physical_replication_slot('standby1_slot');

备库的postgresql.conf里,hot_standby要打开(默认就是on),这样备库在重放 WAL 的同时还能接受只读查询:

hot_standby = on

3.5 启动备库并验证复制状态

备库启动后,它会自动连上主库开始拉 WAL。验证分两步走。

在主库上查发送进程:

SELECT client_addr, state, sync_state, sent_lsn, replay_lsn FROM pg_stat_replication;

如果看到一行记录,state是streaming,说明复制链路通了。sync_state是async还是sync取决于你有没有配同步提交。

在备库上查恢复状态:

SELECT pg_is_in_recovery(); -- 返回 t 表示这是备库 SELECT now() - pg_last_xact_replay_timestamp() AS replay_delay;

replay_delay就是备库落后主库的时间,正常情况下应该是毫秒级甚至更小。如果这个值持续增大,说明备库重放跟不上,得查磁盘 IO 或者网络。

4. 实测中最容易翻车的几个点:延迟、断连与权限

4.1 备库延迟忽高忽低,先分清是"接收慢"还是"重放慢"

线上最常见的抱怨就是"备库延迟好几百毫秒甚至几秒"。这时候别急着调参数,先定位瓶颈在哪一段。复制链路分两段:主库发送 WAL 到备库(接收),备库把 WAL 应用到数据文件(重放)。

查接收延迟,看主库的pg_stat_replication:

SELECT client_addr, pg_wal_lsn_diff(sent_lsn, replay_lsn) AS pending_bytes FROM pg_stat_replication;

pending_bytes是主库已经发出去、但备库还没确认重放完的字节数。如果这个值很大且持续增长,说明备库重放跟不上。

重放慢的常见原因有三个:备库磁盘 IO 太差(尤其是机械盘)、备库上有长查询阻塞了 WAL 重放(hot_standby_feedback相关)、或者max_standby_streaming_delay设得太大导致重放被推迟。我一般会把备库的数据盘换成 SSD,并且把max_standby_streaming_delay设成一个合理值(比如 30s),避免只读查询无限期阻塞重放。

4.2 备库断线重连后复制槽把主库磁盘撑爆

前面提过复制槽的风险,这里给一个真实的排查思路。某次备库因为网络割接下线了两天,运维没注意,结果主库pg_wal目录从几个 G 涨到了几百 G,差点把盘写满。查原因就是复制槽一直保留着备库没消费的 WAL。

排查命令:

SELECT slot_name, active, pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) AS retained_wal FROM pg_replication_slots;

retained_wal就是该槽保留的 WAL 量。如果active = f且这个值很大,说明备库掉线了但槽还在攒 WAL。

处理方式有两种:要么赶紧把备库拉起来让它消费,要么确认备库不再需要后删掉槽:

SELECT pg_drop_replication_slot('standby1_slot');

删槽之前一定要确认备库真的不用了,否则备库重连时会因为找不到槽而报错。我的经验是给复制槽配监控告警,retained_wal超过阈值就通知,别等磁盘爆了才发现。

4.3 权限和认证的坑:连接被拒的几种典型报错

搭复制时最常撞的墙就是备库连不上主库。报错信息通常很直白,但新手容易懵。我整理了几种典型情况:

报错关键词根本原因解决方向
no pg_hba.conf entry主库没放行备库 IP 的 replication 连接检查pg_hba.conf的网段和数据库名
password authentication failed复制账号密码错或认证方式不匹配核对密码,确认scram-sha-256配置
replication slot does not exist配了primary_slot_name但主库没建槽主库执行pg_create_physical_replication_slot
requested WAL segment has already been removed备库需要的 WAL 被主库删了加大wal_keep_size或启用复制槽

这里有个细节值得强调:pg_hba.conf里 replication 那一行的匹配顺序是从上到下,一旦匹配到就停止。如果你前面有一条宽泛的host all all 0.0.0.0/0 reject,后面的 replication 放行就永远不生效。改完pg_hba.conf记得reload,不用重启。

5. 主库真的挂了怎么办:提升备库与回切的实际操作

5.1 判断该不该切:先确认主库是真死还是假死

主库"挂了"分两种:真死(机器宕机、进程崩溃)和假死(网络抖动、负载过高导致连接超时)。如果是假死,你贸然把备库提升成主库,就会造成脑裂——两个主库同时接受写入,数据直接分叉,后面想合都合不回来。

所以切换前必须确认主库确实无法恢复。手段包括:从多台机器 ping 主库、尝试用psql连主库、看主库所在宿主机是否真的断电。只有确认主库短期内回不来,才执行提升。

5.2 提升备库:一条命令的事,但前后要做的事不少

在备库上执行提升:

pg_ctl promote -D /var/lib/pgsql/14/data

或者用 SQL:

SELECT pg_promote();

提升后,备库会删掉standby.signal,停止恢复模式,开始接受读写。验证一下:

SELECT pg_is_in_recovery(); -- 返回 f 表示已经是主库

提升完成后,原来指向旧主库的应用要改连接串指向新主库。这一步最好有统一的配置中心或者 VIP 机制,否则改一圈连接串能改到怀疑人生。

5.3 旧主库恢复后怎么回切:别直接启动就完事

旧主库修好后,千万别直接把它启动起来当主库用,那样又会脑裂。正确做法是把它重新做成新主库的备库。有两种方式:

第一种,如果旧主库的数据还能用,可以用pg_rewind把它和新主库的差异补齐,速度快,不用全量重做:

pg_rewind \ --target-pgdata=/var/lib/pgsql/14/data \ --source-server="host=node-new-primary port=5432 user=repl_user"

pg_rewind要求旧主库在崩溃前开启了wal_log_hints或者用了数据校验和,否则它没法精确找到分叉点。所以我在主库配置里会提前加上wal_log_hints = on,为的就是将来回切方便。

第二种,如果差异太大或者pg_rewind用不了,就老老实实重新pg_basebackup一遍,把旧主库彻底重建成备库。慢是慢点,但干净。

回切完成后,记得在新主库上重建复制槽,把旧主库重新纳入复制拓扑。整个切换和回切流程,我强烈建议在测试环境演练至少两遍,把每一步的命令和验证点写成脚本,真出事的时候照着敲,比临场发挥靠谱得多。

6. 让这套方案真正扛住生产:监控、演练与参数微调

6.1 必须盯住的几个监控指标

流复制跑起来只是开始,能不能扛住生产,取决于你有没有把关键指标盯住。我列几个必看的:

  • 复制延迟:pg_stat_replication里的replay_lsn和主库pg_current_wal_lsn()的差值,换算成时间或字节。
  • 复制槽保留的 WAL 量:防止磁盘被撑爆。
  • WAL 生成速率:突然飙升往往意味着有大事务或者批量写入,备库可能跟不上。
  • 备库重放冲突:pg_stat_database_conflicts里的confl_tablespace、confl_lock等,反映只读查询和重放的冲突次数。

这些指标用 Prometheus 的postgres_exporter采集,配 Grafana 面板,延迟超过阈值就告警。别等业务方打电话说"备库数据不对"才去查。

6.2 定期做一次"真切换"演练

监控做得再好,不如真刀真枪切一次。我建议每个季度至少做一次切换演练:在业务低峰期,手动把主库停掉,走一遍提升备库、改连接、验证读写的完整流程,然后再把原主库回切成备库。演练的目的不是证明方案能用,而是暴露那些文档里没写、只有动手才会遇到的细节——比如某个应用的连接串写死在代码里、某个定时任务连的是旧主库、VIP 切换脚本有 bug。

演练完把遇到的问题记下来,更新到切换手册里。这份手册才是你真正的"救命文档"。

6.3 几个值得微调的参数

最后分享几个我在生产环境里调过的参数,都是踩过坑之后才改的:

  • wal_keep_size:别设太小,我一般给 1GB 到 2GB,给备库断线留足缓冲。
  • max_wal_senders:按备库数量加 2 来设,留余量。
  • wal_sender_timeout:默认 60s,网络不稳的环境可以适当调大,避免频繁断连重连。
  • hot_standby_feedback:如果备库上有长查询,打开它能减少重放冲突,但代价是主库的 vacuum 会被推迟,可能引起表膨胀。要不要开,取决于你的备库是纯灾备还是承担只读业务。

这套流复制方案我从单机测试一路用到多备库的生产拓扑,最大的体会是:配置本身不难,难的是对边界情况的理解和预案。把复制槽管好、把延迟监控好、把切换演练好,它就能成为你数据库架构里最可靠的一块基石。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询