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 在提交后并不是「一步到位」地出现在备库上的,它要经过一条链路。理解这条链路的每一段,才能理解五个档位到底在等什么:
主库内存 → 主库 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_names | synchronous_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):
# 档位选择(默认 on)
synchronous_commit = on
# 必须在同一实例上指定同步备库,否则上面这行不产生同步复制效果
synchronous_standby_names = 'standby1'
# 提交延迟与合并相关(多数场景保持默认即可)
commit_delay = 0
commit_siblings = 5用 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'); 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)在会话或事务内临时切换档位:
-- 仅当前事务异步提交(批量导入常用)
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 决定是否重启):
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_delay | 0 | 微秒 | 提交前主动延迟,等待合并其他事务的 fsync |
commit_siblings | 5 | — | 活跃事务数达到该值才启用 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:
SELECT pid, usename, state, wait_event_type, wait_event, query
FROM pg_stat_activity
WHERE wait_event = 'SyncRep'; 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 特别慢」的第一手依据。
参数改了却没生效,是运维现场最常见的疑问。排查顺序建议如下:
- 确认
synchronous_standby_names非空。空值时改synchronous_commit毫无效果。 - 确认
pg_reload_conf()已执行,且pg_settings.pending_restart为f。 - 确认备库真的处于
sync状态。在pg_stat_replication.sync_state里看到sync才算真正在同步。 - 确认没有会话级覆盖。应用连接池可能在某处执行过
SET synchronous_commit,或者连接参数里带了options=-c synchronous_commit=off。 - 确认改的是正确的实例与配置文件。多实例机器上改错配置文件很常见。
同步提交的等待机制
理解「后端在哪里等」有助于看懂等待事件。当一条事务被标记为「需要同步复制确认」时,它不会去轮询备库状态,而是把自己挂到一个内部等待队列上,然后休眠。备库的 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 不是一个「危险参数」,而是一个「需要明确业务前提才能用的参数」。可以用下面这组判定条件来判断是否可用:
- 数据是否可重放/可补采?例如埋点日志可以从客户端缓冲重发。
- 丢失是否只影响「最近几百毫秒」?
off的丢失窗口大约 3 ×wal_writer_delay。 - 丢失是否会引发「上游无法对账」?如果会,就不能用。
- 是否有其他机制兜底?例如数据同时写到了 Kafka。
四条全部满足时,off 是一个性价比很高的选择;任何一条不满足,就应该往上走一个档位。
一个档位决策的流程
把上面的判断串起来,可以得到一个简单的决策流程:
能否接受崩溃丢最近若干已提交事务?
├─ 能 → 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 不丢 |
| 「同步复制能防误删数据」 | 逻辑损坏会一起传播到备库,防不了 |
实战示例
示例一:确认当前实例是否真的在同步复制
SELECT name, setting
FROM pg_settings
WHERE name IN ('synchronous_commit', 'synchronous_standby_names'); name | setting
---------------------------+------------
synchronous_commit | on
synchronous_standby_names |
(2 rows)上面的输出说明:synchronous_standby_names 为空,所以尽管 synchronous_commit = on(默认值),该实例实际上没有在同步复制,事务提交只等本地落盘。这是一个非常典型的「看起来配了、其实没配」的状态。
示例二:查看备库的同步状态
SELECT application_name, client_addr, state, sync_state, sync_priority,
write_lsn, flush_lsn, replay_lsn
FROM pg_stat_replication; 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 是否落后。
示例三:批量导入时使用事务级异步提交
\timing on
BEGIN;
SET LOCAL synchronous_commit = off;
-- 假设 big_events 是一张日志表,允许在崩溃时丢失最近若干行
COPY big_events FROM '/data/events_202609.csv' WITH (FORMAT csv, HEADER true);
COMMIT;COPY 4281937
Time: 18422.331 ms (00:18.422)对比默认 on 且配置同步备库的情况,同一批数据的导入时间可能从十几秒上升到数十秒甚至更久(取决于网络往返与备库 fsync 延迟)。SET LOCAL 的作用域是当前事务,提交后自动失效,不会影响同一连接后续的 OLTP 事务——这正是它比 SET(会话级)更安全的地方。
示例四:观察同步复制带来的提交延迟
-- 观察 10 次提交的耗时(同步复制开启、synchronous_commit = on)
\timing on
INSERT INTO t_sync_probe(id, ts) VALUES (1, now());INSERT 0 1
Time: 3.412 ms若把同一操作放到跨机房、RTT 约 30ms 的环境,这个数字会明显上升到 30ms 以上,因为每次提交都要等一次完整的网络往返。同一实例上若把档位改成 remote_apply,还可能再叠加几百微秒到几毫秒的重放等待。
示例五:定位被同步复制卡住的会话
-- 统计当前有多少会话卡在同步复制等待上
SELECT wait_event, count(*) AS sessions,
max(now() - query_start) AS longest
FROM pg_stat_activity
WHERE wait_event = 'SyncRep'
GROUP BY wait_event; wait_event | sessions | longest
------------+----------+--------------
SyncRep | 37 | 00:00:12.884
(1 row)37 个会话同时卡在 SyncRep、最长已等近 13 秒,这是一个明确的故障信号:同步备库没有及时确认。此时应立刻去备库侧检查网络、I/O 与重放进度,而不是继续在应用侧重试。
示例六:对比同一实例在不同档位下的提交延迟
先记录同步档位下的基线:
BEGIN;
SET LOCAL synchronous_commit = on;
INSERT INTO t_sync_probe(id, ts) SELECT g, now() FROM generate_series(1, 1000) g;
COMMIT;INSERT 0 1000
Time: 1284.775 ms (00:01.285)再在同一事务结构下把档位降为 off:
BEGIN;
SET LOCAL synchronous_commit = off;
INSERT INTO t_sync_probe(id, ts) SELECT g, now() FROM generate_series(1001, 2000) g;
COMMIT;INSERT 0 1000
Time: 96.412 ms两次的差距(约 13 倍)主要来自「1000 次提交等待」与「近乎零等待」的区别。注意这里插入的是 1000 行但只有一次 COMMIT,如果换成 1000 条独立小事务,差距会进一步拉大,因为等待次数也变成 1000 次。
示例七:清理被 ALTER SYSTEM 覆盖的配置
-- 查看自动配置里是否残留了 synchronous_commit
SELECT sourcefile, sourceline, name, setting
FROM pg_settings
WHERE name = 'synchronous_commit'; sourcefile | sourceline | name | setting
--------------------------+------------+--------------------+---------
/var/lib/pgsql/18/data/postgresql.auto.conf | 12 | synchronous_commit | off
(1 row)sourcefile 指向 postgresql.auto.conf,说明这个值来自某次 ALTER SYSTEM。要恢复默认,应该执行:
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 才能生效,这一点在「改了参数但业务侧没变化」时经常被忽略。
最佳实践
先明确 RPO 目标,再选档位。 「能不能接受丢 1 秒数据」这个问题的答案直接决定用
off、local还是同步档位,不要先选参数再想业务影响。默认保持
on,只在有明确理由时降级。 在单机或异步复制场景下on只花本地 fsync 的代价,是安全与性能的合理平衡点。同步复制必须同时配置
synchronous_standby_names。 把这两个参数写进同一段配置注释里,避免后人只改一半。对「允许丢一点」的批量任务,用会话级或事务级
off,而不是改全局。SET LOCAL synchronous_commit = off的爆炸半径最小,事务结束自动恢复。remote_apply只用在真正需要「写后立即可读」的场景。 大多数读写分离业务用on已经足够;只有「跨实例强一致读」的需求才值得付remote_apply的延迟代价。监控
wait_event = 'SyncRep'的会话数与等待时长。 这是最早能发现「同步备库变慢或掉线」的信号,比业务侧超时告警更早。跨机房同步复制预留足够的提交延迟预算。 在架构评审阶段就把「RTT × 提交频率」算进去,避免上线后才发现 QPS 上不去。必要时把高频写入改造成批量提交,减少等待次数。
关注
replay_lsn与flush_lsn的差距。 使用remote_apply时,这个差值就是额外等待的主要来源;持续拉大说明备库重放跟不上。备库的存储性能要按「会影响主库提交延迟」的标准来选。 尤其在使用
on/remote_apply时,备库不能被当成「随便配一台就行」的冷备。不要为了「看起来更安全」而无脑上
remote_apply。 过高的档位会带来吞吐下降、级联故障(备库慢拖死主库),反而降低整体可用性。把参数变更纳入变更管理并留痕。 记录「谁在何时把
synchronous_commit从什么改成了什么、为什么」,因为这类参数直接影响数据安全等级。在连接池与应用连接串里做一次全量排查。 确认没有任何一处隐式覆盖了
synchronous_commit,避免出现「配置是同步、实际是异步」的假象。
下一节
参考链接
- PostgreSQL 官方文档:Write Ahead Log — Configuration Parameters
- PostgreSQL 官方文档:Replication — Configuration Parameters
- PostgreSQL 官方文档:Monitoring Database Activity
- PostgreSQL 官方文档:High Availability, Load Balancing, and Replication
- PostgreSQL 官方文档:Log-Shipping Standby Servers
- PostgreSQL 官方文档:System Administration Functions
- PostgreSQL 官方文档:SET — Set Run-time Parameters
- PostgreSQL 官方文档:pgbench
- PostgreSQL 官方文档:Disk Full / Write Ahead Log — wal_writer_delay
- PostgreSQL 官方文档:Transaction Isolation / Commit Semantics