1238 字
3 分钟
缓冲池
一次磁盘随机读 10ms,一次内存读 100ns——5 个数量级的差距。数据库 90% 以上的读取命中缓冲池(Buffer Pool),真正走到磁盘的请求不到 10%。可以说,缓冲池就是数据库的”真实磁盘”——调优缓冲池,比换一块更快的 SSD 见效更快。
但缓冲池远不只是一块缓存。LRU 在什么场景下会失效?脏页刷盘如何避免拖垮前台请求?双写缓冲解决什么问题?这一章拆解缓冲池的设计——内存如何弥合磁盘与 CPU 的性能鸿沟。
一、缓冲池概述
1.1 为什么需要缓冲池
graph TB
subgraph 没有缓冲池
REQ1["读取请求"] --> DISK1["磁盘 I/O<br/>10ms/次"]
end
subgraph 有缓冲池
REQ2["读取请求"] --> BP["Buffer Pool<br/>命中: 0.1ms<br/>未命中: 10ms"]
BP -->|"未命中"| DISK2["磁盘 I/O"]
end
style BP fill:#c8e6c9,stroke:#2e7d32
style DISK1 fill:#ffcdd2,stroke:#c62828
style DISK2 fill:#ffe0b2,stroke:#e65100
| 场景 | 无缓冲池 | 有缓冲池(99% 命中率) |
|---|---|---|
| 1000 次读取 | 10 秒 | 0.11 秒 |
| 100 万次读取 | 10,000 秒 | 1.1 秒 |
Important
缓冲池是数据库性能最重要的单一因素。InnoDB 的 innodb_buffer_pool_size 通常建议设为物理内存的 50–80%。缓冲池命中率低于 99% 通常意味着需要增加内存或优化查询。
1.2 缓冲池架构
graph TB
subgraph BufferPool["InnoDB Buffer Pool 架构"]
subgraph Instance0["Instance 0"]
LRU0["LRU List<br/>Old + Young 子列表"]
FLUSH0["Flush List<br/>脏页列表"]
FREE0["Free List<br/>空闲页列表"]
end
subgraph Instance1["Instance 1"]
LRU1["LRU List"]
FLUSH1["Flush List"]
FREE1["Free List"]
end
HASH["Page Hash<br/>快速查找页面"]
HASH --> Instance0
HASH --> Instance1
end
style HASH fill:#e3f2fd,stroke:#1565c0
style LRU0 fill:#c8e6c9,stroke:#2e7d32
style FLUSH0 fill:#fff9c4,stroke:#f9a825
1.3 缓冲池的核心数据结构
// Buffer Pool 核心数据结构struct BufferPool { // 页面哈希表:page_id → buffer_block hash_table_t *page_hash;
// LRU 列表(改进型) struct { ut_list_base(buf_page_t) young; // 热数据(前 5/8) ut_list_base(buf_page_t) old; // 冷数据(后 3/8) } lru;
// 脏页列表 ut_list_base(buf_page_t) flush_list;
// 空闲页列表 ut_list_base(buf_page_t) free_list;
// 统计信息 uint64_t stat_n_page_reads; // 读取次数 uint64_t stat_n_page_read_hits; // 命中次数};
// 缓冲块struct buf_block_t { page_id_t page_id; // 页面 ID byte *frame; // 数据帧(16KB) lsn_t newest_lsn; // 最新修改 LSN bool is_dirty; // 是否脏页 buf_page_state_t state; // 状态 rw_lock_t lock; // 读写锁};二、LRU 替换策略
2.1 经典 LRU 的问题
经典 LRU(Least Recently Used)在数据库场景中有两个严重问题:
| 问题 | 说明 | 后果 |
|---|---|---|
| 全表扫描污染 | 一次全表扫描会将所有热点数据挤出缓冲池 | 扫描后缓存命中率骤降 |
| 预读污染 | 预读的页面可能不会被访问 | 浪费缓存空间 |
2.2 InnoDB 改良 LRU
InnoDB 将 LRU 列表分为两个子列表:
graph LR
subgraph InnoDB_LRU["InnoDB 改良 LRU"]
direction LR
YOUNG["Young 子列表(热数据)<br/>前 5/8<br/>最近被访问的页面"]
OLD["Old 子列表(冷数据)<br/>后 3/8<br/>新读入的页面先放这里"]
end
NEW_PAGE["新读入的页面"] -->|"插入 Old 头部"| OLD
OLD -->|"再次被访问<br/>且超过停留时间"| YOUNG
YOUNG -->|"淘汰"| EVICT["淘汰页面"]
style YOUNG fill:#c8e6c9,stroke:#2e7d32
style OLD fill:#fff9c4,stroke:#f9a825
style EVICT fill:#ffcdd2,stroke:#c62828
// InnoDB LRU 操作void buf_page_accessed(buf_block_t *block) { if (block->in_old_list) { // 页面在 Old 子列表 // 检查是否在 Old 列表停留足够长时间 if (time_since_first_access(block) > innodb_old_blocks_time) { // 停留时间足够长,移到 Young 子列表头部 move_to_young_head(block); block->in_old_list = false; } // 否则留在 Old 子列表(防止全表扫描污染) } else { // 页面在 Young 子列表 // 移到 Young 子列表头部 move_to_young_head(block); }}2.3 LRU 变体对比
| 变体 | 核心思想 | 抗扫描污染 | 实现复杂度 | 使用场景 |
|---|---|---|---|---|
| LRU | 最近访问的保留 | 差 | 低 | 简单缓存 |
| LRU-2 | 记录最近 2 次访问时间 | 好 | 中 | 通用 |
| 2Q | 两个队列:A1(最近)+ Am(频繁) | 好 | 中 | 数据库 |
| LIRS | 区分热数据和冷数据 | 最好 | 高 | 学术 |
| InnoDB LRU | Old/Young 分区 + 停留时间 | 好 | 中 | InnoDB |
| Clock | 近似 LRU,环形扫描 | 一般 | 低 | 操作系统 |
2.4 2Q 算法详解
# 2Q(Two Queues)算法class TwoQ: def __init__(self, capacity): self.capacity = capacity self.a1_in = [] # 最近访问队列(FIFO) self.a1_out = [] # 最近驱逐队列(只存 Key) self.am = OrderedDict() # 频繁访问队列(LRU)
self.a1_in_max = int(capacity * 0.25) # A1in 占 25% self.am_max = int(capacity * 0.75) # Am 占 75%
def get(self, key): if key in self.am: # 命中 Am:移到头部 self.am.move_to_end(key) return self.am[key]
if key in self.a1_out: # 命中 A1out:提升到 Am(第二次访问) self.a1_out.remove(key) self.am[key] = load_from_disk(key) self.am.move_to_end(key) self._evict_if_needed() return self.am[key]
# 未命中:从磁盘读取,放入 A1in self.a1_in.append(key) self._evict_if_needed() return load_from_disk(key)
def _evict_if_needed(self): # A1in 满了:移到 A1out if len(self.a1_in) > self.a1_in_max: old_key = self.a1_in.pop(0) self.a1_out.append(old_key)
# A1out 满了:丢弃最旧的 while len(self.a1_out) > self.a1_in_max: self.a1_out.pop(0)
# Am 满了:LRU 淘汰 while len(self.am) > self.am_max: self.am.popitem(last=False)三、脏页刷盘
3.1 脏页的产生与刷盘
graph TB
subgraph 脏页生命周期["脏页生命周期"]
READ["读取页面<br/>从磁盘到 Buffer Pool"] --> MODIFY["修改页面<br/>标记为脏页"]
MODIFY --> FLUSH_LIST["加入 Flush List"]
FLUSH_LIST --> BG_FLUSH["后台刷盘<br/>Page Cleaner 线程"]
BG_FLUSH --> DISK["写入磁盘<br/>Doublewrite → 数据文件"]
DISK --> CLEAN["标记为干净页"]
end
style MODIFY fill:#fff9c4,stroke:#f9a825
style BG_FLUSH fill:#e3f2fd,stroke:#1565c0
style DISK fill:#c8e6c9,stroke:#2e7d32
3.2 刷盘策略
| 策略 | 触发条件 | 刷盘量 | 影响 |
|---|---|---|---|
| 自适应刷盘 | 脏页比例超过阈值 | 动态调整 | 平衡 I/O |
| 紧急刷盘 | 脏页比例超过 75% | 大量 | 可能阻塞写入 |
| 检查点刷盘 | 检查点推进 | 指定 LSN 之前的脏页 | 推进检查点 |
| LRU 刷盘 | 需要空闲页 | LRU 尾部的脏页 | 为新页面腾空间 |
# InnoDB 自适应刷盘算法def adaptive_flushing(dirty_page_ratio, redo_generation_rate, io_capacity): """自适应刷盘速率""" # 基于脏页比例 flush_rate_by_ratio = dirty_page_ratio * io_capacity * 2
# 基于 Redo Log 生成速率 # 如果 Redo 生成很快,需要加速刷盘以推进检查点 flush_rate_by_lsn = redo_generation_rate * 1.1
# 取较大值 flush_rate = max(flush_rate_by_ratio, flush_rate_by_lsn)
# 不超过最大 I/O 容量 flush_rate = min(flush_rate, io_capacity * 2)
return flush_rate3.3 刷盘与 Doublewrite
// InnoDB Doublewrite 流程void buf_flush_page(buf_block_t *block) { // 1. 写入 Doublewrite Buffer // Doublewrite 是共享表空间中 2MB 的连续区域 // 可以容纳 128 个页面(128 × 16KB = 2MB) doublewrite_write(block); fdatasync(doublewrite_fd); // 确保 Doublewrite 先落盘
// 2. 写入数据文件 pwrite(block->file_fd, block->frame, PAGE_SIZE, block->offset); fdatasync(block->file_fd);
// 3. 标记为干净页 block->is_dirty = false;
// 崩溃恢复时: // 如果数据文件中的页面损坏(torn page) // 从 Doublewrite Buffer 恢复完整页面}四、预读机制
4.1 预读类型
| 类型 | 触发条件 | 预读范围 | 效果 |
|---|---|---|---|
| 随机预读 | 同一 Extent 中连续 13+ 个页面被访问 | 整个 Extent(1MB) | 适合索引扫描 |
| 线性预读 | 顺序访问同一 Extent 的页面 | 下一个 Extent | 适合全表扫描 |
-- InnoDB 预读配置-- 线性预读阈值SET GLOBAL innodb_read_ahead_threshold = 56;-- 一个 Extent(64 页)中有 56 页被顺序访问时触发预读
-- 随机预读(通常关闭)SET GLOBAL innodb_random_read_ahead = 0;Note
在 OLTP 场景中,预读经常被关闭,因为随机访问模式不适合预读。在 OLAP 场景中,预读可以显著提升全表扫描性能。详见 Ch8 读路径。
五、缓冲池并发控制
5.1 Buffer Pool Instance 分片
InnoDB 将 Buffer Pool 分为多个 Instance,减少锁竞争:
// Buffer Pool Instance 分片// page_id % innodb_buffer_pool_instances 决定使用哪个 Instance
int get_buffer_pool_instance(page_id_t page_id) { return page_id % innodb_buffer_pool_instances;}
// 每个 Instance 有独立的:// - LRU List// - Flush List// - Free List// - Page Hash// - 互斥锁
// 推荐配置:// Instance 数量 = CPU 核心数 / 2(最多 64)5.2 页面锁
// Buffer Pool 页面锁struct buf_block_t { // 读写锁:保护页面内容 rw_lock_t lock; // S 锁(读)/ X 锁(写)
// 互斥锁:保护页面元数据 mutex_t mutex; // 保护 state, is_dirty 等};
// 获取页面的流程buf_block_t *buf_page_get(page_id_t page_id, rw_lock_mode_t mode) { // 1. 在 Page Hash 中查找 buf_block_t *block = hash_search(page_id);
if (block == NULL) { // 2. 未命中:从磁盘读取 block = buf_read_page(page_id); }
// 3. 固定页面(防止被淘汰) block->fix_count++;
// 4. 获取页面锁 if (mode == RW_S_LOCK) { rw_lock_s_lock(&block->lock); } else { rw_lock_x_lock(&block->lock); }
return block;}六、缓冲池调优
6.1 关键参数
| 参数 | 推荐值 | 说明 |
|---|---|---|
innodb_buffer_pool_size | 物理内存 50–80% | 最重要的参数 |
innodb_buffer_pool_instances | CPU 核心数 / 2 | 减少锁竞争 |
innodb_old_blocks_pct | 37(默认) | Old 子列表比例 |
innodb_old_blocks_time | 1000ms(默认) | 页面在 Old 列表的停留时间 |
innodb_max_dirty_pages_pct | 75(默认) | 触发加速刷盘的脏页比例 |
innodb_io_capacity | SSD: 2000 | 每秒 I/O 操作数 |
innodb_io_capacity_max | SSD: 4000 | 最大 I/O 容量 |
6.2 命中率监控
-- Buffer Pool 命中率SELECT (1 - Innodb_buffer_pool_reads / Innodb_buffer_pool_read_requests) * 100 AS hit_ratioFROM ( SELECT variable_value + 0 AS Innodb_buffer_pool_reads FROM performance_schema.global_status WHERE variable_name = 'Innodb_buffer_pool_reads') r,( SELECT variable_value + 0 AS Innodb_buffer_pool_read_requests FROM performance_schema.global_status WHERE variable_name = 'Innodb_buffer_pool_read_requests') rr;
-- 目标:命中率 > 99.5%-- 如果低于 99%,考虑增加 Buffer Pool 大小
-- Buffer Pool 使用情况SELECT page_type, SUM(data_size) / 1024 / 1024 AS size_mb, COUNT(*) AS page_countFROM performance_schema.innodb_buffer_pageGROUP BY page_typeORDER BY size_mb DESC;6.3 PostgreSQL 缓冲区调优
-- PostgreSQL shared_buffers-- 推荐:物理内存的 25%-- shared_buffers = '16GB'
-- 查看缓冲区统计SELECT sum(heap_blks_hit) AS hits, sum(heap_blks_read) AS reads, sum(heap_blks_hit) / NULLIF(sum(heap_blks_hit + heap_blks_read), 0) AS hit_ratioFROM pg_statio_user_tables;
-- 目标:命中率 > 99%七、实战:缓冲池观察
7.1 Buffer Pool 状态
-- InnoDB Buffer Pool 状态SHOW ENGINE INNODB STATUS\G
-- 关键信息:-- Buffer pool size: 262144 pages (4GB)-- Free buffers: 1024 pages-- Database pages: 261120 pages-- Old database pages: 96614 pages-- Modified db pages: 15234 pages-- Buffer pool hit rate: 1000 / 1000-- Young-making: 1234567-- Not young-making: 8765437.2 热点页面分析
-- 查看最热的索引页面SELECT table_name, index_name, COUNT(*) AS page_count, SUM(data_size) / 1024 / 1024 AS size_mbFROM performance_schema.innodb_buffer_pageWHERE table_name IS NOT NULLGROUP BY table_name, index_nameORDER BY page_count DESCLIMIT 20;7.3 脏页刷盘监控
-- 脏页统计SHOW STATUS LIKE 'Innodb_buffer_pool_pages_dirty';SHOW STATUS LIKE 'Innodb_buffer_pool_pages_total';
-- 刷盘统计SHOW STATUS LIKE 'Innodb_data_writes';SHOW STATUS LIKE 'Innodb_data_written';SHOW STATUS LIKE 'Innodb_fsyncs';
-- 计算脏页比例SELECT variable_value + 0 AS dirty_pages, (SELECT variable_value + 0 FROM performance_schema.global_status WHERE variable_name = 'Innodb_buffer_pool_pages_total') AS total_pages, (variable_value + 0) / (SELECT variable_value + 0 FROM performance_schema.global_status WHERE variable_name = 'Innodb_buffer_pool_pages_total') * 100 AS dirty_pctFROM performance_schema.global_statusWHERE variable_name = 'Innodb_buffer_pool_pages_dirty';Warning
缓冲池大小不是越大越好。过大的缓冲池会导致页面淘汰策略失效——LRU 链表过长时,热点页面可能在被访问前就被淘汰。一般建议缓冲池占物理内存的 60-80%,留出足够空间给操作系统页缓存和连接缓冲。
八、总结
| 主题 | 核心要点 | 关键词 |
|---|---|---|
| 缓冲池架构 | Page Hash + LRU + Flush List + Free List,多 Instance 减少锁竞争 | 多 Instance, 锁竞争 |
| LRU 变体 | InnoDB 改良 LRU 用 Old/Young 分区 + 停留时间防止全表扫描污染 | 改良 LRU, Old/Young |
| 脏页刷盘 | 自适应刷盘平衡 I/O 负载,紧急刷盘防止脏页积压 | 自适应刷盘, 脏页 |
| Doublewrite | 防止页面写撕裂,增加约 10–15% 写入开销 | 写撕裂, 双写 |
| 预读 | 线性预读适合顺序扫描,OLTP 场景通常关闭 | 线性预读, 场景适配 |
| 调优 | Buffer Pool 大小是最重要参数,命中率 > 99% 是目标 | 命中率, 容量调优 |
支持与分享
如果这篇文章对你有帮助,欢迎支持作者或分享给更多人
部分信息可能已经过时
相关文章 智能推荐
1
B 树深入
存储 深入 B+ 树的页面组织、分裂与合并算法、Latch 并发控制、前缀压缩与批量加载,理解读优化存储结构的设计精髓。
2
MVCC 与版本管理
存储 深入 MVCC 与版本管理——Undo Log MVCC(InnoDB)、Append-Only MVCC(PostgreSQL)、VACUUM 清理、快照隔离、版本链,理解如何实现无锁并发读。
3
磁盘与 SSD 物理结构
存储 深入磁盘与 SSD 物理结构——HDD 的磁道/柱面/扇区、寻道与旋转延迟,SSD 的 NAND 闪存页/块/擦除、FTL 映射、写入放大与磨损均衡,理解存储系统的物理约束。
4
读路径
存储 深入存储引擎的读路径——从读取请求到数据的完整流程——B+ 树遍历、LSM 树多层级查找、Bloom Filter 过滤、Block Cache 缓存、预取策略,对比 InnoDB 与 RocksDB 的读路径差异。
5
LSM 树深入
存储 深入 LSM 树——MemTable 与 SSTable 的组织、Leveled/Tiered/FIFO Compaction 策略、写放大分析、布隆过滤器与分区索引,理解写优化存储结构的设计精髓。






