Skip to content

WAL 写入流程(Write Flow) ​

前两节讲了 WAL 是什么、LSN 怎么表示位置。本节回答一个更工程化的问题:一条 WAL 记录从产生到落盘,中间经过了哪些环节、拿了哪些锁、谁在什么时候 fsync。搞清楚这条路径,才能理解为什么 synchronous_commit 能调、commit_delay 有什么用、什么情况下会出现「WAL 写入瓶颈」。

这是 WAL 全部内容中最贴近性能的一节。理解了写入流程,绝大部分 WAL 相关的性能问题都能自己推导出答案。

前置知识

阅读本节前,建议先了解:LSN(日志序列号)

基础概念 ​

WAL 写入路径上有若干角色和同步原语,先统一术语:

术语英文说明
WAL 记录生成Record Generation后端进程在修改数据页时同步生成对应的 WAL 记录
WAL 缓冲区WAL Buffer共享内存环形缓冲区,暂存尚未写盘的 WAL 记录
插入锁WALInsertLock多把(默认 8 把)分段锁,控制并发向 WAL 缓冲区追加
写锁WALWriteLock单把互斥锁,串行化「写文件 + fsync」动作
刷盘Flush把 WAL 从内核页缓存 fsync 到持久存储
组提交Group Commit多个并发事务合并成一次 fsync
提交等待Commit Wait提交时等待自己的 LSN 被刷盘完成
WAL 写入进程WAL Writer后台进程,周期性把 WAL 缓冲区刷盘,避免后端自己等
同步提交Synchronous Commitsynchronous_commit 控制提交等待到哪个阶段
异步提交Asynchronous Commitsynchronous_commit = off,提交不等 fsync

写入路径上的四个参与者 ​

text
┌────────────────┐   ①追加记录    ┌──────────────┐
│  Backend 进程   │──────────────►│ WAL Buffer   │
│ (执行 DML)    │               │ (共享内存)  │
└───────┬────────┘               └──────┬───────┘
        │                               │
        │ ④ COMMIT 时等待               │ ②刷盘
        │   自己的 LSN 被刷完            ▼
        │                        ┌──────────────┐
        │                        │   pg_wal     │
        │                        │ (磁盘)      │
        │                        └──────────────┘
        │                               ▲
        │                               │ ③周期性刷盘
        │                        ┌──────┴───────┐
        └──── 也可能自己触发刷盘 ──│ WAL Writer   │
                                │ (后台进程)  │
                                └──────────────┘

四个参与者各司其职:

参与者职责是否阻塞用户事务
Backend 进程生成记录、追加到缓冲区、提交时触发并等待刷盘提交时会等待
WAL Buffer缓冲写入,把大量小写合并成顺序 I/O否
WAL Writer周期性刷盘,减少后端自己 fsync 的次数否
pg_wal 文件持久化落点否

核心机制 ​

第一步:生成 WAL 记录 ​

WAL 记录不是事后补记的,而是在修改数据页的同一时刻由同一个后端进程生成:

text
UPDATE users SET name = 'bob' WHERE id = 1;
   │
   ├─ 定位到目标堆页(shared_buffers 中)
   │
   ├─ 准备新元组版本(含新的 xmin、ctid 等)
   │
   ├─ 【关键】调用 XLogInsert() 为这次页修改生成 WAL 记录
   │      ├─ 判断该页是否本检查点后首次修改
   │      │     ├─ 是 → 附带 FPI 全页镜像(记录变长很多)
   │      │     └─ 否 → 只记录增量修改
   │      │
   │      └─ 记录内容:rmgr=Heap / desc=UPDATE / blkref / 元组数据
   │
   ├─ WAL 记录追加到 WAL Buffer(拿 WALInsertLock)
   │
   └─ 数据页在内存中变脏(此刻不写盘)

一个必须理解的设计:WAL 记录与数据页修改是同一个后端进程在同一段代码里做的。这保证了「不会出现改了页却没记日志」的情况——两者在代码层面就是绑定的,不存在遗漏的可能。

第二步:插入 WAL 缓冲区(分段锁) ​

WAL 缓冲区是高并发写入的热点,PostgreSQL 用多把分段锁来降低竞争:

