Skip to content

Background Writer(后台写入进程) ​

Background Writer(后台写入进程,简称 bgwriter)是 PostgreSQL 中的常驻辅助进程,它的唯一职责是提前把共享缓冲区中的脏页刷到磁盘,从而减轻后端进程在申请缓冲区时被迫同步刷脏的压力。本节将讲解 bgwriter 的定位、与检查点和后端刷脏的分工、关键参数,以及如何通过统计视图判断它是否工作良好。

前置知识

阅读本节前,建议先了解:Backend 后端进程(Backend Process)

基础概念 ​

术语英文说明
共享缓冲区Shared Buffersshared_buffers 指定大小的共享内存池,所有进程读写数据页的入口
脏页Dirty Page / Dirty Buffer内容已被修改但尚未写回磁盘的数据页(对应缓冲区头 BM_DIRTY 标记)
后台写入进程Background Writer(bgwriter)常驻辅助进程,周期性扫描缓冲区并提前刷脏
后端刷脏Backend Flush后端进程找不到干净缓冲区时,自己把某个脏页同步写盘
检查点Checkpoint由 checkpointer 进程触发的全量刷脏 + 记录检查点,保证崩溃恢复起点
时钟扫描Clock Sweepshared_buffers 的缓冲区替换算法,模拟 LRU 但不做全排序
使用计数Usage Count缓冲区头中的访问计数(0~5),时钟扫描据此判断「是否常被访问」
缓冲映射Buffer Mapping把 (relfilenode, fork, block) 映射到缓冲区槽位的哈希表
刷盘节流fsync Throttling / *_flush_after一次写多少数据后主动停顿,避免内核脏页堆积形成 I/O 尖峰

常见误区:bgwriter 不是为了「让数据落盘」

即使 bgwriter 完全不工作,数据也不会丢——真正的持久性由 WAL(预写日志) 保证,而不是由数据页刷盘保证。bgwriter 存在的唯一目的是性能:让脏页在后台被平滑地洗掉,避免后端进程在某个瞬间被迫做大量同步写,导致查询延迟尖刺。

换句话说:

  • 数据页刷盘 是「为了腾出可复用缓冲区 / 缩短恢复时间」,可以延迟。
  • WAL 刷盘 是「为了事务持久性」,提交时必须完成。

两者是完全独立的路径。

脏页的三种归宿 ​

一个脏页最终被写到磁盘,可能经由三条完全不同的路径:

text
              ┌──────────────────────────────────────────────┐
              │            Shared Buffers(共享缓冲区)        │
              │   ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ │
              │   │ 干净页 │ │ 脏页   │ │ 脏页   │ │ 干净页 │ │
              │   └────────┘ └───┬────┘ └───┬────┘ └────────┘ │
              └───────────────────┼──────────┼────────────────┘
                                  │          │
        ① bgwriter 主动刷脏       │          │      ③ checkpointer 全量刷脏
       (平滑、后台、无关请求)    │          │     (检查点触发、批量、有节流)
                                  ▼          ▼
                          ┌──────────────────────────┐
                          │        磁盘数据文件       │
                          └──────────────────────────┘
                                  ▲
                                  │
        ② backend 被迫刷脏:后端需要缓冲区但找不到干净页,
         于是自己选一个脏页写盘,然后占用该槽位(同步阻塞该查询)
  • 路径 ①:bgwriter 周期唤醒,主动挑一批脏页写盘。它是纯收益——几乎不影响任何前台查询。
  • 路径 ②:后端进程被迫自己刷脏。这是有代价的——该查询会被 I/O 阻塞。bgwriter 的全部价值就是把这条路径的占比压到最低。
  • 路径 ③:checkpointer 在检查点时全量刷脏。它由 checkpoint_timeout / max_wal_size 触发,属于周期性「大扫除」。

性能优化的目标可以一句话概括:让 ① 尽可能多、② 尽可能少、③ 尽可能平滑。

