PostgreSQL笔记24: Background Writer 后台写进程的原理、参数调优与可观测性演进

纲要

  • Background Writer 进程概述
    • 核心职责:定期将 shared_buffers 中的脏页写入 Page Cache
    • 设计目标:分摊 Checkpoint 的 I/O 压力,减少后端进程(Backend)主动刷脏
  • 核心参数
    • bgwriter_delay
    • bgwriter_lru_maxpages
    • bgwriter_lru_multiplier
    • bgwriter_flush_after
  • 监控视图
    • pg_stat_bgwriter(PG 16 及更早)
      • buffers_clean
      • buffers_backend
      • maxwritten_clean
    • pg_stat_io(PG 16+)
    • pg_stat_checkpointer(PG 17+)
  • PG 17 的重大变更
    • buffers_backendbuffers_backend_fsyncpg_stat_bgwriter 中移除
    • 可观测性向 pg_stat_io 迁移
  • 多语言示例
    • Node.js(pg
    • Go(pgx
    • Python(psycopg2
    • Java(JDBC)
  • 调优策略与最佳实践

一、Background Writer 的设计背景与核心职责

PostgreSQL 使用 shared_buffers 作为共享缓冲区缓存,所有数据页的读写都经过这一层。Checkpoint 进程负责将 shared_buffers 中的所有脏页刷入磁盘,但若两次 Checkpoint 间隔较长且写负载较高,累积的大量脏页会在单一 Checkpoint 瞬间触发巨大的 I/O 突发(I/O Spike),直接影响查询响应时间。

为解决这一问题,PostgreSQL 引入了独立的 Background Writer 后台写进程。其核心职责包括:

  1. 周期性扫描 shared_buffers :以固定间隔唤醒,扫描缓冲区中的脏页。
  2. 将脏页写入 Page CacheBackground Writer 仅将数据写入操作系统 Page Cache,实际的 fsync 落盘由 Checkpoint 进程负责。
  3. Checkpoint 分摊压力 :提前将部分脏页写出,Checkpoint 执行时可跳过已写入的数据块,从而减少 I/O 突发。
  4. 减少后端进程主动刷脏 :当 shared_buffers 空闲缓冲区不足时,后端进程需自行选择脏页淘汰并刷盘(称为 buffers_backend)。Background Writer 若能及时释放缓冲区,可显著降低后端进程的查询延迟。

#mermaid-svg-K07Rse1N1aEnf9Go{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-K07Rse1N1aEnf9Go .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-K07Rse1N1aEnf9Go .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-K07Rse1N1aEnf9Go .error-icon{fill:#552222;}#mermaid-svg-K07Rse1N1aEnf9Go .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-K07Rse1N1aEnf9Go .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-K07Rse1N1aEnf9Go .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-K07Rse1N1aEnf9Go .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-K07Rse1N1aEnf9Go .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-K07Rse1N1aEnf9Go .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-K07Rse1N1aEnf9Go .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-K07Rse1N1aEnf9Go .marker{fill:#333333;stroke:#333333;}#mermaid-svg-K07Rse1N1aEnf9Go .marker.cross{stroke:#333333;}#mermaid-svg-K07Rse1N1aEnf9Go svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-K07Rse1N1aEnf9Go p{margin:0;}#mermaid-svg-K07Rse1N1aEnf9Go .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-K07Rse1N1aEnf9Go .cluster-label text{fill:#333;}#mermaid-svg-K07Rse1N1aEnf9Go .cluster-label span{color:#333;}#mermaid-svg-K07Rse1N1aEnf9Go .cluster-label span p{background-color:transparent;}#mermaid-svg-K07Rse1N1aEnf9Go .label text,#mermaid-svg-K07Rse1N1aEnf9Go span{fill:#333;color:#333;}#mermaid-svg-K07Rse1N1aEnf9Go .node rect,#mermaid-svg-K07Rse1N1aEnf9Go .node circle,#mermaid-svg-K07Rse1N1aEnf9Go .node ellipse,#mermaid-svg-K07Rse1N1aEnf9Go .node polygon,#mermaid-svg-K07Rse1N1aEnf9Go .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-K07Rse1N1aEnf9Go .rough-node .label text,#mermaid-svg-K07Rse1N1aEnf9Go .node .label text,#mermaid-svg-K07Rse1N1aEnf9Go .image-shape .label,#mermaid-svg-K07Rse1N1aEnf9Go .icon-shape .label{text-anchor:middle;}#mermaid-svg-K07Rse1N1aEnf9Go .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-K07Rse1N1aEnf9Go .rough-node .label,#mermaid-svg-K07Rse1N1aEnf9Go .node .label,#mermaid-svg-K07Rse1N1aEnf9Go .image-shape .label,#mermaid-svg-K07Rse1N1aEnf9Go .icon-shape .label{text-align:center;}#mermaid-svg-K07Rse1N1aEnf9Go .node.clickable{cursor:pointer;}#mermaid-svg-K07Rse1N1aEnf9Go .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-K07Rse1N1aEnf9Go .arrowheadPath{fill:#333333;}#mermaid-svg-K07Rse1N1aEnf9Go .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-K07Rse1N1aEnf9Go .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-K07Rse1N1aEnf9Go .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-K07Rse1N1aEnf9Go .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-K07Rse1N1aEnf9Go .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-K07Rse1N1aEnf9Go .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-K07Rse1N1aEnf9Go .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-K07Rse1N1aEnf9Go .cluster text{fill:#333;}#mermaid-svg-K07Rse1N1aEnf9Go .cluster span{color:#333;}#mermaid-svg-K07Rse1N1aEnf9Go div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-K07Rse1N1aEnf9Go .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-K07Rse1N1aEnf9Go rect.text{fill:none;stroke-width:0;}#mermaid-svg-K07Rse1N1aEnf9Go .icon-shape,#mermaid-svg-K07Rse1N1aEnf9Go .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-K07Rse1N1aEnf9Go .icon-shape p,#mermaid-svg-K07Rse1N1aEnf9Go .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-K07Rse1N1aEnf9Go .icon-shape .label rect,#mermaid-svg-K07Rse1N1aEnf9Go .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-K07Rse1N1aEnf9Go .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-K07Rse1N1aEnf9Go .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-K07Rse1N1aEnf9Go :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 定期扫描脏页
fsync
Backend 主动刷脏
用户查询
Backend Process
shared_buffers
Background Writer
Page Cache
Checkpoint
磁盘
shared_buffers 不足

二、Background Writer 核心参数详解

以下参数均在 postgresql.conf 中配置,修改后需重新加载(pg_reload_conf()SELECT pg_reload_conf();)或重启生效。

2.1 bgwriter_delay

含义Background Writer 两次活动轮次之间的延迟时间(毫秒)。

默认值200ms

说明 :每轮中 Background Writer 会写入若干脏页(由后续参数控制),然后休眠 bgwriter_delay 毫秒后重复。若某次扫描未发现任何脏页,进程将进入更长的休眠。注意,许多系统上睡眠延迟的有效分辨率仅为 10 毫秒,设置为非 10 的倍数将向上取整。

调优建议 :写入密集型工作负载可适当降低该值(如 100ms),使 Background Writer 更频繁地工作,分散 I/O 负载。

2.2 bgwriter_lru_maxpages

含义 :每轮中 Background Writer 最多写入的缓冲区数量。

默认值100 个缓冲区。

说明 :设置为 0 将禁用背景写入(Checkpoint 活动不受影响)。该参数与 bgwriter_lru_multiplier 共同决定每轮实际写入量。

调优建议 :I/O 能力充足的系统可适当提高该值(如 200 或更高),让 Background Writer 每轮处理更多脏页。

2.3 bgwriter_lru_multiplier

含义:每轮写入脏页数量的乘数因子。

默认值2.0

说明 :每轮写入量基于"最近几轮中后端进程所需的新缓冲区数量"的平均值乘以该乘数。设置 1.0 代表"准时制"策略------恰好写入预测所需的数量;更大的值提供应对突发需求的缓冲,更小的值则有意将写入工作留给后端进程。

调优建议

  • buffers_backend 过高,可适当提高 bgwriter_lru_multiplier(如 2.53.0),使 Background Writer 更积极地清理脏页。
  • 若 I/O 资源紧张,可降低该值以减少背景写带来的额外 I/O 开销。

2.4 bgwriter_flush_after

含义Background Writer 写入超过此字节数后,强制操作系统将写入下发到底层存储。

默认值512kB(Linux)。

说明 :该参数控制 Background Writer 调用 posix_fadvise 等接口的频率,避免 Page Cache 中积累过多脏页。

三、监控视图与指标解读

3.1 pg_stat_bgwriter(PG 16 及更早)

该视图始终包含单行全局统计数据。核心字段如下:

字段 类型 说明
buffers_checkpoint bigint Checkpoint 期间写入的缓冲区数
buffers_clean bigint Background Writer 写入的缓冲区数
maxwritten_clean bigint Background Writer 因写入过多而停止清理扫描的次数
buffers_backend bigint 后端进程直接写入的缓冲区数
buffers_backend_fsync bigint 后端进程执行 fsync 的次数
buffers_alloc bigint 分配的缓冲区数
sql 复制代码
-- 查询 Background Writer 相关统计(PG 16 及更早)
SELECT
    buffers_clean,
    buffers_backend,
    buffers_backend_fsync,
    maxwritten_clean,
    buffers_alloc
FROM pg_stat_bgwriter;

指标解读

  • buffers_clean 应远大于 buffers_backend,表明大多数刷脏工作由 Background Writer 完成。
  • buffers_backend 过高,说明后端进程频繁主动刷脏,应调优 Background Writer 参数使其更积极地工作。
  • maxwritten_clean 过高说明 Background Writer 每轮写入量达到 bgwriter_lru_maxpages 上限,可考虑提高该值。

3.2 pg_stat_io(PG 16+)

PG 16 引入了 pg_stat_io 视图,按后端类型、对象类型和 I/O 上下文细粒度统计 I/O 操作。这是 PostgreSQL 可观测性的一次重大提升。

sql 复制代码
-- 查询后端进程直接写入的统计(PG 16+)
SELECT
    backend_type,
    object,
    context,
    writes,
    write_bytes
FROM pg_stat_io
WHERE backend_type = 'client backend'
   OR backend_type = 'background writer';

3.3 pg_stat_checkpointer(PG 17+)

PG 17 将 Checkpoint 相关统计从 pg_stat_bgwriter 中剥离,独立为 pg_stat_checkpointer 视图。

sql 复制代码
-- PG 17+ 查询 Checkpoint 统计
SELECT
    checkpoints_timed,
    checkpoints_req,
    checkpoint_write_time,
    checkpoint_sync_time,
    buffers_checkpoint
FROM pg_stat_checkpointer;

四、PG 17 的重大变更:buffers_backend 的移除与可观测性演进

4.1 变更内容

PostgreSQL 17 对 pg_stat_bgwriter 视图进行了重要重构:

  1. 移除的字段buffers_backendbuffers_backend_fsync
  2. 迁移路径 :这些信息现已通过 pg_stat_io 视图提供。
  3. Checkpoint 统计独立checkpoints_timedcheckpoints_reqcheckpoint_write_timecheckpoint_sync_timebuffers_checkpoint 迁移至 pg_stat_checkpointer

4.2 为什么 buffers_backend 是"误导人的指标"

原素材中提到的"buffers_backend 是一个误导人的指标",其根本原因在于:

buffers_backend 不仅统计普通后端进程因缓冲区不足而主动刷脏的次数,还统计了 autovacuum 等后台进程的写入操作。这意味着该指标混杂了多种来源的写入,无法准确反映"查询后端因 Background Writer 工作不及时而被迫刷脏"的真实情况。

PG 17 通过 pg_stat_iobackend_type 字段实现了精确区分,运维人员可按需过滤:

sql 复制代码
-- PG 17+ 精确查询 client backend 的主动写入
SELECT
    writes AS backend_direct_writes,
    write_bytes AS backend_direct_write_bytes
FROM pg_stat_io
WHERE backend_type = 'client backend'
  AND context = 'normal';

4.3 版本兼容建议

PostgreSQL 版本 pg_stat_bgwriter 可用字段 后端写入统计来源
≤ 16 buffers_backend, buffers_backend_fsync pg_stat_bgwriter
≥ 17 移除 buffers_backend pg_stat_io

若需编写跨版本兼容的监控脚本,可先检查 PostgreSQL 主版本号,再选择对应的查询语句。

五、多语言示例

以下示例演示如何通过不同编程语言连接 PostgreSQL,查询 Background Writer 相关统计信息。所有示例均使用原生驱动,并附有对应的 PostgreSQL 原生 SQL 注释。

5.1 Node.js(pg 库)

ts 复制代码
import { Client } from 'pg';

async function getBgWriterStats() {
    const client = new Client({
        host: 'localhost',
        port: 5432,
        database: 'postgres',
        user: 'postgres',
        password: 'your_password',
    });

    await client.connect();

    try {
        // 查询 pg_stat_bgwriter(PG 16 及更早)
        // SELECT buffers_clean, buffers_backend, maxwritten_clean
        // FROM pg_stat_bgwriter;
        const res = await client.query(`
            SELECT
                buffers_clean,
                buffers_backend,
                maxwritten_clean
            FROM pg_stat_bgwriter
        `);

        console.log('Background Writer Stats:');
        console.log(`  buffers_clean: ${res.rows[0].buffers_clean}`);
        console.log(`  buffers_backend: ${res.rows[0].buffers_backend}`);
        console.log(`  maxwritten_clean: ${res.rows[0].maxwritten_clean}`);
    } finally {
        await client.end();
    }
}

getBgWriterStats().catch(console.error);

5.2 Go(pgx 库)

go 复制代码
package main

import (
    "context"
    "fmt"
    "log"

    "github.com/jackc/pgx/v5"
)

func main() {
    conn, err := pgx.Connect(context.Background(), "postgres://postgres:your_password@localhost:5432/postgres")
    if err != nil {
        log.Fatal(err)
    }
    defer conn.Close(context.Background())

    // 查询 pg_stat_bgwriter(PG 16 及更早)
    // SELECT buffers_clean, buffers_backend, maxwritten_clean
    // FROM pg_stat_bgwriter;
    var buffersClean, buffersBackend, maxwrittenClean int64
    err = conn.QueryRow(context.Background(), `
        SELECT
            buffers_clean,
            buffers_backend,
            maxwritten_clean
        FROM pg_stat_bgwriter
    `).Scan(&buffersClean, &buffersBackend, &maxwrittenClean)
    if err != nil {
        log.Fatal(err)
    }

    fmt.Printf("Background Writer Stats:\n")
    fmt.Printf("  buffers_clean: %d\n", buffersClean)
    fmt.Printf("  buffers_backend: %d\n", buffersBackend)
    fmt.Printf("  maxwritten_clean: %d\n", maxwrittenClean)
}

5.3 Python(psycopg2 库)

python 复制代码
import psycopg2

def get_bgwriter_stats():
    conn = psycopg2.connect(
        host="localhost",
        port=5432,
        database="postgres",
        user="postgres",
        password="your_password"
    )

    try:
        with conn.cursor() as cur:
            # 查询 pg_stat_bgwriter(PG 16 及更早)
            # SELECT buffers_clean, buffers_backend, maxwritten_clean
            # FROM pg_stat_bgwriter;
            cur.execute("""
                SELECT
                    buffers_clean,
                    buffers_backend,
                    maxwritten_clean
                FROM pg_stat_bgwriter
            """)
            row = cur.fetchone()

            print("Background Writer Stats:")
            print(f"  buffers_clean: {row[0]}")
            print(f"  buffers_backend: {row[1]}")
            print(f"  maxwritten_clean: {row[2]}")
    finally:
        conn.close()

if __name__ == "__main__":
    get_bgwriter_stats()

5.4 Java(JDBC)

java 复制代码
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.Statement;

public class BgWriterStats {
    public static void main(String[] args) {
        String url = "jdbc:postgresql://localhost:5432/postgres";
        String user = "postgres";
        String password = "your_password";

        try (Connection conn = DriverManager.getConnection(url, user, password);
             Statement stmt = conn.createStatement()) {

            // 查询 pg_stat_bgwriter(PG 16 及更早)
            // SELECT buffers_clean, buffers_backend, maxwritten_clean
            // FROM pg_stat_bgwriter;
            String sql = """
                SELECT
                    buffers_clean,
                    buffers_backend,
                    maxwritten_clean
                FROM pg_stat_bgwriter
            """;

            try (ResultSet rs = stmt.executeQuery(sql)) {
                if (rs.next()) {
                    System.out.println("Background Writer Stats:");
                    System.out.println("  buffers_clean: " + rs.getLong("buffers_clean"));
                    System.out.println("  buffers_backend: " + rs.getLong("buffers_backend"));
                    System.out.println("  maxwritten_clean: " + rs.getLong("maxwritten_clean"));
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

5.5 多语言对比

维度 Node.js (pg) Go (pgx) Python (psycopg2) Java (JDBC)
驱动 pg pgx/v5 psycopg2 org.postgresql.Driver
连接方式 连接字符串 / 对象 DSN 连接字符串 关键字参数 JDBC URL
查询接口 client.query() QueryRow() / Query() cursor.execute() Statement.executeQuery()
结果处理 rows[0].column Scan(&var) fetchone()[idx] rs.getLong("column")
资源管理 手动 end() defer Close() try-finally try-with-resources
类型安全 弱类型(JS) 强类型 弱类型(Python) 强类型
适用场景 快速原型、Web 后端 高性能微服务 数据科学、自动化运维 企业级应用

六、调优策略与最佳实践

6.1 判断 Background Writer 是否"懒惰"

通过对比 buffers_cleanbuffers_backend 可判断 Background Writer 的工作效率:

  • 健康状态buffers_clean >> buffers_backend,表明刷脏工作主要由 Background Writer 完成。
  • 需调优状态buffers_backend 接近或超过 buffers_clean,表明后端进程频繁主动刷脏,应考虑:
    1. 降低 bgwriter_delay(如 100ms),提高唤醒频率。
    2. 提高 bgwriter_lru_maxpages(如 200),增加每轮写入量。
    3. 提高 bgwriter_lru_multiplier(如 2.53.0),使 Background Writer 更积极。

6.2 注意 PG 17 的监控变更

若使用 PG 17 及以上版本:

  • 使用 pg_stat_io 替代 buffers_backend 监控后端写入。
  • 使用 pg_stat_checkpointer 监控 Checkpoint 相关指标。
  • 编写监控脚本时需做版本适配。

6.3 CheckpointBackground Writer 的协同

Background Writer 的目标是减少 而非消除 Checkpoint 的 I/O 压力。过度激进的 Background Writer 可能增加整体 I/O 负载(因为同一脏页可能被多次写出)。调优时需在"减少 Checkpoint 突发"与"控制背景写额外开销"之间取得平衡。

七、官方文档

八、参考链接

总结

Background Writer 是 PostgreSQL 中负责将 shared_buffers 脏页定期写入 Page Cache 的后台进程,其核心价值在于为 Checkpoint 分摊 I/O 压力并减少后端进程主动刷脏带来的查询延迟。关键调优参数包括 bgwriter_delaybgwriter_lru_maxpagesbgwriter_lru_multiplierbgwriter_flush_after,需根据 buffers_cleanbuffers_backend 的比例进行动态调整。

PG 17 将 buffers_backendpg_stat_bgwriter 中移除并迁移至 pg_stat_io,同时将 Checkpoint 统计独立为 pg_stat_checkpointer,显著提升了可观测性的精细度。多语言 Demo 展示了 Node.js、Go、Python、Java 四种主流技术栈访问 pg_stat_bgwriter 的实践方式,便于运维与开发团队快速集成监控能力。

相关推荐
Oll Correct1 小时前
Adobe illustrator 案例六:手枪图标绘制
笔记·ui·adobe·illustrator
Python私教2 小时前
从表格到管理系统:别先写页面,先补齐权限、流程和审计
数据库·后端·架构
陈皮波比茶2 小时前
Redis学习
数据库·redis·学习
—Miss. Z—2 小时前
备份与恢复
数据库·oracle
ltl2 小时前
RocksDB Leveled Compaction:层级不变式与 CompactionPicker
数据库
ltl2 小时前
DuckDB 向量化与 Morsel-Driven Pipeline
数据库
NJCloud2 小时前
TiDB 集群部署与 MySQL 兼容性适配实战
linux·运维·数据库·mysql·tidb
喜欢打篮球的普通人3 小时前
LLVM Backend Lowering 从入门到实战:把 IR 变成机器码的完整链路
android·java·数据库
摇滚侠3 小时前
《Docker技术入门与实战 第4版》阅读笔记 2 Docker 镜像
笔记·docker·容器