PostgreSQLNotes

第 19 章:Buffer Manager

zjc 于 2026-01-19 发布

这是《PostgreSQL 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开,代码块保留了原书可直接运行的版本。 Buffer Manager 管理 shared_buffers 中的数据页缓存,决定哪些页留在内存、哪些页被写出以及如何减少磁盘 IO。

19.1 页面结构

PostgreSQL 数据文件按固定页组织,通常为 8KB:

Table file
  Page 0
  Page 1
  Page 2

每个页包含页头、行指针、空闲空间和行数据。表和索引的页结构不同,但都由 Buffer Manager 统一缓存。

19.2 shared_buffers

SHOW shared_buffers;
SHOW effective_cache_size;

shared_buffers 是 PostgreSQL 自己申请的共享缓存;effective_cache_size 告诉优化器操作系统缓存大概有多少,不实际分配内存。

经验起点:

专用数据库服务器:内存的 25% 左右
再结合业务、磁盘和并发压测调整

19.3 缓冲命中

查看数据库级:

SELECT
    blks_read,
    blks_hit,
    round(blks_hit::numeric /
          greatest(blks_hit + blks_read, 1), 4) AS hit_ratio
FROM pg_stat_database
WHERE datname = current_database();

命中率低不一定是 shared_buffers 不足,也可能是:

  1. 冷数据或随机访问;
  2. 扫描量过大;
  3. 索引缺失;
  4. 内存被并发竞争;
  5. 操作系统缓存不足。

19.4 脏页刷写

相关进程:

进程 职责
backend 可能直接刷脏页
background writer 提前刷可淘汰脏页
checkpointer 检查点统一刷盘

查看:

SELECT * FROM pg_stat_bgwriter;

19.5 检查点与 IO 抖动

检查点把大量脏页刷盘,可能引发 IO 高峰:

checkpoint_completion_target
checkpoint_timeout
max_wal_size

适当增加 max_wal_size、拉长完成目标可以分散写入,但会延长恢复窗口。

19.6 work_mem 与中间结果

SHOW work_mem;

用于排序、哈希、聚合等操作。每个查询可能有多个节点同时使用该内存,并发越大总内存越高。

溢写磁盘示例:

Sort Method: external merge
Hash Batch:...

处理:

  1. 会话级调整;
  2. 优化 SQL;
  3. 减少数据量;
  4. 建匹配索引;
  5. 控制并发。

19.7 预读与顺序扫描

顺序扫描会读取连续页,操作系统和存储层可能做预读。大范围分析查询应关注:

  1. shared_blks_read;
  2. 临时文件;
  3. IO 等待事件;
  4. 索引选择;
  5. 分区剪枝。

19.8 缓存观测

pg_buffercache 扩展:

CREATE EXTENSION IF NOT EXISTS pg_buffercache;

SELECT
    c.relname,
    count(*) AS buffer_pages,
+    pg_size_pretty(count(*) * 8192) AS size
FROM pg_buffercache b
JOIN pg_class c ON c.oid = b.relfilenode
GROUP BY c.relname
ORDER BY buffer_pages DESC;

19.9 常见问题

现象 方向
命中率下降 工作集变大或扫描变多
IO wait 高 缓存、索引、检查点
数据库重启后慢 缓存冷启动
临时文件多 work_mem 或 SQL 中间结果
IO 周期抖动 检查点

本章小结

Buffer Manager 决定数据页的内存命中率。shared_buffers、effective_cache_size、work_mem、检查点和后台刷写共同影响 IO 表现。优化要结合 buffers 执行计划和系统指标,而不是只调一个参数。

思考题

  1. shared_buffers 和 effective_cache_size 有什么区别?
  2. 命中率低说明什么?
  3. work_mem 为什么不能盲目调大?
  4. 检查点如何造成 IO 抖动?
  5. 如何查看哪些对象占用缓冲?