Skip to content

synchronous_commit 配置 ​

synchronous_commit 是 PostgreSQL 里控制「事务提交要不要等 WAL 落盘」的参数。它最常被误解的地方是名字:很多人以为设置了它就会开启同步复制,实际上它只控制「等待的档位」,而「等谁、等几个」由另一个参数 synchronous_standby_names 决定。所以本页会先把这个参数单独讲透,下一节再讲它真正的搭档。

从语义上看,synchronous_commit 决定的是「客户端收到 COMMIT 成功响应时,这条事务的 WAL 到底走到了哪一步」。走的位置越靠后(越接近备库可见),数据安全性越高,但提交延迟也越大。它提供了一组递进的档位:off、local、remote_write、on、remote_apply,其中 on 是默认值。

需要特别强调的一点是:单独设置 synchronous_commit 不会产生同步复制。如果 synchronous_standby_names 为空字符串(这正是默认值),那么无论把 synchronous_commit 设成 remote_apply 还是 on,效果都退化成本地落盘等待——因为它没有任何同步备库可以等。这个坑在实际运维中非常常见:参数改了、reload 也做了,但同步复制压根没有生效,而监控上又看不出任何异常,因为事务确实在等落盘,只是等的是本地磁盘。

理解这个参数的另一个角度是「它描述的是数据安全等级,而不是复制拓扑」。复制拓扑负责把 WAL 送到备库,synchronous_commit 负责声明「送到哪一步才算这次提交成功」。两者配合才构成完整的同步复制:拓扑告诉你有哪些备库可用,档位告诉你需要等到什么程度。这也解释了为什么 PostgreSQL 把这两个参数放在不同的配置文档章节里——一个属于 WAL 配置,一个属于复制配置。

前置知识

建议先阅读:延迟备库

基础概念 ​

在展开参数细节之前,先把几个容易混淆的概念区分清楚。这些概念贯穿整个同步复制章节,后面的页会反复用到。特别要区分「write(写入)」与「flush(落盘)」这两个动作:前者只是把数据交给操作系统,后者才真正落实到持久化存储上;在很多讨论中这两者被笼统地称为「写磁盘」,从而产生误解。

概念说明
WAL 写入(write)把 WAL 记录写进操作系统的页缓存,尚未落到物理磁盘
WAL 落盘(flush / fsync)操作系统把页缓存中的数据真正写入持久化存储
WAL 重放(replay / apply)备库把收到的 WAL 记录应用到自己的数据文件上
本地落盘主库自己的 WAL 是否已 fsync 到磁盘
同步复制事务提交需要等待至少一个同步备库确认
异步复制事务提交只等本地落盘,不等备库确认
RPO允许丢失多少数据,同步复制直接决定这个指标

提交路径上的四个位置 ​

一条事务的 WAL 在提交后并不是「一步到位」地出现在备库上的,它要经过一条链路。理解这条链路的每一段,才能理解五个档位到底在等什么:

text
主库内存 → 主库 OS 缓存 → 主库磁盘
                              │
                              ▼
                        网络传输
                              │
                              ▼
备库内存 → 备库 OS 缓存 → 备库磁盘 → 备库重放 → 备库可查询

remote_write 只要求走到「备库 OS 缓存」;on 要求走到「备库磁盘」;remote_apply 更进一步,要求走到「备库重放并可查询」。local 则根本不管备库,只要求主库自己落盘。off 连主库落盘都不等。

这里有一个容易忽略的细节:主库侧无论哪个档位,WAL 都是先写入、后 flush 的,区别只在于提交响应是否等待 flush 完成。也就是说,off 并不是「不写 WAL」,WAL 依然会被写入并最终落盘,只是提交的动作不等它。

五个档位速览 ​

档位等待备库备库落盘备库可查询说明
off否否否不等本地落盘,最快,可能丢最近若干事务
local否否否等本地落盘,不等备库
remote_write是否否等备库写入 OS 缓存
on是是否等备库 flush 到磁盘(默认值)
remote_apply是是是等备库完成重放,查询可见

因为档位是递进的,实践中很少需要同时记住四个动作的布尔组合,记住「越往后越安全、越慢」这条主线即可。真正需要精确区分的是 on 与 remote_apply(差一次备库重放)以及 on 与 remote_write(差一次备库 fsync)。

on 是默认值 ​

on 是 synchronous_commit 的默认值。这一点值得记住,因为它意味着:在一个没有配置同步复制的实例上,事务提交默认已经会等待本地 WAL 落盘。很多人以为默认是「性能优先」,其实 PostgreSQL 默认就是「不丢已提交事务」的本地保证;真正「什么都等」的激进取值反而是 off。

on 这个名字本身也带有一点历史包袱。在只讨论单机时,on 的语义就是「本地 flush」;只有在配置了同步备库之后,on 才额外包含「等备库 flush」。因此读到「on 表示等待同步备库」这句话时,要记得它有一个前提:synchronous_standby_names 非空。

与 synchronous_standby_names 的关系 ​

