44.3 存储引擎:缓冲池与页面管理
一句话
缓冲池是数据库 “用内存换 IO” 的核心:页在内存里就直接返回,不在就从磁盘读并淘汰一个页;命中率每下降 10%,磁盘 IO 可能翻倍——所以淘汰算法、预读与页大小,比 “加索引” 更能决定读性能。
缓冲池的四个部件
| 部件 | 作用 | 关键点 |
|---|---|---|
| 页表(hash) | page_id → frame | 命中路径必须无锁或分段锁 |
| 淘汰链表 | 记录 “谁最该被替换” | LRU / Clock / LRU-K |
| 脏页链表 | 已改未刷的页 | 刷盘策略影响 fsync 与崩溃恢复 |
| 预读 | 顺序扫描时提前读后续页 | 线性预读 / 随机预读 |
实测:LRU 命中率随容量的变化(Zipf α=1.2,1000 页工作集)
| 缓冲池容量 | 命中率 |
|---|---|
| 50 页 | 66.4% |
| 100 页 | 75.7% |
| 200 页 | 83.3% |
注意曲线是边际递减的:容量翻 4 倍(50→200),命中率只从 66% 到 83%。这意味着 “盲目加内存” 的收益有限;要提升命中率,更该做的是减少工作集(索引、分区、列裁剪)或改进访问局部性。
磁盘 IO ≈ 总访问 × (1 - 命中率)
命中率 66% → 34% 走磁盘;83% → 17% 走磁盘(IO 减半)
LRU 的三个经典缺陷与对策
| 缺陷 | 现象 | 对策 |
|---|---|---|
| 顺序扫描污染 | 一次全表扫描把热页全冲掉 | LRU-K / 分段 LRU(新页进 “冷区”) |
| 偶发大查询 | 大查询挤掉 OLTP 热数据 | 按租户/查询类型分池 |
| 预读浪费 | 预读的页没被用到 | 自适应预读(按命中反馈调整) |
// 简化 LRU-K(2):记录最近两次访问,K 次以上才算 “热”
type Frame struct{ id int; last, prev int64 }
func (c *Cache) Touch(f *Frame, now int64) {
f.prev, f.last = f.last, now // 第 2 次访问才进入热区
if f.prev != 0 { c.hot.MoveToFront(f) }
}
反模式
| 反模式 | 后果 |
|---|---|
| 缓冲池用全局大锁 | 高并发下命中路径变串行,CPU 打满 |
| 脏页刷盘不限速 | 刷盘风暴抢 IO,查询延迟尖刺 |
| 全表扫描与 OLTP 共用池 | 一次报表冲掉热数据,命中率断崖 |
| 页太小(4KB)存大行 | 链式页/溢出页,读放大 |
| 页太大(64KB) | 内存碎片、预读浪费、写放大 |
| 不监控命中率 | 以为 “内存够”,其实一直读盘 |
三条铁律
🔑 命中率是读性能的一阶变量;先看它,再谈索引。
🔑 扫描类查询必须与 OLTP 隔离(分池/只读副本)。
🔑 脏页刷盘要限速 + 可观测,否则它会在高峰期制造尖刺。
自检
Q1 为什么命中率从 66% 提到 83%,磁盘 IO 几乎减半?
▸ 答案:因为 IO 次数正比于未命中率:66% → 34% 走盘,83% → 17% 走盘,后者约为前者的一半。命中率的小幅提升,对 IO 是成比例的放大效应——这也是 “缓冲池够大” 比 “索引更多” 更直接的原因。
Q2 顺序扫描为什么会 “污染” LRU?
▸ 答案:因为 LRU 按 “最近使用” 淘汰,扫描会把大量只用一次的页放进热区,把真正的热页挤出去(cache pollution)。对策是 LRU-K/分段 LRU:新页先进冷区,被访问多次才升级,扫描页很快被淘汰。
Q3 页大小怎么选?
▸ 答案:按行大小与访问模式:行小、点查多 → 小页(8/16KB)减少读放大;行大、顺序扫描多 → 大页减少页链。没有通用最优,PostgreSQL 默认 8KB、InnoDB 16KB 都是折中。改了页大小等于改存储格式,迁移成本高。
Q4 脏页为什么要 “后台刷 + 检查点” 双机制?
▸ 答案:后台刷控制内存里脏页比例(避免一次刷太多),检查点控制恢复时间(把日志位点前推)。只有后台刷,崩溃恢复要从很早的日志重放;只有检查点,脏页会堆积到内存压力才集中刷,造成尖刺。
Q5 为什么缓冲池命中路径要避免全局锁?
▸ 答案:因为读路径是最高频的操作(每次查询多次页访问),全局锁会把并发读串行化,CPU 花在等锁上。优化是分段锁/每核本地缓存(类似 LSM 的 memtable),用 “少量不一致” 换吞吐。
Q6 预读什么时候会帮倒忙?
▸ 答案:随机访问时:预读把 “用不到的页” 读进来,既浪费 IO 又污染缓存。自适应预读会观察 “预读页是否被用到”,动态关闭;很多数据库在检测到随机访问模式时会退化为 “只读当前页”。
Q7 内存加到多少算 “够”?
▸ 答案:没有固定值,看工作集大小与命中率曲线:当 “再加内存命中率提升 < 2%” 时,边际收益已经很低,应该转而优化工作集(分区、索引、列裁剪)。用命中率曲线做决策,比 “内存占比 70%” 这种经验值可靠。
关联
- 前置:44.1 存储引擎:B+ 树与 LSM 树、44.2 存储引擎:WAL 与崩溃恢复
- 并列:11.2 内存管理与虚拟内存、44.5 存储引擎:读写放大与空间放大
- 下游:44.8 索引原理 (B+ 树 / 哈希 / 覆盖索引 / 最左前缀)、44.9 执行计划与慢查询