Skip to content

WAL Writer(预写日志写入进程) ​

WAL Writer(预写日志写入进程,简称 walwriter)是 PostgreSQL 的常驻辅助进程,负责把共享内存中的 WAL 缓冲区(WAL buffers)周期性写入磁盘,从而减少后端进程在提交事务时必须自己刷 WAL 的次数。本节将讲解 WAL 写入的两阶段模型、组提交(Group Commit)机制、walwriter 与后端提交刷盘的分工,以及相关参数的实测调优方法。

前置知识

阅读本节前,建议先了解:Background Writer(后台写入进程)

基础概念 ​

术语英文说明
预写日志WAL, Write-Ahead Logging「先写日志、再写数据文件」的持久化机制,保证崩溃后可重放恢复
WAL 缓冲区WAL Buffers共享内存中暂存 WAL 记录的区域,由 wal_buffers 指定大小
WAL 写入进程WAL Writer(walwriter)常驻辅助进程,周期把 WAL 缓冲区 write() 到 WAL 文件
日志序列号LSN, Log Sequence NumberWAL 中的字节位置,单调递增,用于描述持久化进度
组提交Group Commit多个事务的提交记录合并后一次性刷盘,摊薄 fsync 成本
同步提交Synchronous Commitsynchronous_commit 控制提交时是否等待 WAL 落盘确认
刷写Write数据从用户态内存进入内核页缓存(write() 系统调用)
落盘Flush / Sync内核把页缓存真正写到持久介质(fsync() / fdatasync())
写入位置pg_current_wal_insert_lsn()WAL 缓冲区中已插入记录的末端
刷盘位置pg_current_wal_flush_lsn()已保证落盘的 WAL 末端,提交可见性的判据

常见误区:write ≠ flush

这是理解 WAL 写入性能的核心。write() 只把数据交给操作系统页缓存,此时进程即可返回;fsync() 才真正等待数据落到持久介质。二者耗时相差好几个数量级(write 通常是微秒级,机械盘 fsync 是毫秒级)。

walwriter 只做 write(把 WAL 缓冲区搬到内核),它不做 fsync。真正保证持久性的 fsync 由「事务提交者」(后端进程或 walsender)或 walwriter 在必要时触发。

这个分工的意义在于:walwriter 提前把 WAL 推进到内核页缓存,后端提交时往往只需一次 fsync(甚至因为别人已经 fsync 过而完全不用等),从而缩短提交延迟。

WAL 写入的两阶段模型 ​

text
后端进程执行 UPDATE/COMMIT
        │
        │ ① 生成 WAL 记录,追加到共享内存的 WAL 缓冲区
        ▼
┌─────────────────────────────────────────────┐
│            WAL Buffers(wal_buffers)        │
│  ┌───────────────────────────────────────┐  │
│  │ record1 | record2 | record3 | ...     │  │
│  └───────────────────────────────────────┘  │
└───────────────────┬─────────────────────────┘
                    │
        ② write()   │  ◀── walwriter 周期性执行(wal_writer_delay)
                    │  ◀── 后端提交时也执行(同步提交路径)
                    ▼
┌─────────────────────────────────────────────┐
│         OS Page Cache(内核页缓存)          │
│  (断电会丢,但进程崩溃不会丢)               │
└───────────────────┬─────────────────────────┘
                    │
        ③ fsync()   │  ◀── 后端提交时必须执行(synchronous_commit = on)
                    │  ◀── 或由 walwriter 的 flush 逻辑推动
                    ▼
┌─────────────────────────────────────────────┐
│             持久化介质(磁盘 / SSD)          │
└─────────────────────────────────────────────┘

对应到 LSN,可以这样理解三种「进度」:

text
pg_current_wal_insert_lsn()  — 已写入 WAL 缓冲区的末端(最新)
pg_current_wal_lsn()         — 同上(历史别名,等价于 insert)
pg_current_wal_flush_lsn()   — 已 fsync 落盘的末端(最保守)
sql
-- 观察三种 LSN 的关系:flush_lsn 通常落后于 insert_lsn
SELECT
  pg_current_wal_insert_lsn() AS insert_lsn,
  pg_current_wal_lsn()        AS write_lsn,
  pg_current_wal_flush_lsn()  AS flush_lsn,
  pg_wal_lsn_diff(pg_current_wal_insert_lsn(), pg_current_wal_flush_lsn()) AS lag_bytes;