这是本页最核心的一组关系,值得单独用一张表说清。可以这样记:synchronous_standby_names 是「开关」,synchronous_commit 是「旋钮」。开关没打开,旋钮转多少都没用。

synchronous_standby_namessynchronous_commit 的有效语义
空(默认)off = 不等;local / on / remote_write / remote_apply 全部等价于「等本地落盘」
非空五个档位各自按上表生效,remote_write / on / remote_apply 会等同步备库

所以配置同步复制的正确姿势是两个参数一起设:用 synchronous_standby_names 指定「等谁、等几个」,用 synchronous_commit 指定「等到哪一步」。只配其中一个都属于半成品。

事务级与会话级覆盖 ​

synchronous_commit 的一个非常实用的特性是:它可以在会话级、事务级被临时覆盖,而不必每次都改全局配置。

  • 会话级:SET synchronous_commit = off;,影响当前会话后续所有事务。
  • 事务级:SET LOCAL synchronous_commit = off;,只影响当前事务,事务结束自动恢复。

这个能力让「整体同步、局部异步」成为可能——例如一个 OLTP 应用默认开启同步提交保证不丢单,但在执行大批量导入任务时,把这个任务所在会话临时设为 off,就能显著提速。反过来也可以:全局设为较宽松的档位,对少数关键业务用会话级提到 remote_apply。

需要注意 SET LOCAL 与 SET 的作用域差异,这是最容易用错的点:

语句作用域结束条件典型用法
SET LOCAL synchronous_commit = off当前事务事务提交/回滚一次性批量导入
SET synchronous_commit = off当前会话连接断开或再次 SET整个批处理连接
ALTER DATABASE ... SET指定数据库的所有新会话再次修改按库分级
ALTER ROLE ... SET指定角色的所有新连接再次修改按应用分级
ALTER SYSTEM SET整个实例再次修改全局基线

从爆炸半径来看,SET LOCAL < SET < ALTER ROLE / ALTER DATABASE < ALTER SYSTEM。做临时提速时,应始终选择最小半径的那一档。

WAL 中的提交记录 ​

从 WAL 的视角看,一次事务提交对应一条 XLOG_XACT_COMMIT(或 XLOG_XACT_COMMIT_PREPARED)记录。同步复制等待的正是「这条提交记录在备库上到达了指定位置」。

理解这一点有两个用途:

  • 可以用 pg_waldump 在 WAL 中找到具体事务的提交记录,从而定位提交延迟发生在哪个 LSN 之前。
  • 可以理解为什么「同步复制保护的是提交,不是语句」——未提交事务的 WAL 不需要等待确认,只有提交那一刻才产生等待。

因此一个长时间运行的大事务(例如一条 UPDATE 跑 10 分钟)只在最后 COMMIT 时产生一次同步等待,中间过程不会。这也意味着「大事务」与「高频小事务」在同步复制下的延迟表现完全不同:前者延迟一次但单次很长,后者延迟很短但次数极多。

语法与用法 ​

在配置文件中设置(postgresql.conf):

conf
# 档位选择(默认 on)
synchronous_commit = on

# 必须在同一实例上指定同步备库,否则上面这行不产生同步复制效果
synchronous_standby_names = 'standby1'

# 提交延迟与合并相关(多数场景保持默认即可)
commit_delay = 0
commit_siblings = 5

用 SQL 查看当前值及其来源:

sql
SELECT name, setting, unit, context, pending_restart
FROM pg_settings
WHERE name IN ('synchronous_commit', 'synchronous_standby_names',
               'commit_delay', 'commit_siblings', 'wal_sync_method');
text
         name         | setting   | unit |  context   | pending_restart
----------------------+-----------+------+------------+-----------------
 commit_delay         | 0         |      | superuser  | f
 commit_siblings      | 5         |      | user       | f
 synchronous_commit   | on        |      | user       | f
 synchronous_standby_names |          |      | sighup     | f
 wal_sync_method      | fdatasync |      | sighup     | f
(5 rows)

在会话或事务内临时切换档位:

sql
-- 仅当前事务异步提交(批量导入常用)
BEGIN;
SET LOCAL synchronous_commit = off;
COPY big_table FROM '/data/big.csv' WITH (FORMAT csv);
COMMIT;

-- 整个会话改为异步
SET synchronous_commit = off;

-- 恢复为默认(或不写,会话结束时自动恢复)
SET synchronous_commit = on;

-- 也可以显式重置
RESET synchronous_commit;

通过 ALTER SYSTEM 修改全局值(需要 reload 或按 context 决定是否重启):

sql
ALTER SYSTEM SET synchronous_commit = 'remote_apply';
SELECT pg_reload_conf();

context 为 user 的参数(synchronous_commit、commit_siblings)对新会话立即生效,既有会话需要重新连接或自己执行 SET;sighup 类参数(synchronous_standby_names、wal_sync_method)通过 pg_reload_conf() 生效,不需要重启。这也是排查「参数改了没生效」时必须先确认的一个维度。