核心机制 ​

缓冲区替换与「为什么需要提前刷脏」 ​

PostgreSQL 的 shared_buffers 是一块固定大小的内存池,用时钟扫描(Clock Sweep) 算法复用缓冲区。当后端进程需要读入一个新页面时:

text
1. 在缓冲映射表中查找该页 → 命中则直接使用(buffer hit),结束
2. 未命中 → 需要从空闲链表或替换链表中取一个缓冲区
3. 取出候选缓冲区时检查:
   - 引用计数 refcount > 0            → 被人正在用,跳过
   - 标记为脏(BM_DIRTY)             → 必须先写盘(路径 ②,后端被迫刷脏)
   - 使用计数 usage_count > 0         → 说明最近被访问过,减 1 并跳过
4. 找到干净且 usage_count = 0 的缓冲区 → 直接复用(无 I/O 成本)

关键点在第 3 步:如果候选页是脏的,当前这个后端就必须等它写完。这个等待直接叠加到用户查询的延迟上。

bgwriter 的作用就是在后端来找缓冲区之前,按 LRU 思路提前把这些脏页洗掉,让第 3 步能走第 4 步的快路径。

bgwriter 的工作循环 ​

bgwriter 是一个无限循环的进程,核心逻辑可以简化成下面这段伪代码:

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

    /* 2. 从上次停下的位置继续扫描缓冲区,最多洗 bgwriter_lru_maxpages 页 */
    for (i = 0; i < bgwriter_lru_maxpages; i++)
    {
        /* 3. 估算「下一次唤醒前,后端大约需要多少个缓冲区」 */
        recent_alloc   = 距上次唤醒期间被后端占用的缓冲区数;
        next_expected  = recent_alloc * bgwriter_lru_multiplier;

        /* 4. 该页在时钟扫描中的「未来使用时间」可被安全推迟 → 无需刷 */
        if (next_expected < 距该页被复用的天数)
            break;

        /* 5. 刷新一个脏页到磁盘 */
        if (buffer_is_dirty(buf))
            flush_buffer(buf);
    }

    /* 6. 更新统计:buffers_clean++ */
}

上面的第 3、4 步是 bgwriter 的「智能」所在——它不是无脑地把所有脏页刷完,而是根据最近后端申请缓冲区的速度(recent_alloc)动态推算未来需求,只刷「反正马上就要被复用」的那部分脏页。这样既避免了后端被迫刷脏,又不会做无效 I/O(刷了马上又要改)。

关键参数 ​

参数默认值作用
bgwriter_delay200msbgwriter 每轮之间的休眠时间。越短越平滑,但会占用更多 CPU 去扫描缓冲区
bgwriter_lru_maxpages100每轮最多刷多少个脏页。设为 0 等于关闭 bgwriter
bgwriter_lru_multiplier2.0预测倍率:未来需求 = 最近分配量 × 该值。越大越激进(提前多刷),越小越保守
bgwriter_flush_after512kB(默认按 shared_buffers 推算,上限 2MB)累计写满该字节数后强制停顿,让内核把脏页写出去,避免 I/O 尖峰
ini
# postgresql.conf
# 常规 OLTP 场景的温和加强版:更短的间隔 + 更多的每轮刷页数
bgwriter_delay = 200ms           # 保持默认,200ms 已足够平滑
bgwriter_lru_maxpages = 200      # 从 100 提到 200,让每轮能洗更多脏页
bgwriter_lru_multiplier = 4.0    # 从 2.0 提到 4.0,更激进地提前刷脏
bgwriter_flush_after = 512kB     # 保持默认;机械盘可考虑调小到 256kB

bgwriter_lru_maxpages 是一个「安全阀」

设成很大的值(比如 1000)并不会让 bgwriter 疯狂刷盘——它仍然受 bgwriter_delay 与内部估算逻辑约束,只是「这一轮最多洗这么多」。真正决定刷不刷的是「是否马上就要被复用」。而设成 0 则是明确关闭,此时所有脏页都只能走后端被迫刷脏或检查点两条路径。