text
 insert_lsn  | write_lsn  | flush_lsn  | lag_bytes
-------------+------------+------------+-----------
 0/1A3E2B70  | 0/1A3E2B70 | 0/1A3DFF08 |         0

在高并发提交场景下,lag_bytes 会频繁抖动,那是正常现象;它的稳定值反映的是「WAL 缓冲区到磁盘」的时延。

核心机制 ​

walwriter 的工作循环 ​

walwriter 的逻辑比 bgwriter 简单得多——它不挑页,只负责「把已有的 WAL 推进下去」:

c
/* 概念性伪代码,非 PostgreSQL 源码原文 */
for (;;)
{
    /* 1. 按 wal_writer_delay 休眠(默认 200ms) */
    pg_usleep(wal_writer_delay);

    /* 2. 取当前写入位置(insert LSN) */
    cur = GetXLogInsertRecPtr();

    /* 3. 把 [上次已写位置, cur) 区间 write() 到 WAL 文件 */
    WriteXLog(cur);

    /* 4. 记录本轮的 write 位置,供后端提交时判断「是否已在内核中」 */
    LogwrtResult.Write = cur;

    /* 5. 若距上次 flush 超过 wal_writer_flush_after 字节,或该值为 0,
          则额外执行 fsync,把 WAL 真正推到磁盘 */
    if (wal_writer_flush_after == 0 ||
        bytes_written_since_flush >= wal_writer_flush_after)
        FlushXLog(cur);
}

关键点在第 4、5 步:

  • 第 4 步:walwriter 记录下「我已经 write 到哪个位置」。后端进程提交时,只要发现目标 LSN 已经在这个位置之前,就知道不需要再做 write(数据已在内核中),可以直接进入 fsync 或直接返回。
  • 第 5 步:只有在 wal_writer_flush_after = 0(表示「每次都 flush」)或累计写入超过该字节数时,walwriter 才做 fsync。默认值 1MB 意味着 walwriter 通常只 write 不 flush,把 fsync 的「责任」留给提交者。

组提交(Group Commit):让 fsync 被摊薄 ​

fsync 是提交路径上最昂贵的操作。如果每个事务都独立 fsync,机械盘上 QPS 会被限制在几百(每次 fsync 约 5~10ms)。PostgreSQL 用组提交把多个事务的 fsync 合并:

text
时刻 T0:事务 A 完成,进入提交,发现 WAL 未刷盘 → 开始 fsync,持有 WAL 插入/刷盘锁
时刻 T0+ε:事务 B、C、D 也完成,想提交
          发现 A 正在 fsync,于是它们「挂起等待」,把自己的目标 LSN 记下来
时刻 T0+5ms:A 的 fsync 完成,把 WAL 推进到 LSN_X
          → B、C、D 的目标 LSN 都 ≤ LSN_X,全部立即返回「提交成功」
          → 4 个事务只花了 1 次 fsync

这个过程是自动发生的,不需要配置。当并发提交数较高时,组提交会自动把 fsync 次数压到接近「每 5ms 一次」,这正是 PostgreSQL 能在普通机械盘上跑出上万 TPS 的原因。

可视化理解:fsync 的耗时(灰色)被多个事务共享,事务数越多,单位事务摊销的 fsync 成本越低。

text
事务数少(无组提交效果)           事务数多(组提交生效)
A ████████                        A ████████
B         ████████                B ┘ 共享一次
C                 ████████        C ┘ fsync
D                         ████████ D ┘
时间 ──────────────────▶          时间 ──▶

commit_delay 与 commit_siblings 是「主动等待以凑组」

组提交默认是「谁先到谁发起,后来者搭便车」。如果你希望主动多等一会儿以凑更大的组,可以设置:

  • commit_delay(微秒,默认 0):发起 fsync 前先短暂睡眠,给其他事务「赶到」的机会。
  • commit_siblings(默认 5):只有当当前活跃事务数 ≥ 该值时,commit_delay 才生效。

