这是《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 不足,也可能是:
- 冷数据或随机访问;
- 扫描量过大;
- 索引缺失;
- 内存被并发竞争;
- 操作系统缓存不足。
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:...
处理:
- 会话级调整;
- 优化 SQL;
- 减少数据量;
- 建匹配索引;
- 控制并发。
19.7 预读与顺序扫描
顺序扫描会读取连续页,操作系统和存储层可能做预读。大范围分析查询应关注:
- shared_blks_read;
- 临时文件;
- IO 等待事件;
- 索引选择;
- 分区剪枝。
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 执行计划和系统指标,而不是只调一个参数。
思考题
- shared_buffers 和 effective_cache_size 有什么区别?
- 命中率低说明什么?
- work_mem 为什么不能盲目调大?
- 检查点如何造成 IO 抖动?
- 如何查看哪些对象占用缓冲?