PostgreSQL 缓冲区管理器
为什么需要缓冲区管理器
应用执行: SELECT * FROM users WHERE id = 1;
问题:每次查询都读磁盘?
回答:不行,太慢了
解决:把热点数据放在内存里PostgreSQL 使用 共享缓冲区(shared_buffers) 来缓存磁盘上的数据页。
缓冲区管理器架构
┌─────────────────────────────────────────────────┐
│ PostgreSQL │
│ ┌─────────────────────────────────────────┐ │
│ │ 共享缓冲区 │ │
│ │ ┌─────┬─────┬─────┬─────┬─────┬─────┐ │ │
│ │ │ 0 │ 1 │ 2 │ ... │n-1 │ n │ │ │
│ │ │8KB │8KB │8KB │ │8KB │8KB │ │ │
│ │ └──┬──┴──┬──┴──┬──┴──┬──┴──┬──┴──┬──┘ │ │
│ │ │ │ │ │ │ │ │ │
│ │ ↓ ↓ ↓ ↓ ↓ ↓ │ │
│ │ ┌──────────────────────────────────┐ │ │
│ │ │ 缓冲区描述符数组 │ │ │
│ │ │ (tag, pin_count, is dirty, etc) │ │ │
│ │ └──────────────────────────────────┘ │ │
│ └─────────────────────────────────────────┘ │
│ ↓ │
│ ┌─────────────────────────────────────────┐ │
│ │ 操作系统缓存 │ │
│ └─────────────────────────────────────────┘ │
│ ↓ │
│ 磁盘 │
└─────────────────────────────────────────────────┘核心概念
数据页(Page/Block)
- 每个页大小:8KB(不可配置)
- 所有数据(表、索引)都按页存储
- 读磁盘的最小单位
缓冲区描述符
每个缓冲区有一个描述符,包含:
tag: -- 数据页的标识 (relfilenode, forkNum, blockNum)
state: -- 状态 (pinned, dirty, etc)
usage_count: -- 使用频率(用于淘汰)
refcount: -- 正在访问的进程数页面替换策略
使用 时钟扫描算法(Clock-Sweep):
sql
-- 等效于 LRU,但更高效
-- 每个缓冲区有一个 usage_count
-- 每被访问一次 count++
-- 需要淘汰时,扫描一圈,count--
-- count 归零的可以被淘汰工作流程
读取页面
sql
SELECT * FROM users WHERE id = 1;
-- 1. 计算页面的 tag(哪个表,哪个块)
-- 2. 在缓冲区中查找 tag
-- 找到 → 命中 (hit)
-- 未找到 → 需要加载 (miss)
-- 3. 如果 miss:
-- a. 找一个空闲/可淘汰的缓冲区
-- b. 如果淘汰的缓冲区是脏的,先写盘
-- c. 从磁盘读取目标页到缓冲区
-- d. 返回数据写入页面
sql
UPDATE users SET name = '新名字' WHERE id = 1;
-- 1. 找到对应的缓冲区
-- 2. 在缓冲区中直接修改(写的是内存)
-- 3. 标记页面为 dirty(需要写盘)
-- 4. 后续由 CHECKPOINT 或后台进程刷盘脏页刷新
sql
-- 检查点触发全量刷新
CHECKPOINT;
-- 或者 bgwriter 后台慢慢刷
-- 配置项:
show bgwriter_delay; -- 刷新间隔 (200ms)
show bgwriter_lru_maxpages; -- 每次最多刷多少页
show bgwriter_lru_multiplier; -- 预估下次需要的缓冲区的倍数shared_buffers 配置
sql
-- 查看当前配置
SHOW shared_buffers;
-- 设置(需要重启)
-- postgresql.conf:
-- shared_buffers = 4GB (建议:系统内存的 25%)配置建议
| 系统内存 | shared_buffers | 说明 |
|---|---|---|
| 1GB | 256MB | 最小建议 |
| 2GB | 512MB | |
| 4GB | 1GB | |
| 8GB | 2GB | |
| 16GB | 4GB | |
| 32GB | 8GB | |
| 64GB | 16GB | |
| 128GB | 32GB | 最大建议 |
注意:不是越大越好,超过 40% 可能适得其反。
与操作系统缓存的关系
PostgreSQL 缓冲区 ←→ OS 缓存
↓ ↓
直接 IO? 间接 IO
↓ ↓
磁盘 磁盘两种 IO 模式
直接 IO(Direct IO):
- PostgreSQL 直接读写磁盘
- 绕过 OS 缓存
- 需要自己管理缓存
- Linux 支持,PostgreSQL 不常用
间接 IO(Buffered IO):
- PostgreSQL 通过 OS 缓存读写
- 默认模式
- OS 负责缓存管理
典型读取过程
sql
-- 查询
SELECT * FROM users WHERE id = 1;
-- 第一次读取:
-- shared_buffers miss → OS cache miss → 磁盘读取 → 放入 OS cache 和 shared_buffers
-- 第二次读取:
-- shared_buffers hit → 直接返回(快!)缓存命中率
sql
-- 查看表的缓存命中率
SELECT
relname,
heap_blks_read, -- 从磁盘/ OS 读取
heap_blks_hit, -- 从 shared_buffers 命中
round(100 * heap_blks_hit /
nullif(heap_blks_hit + heap_blks_read, 0), 2) as hit_ratio
FROM pg_statio_user_tables
WHERE schemaname = 'public'
ORDER BY heap_blks_read DESC;命中率参考:
- 95%+:很好
- 80-95%:一般,需要优化
- <80%:糟糕,需要关注
索引缓存命中率
sql
SELECT
relname,
indexrelname,
idx_blks_read,
idx_blks_hit,
round(100 * idx_blks_hit /
nullif(idx_blks_hit + idx_blks_read, 0), 2) as hit_ratio
FROM pg_statio_user_indexes
WHERE schemaname = 'public'
ORDER BY idx_blks_read DESC;常见问题
缓存太小
sql
-- 症状:频繁的磁盘 IO
-- 解决:增加 shared_buffers 或减少数据量
-- 查看 IO 情况
SELECT * FROM pg_stat_activity
WHERE state = 'active'
AND wait_event_type = 'BufferPin';脏页过多
sql
-- 症状:写入变慢,CHECKPOINT 耗时过长
-- 解决:调整 checkpoint 参数
SHOW checkpoint_timeout; -- 5min
SHOW checkpoint_completion_target; -- 0.9 (分摊刷新,0.1 的时间完成)
-- checkpoint 不是瞬间完成,而是渐进式内存碎片
sql
-- 症状:shared_buffers 很多但效率不高
-- 解决:VACUUM 清理碎片,定期 REINDEX内部结构(进阶)
缓冲区表(Buffer Table)
c
// 内部实现(简化)
typedef struct BufferDesc {
BufferTag tag; // 唯一标识
uint32 usage_count; // 使用频率
unsigned shared:1; // 共享标记
unsigned dirty:1; // 脏页标记
unsigned pinned:1; // 固定标记
LocalBufferPriority occ:16; // 用于轻量锁
} BufferDesc;BufferTag 结构
c
typedef struct BufferTag {
Oid relfilenode; // 表/索引文件
ForkNumber forkNum; // 哪个 fork(main, fsm, vm)
BlockNumber blockNum; // 第几个块
} BufferTag;IO 操作类型
sql
-- 普通读取
-- 1. 尝试从 shared_buffers 读
-- 2. miss 时从 OS cache 读
-- 3. OS cache miss 才读磁盘
-- 同步读取(VISIBILITYMAP 之类)
-- 可能直接读磁盘绕过 shared_buffers
-- 批量读取(DECLARE CURSOR FORWARD)
-- 预读机制,读取相邻页面监控与调优
关键监控指标
sql
-- 1. 缓冲区使用统计
SELECT * FROM pg_buffer_cache();
-- 2. 脏页比例
SELECT
(SELECT COUNT(*) FROM pg_buffer_cache WHERE isdirty) as dirty_pages,
(SELECT COUNT(*) FROM pg_buffer_cache) as total_pages;
-- 3. 等待缓冲区的事件
SELECT * FROM pg_stat_activity
WHERE wait_event = 'BufferPin';调优参数汇总
sql
-- postgresql.conf
shared_buffers = 4GB -- 建议 RAM 的 25%
effective_cache_size = 12GB -- 建议 RAM 的 75%
work_mem = 64MB -- 每个排序/哈希操作可用内存
maintenance_work_mem = 512MB -- VACUUM, CREATE INDEX 等操作
-- 刷脏页策略
checkpoint_timeout = 10min
checkpoint_completion_target = 0.9
bgwriter_delay = 200ms
bgwriter_lru_maxpages = 100
bgwriter_lru_multiplier = 2.0设计思想
1. 局部性原理
- 刚访问的数据很可能再次访问
- 访问过的数据附近的数据很可能被访问
2. 时间局部性
- 热点数据常驻内存
3. 空间局部性
- 预读相邻页面
- 利用顺序 IO
4. 写合并
- 多次小写变成一次大写
- 通过 WAL 保证持久性
- 脏页由后台慢慢刷盘与其他数据库对比
| 特性 | PostgreSQL | MySQL (InnoDB) | Oracle |
|---|---|---|---|
| 缓冲单位 | 8KB 页 | 16KB 页 | 可配置 |
| 缓冲算法 | Clock-Sweep | LRU + 改进 | LRU |
| 日志 | WAL | redo log | redo log |
| 脏页刷新 | bgwriter/checkpoint | master thread | DBWn |
| 内存管理 | 固定大小 | 动态 | SGA/PGA |