text
WAL Buffer(环形,默认约 shared_buffers/32,上限 16MB)
┌────────────────────────────────────────────────────────────────┐
│ 段0  │ 段1  │ 段2  │ 段3  │ 段4  │ 段5  │ 段6  │ 段7  │         │
│🔒L0  │🔒L1  │🔒L2  │🔒L3  │🔒L4  │🔒L5  │🔒L6  │🔒L7  │         │
└──▲───┴──▲───┴──────┴──────┴──────┴──────┴──────┴──────┴─────────┘
   │      │
 事务A   事务B     ← 两个并发事务落在不同段,可以并行插入
  • 默认有 8 把插入锁(NUM_XLOGINSERT_LOCKS,编译期常量),每把保护缓冲区的一段。
  • 插入时只拿自己那段的锁,因此只要并发事务的 LSN 落在不同段,就能真正并行。
  • 这一点是 PostgreSQL 在 WAL 写入上能撑高并发的关键:若只有一把全局锁,所有写入会串行化。

WAL 缓冲区的环形回收

WAL 缓冲区是环形的:写到末尾后会绕回开头。绕回之前必须确认「开头那段的内容已经刷盘」,否则会覆盖未持久化的数据。这个检查由 WALWriteLock 与刷盘位点共同保证,是插入路径上唯一可能被迫等待刷盘的点(wal_buffers_full 计数增加就发生在这里)。

第三步:刷盘(WALWriteLock 串行化) ​

把 WAL 从缓冲区写到磁盘需要拿 WALWriteLock(单把互斥锁),因此同一时刻只有一个进程在写 WAL 文件:

text
进程要刷盘
   │
   ├─ 拿 WALWriteLock(拿不到就等,说明别人正在刷)
   │
   ├─ 计算要刷的范围:从上次刷盘位点 到 本次请求的 LSN
   │
   ├─ write() 到 pg_wal 段文件(顺序写)
   │
   ├─ fsync() 强制落盘
   │
   ├─ 更新 flush_lsn(此后所有 ≤ flush_lsn 的提交都算持久化)
   │
   └─ 释放 WALWriteLock,唤醒所有在等这个 LSN 的进程

关键点:一次刷盘可以同时满足多个进程的等待。假设事务 A 要刷到 LSN=100、事务 B 要刷到 LSN=250,若 A 先拿到锁,它会把范围刷到 100 就返回——但 B 随后会再刷一次到 250。反之若 B 先拿到锁刷到 250,则 A 的 100 也被顺带满足,A 无需再刷。这就是组提交的自然形式。

第四步:提交等待与 synchronous_commit ​

事务提交时,后端会调用 XLogFlush(commit_lsn) 并等待自己的 LSN 落盘。等待到哪个阶段由 synchronous_commit 决定:

取值等待到掉电可能丢失典型用途
off不等(数据留在 WAL 缓冲区/内核缓存)最近几百毫秒内已提交事务批量导入、日志类写入
local本地 fsync 完成不丢(本机)单实例 + 可接受备库延迟
on(默认)本地 fsync + 同步备库收到并写入不丢(本机 + 备库)绝大多数场景
remote_write本地 fsync + 同步备库 write() 完成不丢(本机);备库可丢备库 fsync 慢时的折中
remote_apply本地 fsync + 同步备库已重放不丢,且备库可读最新需要读一致性备库
text
synchronous_commit = on
   提交 ──► 本地 fsync ──► 等同步备库 flush ──► 返回成功
   延迟 = 本地 fsync + 网络往返 + 备库 fsync

synchronous_commit = remote_write
   提交 ──► 本地 fsync ──► 等同步备库 write() ──► 返回成功
   延迟 = 本地 fsync + 网络往返
   (省掉了备库 fsync 的等待,但备库掉电会丢这些事务)

remote_write 与 remote_apply 的取舍

remote_write 省掉了「等备库 fsync」的时间,在网络 RTT 主导延迟的场景下收益明显;代价是备库操作系统崩溃时会丢事务(但备库进程崩溃不会丢)。remote_apply 反过来,要等备库重放完成,延迟最大,但保证了「同步备库上能立刻读到刚提交的数据」,适合读一致性要求高的场景。绝大多数生产环境用默认的 on 即可。

组提交:commit_delay 与 commit_siblings ​

当并发事务很多时,一次 fsync 可以同时满足多个事务。commit_delay 让「第一个到达的提交者」多等一小会儿,攒更多事务一起刷:

text
commit_delay = 0(默认)
  t0  事务A 提交 ──► fsync ──► 返回       (1 次 fsync, 1 个事务)
  t1  事务B 提交 ──► fsync ──► 返回       (1 次 fsync, 1 个事务)
  t2  事务C 提交 ──► fsync ──► 返回       (1 次 fsync, 1 个事务)
  总计:3 次 fsync

commit_delay = 100us, commit_siblings = 5
  t0  事务A 提交 ─┐
  t0+  事务B 提交 ─┤
  t0+  事务C 提交 ─┼─► 攒够活跃事务 ──► fsync 一次 ──► 三个都返回
  t0+  事务D 提交 ─┤                     (1 次 fsync, 4 个事务)
  t0+  事务E 提交 ─┘
  总计:1 次 fsync

两个参数的配合逻辑:

  • commit_siblings 是门槛:只有当同时活跃的事务数 ≥ 该值,才考虑等待。默认 5,避免低并发时白白增加延迟。
  • commit_delay 是等待时长(微秒):等满这个时间或攒够一批就刷。

一个常见的误解是「commit_delay 会降低吞吐」。事实相反:它牺牲极小的单事务延迟,换取 fsync 次数的下降,在高并发小事务场景下吞吐通常提升。但在低并发场景下它纯属浪费——所以它默认是 0(关闭)。

WAL Writer 的角色 ​

WAL Writer 是后台辅助进程,周期性把 WAL 缓冲区刷盘,作用是把刷盘工作从提交路径上「抢跑」出去:

text
没有 WAL Writer(假设)
  事务A COMMIT ──► 攒了 100KB WAL ──► 后端自己 fsync(等 ~1ms)
  事务B COMMIT ──► 攒了 100KB WAL ──► 后端自己 fsync(再等 ~1ms)

有 WAL Writer
  WAL Writer 每 200ms 主动刷一次 ──► flush_lsn 已经推进了
  事务A COMMIT ──► 发现自己的 LSN ≤ flush_lsn ──► 无需等待,立即返回 ✔

判断逻辑是:提交时先比较自己的 LSN 与 flush_lsn,若已被刷过就直接返回。所以 WAL Writer 刷得越及时,提交时越可能「免费通过」。

ini
# postgresql.conf
# WAL Writer 的刷盘节奏:间隔越短,提交时的"免等"概率越高
wal_writer_delay = 200ms

# 累积到这个量就强制 flush,避免 WAL 长时间滞留在内核页缓存
# 注意:0 表示关闭该行为,可能导致数据滞留
wal_writer_flush_after = 1MB

wal_writer_delay 与 commit 延迟的关系

wal_writer_delay 决定的是「最坏情况下,一个事务要等多久才会被 WAL Writer 顺带刷盘」。但注意:事务提交时不会等 WAL Writer,它会自己触发刷盘。WAL Writer 的价值在于「提前把 WAL 刷掉」,让后续提交免等。因此它的延迟并不直接等于提交延迟,只是影响「免等命中率」。

一条 UPDATE 的完整时序 ​

把上述所有环节串起来,看两个并发事务的实际交错:

text
时刻   事务 A(UPDATE + COMMIT)              事务 B(UPDATE + COMMIT)
────────────────────────────────────────────────────────────────────────
t1    生成 WAL 记录(含 FPI)                    
t2    拿 WALInsertLock#3,追加到缓冲区            生成 WAL 记录
      insert_lsn = 0/A000100                    
t3                                             拿 WALInsertLock#5,追加缓冲区
                                               insert_lsn = 0/A000280
t4    COMMIT → 请求 XLogFlush(0/A000100)
t5    拿 WALWriteLock
t6    write(0/A000000..0/A000100) + fsync
      flush_lsn = 0/A000100
t7    释放 WALWriteLock,A 返回成功 ✔
t8                                              COMMIT → 请求 flush(0/A000280)
t9    发现 flush_lsn = 0/A000100 < 0/A000280
                                               需要再刷 → 拿 WALWriteLock
t10                                             write + fsync 到 0/A000280
                                               B 返回成功 ✔

总 fsync 次数:2(若 B 在 t5 前提交且被合并,则可能只需 1 次)