绝大多数场景不需要设置这两个参数。只有当你在极高并发、且确认 fsync 是瓶颈、同时 CPU 有空闲时才值得尝试。设错了会平白增加每次提交的延迟。

后端进程在提交路径上的动作 ​

walwriter 只是「辅助加速」,真正的正确性保证在提交路径上。后端进程在提交时依次做三件事:

text
COMMIT 到达
  │
  ├─ 1. 生成 commit WAL 记录,追加到 WAL 缓冲区
  │
  ├─ 2. 检查 synchronous_commit 设置:
  │       off / local      → 不等待刷盘,立即返回「提交成功」(异步提交,崩溃可能丢最近事务)
  │       on / remote_write / remote_apply → 需要等待
  │
  ├─ 3. 若需要等待:
  │       WalWriter 是否已经 write 过这个位置?
  │         是 → 跳过 write,直接进 fsync 判断
  │         否 → 自己执行 write
  │       是否已经被 flush 过(flush_lsn ≥ 目标)?
  │         是 → 直接返回(可能完全是别人的 fsync 帮了忙)
  │         否 → 自己执行 fsync(此时若已有他人正在 fsync,则等待并搭便车)
  │
  └─ 4. 返回提交成功给客户端

注意第 3 步:后端提交时的 write 和 fsync 都可能被 walwriter 或并发事务「提前完成」。这就是 walwriter 的价值——它把 WAL 提前推进到内核,使得大量提交只需等待(或复用)一次 fsync。

sql
-- 当前会话的同步提交模式
SHOW synchronous_commit;
SHOW wal_writer_delay;
SHOW wal_writer_flush_after;
SHOW wal_buffers;
text
 synchronous_commit
--------------------
 on

 wal_writer_delay
------------------
 200ms

 wal_writer_flush_after
------------------------
 1MB

 wal_buffers
------------
 512kB

关键参数 ​

参数默认值作用
wal_buffers-1(自动,约为 shared_buffers 的 1/32,上限 16MB)WAL 缓冲区大小;-1 表示自动计算
wal_writer_delay200mswalwriter 每轮之间休眠多久
wal_writer_flush_after1MB累计写入多少字节后 walwriter 才做 fsync;0 表示每轮都 flush
synchronous_commiton提交时等待 WAL 落盘的程度
wal_sync_method平台相关(Linux 通常 fdatasync)用哪个系统调用落盘
commit_delay0主动延迟以凑组提交(微秒)
commit_siblings5commit_delay 生效所需的并发事务数下限
full_page_writeson检查点后首次修改页时写整页镜像(属于 WAL 内容层,见后续章节)

synchronous_commit = off 的取舍

设为 off(或 local)会让提交不等 WAL 落盘就返回成功,吞吐可提升数倍(尤其在机械盘上),代价是崩溃时可能丢失最近若干毫秒(通常 < 200ms)已提交的事务,并且可能造成「已提交数据消失」的语义破坏。

适用场景:日志、埋点、缓存回填等能容忍少量丢失的写入。不适合任何有账务、订单、状态机语义的业务。

注意 off 丢的是「最近一小段时间」的事务,而不是「整库不一致」——数据库自身仍然是一致的(因为 WAL 保证原子性),只是最近提交的事务可能整体消失。

详细说明 ​

walwriter 只 write 不 flush 的设计意图 ​

为什么默认 wal_writer_flush_after = 1MB,而不是让 walwriter 每次都 fsync?

text
若 walwriter 每 200ms 就 fsync 一次(即 flush_after = 0):
  - 好处:后端提交时很可能发现「已 flush」,直接返回,延迟极低
  - 坏处:每秒固定 5 次 fsync,若写入量很小,这些 fsync 是纯浪费
          (而且长跑的后台 fsync 会与前台 fsync 争抢 I/O)