详细说明 ​

每个档位「等到哪一步」 ​

把五个档位对应到提交路径上的具体位置,是本参数最需要掌握的内容。官方文档用一张能力矩阵来描述,这里转述为更直观的表格:

档位等本地 write等本地 flush等备库收到等备库 flush等备库 replay
off否否否否否
local是是否否否
remote_write是是是否否
on是是是是否
remote_apply是是是是是

这张表可以这样理解:「本地 write / flush」是每一档的下限(除了 off),之后按档位逐级向备库推进。remote_apply 是唯一要求备库「重放完成」的档位。

有一个细节容易被忽略:remote_write / on / remote_apply 都包含「等本地 flush」。也就是说,即使把同步备库配好,主库自己的持久化保证并不会被放弃,三个同步档位都是「本地 + 备库」的双重等待。

off 与 local:不等备库的两档 ​

off 最容易被误读为「不安全、可能导致数据不一致」。准确的说法是:off 下事务提交不等待 WAL 落盘,因此在崩溃场景下可能丢失最近若干个已提交的事务;但不会造成数据库内部不一致。

原因是 PostgreSQL 对 WAL 的写入顺序有保证:即使 synchronous_commit = off,WAL 记录也一定是先进入操作系统缓存、再按顺序落盘。崩溃后如果某些尾部的 WAL 记录还没落盘,那么这批记录对应的事务就整体消失,数据库会回到一个「这批事务从未来过」的一致状态。官方文档把这描述为「与事务被干净地中止(cleanly aborted)等同的状态」。

需要留意的另一个细节是:off 下丢失的并不一定是「最近 N 毫秒」,而是可能不一致地丢——即可能丢一部分、留下一部分,取决于 WAL 写入与操作系统刷盘的时机。因此 off 适合那些「偶发丢几条最近数据可接受、但绝不允许数据错乱」的场景,例如日志采集、埋点上报、监控指标。

off 也并非「永远不落盘」。后台的 WAL writer 会周期性地刷盘,synchronous_commit = off 模式下的最大数据丢失窗口约为 3 × wal_writer_delay。默认 wal_writer_delay 是 200ms,因此最坏情况下大约有 600ms 的提交可能丢失。这个数字可以用来做风险估算:如果能接受「崩溃丢几百毫秒」,off 就是一个划算的选择。

local 则表示「等本地 WAL flush,但不等任何备库」。在配置了同步备库的实例上,local 相当于「放弃同步复制保护,只保证本地持久化」。它比 on 快(少了网络往返),比 off 安全(不会丢已提交事务)。

一个常见的用法是:把某个只关心本机持久化的后台批处理会话设为 local,从而在不牺牲本地持久性的前提下绕开同步备库的网络等待。注意 local 与 off 的关键差别在于「已提交事务会不会丢」——local 不会丢,off 会。

remote_write:等备库写入但不落盘 ​

remote_write 要求备库确认「WAL 已经写进它的文件系统(页缓存)」,但不要求备库把它 fsync 到磁盘。这意味着:

  • 主库崩溃:数据安全(备库已有 WAL,可以继续提供)。
  • 备库崩溃:可能丢失这批 WAL(因为它们还在备库的 OS 缓存里,没有落盘)。

因此 remote_write 的定位是「防止主库单点故障,但不防备库自身崩溃」。它比 on 快一点点(省掉一次备库 fsync),代价是备库崩溃时会丢数据。如果备库与应用在同一机架、同一电源域,那么「备库整体掉电」的概率并不比主库低,此时 remote_write 的收益就很有限。

在一些对延迟极其敏感、又要求「主库挂了不丢数据」的场景,remote_write 是一个折中:它把等待链缩短到「网络 + 备库内存写入」,通常比 on 节省 0.5~2ms(取决于备库存储的 fsync 开销)。

on:等待备库落盘 ​

on 在配置了同步备库后,要求备库把 WAL fsync 到持久化存储才返回成功。这是同步复制里最常用的档位,也是「主库挂了数据不丢」的标准配置。它的代价是每次提交多了一次网络往返 + 一次备库磁盘 fsync 的等待。

在跨机房的部署中,这个等待直接等于「网络 RTT + 备库磁盘 fsync 延迟」。同机房通常 1~5ms,跨机房可能到 10~50ms,这也是同步复制最常被抱怨「变慢」的原因。需要清楚的是,延迟是每次提交都付一次的,而不是每秒付一次,所以高频小事务的吞吐会按 1 / 提交延迟 受到硬性限制。

在 on 档位下,「备份库的存储性能」等价于「影响主库提交延迟的变量」。这是一个很重要的运维结论:备库不能配得太差,否则主库会被拖慢。备库的 fsync 延迟抖动会直接表现为主库提交延迟抖动。

remote_apply:备库重放完成 ​

