1. 为什么流复制是 PostgreSQL 高可用方案里最值得先啃的硬骨头很多团队第一次接触 PostgreSQL 高可用脑子里蹦出来的往往是各种分布式中间件、共享存储集群或者逻辑订阅方案结果折腾一圈发现最稳、最省事、延迟最低的反而是数据库自带的流复制Streaming Replication。它不是什么外挂组件而是 PostgreSQL 内核原生支持的一套物理复制机制主库把预写日志WAL源源不断地推给备库备库一边接收一边重放最终达到和主库几乎一模一样的数据状态。我先把结论摆在这儿如果你要的是主从实时同步数据也就是备库能近乎实时地跟上主库的写入流复制是首选。它解决的核心问题是——当主库因为硬件故障、误操作或者机房问题挂掉时你手里还有一个数据几乎不丢的备库可以顶上来同时备库还能分担一部分只读查询压力。这套方案适合谁适合所有用 PostgreSQL 承载核心业务、又不想引入过重外部依赖的开发和运维同学。哪怕你之前没碰过复制只要会改配置文件、会用psql跟着走一遍就能跑通。但我要先泼一盆冷水流复制看着简单真正落地时坑不少。同步还是异步、wal_level设成什么、复制槽要不要建、备库能不能被提升、主库崩了之后怎么切回来这些细节决定了你的方案是能跑还是敢上生产。这篇内容我会把原理、配置、验证、排错、切换这几块掰开揉碎讲清楚重点放在为什么这么配而不是甩给你一堆参数让你抄。你照着做能搭出一套可用的主从实时同步环境你理解了背后的逻辑遇到线上抖动才知道往哪儿查。在正式开始之前先明确一个概念边界流复制是物理复制复制的是 WAL 记录的字节流所以主备两边的 PostgreSQL 大版本必须一致操作系统架构最好也一致。它和逻辑复制Logical Replication不是一回事逻辑复制复制的是行级变更、可以跨版本、可以只复制部分表但延迟和稳定性通常不如物理流复制。我们要做的是前者。2. 流复制的底层机制WAL、复制槽与同步提交到底在干什么2.1 WAL 是整套复制的地基要理解流复制绕不开 WALWrite-Ahead Logging预写式日志。PostgreSQL 修改任何数据之前会先把这次修改的意图写进 WAL 日志然后再去改实际的数据文件。这个顺序不能反因为一旦中途崩溃数据库可以靠重放 WAL 把数据恢复到一致状态。你可以把 WAL 想象成一本操作流水账数据文件是最终账本流水账先记账本后改。流复制正是复用了这套机制。主库产生的 WAL 不只是本地写完就完事它还会被发送到备库。备库收到 WAL 后先写到自己的 WAL 文件里这叫接收然后再按顺序重放这些记录到数据文件这叫重放或应用。所以备库天然处于一个持续恢复的状态它本质上是一台一直在做崩溃恢复的机器。这里有个关键参数wal_level它决定了 WAL 里记录多少信息。默认的replica级别就足够支撑流复制它记录了重建备库所需的所有信息。如果你设成minimalWAL 记录会精简但流复制就跑不起来。所以第一步配置里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 hostnode-primary port5432 userrepl_user passwordyour_strong_password如果你用了复制槽还要加上primary_slot_name standby1_slot复制槽需要在主库提前创建SELECT pg_create_physical_replication_slot(standby1_slot);备库的postgresql.conf里hot_standby要打开默认就是on这样备库在重放 WAL 的同时还能接受只读查询hot_standby on3.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_replicationSELECT 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_slotrequested 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或者用 SQLSELECT 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-serverhostnode-new-primary port5432 userrepl_userpg_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 会被推迟可能引起表膨胀。要不要开取决于你的备库是纯灾备还是承担只读业务。这套流复制方案我从单机测试一路用到多备库的生产拓扑最大的体会是配置本身不难难的是对边界情况的理解和预案。把复制槽管好、把延迟监控好、把切换演练好它就能成为你数据库架构里最可靠的一块基石。