bgwriter_flush_after 与 SSD

该参数的设计目标是限制内核脏页队列长度。在带有电池保护写缓存(BBU)的 RAID 卡或企业级 SSD 上,内存中的写合并本身很快,此时过小的 *_flush_after 反而会降低吞吐。若你的存储设备能在断电时保住缓存数据,可以把它设为 0(禁用强制停顿),让内核自己调度。

与检查点(Checkpointer)的分工 ​

初学者最容易把 bgwriter 和 checkpointer 混淆,两者都「刷脏」,但目标与节奏完全不同:

维度Background Writer(bgwriter)Checkpointer(checkpointer 进程)
触发条件周期性(bgwriter_delay)达到 checkpoint_timeout 或 max_wal_size
刷页范围只刷「马上会被复用」的少数脏页刷全部脏页(遍历整个缓冲区)
节奏控制天然分散、极细粒度检查点期间集中,由 checkpoint_completion_target 摊平
是否记录检查点否是,写入 pg_control 与 WAL 中的 checkpoint 记录
是否影响恢复点否是,检查点决定了崩溃恢复的起点
统计视图pg_stat_io(PG16+)、旧 pg_stat_bgwriterpg_stat_checkpointer(PG17+)、旧 pg_stat_bgwriter

一句话记忆:bgwriter 只关心「缓冲区够不够用」,checkpointer 才关心「崩溃后从哪里恢复」。

统计视图的版本变迁(重要) ​

统计 bgwriter 行为的视图在近几个大版本中变化剧烈,这是排查时最容易踩坑的地方:

text
PG 15 及以前:
  pg_stat_bgwriter 承载了「bgwriter + checkpointer + backend 刷脏」的全部计数

PG 16 起:
  引入 pg_stat_io 视图,把各后台进程的 I/O 行为按 backend_type 拆分统计
  (bgwriter、checkpointer 的读写次数/字节/耗时都能在 pg_stat_io 按类型查)

PG 17 起:
  pg_stat_checkpointer 独立成视图(checkpoints_timed / checkpoints_req / ...)
  pg_stat_bgwriter 中与检查点相关的列被移除,只保留 bgwriter 自己的计数
  受影响的列:checkpoints_timed、checkpoints_req、checkpoint_write_time、
              checkpoint_sync_time、buffers_checkpoint、buffers_clean 等
sql
-- PG 17+ 推荐:从专用视图分别查看
SELECT
  buffers_clean,        -- bgwriter 刷的缓冲区数
  maxwritten_clean,     -- 因超过 bgwriter_lru_maxpages 而提前结束的轮次数
  buffers_alloc,        -- 后端分配的缓冲区总数
  stats_reset
FROM pg_stat_bgwriter;
sql
-- PG 16 及以前:所有计数都在 pg_stat_bgwriter 里
SELECT
  checkpoints_timed,     -- 按 checkpoint_timeout 触发的检查点数
  checkpoints_req,       -- 按 max_wal_size 等原因强制触发的检查点数
  buffers_checkpoint,    -- 检查点刷的缓冲区数
  buffers_clean,         -- bgwriter 刷的缓冲区数
  buffers_backend,       -- 后端被迫刷的缓冲区数
  buffers_backend_fsync, -- 后端自己执行 fsync 的次数
  buffers_alloc          -- 后端分配的缓冲区总数
FROM pg_stat_bgwriter;

判断 bgwriter 是否称职的三个比值

拿到这些计数后,重点看三个比值(buffers_alloc 为分母):

  • buffers_clean / buffers_alloc —— bgwriter 的贡献率,健康时应「明显大于 0」,但没有绝对值标准。
  • buffers_backend / buffers_alloc —— 后端被迫刷脏的代价率,应尽量低。持续偏高说明 bgwriter 跟不上脏页产生速度。
  • maxwritten_clean —— 应「基本不增长」。如果它每轮都在涨,说明 bgwriter_lru_maxpages 太小,bgwriter 每轮都没洗完就被截断了。