remote_apply 是要求最高的一档:不仅要备库落盘,还要求备库把 WAL 重放(apply)完成,使该事务在备库上「对查询可见」。这样做的直接收益是:在备库上执行只读查询时,读到的一定是主库已经提交过的数据,不会出现「刚在主库提交,去备库查却查不到」的延迟。

它的代价也最大:提交延迟包含了备库重放的时间,而重放时间受备库硬件、是否有长事务阻塞重放(如 max_standby_streaming_delay)、备库上的查询冲突处理等因素影响。因此 remote_apply 通常只用于对「备库读一致性」有强要求的场景,例如读写分离下要求「写后立即可读」。

remote_apply 是 PostgreSQL 9.6 引入的档位,在此之前没有能让备库「重放完成才返回」的选项。它带来的一个隐性风险是:如果备库的重放被某个长查询卡住,主库的提交也会被一起卡住。因此使用 remote_apply 时必须同时关注备库侧的查询冲突与 max_standby_streaming_delay 设置。

性能与数据安全的权衡 ​

把五个档位按「延迟从低到高」和「安全性从低到高」排列,可以得到一条清晰的权衡曲线:

档位提交延迟量级主库故障备库故障典型用途
off最低可能丢最近若干事务不受影响日志类、可容忍丢失的埋点数据
local低不丢不受影响本机持久化优先、不需同步复制
remote_write中不丢可能丢最近若干事务防主库单点、允许备库崩丢
on中高不丢不丢通用高可用,主流业务选择
remote_apply最高不丢不丢且立即可读读写分离强一致读、核心交易

从这张表可以读出一个不太直观的结论:off 和 local 在「备库故障」一列都是「不受影响」,因为它们根本不等备库。这一点在评估「同步复制会不会带来可用性风险」时很关键——不同步就没有「备库挂了拖死主库」的问题,但也就没有对应的数据保护。

commit_delay 与 commit_siblings ​

commit_delay 允许一个即将提交的事务在真正做 WAL flush 之前主动等待一小段时间,目的是让同一时间窗内其他并发事务也把 WAL 攒进来,从而把多次 fsync 合并成一次,减少 I/O 次数。commit_siblings 是触发这个延迟的门槛——只有当当前活跃事务数不少于 commit_siblings 时,commit_delay 才会被使用。

参数默认值单位作用
commit_delay0微秒提交前主动延迟,等待合并其他事务的 fsync
commit_siblings5—活跃事务数达到该值才启用 commit_delay

这两个参数属于「用延迟换吞吐」的调优手段:通过让第一个提交者多等一会儿,把随后到达的一批事务合并到同一次 fsync 中。它们对本地 fsync 密集的场景有效,但在同步复制场景下很少是瓶颈的主因(同步复制的瓶颈通常是网络往返),因此不要把它们当作解决同步复制变慢的首选。

需要特别注意 commit_delay 的单位是微秒:设成 1000 表示 1 毫秒而不是 1 秒。设置过大反而会人为拉高所有提交的延迟。

相关参数与版本演进 ​

wal_sync_method 决定主库用哪种系统调用把 WAL 刷到磁盘,可选值包括 open_datasync、fdatasync、fsync、fsync_writethrough、open_sync。它只影响本地落盘这一步,不影响备库的等待。

在 synchronous_commit = off 时,wal_sync_method 对提交路径几乎不产生影响(因为根本不 fsync);在 local / on / remote_apply 时,它会影响本地 flush 的耗时。需要注意 fsync_writethrough 会绕过磁盘写缓存,语义最严格但也最慢,某些平台上不可用。

synchronous_commit 的档位不是一次到位的,了解版本差异有助于读老文档:

版本变化
9.5 及更早主要是 off / local / on 三档
9.6新增 remote_apply,形成今天的五档
10引入 quorum 提交(ANY 语法),配合本参数使用
12新增 pg_promote() 等提升相关能力

监控同步提交等待与生效性排查 ​

当后端正在等待同步复制的确认时,它会在 pg_stat_activity 中表现为等待事件 SyncRep:

sql
SELECT pid, usename, state, wait_event_type, wait_event, query
FROM pg_stat_activity
WHERE wait_event = 'SyncRep';
text
  pid  | usename  | state  | wait_event_type | wait_event |        query
-------+----------+--------+-----------------+------------+---------------------
 18923 | appuser  | active | IPC             | SyncRep    | UPDATE orders SET ...
 19011 | appuser  | active | IPC             | SyncRep    | INSERT INTO ...
(2 rows)

要点:等待同步复制的后端,其 wait_event_type 是 IPC、wait_event 是 SyncRep。这是判断「提交是否被同步复制卡住」最直接的信号。如果同时发现大量会话停在这个等待事件上,说明同步备库没有及时确认。

除了 pg_stat_activity,还可以从备库侧、pg_stat_replication 侧交叉验证:

视图 / 字段观察什么
pg_stat_activity.wait_event = 'SyncRep'主库上正在等待同步确认的后端
pg_stat_replication.sync_state该 walsender 对应备库的同步角色(sync / potential / async / quorum)
pg_stat_replication.sync_priority该备库在 FIRST 模式下的优先级
pg_stat_replication.flush_lsn备库已 flush 到的位置
pg_stat_replication.replay_lsn备库已重放到的位置