关键观察:WALInsertLock 是分段并行的(t2/t3 并发),WALWriteLock 是全局串行的(t5/t9 互斥)。所以 WAL 写入的吞吐上限往往由「fsync 次数 × 单次 fsync 耗时」决定,而不是由插入锁决定。

相关参数 ​

参数默认值生效方式说明
synchronous_commitonreload + 会话提交等待阶段:off/local/on/remote_write/remote_apply
wal_buffers-1(自动)重启缓冲区大小,自动约 shared_buffers/32,上限 16MB
wal_writer_delay200msreloadWAL Writer 刷盘间隔
wal_writer_flush_after1MBreload累积该量强制 flush(IO 方向提示,0 关闭)
commit_delay0reload组提交等待时长(微秒)
commit_siblings5reload触发 commit_delay 的活跃事务数门槛
synchronous_standby_names空reload哪些备库算「同步备库」,决定 on 等谁
fsyncon重启是否真正 fsync;关掉等于放弃持久性
ini
# postgresql.conf
# —— 高频小事务、机械盘/网络存储(fsync 慢):组提交收益最大 ——
commit_delay = 500us                 # 实测调优,不要照搬;先测 fsync 平均耗时
commit_siblings = 5

# —— 让 WAL Writer 更积极,提高提交"免等"命中率 ——
wal_writer_delay = 200ms
wal_writer_flush_after = 1MB

# —— 缓冲区:写入压力大且并发高时可显式放大(需重启) ——
wal_buffers = 16MB

# —— 有同步备库时,按延迟敏感度选择等待级别 ——
synchronous_commit = on              # 默认;跨机房可用 remote_write 降低延迟

详细说明 ​

观察提交延迟的构成 ​

提交延迟 = fsync 等待 + 可能的锁等待。可以用 pg_test_fsync 先量化存储的 fsync 性能:

bash
# 在数据库主机上测量单次 fsync 的真实耗时(需停止写入以获得准确值)
pg_test_fsync -s 5 -f /var/lib/pgsql/18/data
text
5 seconds per test
O_DIRECT supported on this platform for open_datasync and open_sync.

