1. 为什么流复制是 PostgreSQL 高可用架构的基石第一次接触 PostgreSQL 主从同步的人往往会被一堆名词绕晕物理复制、逻辑复制、流复制、归档恢复、级联复制……其实剥开这些术语的外壳核心问题只有一个——怎么让备库的数据跟主库保持一致并且延迟尽可能低。流复制Streaming Replication就是 PostgreSQL 官方给出的答案它基于 WALWrite-Ahead Logging预写式日志机制把主库产生的日志记录近乎实时地推送到备库备库收到后立即重放从而实现数据的持续同步。我最初搭建这套架构是在一个内部数据平台上当时的需求很朴素主库扛写入备库扛只读查询同时备库要能在主库出问题时快速顶上。用流复制的好处是它是 PostgreSQL 原生能力不需要装额外插件配置量小延迟可以压到毫秒级。相比逻辑复制物理流复制是块级别的复制整个数据库集群完全一致包括索引、序列、权限等所有对象运维起来心智负担小很多。这篇文章适合谁看如果你正在规划 PostgreSQL 的高可用方案或者已经有一主一备但同步总是出问题又或者你只是想知道流复制到底怎么配、参数怎么调、坑在哪里那接下来的内容应该能帮到你。我会从架构设计讲到参数配置再到实操步骤和排错经验尽量把每一步背后的“为什么”说清楚而不是只丢一堆命令让你照抄。需要提前说明的是流复制解决的是数据同步问题它本身不负责自动故障切换。也就是说主库挂了备库不会自己变成主库你需要配合额外的切换工具或手动执行提升操作。这一点很多新手会误解以为配了流复制就万事大吉实际上高可用是一个组合拳流复制只是其中最关键的一环。2. 流复制的核心原理与架构选型2.1 WAL 机制流复制的发动机要理解流复制必须先理解 WAL。PostgreSQL 在修改任何数据之前会先把这次修改的意图写进 WAL 日志然后再去改实际的数据文件。这个顺序不能反因为一旦系统崩溃PostgreSQL 可以靠重放 WAL 把数据恢复到一致状态。WAL 是顺序追加写的所以效率很高这也是 PostgreSQL 事务性能的重要保障。流复制的本质就是让备库也去重放主库的 WAL。主库有一个 WAL 发送进程walsender备库有一个 WAL 接收进程walreceiver两者通过 PostgreSQL 的复制协议建立长连接。主库每产生一段 WAL就通过这个连接推给备库备库收到后写入本地 WAL 文件再由恢复进程重放。整个过程是流式的不需要等一个 WAL 文件写满才发送所以延迟很低。这里有个关键概念叫LSNLog Sequence Number可以理解为 WAL 的字节偏移量。主库和备库都会记录自己当前的 LSN通过比较 LSN 就能知道备库落后了多少。你在主库执行SELECT pg_current_wal_lsn();在备库执行SELECT pg_last_wal_replay_lsn();两者一减再结合 WAL 的生成速率就能估算出同步延迟。2.2 同步复制与异步复制一致性和性能的权衡流复制支持两种模式异步复制和同步复制。异步复制下主库提交事务时不等备库确认只要本地 WAL 写完就返回成功。这种模式性能最好但存在数据丢失风险——如果主库在 WAL 还没传到备库时就宕机这部分数据就丢了。同步复制则要求主库等待至少一个备库确认收到 WAL 后才提交数据安全性高但会引入额外的网络往返延迟。选择哪种模式取决于你的业务对数据一致性的要求。如果是内部报表、日志分析这类场景丢几秒数据无所谓异步复制就够了。如果是交易、订单这类不能丢数据的场景那就得用同步复制并且要接受性能上的损耗。PostgreSQL 还支持synchronous_commit参数的细粒度控制比如remote_write表示备库写入本地 WAL 即可确认on表示备库必须完成重放才确认后者最安全但最慢。我的建议是先上异步复制把链路跑通观察延迟和稳定性再根据业务需要决定是否切同步。很多团队一上来就配同步复制结果发现网络抖动导致主库频繁卡顿反而影响可用性。同步复制对网络质量要求很高跨机房部署时要特别谨慎。2.3 物理复制 vs 逻辑复制为什么选流复制PostgreSQL 的复制分两大类物理复制和逻辑复制。物理复制就是流复制和归档恢复复制的是块级别的 WAL备库和主库完全一致连物理文件结构都一样。逻辑复制则是基于发布订阅机制复制的是行级别的数据变更可以选择只复制某些表也支持跨版本复制。流复制属于物理复制它的优势是简单、高效、全量一致。你不需要关心哪些表要复制整个集群自动同步。缺点是备库只能只读不能写入而且主备必须大版本一致。逻辑复制更灵活但配置复杂延迟通常也更高而且不复制 DDL 操作。如果你的需求是“备库作为只读副本随时能顶替主库”那流复制是首选。如果你需要“把某几张表的数据同步到另一个库做分析”那逻辑复制更合适。两者不是替代关系而是互补关系实际架构中经常同时使用。3. 主从环境搭建的完整实操流程3.1 环境规划与前置检查在动手之前先把环境规划清楚。假设我们有两台服务器主库叫 node1备库叫 node2都装好了同版本的 PostgreSQL。版本必须一致这是硬性要求主库 15 备库 14 是跑不通物理复制的。操作系统层面建议关闭防火墙对复制端口的拦截或者明确放行 5432 端口。前置检查清单如下主备 PostgreSQL 大版本一致小版本尽量一致主备系统时间同步否则日志时间戳会对不上主备网络互通延迟越低越好备库数据目录为空或者做好备份后清空主库的wal_level参数至少为replicawal_level这个参数控制 WAL 记录的详细程度minimal只记录崩溃恢复必需的信息replica额外记录复制所需的信息logical再额外记录逻辑解码所需的信息。流复制至少要用replica这个参数需要重启 PostgreSQL 才能生效。3.2 主库配置打开复制的开关主库的postgresql.conf需要修改以下几项# 监听所有地址或者至少监听备库能访问的地址 listen_addresses * # WAL 级别流复制必须至少为 replica wal_level replica # 允许的最大 WAL 发送进程数一主多备时要注意 max_wal_senders 10 # 保留的 WAL 段数量防止备库落后太多导致 WAL 被删 wal_keep_size 1GB # 同步复制的备库列表异步复制时留空即可 synchronous_standby_names max_wal_senders默认是 10一般够用但如果你的备库很多或者有级联复制就要相应调大。wal_keep_size是个容易踩坑的参数它决定主库至少保留多少 WAL 不被回收。如果备库断连时间较长主库的 WAL 被删了备库重连后就无法继续同步只能重建。所以这个值要根据你的业务写入量和可接受的断连时间来估算。假设你的业务每秒产生 5MB WAL你希望备库断连后 10 分钟内能自动恢复那wal_keep_size至少要设为 5MB × 600 3GB。当然更稳妥的做法是配置 WAL 归档这样即使主库删了 WAL备库也能从归档中拉取。接着配置pg_hba.conf允许备库的复制连接# 允许 node2 以 replication 身份连接 host replication replica_user 192.168.1.2/32 scram-sha-256这里我建议单独创建一个复制专用账号而不是用超级用户。复制账号只需要REPLICATION权限CREATE ROLE replica_user WITH REPLICATION LOGIN PASSWORD your_password;用专用账号的好处是权限最小化万一密码泄露影响范围可控。密码认证方式推荐scram-sha-256比md5更安全。3.3 备库基础备份用 pg_basebackup 克隆主库备库要加入复制集群第一步是把主库的现有数据完整复制过来。PostgreSQL 提供了pg_basebackup工具专门做这件事。在备库上执行pg_basebackup \ -h 192.168.1.1 \ -p 5432 \ -U replica_user \ -D /var/lib/postgresql/15/main \ -Fp \ -Xs \ -P \ -R参数逐个解释一下。-h和-p指定主库地址和端口-U是复制账号-D是备库的数据目录。-Fp表示以普通文件格式输出-Xs表示 WAL 通过流式传输而不是等备份结束再传这样备份过程中产生的 WAL 也能一并拿到。-P显示进度-R是关键它会自动在备库数据目录生成standby.signal文件并在postgresql.auto.conf中写入primary_conninfo连接串。standby.signal文件的作用是告诉 PostgreSQL“我是一个备库启动后进入恢复模式持续重放主库传来的 WAL”。这个文件在 PostgreSQL 12 之前叫recovery.conf12 之后改成了信号文件加参数配置的方式更简洁。执行pg_basebackup前确保备库的数据目录是空的否则会报错。如果备库已经初始化过先停掉 PostgreSQL清空数据目录再执行。备份时间取决于数据库大小和网络带宽大库可能要几个小时建议在业务低峰期操作。3.4 备库关键参数调优pg_basebackup -R会自动生成primary_conninfo但还有一些参数需要手动调整。备库的postgresql.conf中# 热备模式允许备库接受只读查询 hot_standby on # 备库反馈给主库的信息级别影响同步复制的精度 hot_standby_feedback on # 备库重放 WAL 时应用的延迟用于延迟复制场景 recovery_min_apply_delay 0 # 主库连接信息pg_basebackup -R 会自动写入 primary_conninfo host192.168.1.1 port5432 userreplica_user passwordyour_passwordhot_standby必须开启否则备库无法接受查询连接。hot_standby_feedback是个双刃剑开启后备库会把正在执行的查询所需的最老快照反馈给主库主库据此延迟清理死元组避免备库查询因主库 vacuum 而报冲突。但这也可能导致主库膨胀因为死元组迟迟不能清理。我的经验是如果备库查询比较重且不能容忍冲突就开启如果备库只做轻量查询可以关闭靠max_standby_streaming_delay来控制冲突处理。recovery_min_apply_delay用于延迟复制比如设成5min备库就会比主库慢 5 分钟重放。这个功能在误操作恢复场景很有用——主库误删数据后你有 5 分钟窗口从备库抢救。但延迟复制会增加 RPO要根据业务权衡。4. 同步状态监控与延迟排查4.1 主库视角查看备库连接状态主库上执行以下查询可以看到所有备库的连接和同步状态SELECT client_addr, state, sent_lsn, write_lsn, flush_lsn, replay_lsn, sync_state, pg_wal_lsn_diff(sent_lsn, replay_lsn) AS replay_lag_bytes FROM pg_stat_replication;state为streaming表示正在流式复制startup表示正在启动backup表示正在做基础备份。sync_state有async、sync、potential三种分别表示异步、同步、潜在同步。replay_lag_bytes是发送位点和重放位点的字节差可以直观看到备库落后多少。如果这个查询返回空说明主库没有检测到任何备库连接。排查方向备库的primary_conninfo是否正确、网络是否通、pg_hba.conf是否放行、复制账号密码是否正确。主库的日志里通常会有详细报错先看日志再动手。4.2 备库视角确认恢复进度备库上执行SELECT pg_is_in_recovery() AS is_standby, pg_last_wal_receive_lsn() AS receive_lsn, pg_last_wal_replay_lsn() AS replay_lsn, pg_last_xact_replay_timestamp() AS last_replay_time, now() - pg_last_xact_replay_timestamp() AS replay_delay;pg_is_in_recovery()返回t表示当前是备库。replay_delay是备库最后重放的事务时间与当前时间的差这个值能反映同步延迟。如果这个值持续增大说明备库重放跟不上主库的写入速度可能是备库硬件性能不足或者有长查询阻塞了重放。注意pg_last_xact_replay_timestamp()在没有新事务时不会更新所以如果主库长时间没有写入这个延迟值会一直增大但这并不代表同步有问题。判断延迟要结合主库的写入情况。4.3 延迟根因分析与处理延迟的常见原因有几类我整理成表格方便对照现象可能原因排查方法处理建议replay_lag 持续增大备库重放慢看备库 CPU、IO升级备库硬件减少备库查询延迟忽高忽低网络抖动ping、traceroute优化网络链路备库查询报冲突主库 vacuum 清理了备库需要的行看备库日志开启 hot_standby_feedback备库断连后无法重连WAL 被主库删除看备库日志增大 wal_keep_size配置归档同步复制下主库卡顿备库确认慢看 pg_stat_replication检查备库负载和网络备库查询冲突是个高频问题。当备库正在执行一个长查询而主库的 vacuum 清理了该查询需要的历史版本时备库会强制中断查询日志里会出现canceling statement due to conflict with recovery。解决办法有两个一是开启hot_standby_feedback让主库知道备库还在用这些行二是调大max_standby_streaming_delay让备库多等一会儿再中断查询。前者影响主库后者影响备库查询的确定性需要根据实际情况选择。5. 故障切换与日常运维经验5.1 手动提升备库为主库主库宕机后如果确认无法快速恢复就需要把备库提升为主库。操作很简单在备库上执行pg_ctl promote -D /var/lib/postgresql/15/main或者在 SQL 中执行SELECT pg_promote();。执行后备库会退出恢复模式变成可读写的主库。原来的standby.signal文件会被删除pg_is_in_recovery()返回f。提升之后你要做几件事确认新主库的wal_level、max_wal_senders等参数是否适合作为主库把其他备库的primary_conninfo指向新主库如果有应用连接配置更新连接地址。这些步骤最好提前写成脚本故障时直接执行减少人工操作时间。5.2 主库恢复后重新加入集群原主库修复后不能直接启动就当主库用因为它的数据可能比新主库旧。正确做法是把它作为备库重新加入。具体步骤是停掉原主库的 PostgreSQL清空数据目录用pg_basebackup从新主库重新克隆然后配置primary_conninfo指向新主库启动即可。这个过程听起来简单但实际操作中容易忽略一点原主库可能有比其他备库更新的数据。如果这些数据没有同步到新主库直接清空就丢了。所以故障切换前如果原主库还能访问尽量把它的 WAL 抢救出来或者至少确认新主库的数据完整性。5.3 日常巡检清单流复制集群的日常巡检我习惯看这几个指标主库pg_stat_replication中备库的state是否为streamingreplay_lag_bytes是否在可接受范围内备库pg_last_xact_replay_timestamp()是否持续更新主库 WAL 目录的磁盘占用是否正常备库日志有无冲突报错或连接断开记录这些检查可以写成脚本定时执行异常时告警。我见过不少团队配好流复制后就不管了直到主库挂了才发现备库早就断连几个月这种事故完全可以通过日常巡检避免。实操心得备库的primary_conninfo中建议加上connect_timeout和keepalives相关参数避免网络闪断后连接长时间不重建。比如connect_timeout10让备库在连接失败后快速重试。6. 常见问题速查与避坑指南6.1 备库无法启动或一直处于恢复中备库启动后如果一直卡在恢复模式先看日志。常见原因包括primary_conninfo配置错误、主库pg_hba.conf未放行、复制账号密码错误、主库max_wal_senders已满。日志里通常会有could not connect to the primary server或no pg_hba.conf entry这类明确提示。另一个容易忽略的点是备库的restore_command。如果你没有配置 WAL 归档但postgresql.conf里残留了restore_command配置备库可能会尝试从归档拉取 WAL 而卡住。检查一下这个参数不需要就注释掉。6.2 复制槽的正确使用姿势复制槽Replication Slot是防止主库删除备库还需要 WAL 的利器。创建复制槽后主库会为这个槽保留 WAL直到备库确认接收。创建命令SELECT pg_create_physical_replication_slot(standby1_slot);然后在备库的primary_conninfo中加上slot_namestandby1_slot。这样即使备库断连很久主库也不会删除它需要的 WAL。但复制槽也有风险如果备库永久下线而槽没删除主库的 WAL 会无限堆积最终撑爆磁盘。所以用复制槽一定要配合监控备库确认不再使用后及时删除槽。6.3 大事务对复制延迟的影响一个大事务比如一次性插入几百万行会产生大量 WAL。备库重放时必须等整个事务的 WAL 都接收完才能开始重放所以延迟会突然飙升。这不是故障而是机制决定的。应对办法是尽量拆分大事务或者接受这种周期性延迟。如果业务上无法避免大事务可以考虑用逻辑复制替代因为逻辑复制是按行传输的大事务的影响相对小。但逻辑复制的配置和维护成本更高需要权衡。6.4 版本升级时的复制兼容性PostgreSQL 的大版本升级物理流复制不能跨版本。比如从 14 升到 15不能直接让 15 的备库连 14 的主库。正确做法是先用pg_upgrade升级主库或者搭建新版本的主备集群用逻辑复制做数据迁移切换后再重建物理流复制。这个过程比较复杂建议单独规划升级窗口不要和日常变更混在一起。7. 性能优化与扩展思路7.1 网络与磁盘的优化点流复制的性能瓶颈通常在网络和备库的磁盘 IO。网络方面主备之间尽量走内网带宽要足够。如果 WAL 生成速率是 50MB/s网络带宽至少要有 100MB/s 的余量。磁盘方面备库的 WAL 写入和重放都是 IO 密集型操作建议用 SSD并且 WAL 目录和数据目录分开挂载避免 IO 争抢。主库的wal_buffers和wal_writer_delay也会影响 WAL 生成效率但在流复制场景下这些参数的影响相对较小优先保证网络和磁盘。7.2 级联复制减轻主库压力如果备库数量多主库的walsender进程会成为瓶颈。这时可以用级联复制主库只连一个中间备库其他备库从中间备库同步。中间备库需要开启hot_standby并配置max_wal_senders它既重放主库的 WAL又把自己的 WAL 发给下游备库。级联复制的好处是减轻主库负担缺点是增加了一层延迟而且中间备库故障会影响下游。适合备库数量多、主库压力大的场景。7.3 延迟复制作为误操作保险前面提到recovery_min_apply_delay这里再展开说一下。假设你设了 1 小时延迟备库会比主库慢 1 小时。如果主库误删了一张表你有 1 小时时间发现并从这个备库导出数据。这个窗口对于很多业务来说足够做应急恢复。但延迟复制不能替代备份。它只能防误操作不能防磁盘损坏、机房故障。而且延迟复制期间备库的数据不是最新的不能用于实时查询。所以通常是单独搭一个延迟备库专门做保险不承担读流量。8. 我个人在实际操作中的几点体会流复制这套东西配置本身不难难的是长期稳定运行。我踩过的最大的坑是复制槽忘了删导致主库磁盘被 WAL 撑满整个集群不可写。从那以后我给所有复制槽都加了监控并且规定备库下线超过 24 小时就必须清理槽。另一个体会是不要迷信同步复制。很多团队觉得同步复制才安全结果网络一抖主库就卡住业务方投诉不断。后来改成异步复制加定期备份反而更稳定。数据安全是个系统工程流复制只是其中一环备份、归档、监控缺一不可。最后分享一个小技巧备库的primary_conninfo里可以配置多个主库地址用逗号分隔。这样在主库切换后备库会自动尝试下一个地址减少人工干预。虽然不能完全替代切换工具但在简单场景下很实用。这套架构后续还可以扩展的方向包括配合 Patroni 做自动故障切换、用 pgBackRest 做 WAL 归档和备份、引入连接池中间件做读写分离。每一步扩展都要先确保基础流复制稳定否则上层工具再花哨也白搭。