若默认每 1MB 才 flush:
  - 好处:写入量少时不产生无谓 fsync;写入量大时 flush 频率自然升高
  - 坏处:低并发提交场景下,后端仍需自己 fsync(此时组提交帮不上忙)

结论:walwriter 的 write 是几乎零成本的高收益动作(把数据从共享内存搬到内核),因此它周期性地做;而 flush 是有成本的动作,因此只在明确的量阈值或显式配置下做。

高并发提交场景可以尝试 wal_writer_flush_after = 0

如果你的负载是「大量小事务持续提交」,且观察发现后端提交仍然频繁自己 fsync,把 wal_writer_flush_after 设为 0 让 walwriter 每轮(200ms)主动 flush 一次,往往能显著降低提交延迟的 P99。原因是此时前端几乎总能「搭上便车」,不必自己触发 fsync。

反之,如果是「写入稀疏」的负载(如每分钟几笔),保持默认即可。

用 pg_stat_io 观测 WAL 的 write 与 fsync(PG 16+) ​

PG 16 引入的 pg_stat_io 视图把 WAL 的写入与同步拆开统计:

sql
-- PG 16+:查看 WAL 文件的写入与 fsync 计数
SELECT
  backend_type,
  object,
  writes,
  write_bytes,
  fsyncs,
  fsync_time
FROM pg_stat_io
WHERE object = 'wal'
ORDER BY writes DESC;
text
  backend_type   | object |  writes | write_bytes | fsyncs | fsync_time
-----------------+--------+---------+-------------+--------+------------
 client backend  | wal    |  892341 |  1843201920 |  12874 |    1842190
 walwriter       | wal    |  742113 |  1520334120 |     41 |       6120
 walsender       | wal    |   18220 |    38881204 |    812 |     102330
 checkpointer    | wal    |    1201 |     2482190 |    302 |      90210

关键解读:

  • walwriter 的 fsyncs 极少(41 次):印证了「默认只 write 不 flush」。
  • client backend 的 fsyncs 最多(12874 次):这是提交路径上的 fsync,是持久性的真实成本所在。
  • walwriter 的 writes 次数高但 fsyncs 低:说明它有效分担了 write 工作,让后端提交时可以跳过 write 直接 fsync。

要评估 walwriter 的效果,可以对比 walwriter 的 write_bytes 占总 WAL write_bytes 的比例:

sql
-- PG 16+:walwriter 承担了多少比例的 WAL 写入量
SELECT
  sum(write_bytes) FILTER (WHERE object = 'wal')                                 AS total_wal_write,
  sum(write_bytes) FILTER (WHERE object = 'wal' AND backend_type = 'walwriter')  AS by_walwriter,
  round(100.0 * sum(write_bytes) FILTER (WHERE object = 'wal' AND backend_type = 'walwriter')
        / NULLIF(sum(write_bytes) FILTER (WHERE object = 'wal'), 0), 2)          AS pct_by_walwriter
FROM pg_stat_io;
text
 total_wal_write | by_walwriter | pct_by_walwriter
-----------------+--------------+------------------
      3427199734 |   1520334120 |            44.36

pct_by_walwriter 在 30%~60% 区间属于常见健康值。若它接近 0,说明 walwriter 基本没起作用(可能是 wal_writer_delay 过大或写入过于稀疏)。

walwriter 与 WAL 缓冲区大小的关系 ​

wal_buffers 决定 WAL 记录能暂存多少。它的自动值规则是:

text
wal_buffers = -1(默认)时:
  自动取值 ≈ shared_buffers / 32
  下限:64kB(在极小 shared_buffers 时,至少要能容下几个页的 WAL)
  上限:16MB(即使 shared_buffers 很大也不会超过)

例:
  shared_buffers = 128MB  → wal_buffers ≈ 4MB
  shared_buffers = 8GB    → wal_buffers = 16MB(触顶)
sql
-- 查看当前的自动值与来源
SELECT name, setting, unit, source, pending_restart
FROM pg_settings
WHERE name = 'wal_buffers';
text
    name     | setting | unit | source  | pending_restart
-------------+---------+------+---------+-----------------
 wal_buffers | 4096    | 8kB  | default | f