需要注意的是:buffers_backend 不是越低越好,因为后端自己刷的那一页是「它本来就必须等待的」,有些 I/O 无论如何都躲不掉。优化目标是压低「被迫额外等待」的部分。

详细说明 ​

脏页产生速率与 bgwriter 的追赶能力 ​

脏页的产生速率由写入型负载决定:UPDATE、INSERT、DELETE、大批量 COPY、索引维护、以及 VACUUM 都会弄脏数据页。bgwriter 的清洗能力则由三个参数共同决定:

text
理论清洗上限 ≈ bgwriter_lru_maxpages × (1000 / bgwriter_delay_ms) 页/秒
             = 100 × (1000 / 200) = 500 页/秒

按 8KB/页换算:
             500 × 8KB ≈ 4 MB/s

这个数字在机械盘上勉强够用,在高写入负载下则明显偏低。当脏页产生速率长期高于 bgwriter 的清洗上限时,后果不是「bgwriter 追不上」这么简单,而是会形成恶性循环:

text
脏页产生太快
   ↓
bgwriter 每轮都被 bgwriter_lru_maxpages 截断(maxwritten_clean 持续增长)
   ↓
缓冲区里干净页减少,后端找不到可复用槽位
   ↓
后端被迫走「路径 ②」自己刷脏并同步 fsync(buffers_backend / backend_fsync 增长)
   ↓
用户查询延迟出现尖刺,QPS 下降,可用的 CPU 被浪费在等待 I/O 上

此时正确的处理顺序是:

  1. 先看是不是有超大批量写入(如一次 UPDATE 上百万行、无节制的 COPY)——这是最常见的根因,应拆批。
  2. 再看 bgwriter_lru_maxpages / bgwriter_lru_multiplier 是否过小。
  3. 最后才考虑调大 shared_buffers——缓冲区越大,单次检查点需要刷的页越多,恢复时间也越长,是有代价的。

后端刷脏(path ②)与 backend_fsync ​

后端被迫刷脏时有两种可能:

  • 普通刷脏:把脏页写入操作系统页缓存即返回(buffers_backend 计数,但没有对应的 fsync)。此时若内核随后成功落盘,数据不丢。
  • 带 fsync 的刷脏:当该缓冲区是「必须保证落盘」的场景(如正在做崩溃安全的检查点收尾、或 wal_sync_method 相关约束)时,后端需要自己调用 fsync(buffers_backend_fsync 计数)。

buffers_backend_fsync > 0 是非常值得警惕的信号,它意味着用户请求被卡在了真实磁盘 I/O 上。不过要注意:

buffers_backend_fsync 不总是问题

在检查点期间,后端协助刷脏并 fsync 一部分页面是有意为之的设计——它能让检查点更快完成、把工作分摊出去。所以该值偶尔为正是正常的。真正需要担心的是它在非检查点期间的持续增长。

sql
-- PG 16 及以前:一次性算出三条刷脏路径的占比
SELECT
  buffers_clean,
  buffers_checkpoint,
  buffers_backend,
  buffers_backend_fsync,
  round(100.0 * buffers_clean    / NULLIF(buffers_alloc, 0), 2) AS pct_clean,
  round(100.0 * buffers_checkpoint / NULLIF(buffers_alloc, 0), 2) AS pct_checkpoint,
  round(100.0 * buffers_backend / NULLIF(buffers_alloc, 0), 2) AS pct_backend
FROM pg_stat_bgwriter;
text
 buffers_clean | buffers_checkpoint | buffers_backend | buffers_backend_fsync | pct_clean | pct_checkpoint | pct_backend
---------------+--------------------+-----------------+-----------------------+-----------+----------------+-------------
       1842331 |             923044 |           41208 |                  1156 |     65.31 |          32.72 |        1.46