Compare file sync methods using one 8kB write:
(in wal_sync_method preference order, except fdatasync is Linux's default)
        open_datasync                    1843.512 ops/sec    543 usecs/op
        fdatasync                        1856.221 ops/sec    539 usecs/op
        fsync                            1801.443 ops/sec    555 usecs/op
        fsync_writethrough                      n/a
        open_sync                        1815.902 ops/sec    551 usecs/op

Compare file sync methods using two 8kB writes:
        ...

结果解读:

  • fdatasync 约 539 微秒/次,这就是「一次提交至少要等 0.54ms」的物理下限。存储的 fsync 越慢,组提交的价值越大。
  • 若这里显示几毫秒甚至几十毫秒(常见于云盘、机械盘、虚拟化环境),则单事务提交延迟一定很高,只能靠组提交或降级 synchronous_commit 来提升吞吐。
  • open_datasync 与 fdatasync 相近,说明该存储不支持写缓存优化。若某方法明显更快,可以把 wal_sync_method 显式设成它。

用视图观测 WAL 写入压力 ​

PG14+ 的 pg_stat_wal 直接给出写入链路的健康指标:

sql
-- WAL 写入链路的四个关键指标
SELECT wal_records,
       wal_fpi,
       pg_size_pretty(wal_bytes) AS wal_bytes,
       wal_buffers_full,
       stats_reset
FROM pg_stat_wal;
text
 wal_records | wal_fpi | wal_bytes | wal_buffers_full |          stats_reset
-------------+---------+-----------+------------------+-------------------------------
     1284300 |    5218 | 2148 MB   |              184 | 2026-09-20 09:00:00.124511+08
指标含义异常时的处置
wal_records记录条数突增说明写入模式变碎(如逐行更新)
wal_fpi全页镜像次数占 WAL 大头;用 wal_compression 压缩
wal_bytes生成的 WAL 总量换算成速率做容量规划
wal_buffers_full缓冲区不足被迫等待刷盘的次数持续增长 → 增大 wal_buffers(需重启)

wal_buffers_full 是唯一一个明确指向「写路径有瓶颈」的指标:它增加意味着插入 WAL 时发现了「缓冲区段尚未刷盘」而必须等待。

提交延迟的分型诊断 ​

不同原因导致的提交延迟,特征完全不同:

现象特征可能原因处置
提交延迟随并发升高P99 高,P50 正常fsync 竞争,组提交未生效调大 commit_delay,检查 commit_siblings
提交延迟恒定偏高P50 与 P99 接近,都高存储 fsync 本身慢换存储、wal_sync_method 优化,或降级 synchronous_commit
提交延迟尖峰偶发秒级延迟检查点刷盘与 WAL fsync 争抢 I/O调 checkpoint_completion_target,WAL 与数据分盘
提交延迟随备库变化主库延迟与备库状态相关同步备库 fsync 慢或网络抖动评估 synchronous_commit 级别、synchronous_standby_names
写入吞吐上不去但延迟不高wal_buffers_full 增长WAL 缓冲区偏小增大 wal_buffers(需重启)
sql
-- 区分"等锁"还是"等 fsync":看等待事件
SELECT wait_event_type, wait_event, count(*) AS sessions
FROM pg_stat_activity
WHERE state = 'active'
GROUP BY 1, 2
ORDER BY sessions DESC
LIMIT 10;
text
 wait_event_type |   wait_event    | sessions
-----------------+-----------------+----------
 IO              | WALWriteLock    |        4
 LWLock          | WALWriteLock    |        6
 Client          | ClientRead      |        3
  • 出现 WALWriteLock 等待说明多个进程在争「写 WAL 文件」这把串行锁,是 fsync 成为瓶颈的直接证据。
  • WALInsertLock 的 LWLock 等待则说明缓冲区插入竞争激烈(并发极高或 wal_buffers 偏小)。

实战示例 ​

示例 1:测量提交延迟与组提交效果 ​

用一个完整的对比实验量化 commit_delay 的价值:

sql
-- 准备:建一张简单表
DROP TABLE IF EXISTS commit_bench;
CREATE TABLE commit_bench (id bigserial PRIMARY KEY, note text);
bash
# 用 pgbench 生成一组高并发小事务的负载
# -c 32 个并发连接,-T 20 秒,每事务一次插入
pgbench -i -s 1 demo
pgbench -c 32 -j 4 -T 20 -f - demo <<'SQL'
INSERT INTO commit_bench (note) VALUES ('bench');
SQL
text
-- commit_delay = 0(默认),32 并发插入
transaction type: stdin
scaling factor: 1
query mode: simple
number of clients: 32
number of threads: 4
duration: 20 s
number of transactions actually processed: 186432
latency average = 3.428 ms
latency stddev = 2.114 ms
initial connection time = 62.148 ms
tps = 9284.712043 (without initial connection time)
ini
# postgresql.conf:开启组提交后再测一次
commit_delay = 500us
commit_siblings = 5
text
-- commit_delay = 500us,同样的负载
number of transactions actually processed: 264918
latency average = 2.414 ms
latency stddev = 1.687 ms
tps = 13201.443921 (without initial connection time)

结果解读:

  • TPS 从 9285 提升到 13201(约 +42%),同时平均延迟从 3.43ms 降到 2.41ms。这看似矛盾——加了等待反而更快?原因是:等待 500us 让更多事务合并成一次 fsync,每个事务平摊到的 fsync 时间大幅下降,净效果是延迟与吞吐双改善。
  • 收益的前提是高并发(32 个连接同时提交)。commit_siblings = 5 保证低并发时不会白白等待。
  • 这个实验也说明 commit_delay 必须实测:它与存储的 fsync 耗时长(本例约 0.5ms 量级)强相关。fsync 很快的 NVMe 上收益会小得多。
  • 别忘了这是个「有得有失」的调优:稳态延迟可能上升零点几毫秒。对延迟极敏感的场景应谨慎。

示例 2:验证 synchronous_commit 的取舍 ​

用一个受控序列观察不同 synchronous_commit 级别对提交延迟的影响,并理解其语义边界:

sql
-- 1) 记录基线:默认 synchronous_commit 与当前 flush 位点
SHOW synchronous_commit;

SELECT pg_current_wal_insert_lsn() AS insert_lsn,
       pg_current_wal_flush_lsn() AS flush_lsn,
       pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_insert_lsn(),
                                      pg_current_wal_flush_lsn())) AS unflushed;

