1. 初识PostgreSQL主从复制:这些参数决定了你的数据一致性
做数据库运维和架构设计的朋友们都知道,PostgreSQL的主从复制是一个老生常谈却又极其重要的话题。每次遇到同步延迟报警、主从切换失败、数据丢失争议,翻来覆去排查的根源,往往就落在几个核心参数上。很多开发者最初接触PostgreSQL复制时,习惯直接照搬网上模板,把wal_level设成logical、synchronous_commit设成on、配一堆max_wal_senders,结果上线后出现性能骤降或者从库永远追不上主库的情况,一头雾水。
这篇文章我从实际运维角度出发,把PostgreSQL主从同步涉及的复制参数逐个拆开,讲清楚每个参数在同步链路中的作用、内在逻辑、典型配置值,以及参数之间如何相互影响。适合正在维护PostgreSQL生产环境的DBA、刚接触复制机制的后端工程师,以及准备做高可用方案设计的架构师参考。
先简单梳理一条主从同步的完整链路:主库的事务写入WAL日志,WAL日志通过流复制协议传给从库,从库接收WAL并重放到本地数据文件。这条链路上的每一个环节都有对应参数在把关。主库侧有wal_level决定记什么日志,max_wal_senders决定能开多少条复制连接,wal_keep_size决定保留多少WAL供从库追赶,synchronous_commit决定事务要不要等从库确认;从库侧有hot_standby决定恢复期能否提供只读服务,primary_conninfo决定从库连谁,recovery_target_timeline决定追哪个时间线。这些参数单独看各有职责,但真正决定主从同步质量的,是它们之间的协作关系。
我把这些参数分成异步复制、同步复制、从库恢复三类来讲解。异步复制是PostgreSQL默认模式,性能最好但有丢数据风险;同步复制通过synchronous_commit和synchronous_standby_names实现,数据安全好但延迟增加;从库侧的参数则决定了故障切换时的表现。理解清楚这三类的权衡,生产环境选型就心里有底了。
2. 异步复制核心参数:性能与数据安全的平衡点在哪
2.1 wal_level到底该设成什么
wal_level是复制的基础前提,它控制Write-Ahead Log中记录的信息量。有三个可选值:minimal、replica(在9.6版本之前叫archive)、logical。
minimal只记录重建数据库所需的最小信息,很多操作的细节会被跳过,比如在同一个事务里既有建表又有COPY数据时,可以不记足够的WAL来支持归档和复制,因此它不能作为复制和归档的配置。replica是生产环境最常用的值,记录了完整的WAL信息,支持流复制、归档、PITR恢复,但不包含逻辑解析所需的数据。logical在replica基础上额外记录了逻辑解码所需的信息,如果你要用逻辑复制或者CDC工具(比如Debezium),就必须设成logical。
我见过不少新手为了图省事把wal_level设成logical,即便根本没用逻辑复制,代价是WAL写入量比replica多出5%到10%,在写入密集型场景下这个开销不可忽视。反过来,有人为了追求极致性能把wal_level设成minimal,结果发现备库根本起不来,甚至无法做PITR恢复。如果只做物理主从复制或者物理备份,replica是唯一正确的选择。这个参数是静态参数,修改后需要重启主库,所以不要在业务高峰期动它。
2.2 max_wal_senders怎么理解
max_wal_senders定义了主库上允许同时运行的WAL发送进程的最大数量。每个从库在流复制时会占用一个WAL sender进程,如果你做了级联复制(从库再挂从库),中间层既要接收又要发送,也会占用数量。
这个参数的默认值是10,对大部分中小规模集群够用。但要注意,它不只是为从库服务,pg_basebackup做基础备份时也会临时占用一个WAL sender,你在物理备份期间如果从库比较多,可能出现“所有WAL sender都被占用,新的从库连不上”的情况。此时从库日志里会报"number of requested standby connections exceeds max_wal_senders"的错误。
我建议生产环境把max_wal_senders设为从库数量加5到10的余量,同时监控pg_stat_replication里的状态。这个参数是静态参数,修改后需要重启主库,因此规划集群规模时就要想清楚未来的扩展方向。
2.3 wal_keep_size与复制槽:防止从库追不上主库
wal_keep_size(9.6之前叫wal_keep_segments,老版本用段数量为单位,新版本用字节大小)控制主库为从库保留的WAL文件数量。当从库因为网络波动或者长时间停机,落后主库超过这个保留量时,主库已经清理了从库还没接收的WAL,从库就无法继续追赶,只能重新通过pg_basebackup做全量同步。
wal_keep_size的问题在于它是静态分配的,无法感知从库实际消费进度。WAL写得快,保留的那点文件可能几分钟就被覆盖;WAL写得慢,又白白占用磁盘空间。所以我对生产环境的建议是:优先使用复制槽(replication slot)来动态管理WAL保留,复制槽会记录每个从库的消费位置,主库清理WAL时不会越过还在使用WAL的从库位置,从根本上杜绝了从库追不上的问题。
复制槽带来的代价是主库磁盘使用量不再可控——如果一个从库长时间断开,WAL会无限堆积,直到把磁盘塞满。因此使用复制槽必须配上监控和告警,一旦发现复制槽的restart_lsn远远落后于当前WAL写入位置,要立即处理,要么恢复从库,要么手动删除复制槽。我在某公司的生产环境里就遇到过从库断开一周没人发现,主库磁盘被WAL撑到95%的险情,从那之后复制槽监控就成了我搭建集群后的第一优先级。
3. 同步复制参数详解:从“很快但不保险”到“保险但变慢”
3.1 synchronous_commit的五个级别
synchronous_commit是PostgreSQL主从复制中最容易让人误解的参数。它不是简单的on/off开关,而是有五个级别:off、local、remote_write、remote_apply、on。默认值是on。
on是最严格的级别,主库事务提交时,要等待WAL被写入主库自身磁盘,并且备库接收WAL后写入备库磁盘并刷盘,主库才向客户端返回提交成功。这样保证了即使主库瞬间崩溃,事务也已经存在于备库的WAL里,通过切换可以恢复,不会丢失已提交事务。
remote_write比on宽松一些,它只要求备库把WAL写入操作系统的page cache,不要求刷到磁盘。如果备库操作系统没有崩溃,即使备库的PostgreSQL进程崩溃,WAL也还在page cache里,重启后能继续刷盘并恢复数据。但如果备库的整台机器断电,page cache里的WAL就丢了。这个级别比on性能好,但严格来说还是有极小窗口的数据丢失风险。
remote_apply比on更严格,要求备库不仅接收WAL,还要把WAL里的变更重放到数据文件,事务提交返回时,数据已经在备库可见。这保证了主备切换后不会丢失任何已提交事务,并且从库查询能看到与主库完全一致的数据。代价是主库每个提交都要等待从库完成I/O和重放,延迟显著增加。
local则只管主库本地的WAL刷盘,完全不等待备库确认。off级别更激进,提交时甚至不等WAL刷到主库磁盘,只在内存里标记提交成功。off级别下,主库进程崩溃都可能丢事务,只适合能接受数据丢失且追求极致写入性能的场景。
生产环境中,我推荐大多数场景使用on。如果对数据安全要求极高且从库硬件性能好、网络延迟低,可以考虑remote_apply。off和local只用于可以容忍丢数据的边缘业务,比如日志清洗、临时结果集写入。
3.2 synchronous_standby_names与同步复制的“名单”
仅仅设置synchronous_commit=on还不行,你必须告诉主库“等哪个从库”。synchronous_standby_names就是这个名单。这个参数有几种配置语法,包括FIRST、ANY和直接写从库名称。
它的语义是:主库在提交事务时,会等待名单中的从库完成确认,如果名单里的从库都掉线了,主库提交就会一直阻塞,直到某个从库重连并完成确认。这个行为在PostgreSQL中叫“同步候选者丢失导致写阻塞”。
这里有一个非常关键的运维坑:如果你设置了同步复制但只有一个从库,这个从库一旦宕机,主库的写入会全部卡住,业务直接停摆。所以同步复制在实际部署中,至少需要两个同步从库,配置成FIRST 1 (node_a, node_b)的形式,意思是只要node_a和node_b中任意一个确认就返回成功。当node_a挂掉时,node_b自动顶上成为新的同步候选,主库写入不会中断。
synchronous_standby_names中的名字来自从库在primary_conninfo里的application_name参数,而不是主库视图里显示的host。我在排查问题时见过多次命名错配,主库上synchronous_standby_names写的是从库的hostname,但从库的application_name设成了别的名字,结果是主库永远找不到同步对象,synchronous_commit的语义变成和没有配置一样,数据安全返回降级。配置好后,务必在pg_stat_replication中确认state列显示为sync,而不是async,这是最直观的验证手段。
3.3 同步复制的性能代价怎么量化
同步复制为什么慢?因为每一次事务提交,WAL都要先写到备库,再等待备库回ACK,然后主库事务才算完成。这意味着事务响应时间从“本地一次磁盘写入”变成了“网络往返+备库磁盘刷盘+ACK返回”。
在千兆内网、SSD存储、网络延迟0.2ms级别的环境下,同步复制的单事务提交延迟比异步大约高1到2毫秒。但对单事务写入频繁、并发量高的业务来说,这个延迟是叠加的。比如某系统每秒有2000个独立小事务提交,同步复制会让每个事务多等1毫秒,整体吞吐和响应时间都会受到明显影响。
缓解手段主要有几种:一是把多个更新的提交合并到显式事务中,减少提交次数;二是引入批处理模式,把事务合并到几十毫秒窗口内一起提交;三是用synchronous_commit=remote_write,牺牲极低的持久化保证换取接近异步的性能。还有一个思路是只在关键业务表上使用同步复制,其他表走异步——但PostgreSQL原生不支持按表区分同步级别,这个场景就得依靠中间件或外部路由来实现,复杂度会上升。
我在实际调优中遇到过这样的案例:某业务主库单次事务提交大概0.8毫秒,打开同步复制后涨到2.5毫秒,业务方觉得太慢想回退。后来分析发现大部分所谓“事务”其实可以合并,在应用层做了一个简单的提交合并,把每秒事务数从1500降到300,单事务延迟对整体响应的影响就变得可以接受了。所以不要一上来就否定同步复制,先看看业务事务模型有没有优化空间。
4. 从库侧参数与故障切换:主从同步的最后一公里
4.1 hot_standby与从库“活着”的方式
hot_standby控制备库在应用WAL恢复期间是否允许只读查询。默认值是on,也就是PostgreSQL 9.0之后,备库默认可以在恢复状态下接受SELECT查询。这在生产环境中几乎是必选项——你搭从库的目的不只是备份,通常还要分担读流量。
但hot_standby打开后有一个必须关注的问题:从库上的长查询会阻塞WAL重放。WAL里的变更如果和正在执行的查询冲突,比如查询正在读某张表,而WAL里恰好要对这张表做DROP或ALTER,备库会等待查询结束,或者根据max_standby_streaming_delay设置的时间等待后强制取消查询。
max_standby_streaming_delay默认30秒,意思是备库WAL重放遇到冲突时,等30秒还不行就取消正在执行的查询。这个参数对业务影响很大:如果从库上跑着报表类的长事务,而主库又频繁做大表DDL,备库上的报表查询可能会被强制中断,应用直接收到canceled by administrator的报错。相反,如果从库只做短查询、业务对中断不敏感,可以把max_standby_streaming_delay调大,比如600秒,给查询更多机会在超时前完成。但要注意,调大它意味着备库恢复进度可能落后,极端情况下主库WAL保留不够时,备库也会脱节。
我遇到的实践中,最稳妥的方案是对从库上的长查询做严格治理,避免超过几秒的查询落到从库上。如果实在需要跑分析类负载,建议单独部署分析专用的只读副本,并且用max_standby_streaming_delay控制冲突容忍度。另外,从库的hot_standby_feedback参数如果打开,会让从库把自己正在执行的最小事务ID反馈给主库,主库在做vacuum时不会清理从库还依赖的旧版本数据,能显著减少冲突,但它也会导致主库表膨胀,需要根据实际情况权衡是否开启。
4.2 primary_conninfo与备库的“上线姿势”
primary_conninfo定义在备库的复制配置中,它告诉备库主库在哪里、用哪个用户连接、application_name叫什么。格式类似一个连接字符串,包含host、port、user、application_name等字段。
它有几个关键点需要注意。第一,连接主库的用户必须有REPLICATION权限,通常建议单独建一个专用的复制账号,不要用超级用户去复制,避免权限范围过大带来的安全风险。我在某些公司的环境里见过直接用postgres超级用户做复制的配置,一旦复制链路被攻破,攻击者能拿到整个数据库集群的权限,这在新版管理规范中是不可接受的。
第二,application_name必须和主库的synchronous_standby_names中的名字严格一致,前面已经提到主库按这个名字来确认同步从库。
第三,primary_conninfo里可以加passfile选项指名密码文件路径,也可以配合pg_hba.conf中的ssl配置要求复制连接走SSL加密。跨公网环境的主从复制,建议开启SSL,防止中间人嗅探WAL流量。虽然WAL本身不是明文数据库内容,但泄露WAL等于间接泄露数据变更历史,风险不容小觑。
还有一个容易忽略的点:PostgreSQL 12之前,备库需要配置standby_mode=on并在recovery.conf里放primary_conninfo;从PG 12开始,引入了standby.signal文件机制,不再需要recovery.conf。你要在数据目录里创建一个standby.signal空文件来标识这个节点是备库,然后primary_conninfo等参数放在postgresql.conf里。升级到新版后还按老文档配置,导致备库起不来的情况在我见过的故障中排得上号。
4.3 参数联动下的主从切换与时间线
主从切换不只是“把备库升级为主库”这么简单,时间线(timeline)概念在复制参数中扮演着核心角色。PostgreSQL每次数据库恢复都会产生新的时间线ID,从库在追WAL时,通过recovery_target_timeline参数决定追哪个时间线的历史。
recovery_target_timeline的值可以是latest,意味着只要主库还在推送WAL,备库就不断沿着主库最新时间线应用日志。在自动故障切换场景下通常用latest,因为备库需要跟着新主库走,哪怕主库发生过多次failover和timeline切换。
如果你手动做PITR(时间点恢复),可能会把recovery_target_timeline设成某个特定的时间线ID,或者用recovery_target_time指定恢复到哪个时间点。这两个参数搭配restore_command从归档目录拉取WAL段时,需要确保归档策略能覆盖到目标时间线涉及的所有WAL段,否则恢复会停在某个断点,表现为数据库一直不认识后续日志。
我在实际故障切换演练中总结的经验是:failover脚本里除了要触发pg_ctl promote之外,还需要在旧主库回来后重新配置它成为新主库的从库,此时旧主库的postgresql.conf里要更新primary_conninfo指向新主库,同时清空或重建standby.signal。很多人漏掉后者,导致旧主库回来后以独立主库身份运行,双主脑裂,数据出现分叉,后果极其严重。这个动作我每次做切换演练都会列入checklist,宁可演练多花时间也不能漏掉。
5. 参数配置实操:一套可落地的生产配置参考
5.1 从一个模拟项目开始:某业务系统的复制配置
假设我们维护一套业务系统,核心库写多读少,需要一主一从架构,要求尽量不丢数据但也不能让主库写入性能崩掉。按照上面的分析,我来给出对应的参数组合。
主库侧(postgresql.conf关键项):
wal_level = replica # 物理复制必备 max_wal_senders = 5 # 一从库+备份+余量 wal_keep_size = 0 # 尽量依靠复制槽保留WAL max_replication_slots = 5 # 保留足够复制槽数量 synchronous_commit = on # 事务提交要求备库确认 synchronous_standby_names = 'FIRST 1 (node_s1)' hot_standby = on # 从库提供只读服务注意这里synchronous_standby_names后面只列了一个从库node_s1,意味着如果这个从库挂了,主库写入会被阻塞。对于一主一从架构,这是必须接受的取舍。如果业务无法容忍写入阻塞,就需要把synchronous_commit降级为local或者off,或者干脆保持默认的异步复制,只把同步复制留给对数据一致性要求极高的核心业务。
在实际规划阶段,我建议这样问自己:这个系统能不能接受主库宕机时丢失若干秒已提交事务?能接受就异步复制,不能接受就用同步复制,但同时必须保证从库的高可用性足够硬,比如从库本身做了RAID、有备用电源、网络有多路冗余。同步复制解决的是数据库层面的数据安全,它不能完全替代基础设施的高可用。
从库侧配置(PG 12+结构):
- 用pg_basebackup从主库拉基础备份:
pg_basebackup -h 主库IP -U rep_user -D /var/lib/postgresql/data -Fp -Xs -R - 其中-Xs表示在备份过程中使用流复制传输WAL,-R会自动生成standby.signal并写入primary_conninfo参数到postgresql.conf。
- 手工确认数据目录下存在standby.signal文件,并且postgresql.conf里的primary_conninfo指向主库,application_name设为node_s1。
主库还需要在postgresql.conf同级的pg_hba.conf中添加复制用户的访问规则,允许从库IP以replication权限连接。这里通常建议限制来源IP,不要配成0.0.0.0/0,否则任何能访问5432端口的主机都可能发起复制连接或者消耗WAL sender资源。
5.2 验证同步状态:该看哪些视图和指标
配置完成后不能直接收工,要逐个验证复制链路是否正常工作。
先在主库执行:
SELECT * FROM pg_stat_replication;关注三列:state(应该为streaming)、sync_state(同步复制模式下应该为sync或者quorum)、write_lag/lush_lag/replay_lag(WAL发送到备库、备库刷盘、备库重放的延迟秒数)。
然后在从库执行:
SELECT pg_is_in_recovery();返回true代表备库处于恢复模式,这是正确状态。再执行:
SELECT pg_last_wal_receive_lsn(), pg_last_wal_replay_lsn();比较这两个值,差距越小说明备库追赶主库越及时。
写入测试也是必不可少的:在主库建一张测试表并插入数据,然后到从库查询,看数据是否可见。流复制的物理备库在异步模式下,查询到的数据可能有微小延迟;在同步on模式下,主库事务提交成功就意味着数据已经至少在备库WAL中存在了。这个测试能最直观地验证链路是否串通。
监控告警方面,建议至少对以下指标设置阈值:pg_stat_replication中state不为streaming;replay_lag超过比如10秒;复制槽的active状态为false;主库WAL目录增长异常。这些指标可以通过定期抓取pg_stat_replication输出并与监控系统打通来实现。
5.3 常见问题与排查思路
我整理几个我在运维中反复遇到的故障场景,供大家参考。
场景一:从库一直报“WAL segment not found”错误
原因通常是主库已经清理了从库需要的WAL段,从库的WAL接收位置追不上主库保留窗口。排查步骤:先看主库pg_stat_replication中该从库的sent_lsn,再看从库的received_lsn,确认差距;检查主库是否配置了复制槽,如果没有,从库落后超过wal_keep_size就会中断。临时恢复办法是重新做pg_basebackup全量同步,根治办法是启用复制槽并完善监控。
场景二:同步复制配置正确但sync_state一直是async
优先检查synchronous_standby_names中的名字和从库的application_name是否完全一致,注意大小写。再看从库是否真的通过流复制连接上主库,而不是通过归档恢复追日志。某些中间状态如catchup或者startup不会触发同步确认。另外如果配置了多个同步从库,检查FIRST或者ANY的语义是否符合预期。
场景三:开启同步复制后主库写入偶发卡顿
很可能是从库WAL刷盘速度慢导致主库等待时间过长。排查从库的磁盘IO状况、WAL所在文件系统是否独立、刷盘参数(如wal_sync_method)是否匹配硬件特性。同时看看网络往返时间,如果主备机房跨地域,同步复制会非常痛苦。这种情况下要么把从库挪到同机房,要么重新评估该业务是否真的需要同步复制。
场景四:备库查询被取消,应用报错
这是hot_standby的冲突取消机制。查看备库日志里是否有“canceling statement due to conflict with recovery”相关记录。解决思路包括:缩短从库上的长事务;调整max_standby_streaming_delay;或者打开hot_standby_feedback(注意主库表膨胀问题);如果是某一类确定性的DDL操作,尽量安排在业务低峰执行。
这些场景背后有一个共同的排查原则:从主库的WAL发送视角和从库的WAL接收重放视角分别确认进度,两条线对照着看,绝大多数复制问题都能在几分钟内定位到具体环节。
6. 最后想说的话
PostgreSQL的复制参数不算多,但每一个都有明确的职责边界,组合起来能按需控制数据一致性的强度。我见过不少团队在架构初期忽略了wal_keep_size和复制槽的配置,上线半年后一次网络抖动导致从库脱节,最后不得不停机重新全量同步,教训深刻。
以我个人维护经验来说,给新集群做复制参数配置时,我会把这三件事放在优先位置:第一,主库WAL保留机制必须依赖复制槽,不能用固定的wal_keep_size,磁盘告警要比WAL堆积风险更先触发。第二,同步复制必须配多候选从库,至少两个,否则一个从库宕机就把整个写入链路卡死。第三,从库的冲突取消行为要在架构设计阶段就评估好,不要等业务跑到一半才体会什么是canceling statement due to conflict。
另外一个小技巧:在做主从切换演练时,手动模拟从库断网、主库断电、磁盘写满这三种故障,观察复制参数在实际故障中的表现,远比在测试环境反复开关参数更能帮助你理解这些配置的真实作用。PostgreSQL复制参数的调优没有万能配方,最终的参数组合一定是根据业务对数据安全、可用性和性能三者之间的权重来决定的。