上例中 pct_backend = 1.46% 属于健康区间(一般经验值:< 5% 良好,> 10% 需要介入),pct_clean 占了约三分之二,说明 bgwriter 承担了主要清洗工作。

用 pg_stat_io 精确观测(PG 16+) ​

PG 16 引入的 pg_stat_io 视图按照 backend_type(进程类型)与 object(对象类型)拆分 I/O 统计,可以精确回答「谁在刷盘、刷了多少字节、花了多少时间」:

sql
-- PG 16+:按进程类型查看数据文件的写入情况
SELECT
  backend_type,
  object,
  context,
  writes,
  write_bytes,
  write_time,
  fsyncs,
  fsync_time
FROM pg_stat_io
WHERE object = 'relation'          -- 只看数据文件
  AND writes > 0
ORDER BY writes DESC;
text
  backend_type   |  object  | context |  writes  | write_bytes | write_time | fsyncs | fsync_time
-----------------+----------+---------+----------+-------------+------------+--------+------------
 checkpointer    | relation | normal  |  1239871 |  8123942192 |     912341 |   8821 |     120983
 bgwriter        | relation | normal  |  1842100 | 12079398821 |    1120334 |      0 |          0
 client backend  | relation | normal  |    41201 |   337706496 |     121003 |   1156 |      21908
 autovacuum      | relation | vacuum  |     8123 |    66552220 |      18220 |      0 |          0

关键解读:

  • bgwriter 的 fsyncs 恒为 0:bgwriter 只调用 write() 把页交给内核,它不需要保证落盘(持久性由 WAL 负责),因此不做 fsync。这也解释了为什么它比后端刷脏「便宜」得多。
  • context 区分 normal / vacuum / bulkwrite:可以看出 VACUUM 与批量写入各自的 I/O 贡献。
  • write_time 需要 track_io_timing = on 才有意义,否则为 0。

track_io_timing 有开销

在每次 I/O 前后调用计时函数会带来可测量的性能损耗(在高 IOPS 的 SSD 上尤其明显,可能达到 5%~10%)。除非正在做 I/O 性能诊断,否则不要长期打开。

实战示例 ​

示例 1:读取 bgwriter 统计并判断健康度 ​

sql
-- PG 17+:bgwriter 自身计数(检查点相关列已移到 pg_stat_checkpointer)
SELECT
  buffers_clean,
  maxwritten_clean,
  buffers_alloc,
  round(100.0 * buffers_clean / NULLIF(buffers_alloc, 0), 2) AS pct_clean
FROM pg_stat_bgwriter;
text
 buffers_clean | maxwritten_clean | buffers_alloc | pct_clean
---------------+------------------+---------------+-----------
       1842331 |              127 |       2830482 |     65.09
sql
-- 结合 pg_stat_checkpointer(PG 17+)看三条路径的全貌
SELECT
  c.num_timed,                       -- 超时触发的检查点
  c.num_requested,                   -- 请求(如 max_wal_size)触发的检查点
  b.buffers_clean,                   -- bgwriter 刷脏
  b.buffers_alloc                    -- 后端分配总数
FROM pg_stat_checkpointer c, pg_stat_bgwriter b;
text
 num_timed | num_requested | buffers_clean | buffers_alloc
-----------+---------------+---------------+---------------
        31 |             4 |       1842331 |       2830482

判读要点:

  • num_requested 远小于 num_timed 是好的(说明 max_wal_size 够大,没被 WAL 量逼着频繁检查点)。
  • maxwritten_clean = 127 且增长缓慢可接受;若每次采样都涨几百,则应上调 bgwriter_lru_maxpages。

示例 2:构造高写入负载并观察 bgwriter 是否跟得上 ​

sql
-- 建一张写入压力测试表
CREATE TABLE t_bgwriter_demo (
  id      bigserial PRIMARY KEY,
  payload text NOT NULL,
  flag    integer NOT NULL DEFAULT 0
);