-- 2) 在关闭同步提交的会话里写入,观察"生成"与"刷盘"的差距
SET synchronous_commit = off;

INSERT INTO commit_bench (note)
SELECT 'bulk ' || g FROM generate_series(1, 50000) AS g;

SELECT pg_current_wal_insert_lsn() AS insert_lsn,
       pg_current_wal_flush_lsn() AS flush_lsn,
       pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_insert_lsn(),
                                      pg_current_wal_flush_lsn())) AS unflushed_after_bulk;

-- 3) 恢复默认,再看一次(此时刷盘已被追平)
RESET synchronous_commit;

SELECT pg_current_wal_insert_lsn() AS insert_lsn,
       pg_current_wal_flush_lsn() AS flush_lsn,
       pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_insert_lsn(),
                                      pg_current_wal_flush_lsn())) AS unflushed_after_commit;
text
-- 步骤 1
 synchronous_commit
--------------------
 on

 insert_lsn | flush_lsn  | unflushed
------------+------------+-----------
 0/A000100  | 0/A0000C8  | 56 bytes

-- 步骤 2(插入 5 万行后,flush 落后很多)
 insert_lsn | flush_lsn  | unflushed_after_bulk
------------+------------+----------------------
 0/B8C4220  | 0/A0000C8  | 392 MB

-- 步骤 3(RESET 后执行一次提交,刷盘被追平)
 insert_lsn | flush_lsn  | unflushed_after_commit
------------+------------+------------------------
 0/B8C42A0  | 0/B8C4220  | 128 bytes

结果解读:

  • 步骤 1 中 unflushed = 56 bytes,说明默认状态下 WAL 几乎实时刷盘(WAL Writer 在不停追)。
  • 步骤 2 中 unflushed 暴涨到 392MB:synchronous_commit = off 下,插入产生的大量 WAL 只停留在缓冲区与内核缓存里,没有任何 fsync 把它们强制落盘。这正是「关闭同步提交」真实的行为——不是产生得更少,而是不强制刷盘。
  • 步骤 3 的 RESET 恢复了默认值,随后的一次提交触发 fsync,一句就把 392MB 全部刷平(flush_lsn 追上了 insert_lsn)。
  • 风险边界:若在步骤 2 的状态下断电,那 392MB 里所有对应的已提交事务全部丢失。数据本身不会损坏(WAL 后半段缺失,重启后从检查点重放到最后一个有效记录即可),但事务会丢。这就是「synchronous_commit = off 不损坏数据但会丢事务」的实证。

示例 3:观察 WAL Writer 的「抢跑」效果 ​

用一个序列验证 WAL Writer 能显著减少提交时的等待:

sql
-- 1) 连续提交 100 次单行事务,测量总耗时与 flush 追进情况
SELECT pg_current_wal_insert_lsn() AS insert_lsn,
       pg_current_wal_flush_lsn() AS flush_lsn;

DO $$
DECLARE
  i int;
  t0 timestamptz := clock_timestamp();
BEGIN
  FOR i IN 1..100 LOOP
    INSERT INTO commit_bench (note) VALUES ('tick ' || i);
    -- 每个 INSERT 自动提交,触发一次 XLogFlush
  END LOOP;
  RAISE NOTICE '100 次自动提交耗时: %',
    clock_timestamp() - t0;
END $$;

SELECT pg_current_wal_insert_lsn() AS insert_lsn,
       pg_current_wal_flush_lsn() AS flush_lsn,
       pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_insert_lsn(),
                                      pg_current_wal_flush_lsn())) AS unflushed;
text
-- 步骤 1
 insert_lsn | flush_lsn
------------+------------
 0/C0010A0  | 0/C0000E8

-- 步骤 2 输出
NOTICE:  100 次自动提交耗时: 00:00:00.192483

-- 步骤 3
 insert_lsn | flush_lsn  | unflushed
------------+------------+-----------
 0/C04B8C0  | 0/C04B860  | 96 bytes