4096 × 8kB = 32MB——咦,超过上限了?这是显示上的换算陷阱:

pg_settings.setting 的单位是 8kB 页,不是字节

wal_buffers = 4096 表示 4096 个 8kB 页,即 32MB。但手册规定上限为 16MB。实际观察时以 SHOW wal_buffers 的输出为准(它会显示成 16MB),且需注意 unit 列的含义。

准确做法:

sql
SELECT pg_size_pretty(current_setting('wal_buffers')::bigint) AS wal_buffers_human;
sql
-- 更可靠的读法:用 current_setting 转成人类可读
SELECT current_setting('wal_buffers') AS raw, pg_size_pretty(current_setting('wal_buffers')::bigint);

什么时候需要手动调大 wal_buffers?

  • 单个事务产生大量 WAL(如一次 COPY 或大批量 INSERT)时,WAL 缓冲区容易被填满,后端会在此处同步等待刷盘(产生 WALWriteLock 等等待事件)。
  • 观察 pg_stat_activity 中 wait_event = 'WALWriteLock' 或 'WALBufferFull' 的会话数。

不过一般来说,自动值已经够用,手动调大的收益有限;更好的手段是拆批提交(把大事务拆成多个小事务)。

提交延迟的诊断链路 ​

如果应用反馈「提交慢」,按下面的顺序排查:

text
1. 确认是否真的是 fsync 慢
   → 开启 track_io_timing,然后看 pg_stat_io 中 wal 的 fsync_time / fsyncs 平均值
   → 或对比 wal_sync_method 改为 open_datasync / fdatasync 的差异

2. 确认组提交是否生效
   → 看应用并发度:串行提交(一次只有一个事务在飞)时组提交完全无效
   → 看 client backend 的 fsyncs 是否 ≈ 提交次数(若是,则完全没搭上便车)

3. 确认 walwriter 是否在帮忙
   → 看 pct_by_walwriter 是否过低
   → 若过低,尝试 wal_writer_delay 缩短 或 wal_writer_flush_after = 0

4. 确认 synchronous_commit 是否被误设为 on(在不必要的场景)
   → 非关键业务可评估 off / local

5. 确认存储本身
   → 云盘 IOPS 限制、RAID 卡写缓存未开(无 BBU 时写穿透)、
     SSD 寿命末期降速、文件系统挂载选项(barrier 等)

实战示例 ​

示例 1:测量一次提交的真实 WAL 刷盘延迟 ​

sql
-- 需要 track_io_timing = on 才有耗时数据
SHOW track_io_timing;
text
 track_io_timing
-----------------
 on
sql
-- 记录基线
SELECT sum(fsyncs) AS total_fsyncs, sum(fsync_time) AS total_fsync_time
FROM pg_stat_io
WHERE object = 'wal';
sql
-- 生成 500 次独立提交(每次一个事务,模拟小事务高并发)
DO $$
BEGIN
  FOR i IN 1..500 LOOP
    INSERT INTO t_wal_commit_demo (note) VALUES ('c' || i);
  END LOOP;
END $$;
sql
-- 对照增量
SELECT
  sum(fsyncs) FILTER (WHERE object = 'wal')                    AS wal_fsyncs,
  sum(fsync_time) FILTER (WHERE object = 'wal')                AS wal_fsync_time_us,
  round(sum(fsync_time) FILTER (WHERE object = 'wal')::numeric
        / NULLIF(sum(fsyncs) FILTER (WHERE object = 'wal'), 0), 1) AS avg_us_per_fsync
FROM pg_stat_io;
text
 wal_fsyncs | wal_fsync_time_us | avg_us_per_fsync
------------+-------------------+------------------
        502 |           2482190 |           4945.8

解读:wal_fsyncs ≈ 502(约等于提交次数),说明这次是串行提交、组提交完全没生效,每次提交都实打实付了约 4.9ms 的 fsync 成本。这也解释了为什么「循环里逐条 INSERT 且每条自动提交」的性能极差。