INSERT INTO t_bgwriter_demo (payload)
SELECT repeat('x', 200) FROM generate_series(1, 200000);
sql
-- 记录基线
SELECT buffers_clean, maxwritten_clean, buffers_alloc, stats_reset
FROM pg_stat_bgwriter;
sql
-- 制造持续脏页:反复全表更新(会同时弄脏表页与索引页)
DO $$
BEGIN
  FOR i IN 1..20 LOOP
    UPDATE t_bgwriter_demo SET flag = flag + 1;
  END LOOP;
END $$;
sql
-- 观察增量:buffers_clean 应显著增长;若 maxwritten_clean 也大幅增长则说明每轮被截断
SELECT
  buffers_clean,
  maxwritten_clean,
  buffers_alloc,
  round(100.0 * buffers_clean / NULLIF(buffers_alloc, 0), 2) AS pct_clean
FROM pg_stat_bgwriter;
text
 buffers_clean | maxwritten_clean | buffers_alloc | pct_clean
---------------+------------------+---------------+-----------
       2071244 |              618 |       3120884 |     66.37

上例中 maxwritten_clean 从 127 涨到 618,说明 bgwriter 多轮都在刷满 bgwriter_lru_maxpages 后被截断——这正是「脏页产生速率超过清洗上限」的信号。

ini
# postgresql.conf —— 针对上述信号调参
bgwriter_lru_maxpages = 400      # 每轮上限从 100 提高(示例值,可按需调整)
bgwriter_lru_multiplier = 4.0    # 提高预测倍率,更早开始刷

示例 3:用 pg_stat_io 定位到底是谁在刷盘(PG 16+) ​

sql
-- 清零统计(需超级用户,且会影响所有统计视图)
SELECT pg_stat_reset_shared('bgwriter');
SELECT pg_stat_reset_shared('io');

-- 等待一段时间后,查看关系文件的写入排行
SELECT
  backend_type,
  context,
  writes,
  write_bytes,
  fsyncs
FROM pg_stat_io
WHERE object = 'relation'
ORDER BY writes DESC
LIMIT 5;
text
  backend_type   | context |  writes | write_bytes | fsyncs
-----------------+---------+---------+-------------+--------
 bgwriter        | normal  | 1122230 |  8911333210 |      0
 checkpointer    | normal  |  884120 |  6233882190 |   6621
 client backend  | normal  |   31201 |   255703808 |    812
 autovacuum      | vacuum  |    9021 |    73904128 |      0

结论:bgwriter 承担了六成以上的写入且 fsyncs = 0(纯 write 调用,成本低),client backend 的写入与 fsyncs 都很低,说明后端被迫刷脏的情况不严重。这是健康状态。

示例 4:临时关闭 bgwriter 观察后端刷脏的变化 ​

sql
-- 先记录当前值
SHOW bgwriter_lru_maxpages;
text
 bgwriter_lru_maxpages
-----------------------
 100
sql
-- 关闭 bgwriter(会话级修改,仅影响当前会话发起 SET 的进程,用于实验观察)
-- 注意:要全局验证效果应使用 ALTER SYSTEM + reload
SET bgwriter_lru_maxpages = 0;

-- 注意:该参数标记为 PGC_SIGHUP 级别,SET 只能改当前会话,bgwriter 进程本身不受影响。
-- 真正关闭需在 postgresql.conf 中设置为 0 并 reload。
ini
# postgresql.conf
bgwriter_lru_maxpages = 0        # 关闭 bgwriter,仅用于故障复现或极端特殊场景
sql
-- reload 后观察:buffers_backend 会逐渐增长(PG 16 及以前可直接查 pg_stat_bgwriter)
SELECT buffers_backend, buffers_backend_fsync, buffers_alloc
FROM pg_stat_bgwriter;
text
 buffers_backend | buffers_backend_fsync | buffers_alloc