remote_apply 场景下,真正的瓶颈往往体现在 replay_lsn 落后于 flush_lsn,说明备库「收到了但还没来得及应用」。这个差值也是排查「为什么 remote_apply 特别慢」的第一手依据。

参数改了却没生效,是运维现场最常见的疑问。排查顺序建议如下:

  1. 确认 synchronous_standby_names 非空。空值时改 synchronous_commit 毫无效果。
  2. 确认 pg_reload_conf() 已执行,且 pg_settings.pending_restart 为 f。
  3. 确认备库真的处于 sync 状态。在 pg_stat_replication.sync_state 里看到 sync 才算真正在同步。
  4. 确认没有会话级覆盖。应用连接池可能在某处执行过 SET synchronous_commit,或者连接参数里带了 options=-c synchronous_commit=off。
  5. 确认改的是正确的实例与配置文件。多实例机器上改错配置文件很常见。

同步提交的等待机制 ​

理解「后端在哪里等」有助于看懂等待事件。当一条事务被标记为「需要同步复制确认」时,它不会去轮询备库状态,而是把自己挂到一个内部等待队列上,然后休眠。备库的 walsender 收到确认(或确认信息推进)后,会唤醒队列中所有条件已满足的后端。

这带来几个实际结论:

  • 等待是被动唤醒而不是主动轮询,因此「等待中的后端几乎不消耗 CPU」,监控上看到 37 个会话停在 SyncRep 并不会导致 CPU 打满。
  • 一次备库确认可以同时唤醒多个等待中的后端,所以并发提交数多时,同步复制不会让延迟线性叠加。
  • 唤醒条件是「达到所需档位」,因此如果配置的是 remote_apply,即使备库已经 flush 到该 LSN,只要还没 replay 完,后端就不会被唤醒。

这也解释了为什么同步复制的吞吐下降往往是「延迟上升」而不是「吞吐归零」:只要备库最终确认,等待的后端会一批批被唤醒。

同步复制与「半同步」的区别 ​

业界常说的「半同步复制」(semi-synchronous replication)在 MySQL 语境下有明确定义,但在 PostgreSQL 里并不是一个官方术语,容易引起误会。在 PostgreSQL 的中文资料里谈「半同步」时,通常指的是下面两种情况之一:

说法实际含义
「半同步」= 至少一个备库收到对应 remote_write(收到即可,不要求落盘)
「半同步」= 异步与同步混合部署对应 synchronous_standby_names 只列出部分备库,其余为 async

严格来说,PostgreSQL 的同步复制只有一个机制,就是 synchronous_commit 与 synchronous_standby_names 的组合,档位差别只是「等到哪一步」。因此在跨数据库讨论时,建议直接说档位名(on / remote_apply),避免「半同步」这个含混的词。

与延迟备库的配合 ​

延迟备库(recovery_min_apply_delay)会让备库故意落后主库一段时间。这一点与同步复制放在一起时会产生一个直接后果:滞后时间会全额加到提交延迟上。

如果某个备库同时被列入 synchronous_standby_names 又设置了 recovery_min_apply_delay = '1h',那么在使用 remote_apply 档位时,主库的每次提交都要等 1 小时——这显然是不可接受的。因此延迟备库绝不应该出现在同步备库列表里,它只能作为异步备库存在。

这个组合错误在实践中的表现形式是「数据库突然完全写不动」,而排查时又容易忽略 recovery_min_apply_delay,因为注意力通常都在 synchronous_standby_names 上。延迟备库的本职是「防逻辑误操作时留一个过去的副本」,与同步复制保护「物理故障下不丢数据」是互补的两件事,不要混在一起用。

与复制槽的交互 ​

复制槽(replication slot)解决的是「主库把备库还没收到的 WAL 提前回收掉」的问题,它保证主库为备库保留 WAL 直到备库确认收到。同步复制解决的是「事务提交要不要等备库」的问题。

两者是正交但互补的:

复制槽同步复制
关注点WAL 是否会被提前删除提交是否等待备库确认
防止的问题备库断连后追不上,需要重建主库崩溃时丢失已提交事务
不解决的问题主库崩溃时的数据丢失主库 WAL 被提前回收

生产上的推荐组合是「复制槽 + 同步复制」:复制槽保证备库断连后能续上、不必重建;同步复制保证主库崩溃时不丢已提交数据。只配复制槽不配同步复制,主库崩溃仍会丢数据;只配同步复制不配复制槽,备库短暂断连后再回来可能需要重建。

何时可以放心使用 off ​

off 不是一个「危险参数」,而是一个「需要明确业务前提才能用的参数」。可以用下面这组判定条件来判断是否可用:

  1. 数据是否可重放/可补采?例如埋点日志可以从客户端缓冲重发。
  2. 丢失是否只影响「最近几百毫秒」?off 的丢失窗口大约 3 × wal_writer_delay。
  3. 丢失是否会引发「上游无法对账」?如果会,就不能用。
  4. 是否有其他机制兜底?例如数据同时写到了 Kafka。