结果解读:

  • 100 次单行自动提交耗时约 192ms,平均每次约 1.9ms。若每次提交都要各自等一次 fsync(按示例 1 中 0.54ms 估算),最坏情况将接近 54ms——实际并非如此。
  • 关键在 unflushed = 96 bytes:每次提交前,flush 位点几乎都已经被推进到自己附近。这说明 WAL Writer(每 200ms 刷一次)+ 前一次提交的刷盘,共同让后续提交频繁「免等」。
  • 若把 wal_writer_delay 改为 10s,这个实验的耗时通常会明显增加,因为「免等命中率」下降,更多提交需要自己触发完整 fsync 并等待。
  • 反过来,把 wal_writer_delay 调得过小(如 10ms)会让 WAL Writer 频繁 fsync,与提交路径争抢 WALWriteLock,在低写入量下反而可能增加延迟——这个参数不是越小越好。

注意事项 ​

  • synchronous_commit = off 不等于 fsync = off。前者只是不强制刷盘,掉电丢最近事务;后者跳过 fsync 调用,掉电可能导致数据目录损坏、无法恢复。绝不要为了性能把 fsync 关掉。
sql
-- 会话级降级:只影响当前事务/会话,事务结束自动失效
BEGIN;
SET LOCAL synchronous_commit = off;
-- 批量导入 ...
COMMIT;
ini
# postgresql.conf
# 全局降级:仅用于确认可接受少量事务丢失的场景(如日志采集库)
# 注意:这是 synchronous_commit,不是 fsync
synchronous_commit = off
fsync = on                            # 必须保持 on
  • commit_delay 在低并发下纯粹增加延迟。它由 commit_siblings 把关,但如果你的负载并发很低(如 < 5),设它没有意义,反而可能让延迟变差。
  • wal_buffers 是重启参数,自动值上限为 16MB。若 wal_buffers_full 持续增长,需要显式调大并重启。
  • commit_delay 的单位是微秒,不是毫秒。设成 500 是 0.5ms,设成 500000 是半秒——后者会造成灾难性延迟。
  • synchronous_commit 的生效范围可以细到事务级,用 SET LOCAL 只影响当前事务,这比全局调整安全得多。
sql
-- 只对一次批量导入降级,其他并发事务不受影响
SET LOCAL synchronous_commit = off;
  • 同步复制下主库提交延迟包含备库的 fsync 时间。若备库存储较慢,主库延迟会明显上升,而 pg_stat_replication 中的 flush_lsn 能反映这一点。
  • 不要用轮询 pg_current_wal_flush_lsn() 来「等 WAL 落盘」。这是观测手段,不是同步手段;真实等待由提交路径内部完成。
  • 热备查询不产生 WAL,但 SET 会话参数、临时表操作等在某些情况下仍可能触发表空间级别的 WAL 写入。
  • wal_writer_flush_after = 0 会关闭 flush 提示,可能导致 WAL 长时间停留在内核页缓存,在操作系统崩溃时丢失更多。

最佳实践 ​

  • 先用 pg_test_fsync 量化存储的 fsync 耗时,再决定是否启用 commit_delay。fsync 越快,组提交的收益越小。
  • 高并发小事务场景开启组提交,建议从 commit_delay = 200us 起测,逐步调整;commit_siblings 匹配典型并发度。
ini
# postgresql.conf:高并发 OLTP 的起点配置
commit_delay = 200us
commit_siblings = 5
wal_writer_delay = 200ms
wal_writer_flush_after = 1MB
  • 只在明确可接受丢失的场景降级 synchronous_commit,且优先用 SET LOCAL 限定到具体事务或会话,避免全局影响。
  • 把 wal_buffers_full 纳入监控,它是写路径瓶颈最直接的信号;持续增长才考虑调大 wal_buffers。
  • 为 WAL 与数据目录使用不同物理设备,让顺序写(WAL)与随机写(数据页、检查点)不互相争抢 I/O。
  • 监控 pg_stat_activity 中的 WALWriteLock 等待事件,这是 fsync 竞争的直接证据。
  • 有同步备库时用 remote_write 换取延迟收益,只在需要读一致性时用 remote_apply,并注意备库故障会阻塞主库提交。
  • wal_compression 优先开 lz4(PG15+),在写路径上省下的 I/O 是纯收益。
  • 不要用 commit_delay 掩盖存储性能问题。若 fsync 耗时是几毫秒,先换存储或改用带断电保护的写缓存,再考虑参数调优。
  • 压测时同时看 TPS 与 P99 延迟,而不是只看吞吐。组提交的收益往往体现在 P99 的显著下降,而不是平均值。

下一节

继续学习:Checkpoint(检查点)

参考链接 ​