-----------------+-----------------------+---------------
          184023 |                  8812 |       3021884

恢复 bgwriter_lru_maxpages = 100 并 reload 后,buffers_backend 的增长速率会明显放缓。这个对比实验能直观说明 bgwriter 的价值。

注意事项 ​

  • bgwriter 关闭不会丢数据:持久性由 WAL 保证。关闭 bgwriter 只是让性能变差(后端被迫刷脏),不会影响正确性。
  • bgwriter_lru_maxpages 是 SIGHUP 级参数:SET 命令无法改变运行中的 bgwriter 进程行为,必须写入 postgresql.conf 后 reload 或 ALTER SYSTEM SET + SELECT pg_reload_conf()。
  • 不要同时把 bgwriter 和检查点都调得很激进:两者的 I/O 会叠加,反而更容易打满磁盘。
  • pg_stat_bgwriter 的字段随版本变动:写监控脚本时必须按版本分支,否则升级到 PG 17 会因列不存在而报错。
  • buffers_backend_fsync 在检查点期间为正是正常的:检查点会让后端协助刷脏并 fsync,不要误判为故障。
  • buffers_alloc 统计的是分配次数,不是「读入页数」:命中与未命中都会分配缓冲区(未命中时填充,命中时仅增加引用)。
  • bgwriter_flush_after 不是越大越好:它约束的是单次连续写入的批量,设得过大就失去了「避免内核脏页堆积」的初衷。
  • 统计视图不会自动清零:stats_reset 可能已是很久以前的值,做增量分析前务必先取基线。
  • pg_stat_io 需要 PG 16+:在更早版本只能靠 pg_stat_bgwriter 的聚合数字推断。
  • 内核页缓存仍是双重缓存(Double Buffering)的一部分:bgwriter 用 write() 而非 O_DIRECT,数据先进入内核页缓存;真正落盘时机由内核决定,这也是 *_flush_after 要存在的原因。
ini
# postgresql.conf —— 不推荐:把 bgwriter 当成检查点调节器
# bgwriter_lru_maxpages = 1000    # 不推荐:误以为调大就能替代检查点调优
# bgwriter_delay = 10ms           # 不推荐:过短会让 bgwriter 空转占用 CPU
sql
-- 不推荐:在看统计前不做基线采样
-- SELECT buffers_clean FROM pg_stat_bgwriter;   -- 不推荐:绝对值无意义,必须看增量与比值

最佳实践 ​

  • 保持 bgwriter 开启:bgwriter_lru_maxpages = 0 只应在故障复现或极端排查中使用。
  • 优先用比值而非绝对值判断:关注 pct_clean、pct_backend 与 maxwritten_clean 的增长趋势。
  • maxwritten_clean 持续增长就加 bgwriter_lru_maxpages:这是最直接的调参信号。
  • 写入型负载上调 bgwriter_lru_multiplier:从 2.0 调到 4.0 能让 bgwriter 更早出手,代价是可能多刷一些马上又被修改的页。
  • 机械盘保留 bgwriter_flush_after 默认值,SSD/NVMe 可考虑设为 0 以提升吞吐。
  • 按版本选择正确的统计视图:PG 17+ 用 pg_stat_checkpointer + pg_stat_bgwriter + pg_stat_io,PG 16 用 pg_stat_bgwriter + pg_stat_io。
  • 把 bgwriter 指标纳入常规监控:至少采集 buffers_clean、buffers_backend、maxwritten_clean 三个量的增长率。
  • 先用 pg_stat_io 定位 I/O 来源,再决定调参:盲目调参可能让 checkpointer 与 bgwriter 互相争抢 I/O。
  • 不要指望 bgwriter 解决「I/O 总量过大」的问题:如果写入量本身就是瓶颈,应优化 SQL、拆批提交或升级存储,而不是调 bgwriter。
  • 调参后必须做对照验证:改动前后各取一段等长区间的增量数据,用比值对比,而不是凭感觉。

参考链接 ​