四条全部满足时,off 是一个性价比很高的选择;任何一条不满足,就应该往上走一个档位。

一个档位决策的流程 ​

把上面的判断串起来,可以得到一个简单的决策流程:

text
能否接受崩溃丢最近若干已提交事务?
  ├─ 能 → off(或 local,如果本地也不能丢)
  └─ 不能
       ├─ 备库掉电是否可接受丢数据?
       │    ├─ 可接受 → remote_write
       │    └─ 不可接受
       │         ├─ 是否需要备库「写后立即可读」?
       │         │    ├─ 需要 → remote_apply
       │         │    └─ 不需要 → on

这个流程的每一步都对应一个明确的业务问题,而不是技术偏好。用它来和业务方对齐 RPO,比直接讨论「要不要开同步复制」更有效。

与自动故障转移方案的配合 ​

如果使用 Patroni、repmgr 之类的自动故障转移工具,synchronous_commit 与 synchronous_standby_names 通常由工具自动维护,而不是手工写在 postgresql.conf 里。此时手工去改这两个参数可能被工具覆盖,导致「改完一会儿又变回去了」。

方案对同步参数的处理方式
Patroni由 synchronous_mode / synchronous_mode_strict 控制,自动生成 synchronous_standby_names
repmgr提供同步复制的相关管理命令,需按版本文档确认能力
手工管理自己写 postgresql.conf 并用 reload 生效

使用托管方案时,正确的做法是在工具的配置里声明「要同步复制」,而不是绕过工具去改 PostgreSQL 参数。判断是否被托管的最快方式是看 synchronous_standby_names 是否与 postgresql.conf 中的值一致——如果 ALTER SYSTEM 写入的值被自动改写,就说明有工具在管理它。

配置文件的层级关系 ​

同步参数可能来自多个配置文件,排查「生效值从哪来」时需要理解覆盖顺序:

层级文件 / 方式优先级
基础配置postgresql.conf最低
自动配置postgresql.auto.conf(ALTER SYSTEM 写入)高于 postgresql.conf
命令行postgres -c synchronous_commit=...高于上述两者
连接参数options=-c synchronous_commit=...覆盖数据库/角色设置
会话/事务SET / SET LOCAL最高

ALTER SYSTEM 写入的是 postgresql.auto.conf,它的优先级高于手工编辑的 postgresql.conf。这解释了一个常见的困惑:明明在 postgresql.conf 里改了值,重启后却被改回去了——因为 postgresql.auto.conf 里有一条更早的 ALTER SYSTEM 记录在覆盖它。此时应该用 ALTER SYSTEM RESET synchronous_commit; 清掉自动配置,而不是继续改 postgresql.conf。

同步复制对 RPO 的意义 ​

pg_dump、基础备份、复制槽、同步复制这些机制经常被混为一谈,但在 RPO 这个维度上的贡献完全不同。理解它们的定位,才能明白为什么「同步复制不是可选项而是核心指标的决定因素」。

机制解决什么问题对 RPO 的贡献
pg_dump逻辑层面的数据导出只能恢复到导出时刻,RPO = 备份间隔
基础备份提供恢复起点只能恢复到备份时刻,RPO = 备份间隔
异步流复制主库故障时快速切换可能丢「尚未传输」的 WAL,RPO > 0 且不可控
同步复制提交时确认备库已到位把 RPO 压到 0(对「已提交」事务而言)
复制槽防止 WAL 被提前回收本身不改变 RPO,但保证备库追得上
延迟备库保留一个过去的副本RPO 反而变大,用于防逻辑误操作

结论很清晰:要把 RPO 做到 0,唯一的手段是同步复制。其他机制要么 RPO 不可控(异步流复制的丢失量取决于网络与负载),要么 RPO 等于备份间隔。而 off 档位则主动放弃 RPO = 0 的保证,换取提交性能。

在向业务方解释时,一个有效的说法是:「同步复制不是让数据库更快或更慢的功能,而是给 RPO 定值的开关」。有了这个共识,后面的档位选择就变成了纯粹的量化问题。

常见误解澄清 ​

关于 synchronous_commit,有几个反复出现的错误认知,逐个澄清有助于建立正确的心智模型。

误解事实
「设置 synchronous_commit 就开启了同步复制」必须有非空的 synchronous_standby_names 才算同步复制
「默认是 off,性能优先」默认是 on,即默认等本地落盘
「off 会造成数据库损坏」off 只会丢最近若干已提交事务,不会导致内部不一致
「remote_apply 只是比 on 慢一点点」它多等了一次备库重放,延迟可能成倍增加
「remote_write 可以防备库崩溃」备库掉电可能丢失仍在页缓存中的 WAL
「local 和 off 差不多」差别在于「已提交事务会不会丢」,local 不丢
「同步复制能防误删数据」逻辑损坏会一起传播到备库,防不了