sql
-- 对比:把这 500 条放进一个事务里,只付 1 次 fsync
BEGIN;
INSERT INTO t_wal_commit_demo (note)
SELECT 'b' || g FROM generate_series(1, 500) AS g;
COMMIT;
sql
SELECT
  sum(fsyncs) FILTER (WHERE object = 'wal') AS wal_fsyncs_after
FROM pg_stat_io;
text
 wal_fsyncs_after
------------------
              503

增量只有 1。这就是「批量提交」与「逐条提交」的性能差距来源。

示例 2:用 LSN 观察 walwriter 的推进节奏 ​

sql
-- 建一个辅助观察表,记录 insert_lsn 与 flush_lsn 的差距随时间的变化
CREATE TABLE IF NOT EXISTS t_wal_lsn_sample (
  ts          timestamptz DEFAULT clock_timestamp(),
  insert_lsn  pg_lsn,
  flush_lsn   pg_lsn,
  lag_bytes   bigint
);

-- 采样 10 秒,每 200ms 一次,观察 lag 波动
DO $$
BEGIN
  FOR i IN 1..50 LOOP
    INSERT INTO t_wal_lsn_sample (insert_lsn, flush_lsn, lag_bytes)
    SELECT
      pg_current_wal_insert_lsn(),
      pg_current_wal_flush_lsn(),
      pg_wal_lsn_diff(pg_current_wal_insert_lsn(), pg_current_wal_flush_lsn());
    PERFORM pg_sleep(0.2);
  END LOOP;
END $$;
sql
-- 查看 lag 的分布:最大值反映最坏情况下的刷盘延迟
SELECT
  count(*)                                        AS samples,
  round(avg(lag_bytes))                           AS avg_lag_bytes,
  max(lag_bytes)                                  AS max_lag_bytes
FROM t_wal_lsn_sample;
text
 samples | avg_lag_bytes | max_lag_bytes
---------+---------------+---------------
      50 |             0 |             0
text
(注:空闲实例上 lag 通常恒为 0;只有在持续写入时才会看到非零值)

用 wal_writer_delay 做对照实验

在持续写入的负载下,把 wal_writer_delay 从 200ms 调到 20ms,重新采样 max_lag_bytes,通常会看到明显下降(因为 walwriter 推进更频繁)。代价是 walwriter 进程唤醒更频繁、占用更多 CPU。这就是「延迟」与「开销」的典型权衡。

示例 3:评估 synchronous_commit 的影响 ​

sql
-- 逐条提交 + synchronous_commit = on
SET synchronous_commit = on;
\timing on
DO $$
BEGIN
  FOR i IN 1..200 LOOP
    INSERT INTO t_wal_commit_demo (note) VALUES ('on-' || i);
  END LOOP;
END $$;
text
Time: 1182.430 ms
sql
-- 逐条提交 + synchronous_commit = off
SET synchronous_commit = off;
DO $$
BEGIN
  FOR i IN 1..200 LOOP
    INSERT INTO t_wal_commit_demo (note) VALUES ('off-' || i);
  END LOOP;
END $$;
text
Time: 96.215 ms

差距约 12 倍。这直观地说明:fsync 才是提交路径的瓶颈,而 synchronous_commit = off 本质上是把 fsync 从提交路径上「摘掉」。

但请记住代价:

text
synchronous_commit = off 时,若数据库进程崩溃(崩溃恢复)——
  已提交但未落盘的事务会丢失

若发生操作系统崩溃 / 断电——
  丢失更多(还包括内核页缓存中未落盘的部分)

若仅客户端进程崩溃 / 连接断开——
  不丢数据(因为事务已在数据库内存中完成)

示例 4:对比 wal_writer_flush_after 的两种取值 ​

ini
# postgresql.conf —— 方案 A:默认(只 write,让提交者 fsync)
wal_writer_delay = 200ms
wal_writer_flush_after = 1MB
ini
# postgresql.conf —— 方案 B:每轮都 flush(高并发小事务场景可尝试)
wal_writer_delay = 200ms
wal_writer_flush_after = 0        # 0 表示 walwriter 每轮都执行 fsync
bash
# 施加负载并对比 P99 提交延迟(需基准测试工具,如 pgbench)
pgbench -c 32 -j 4 -T 60 -f insert_only.sql postgres
text
# 方案 A 典型输出
latency average = 1.842 ms
latency stddev  = 2.113 ms
tps = 17021.32 (without initial connection time)