实战示例 ​

示例一:确认当前实例是否真的在同步复制 ​

sql
SELECT name, setting
FROM pg_settings
WHERE name IN ('synchronous_commit', 'synchronous_standby_names');
text
         name              |  setting
---------------------------+------------
 synchronous_commit        | on
 synchronous_standby_names |
(2 rows)

上面的输出说明:synchronous_standby_names 为空,所以尽管 synchronous_commit = on(默认值),该实例实际上没有在同步复制,事务提交只等本地落盘。这是一个非常典型的「看起来配了、其实没配」的状态。

示例二:查看备库的同步状态 ​

sql
SELECT application_name, client_addr, state, sync_state, sync_priority,
       write_lsn, flush_lsn, replay_lsn
FROM pg_stat_replication;
text
 application_name | client_addr |   state   | sync_state | sync_priority | write_lsn | flush_lsn | replay_lsn
------------------+-------------+-----------+------------+---------------+-----------+-----------+------------
 standby1         | 10.0.0.11   | streaming | sync       |             1 | 0/3000120 | 0/3000120 | 0/3000120
 standby2         | 10.0.0.12   | streaming | potential  |             2 | 0/3000120 | 0/3000120 | 0/3000120
 analytics        | 10.0.0.13   | streaming | async      |             0 | 0/2F00A10 | 0/2F00A10 | 0/2F00A10
(3 rows)

sync_state = sync 的备库才是当前真正承担同步确认角色的那一个;potential 是在 FIRST 模式下排在后面、随时可以补位的备库;async 是完全不参与同步确认的备库。这张表同时给出了判断「同步复制是否健康」的三个坐标:sync 是否存在、flush_lsn 是否跟上、replay_lsn 是否落后。

示例三:批量导入时使用事务级异步提交 ​

sql
\timing on
BEGIN;
SET LOCAL synchronous_commit = off;
-- 假设 big_events 是一张日志表,允许在崩溃时丢失最近若干行
COPY big_events FROM '/data/events_202609.csv' WITH (FORMAT csv, HEADER true);
COMMIT;
text
COPY 4281937
Time: 18422.331 ms (00:18.422)

对比默认 on 且配置同步备库的情况,同一批数据的导入时间可能从十几秒上升到数十秒甚至更久(取决于网络往返与备库 fsync 延迟)。SET LOCAL 的作用域是当前事务,提交后自动失效,不会影响同一连接后续的 OLTP 事务——这正是它比 SET(会话级)更安全的地方。

示例四:观察同步复制带来的提交延迟 ​

sql
-- 观察 10 次提交的耗时(同步复制开启、synchronous_commit = on)
\timing on
INSERT INTO t_sync_probe(id, ts) VALUES (1, now());
text
INSERT 0 1
Time: 3.412 ms

若把同一操作放到跨机房、RTT 约 30ms 的环境,这个数字会明显上升到 30ms 以上,因为每次提交都要等一次完整的网络往返。同一实例上若把档位改成 remote_apply,还可能再叠加几百微秒到几毫秒的重放等待。

示例五:定位被同步复制卡住的会话 ​

sql
-- 统计当前有多少会话卡在同步复制等待上
SELECT wait_event, count(*) AS sessions,
       max(now() - query_start) AS longest
FROM pg_stat_activity
WHERE wait_event = 'SyncRep'
GROUP BY wait_event;
text
 wait_event | sessions |   longest
------------+----------+--------------
 SyncRep    |       37 | 00:00:12.884
(1 row)

37 个会话同时卡在 SyncRep、最长已等近 13 秒,这是一个明确的故障信号:同步备库没有及时确认。此时应立刻去备库侧检查网络、I/O 与重放进度,而不是继续在应用侧重试。

示例六:对比同一实例在不同档位下的提交延迟 ​

先记录同步档位下的基线:

sql
BEGIN;
SET LOCAL synchronous_commit = on;
INSERT INTO t_sync_probe(id, ts) SELECT g, now() FROM generate_series(1, 1000) g;
COMMIT;
text
INSERT 0 1000
Time: 1284.775 ms (00:01.285)

再在同一事务结构下把档位降为 off:

sql
BEGIN;
SET LOCAL synchronous_commit = off;
INSERT INTO t_sync_probe(id, ts) SELECT g, now() FROM generate_series(1001, 2000) g;
COMMIT;
text
INSERT 0 1000
Time: 96.412 ms

两次的差距(约 13 倍)主要来自「1000 次提交等待」与「近乎零等待」的区别。注意这里插入的是 1000 行但只有一次 COMMIT,如果换成 1000 条独立小事务,差距会进一步拉大,因为等待次数也变成 1000 次。

示例七:清理被 ALTER SYSTEM 覆盖的配置 ​

sql
-- 查看自动配置里是否残留了 synchronous_commit
SELECT sourcefile, sourceline, name, setting
FROM pg_settings
WHERE name = 'synchronous_commit';
text
        sourcefile        | sourceline |       name         | setting