# 方案 B 典型输出
latency average = 1.511 ms
latency stddev  = 1.204 ms
tps = 19284.07 (without initial connection time)

方案 B 的平均延迟下降、标准差显著降低,这是因为它让前端提交总能搭上 walwriter 的便车,减少了「恰好没人 fsync 就得自己来」的长尾。但请注意:这是在高并发前提下的收益;低并发时方案 B 只是多做了无用的 fsync。

注意事项 ​

  • walwriter 不影响正确性:即使它完全不工作,后端提交路径也会自己完成 write + fsync,数据不会丢。
  • wal_writer_delay 与 bgwriter_delay 都受 SIGHUP 影响:改配置后需 reload,SET 命令无法改变已运行进程的行为。
  • synchronous_commit = off 不等于「不写 WAL」:WAL 仍然照写,只是提交不等 fsync。崩溃恢复仍然可用。
  • synchronous_commit 是会话级参数:可以用 SET 在特定事务/会话上临时放宽(如批量导入时),不必全局关闭。
  • commit_delay 是微秒不是毫秒:设成 1000 是 1ms,不是 1 秒;设错会造成严重性能问题。
  • wal_buffers 手动调大需要重启:该参数是 postmaster 级,必须重启实例。
  • 组提交需要并发才会生效:单连接串行提交时,无论怎么调参都不会有组提交收益,此时应改用批量提交。
  • fsync 慢不总是数据库的问题:云盘 IOPS 限额、文件系统 barrier、RAID 写缓存策略、虚拟化层的 fsync 语义都可能放大延迟。
  • pg_stat_io 需要 PG 16+:更早版本只能通过 pg_stat_bgwriter 与 pg_stat_wal(PG 14+)间接推断。
  • pg_stat_wal(PG 14+)是查看 WAL 生成量的专用视图:wal_records、wal_fpi(全页镜像数)、wal_bytes 都在这里,与 pg_stat_io 互补。
ini
# postgresql.conf —— 不推荐:为提速把持久性全局关掉
# synchronous_commit = off       # 不推荐:全局关闭会让所有业务都面临丢最近事务的风险
# full_page_writes = off         # 不推荐:崩溃后可能无法恢复(见 WAL 全页写入章节)
sql
-- 不推荐:无脑调大 commit_delay 期望提升吞吐
-- SET commit_delay = 10000;      -- 不推荐:10ms,会直接抬高每次提交的延迟

最佳实践 ​

  • 保持 walwriter 与组提交在默认配置:默认值在绝大多数场景下已是最优或接近最优。
  • synchronous_commit 只在会话级放宽:批量导入、日志类写入可在会话内 SET synchronous_commit = off,不要全局关闭。
  • 优先用批量提交替代调参:把 N 条逐条提交改成 1 个事务或分批提交,收益远大于调 wal_writer_*。
  • 高并发小事务场景可试 wal_writer_flush_after = 0:务必用压测对比 P99 后再决定。
  • 用 pg_stat_io 的 object = 'wal' 监控 WAL 的 write/fsync 拆分,不要只看总 I/O。
  • 开启 track_io_timing 仅用于诊断期:长期开启有可测量的开销。
  • 把存储的 fsync 能力纳入选型:能保证断电不丢缓存的企业级 SSD 或带 BBU 的 RAID 卡,能把提交延迟压下一个数量级。
  • 监控 pg_stat_wal.wal_bytes 的增长率:它反映写入压力,也直接影响检查点触发频率(max_wal_size)。
  • 不要用 synchronous_commit = off 掩盖存储问题:如果 fsync 异常慢,先查存储,而不是关掉持久性。
  • 调参必须做 A/B 压测:wal_writer_delay、wal_writer_flush_after、commit_delay 的效果高度依赖负载特征,纸上推导不可靠。

下一节

继续学习:Autovacuum(自动清理进程)

参考链接 ​