--------------------------+------------+--------------------+---------
 /var/lib/pgsql/18/data/postgresql.auto.conf |         12 | synchronous_commit | off
(1 row)

sourcefile 指向 postgresql.auto.conf,说明这个值来自某次 ALTER SYSTEM。要恢复默认,应该执行:

sql
ALTER SYSTEM RESET synchronous_commit;
SELECT pg_reload_conf();

而不是去改 postgresql.conf——后者会被自动配置覆盖,改了也不生效。这是排查「配置不生效」时最容易被忽略的一环。

注意事项 ​

把 synchronous_commit 与 synchronous_standby_names 当成一对参数来看。 只改前者是一个典型的无效操作。任何关于「同步复制是否生效」的判断,都必须先看 synchronous_standby_names 与 pg_stat_replication.sync_state。

off 不等于「数据会不一致」。 它的后果是「崩溃时可能丢失最近若干已提交事务」,数据库本身仍是自洽的。把 off 说成「会损坏数据」是不准确的,会误导决策。

remote_apply 会显著放大提交延迟。 因为等待链里多了「备库重放」这一步,而重放进度可能被备库上的长查询、max_standby_streaming_delay、备库 I/O 能力影响。上线前必须在真实硬件与网络条件下压测。

remote_write 不防备库自己崩溃。 如果备库的 OS 崩溃掉电,仍在页缓存中的 WAL 会丢失;此时即使主库正常,也可能丢数据。对「备库也不许丢」的场景,必须用 on 及以上。

不要在连接池层面隐式设置这个参数。 某些连接池支持通过连接参数注入 options=-c synchronous_commit=off,一旦某个应用这样配置,整个实例的同步复制保证就在该应用的连接上失效了,而且排查时很难发现。

synchronous_commit 可以在事务里改,但要放在写操作之前。 SET LOCAL 的作用域是当前事务,如果在写操作之后才设置,本事务已经发生的提交语义不会追溯改变。

注意 commit_delay 的单位是微秒。 设成 1000 表示 1 毫秒而不是 1 秒,容易写错数量级;生产环境建议先保持默认 0。

同步复制不能防逻辑损坏。 误 DELETE、程序写错数据这类逻辑问题会被同步复制一起传播到备库,同步复制解决的是「物理故障下的数据丢失」,不是「逻辑错误下的数据回退」。后者要靠 PITR 或延迟备库。

synchronous_commit 是 user 级参数,既有会话不会自动更新。 用 ALTER SYSTEM 改完后,老连接仍然沿用旧值,需要重连或显式 SET 才能生效,这一点在「改了参数但业务侧没变化」时经常被忽略。

最佳实践 ​

  1. 先明确 RPO 目标,再选档位。 「能不能接受丢 1 秒数据」这个问题的答案直接决定用 off、local 还是同步档位,不要先选参数再想业务影响。

  2. 默认保持 on,只在有明确理由时降级。 在单机或异步复制场景下 on 只花本地 fsync 的代价,是安全与性能的合理平衡点。

  3. 同步复制必须同时配置 synchronous_standby_names。 把这两个参数写进同一段配置注释里,避免后人只改一半。

  4. 对「允许丢一点」的批量任务,用会话级或事务级 off,而不是改全局。 SET LOCAL synchronous_commit = off 的爆炸半径最小,事务结束自动恢复。

  5. remote_apply 只用在真正需要「写后立即可读」的场景。 大多数读写分离业务用 on 已经足够;只有「跨实例强一致读」的需求才值得付 remote_apply 的延迟代价。

  6. 监控 wait_event = 'SyncRep' 的会话数与等待时长。 这是最早能发现「同步备库变慢或掉线」的信号,比业务侧超时告警更早。

  7. 跨机房同步复制预留足够的提交延迟预算。 在架构评审阶段就把「RTT × 提交频率」算进去,避免上线后才发现 QPS 上不去。必要时把高频写入改造成批量提交,减少等待次数。

  8. 关注 replay_lsn 与 flush_lsn 的差距。 使用 remote_apply 时,这个差值就是额外等待的主要来源;持续拉大说明备库重放跟不上。

  9. 备库的存储性能要按「会影响主库提交延迟」的标准来选。 尤其在使用 on / remote_apply 时,备库不能被当成「随便配一台就行」的冷备。

  10. 不要为了「看起来更安全」而无脑上 remote_apply。 过高的档位会带来吞吐下降、级联故障(备库慢拖死主库),反而降低整体可用性。

  11. 把参数变更纳入变更管理并留痕。 记录「谁在何时把 synchronous_commit 从什么改成了什么、为什么」,因为这类参数直接影响数据安全等级。

  12. 在连接池与应用连接串里做一次全量排查。 确认没有任何一处隐式覆盖了 synchronous_commit,避免出现「配置是同步、实际是异步」的假象。

下一节

继续学习:synchronous_standby_names

参考链接 ​