跳到内容

第 5 卷 / 第 4 章 / 第 1 课

4.1 页式存储、记录布局与 Buffer Pool ​

查询返回一行,不代表磁盘只读一行。存储引擎通常按页搬运数据,用缓冲池复用已读的页。本课沿着一次读取检查页布局、记录定位和脏页写回,解释为什么索引查找可能比顺序扫描更慢,也解释内存命中为什么还不等于事务可见。

本课目标 ​

  • 区分 row、tuple、page、extent/file 与 block device;
  • 解释 slotted page 如何容纳变长 records;
  • 正确比较 sequential scan 与 random lookup;
  • 理解 buffer pool 的 page table、pin、dirty 与 eviction;
  • 识别 OS page cache、database cache 与 durability 的不同职责。

一次查询经过哪些层 ​

text
SQL executor
  -> access method (heap/index)
  -> buffer manager
  -> database page in memory
  -> database file
  -> filesystem / block layer
  -> storage device

query executor 通常不会为每行直接调用一次磁盘读取。它请求 page,buffer manager 判断 page 是否已经驻留;缺页时才通过文件与操作系统读取。

Sequential scan 不是“随机 I/O × 页数” ​

假设 table 有 5,000,000 pages,scan 确实要检查大量 page,但它通常按物理/逻辑 page 顺序读取。操作系统与设备能 readahead、merge requests,现代 SSD 也有并行队列。

因此不能用“单次随机 I/O 10 ms × 500 万”估算顺序扫描。更合理的下界近似从数据量和可持续吞吐开始:

text
scan time >= bytes to read / effective read bandwidth

例如假设每页 8192 bytes、500 万页全部需要从设备读取,总量为 40.96 GB;假设有效带宽为 1 GB/s,则仅传输的理想下界为 40.96 秒。这是指定假设下的计算,不是实测结果。CPU 过滤、解压、可见性检查和结果传输可能重叠执行,也可能形成新的瓶颈,不能把各项耗时机械相加。

若数据已在 buffer pool/OS cache,物理读可能很少;若 query 只需返回大部分 table,sequential scan 可能比大量 index random lookup 更便宜。index 不是“永远把 O(n) 变成 O(log n)”的通用按钮。

Page 是固定大小的管理单位 ​

数据库通常选择固定 page size,具体大小由 engine/build/table setting 决定。一个 page 不能简单用 page_size / row_size 得出 row count,还要考虑:

  • page header;
  • line pointer/slot directory;
  • tuple header、NULL bitmap、alignment;
  • variable-length value;
  • MVCC metadata;
  • free space、fragmentation 和 engine-specific trailer。

所以“4 KiB page、200-byte row 恰好 20 行”只是粗略上限,不是布局事实。

Slotted page ​

常见 slotted-page 结构:

text
+-------------------------------+
| page header                   |
+-------------------------------+
| slot 0 | slot 1 | ...         |  grows downward
+-------------------------------+
|          free space           |
+-------------------------------+
| record ... | record 1 | rec 0 |  grows upward
+-------------------------------+

slot 保存 record offset/length 或状态。record 在 page 内移动时,只更新 slot,外部 record identifier 仍可引用 (page_id, slot_id)。删除 record 后可以标记 slot 空闲,稍后 compact page。

具体实现并不统一。PostgreSQL heap page 的 item identifiers 与 tuples 从两端增长;page header 还包含 LSN、flags 和 free-space boundaries。checksum 是否存在/启用取决于集群配置,不应写成每页必有且始终校验。

Variable-length 与 overflow ​

row 可能含 text/blob。engine 可采用:

  • inline short value;
  • page 内 variable-length record;
  • overflow page/chained storage;
  • compression;
  • PostgreSQL TOAST 一类 out-of-line storage。

大 column 会改变 row width、cache density 和 index feasibility。SELECT * 不只是网络问题,也可能迫使 engine 读取原本可避免的 overflow data。

Record identifier 的稳定性边界 ​

heap-organized table 的 index leaf 常保存 record locator,例如 page/slot。row update 可能:

  • 原地更新;
  • 在同 page 生成新 version;
  • 移到另一 page 并留下 forwarding/version link;
  • 更新所有相关 indexes;
  • 保留旧 version 供并发 snapshot 读取。

因此 locator 往往是 engine-internal identity,不是业务主键。PostgreSQL 的 ctid 会随 row version/location 改变,不应持久化为 application ID。

Buffer Pool 的核心状态 ​

buffer manager 至少要回答:

  • page_id -> frame 是否命中;
  • frame 是否被使用者 pin,能否 eviction;
  • page 是否 dirty;
  • page 最近/频率信息;
  • dirty page 写回前相应 WAL 是否满足 write-ahead rule;
  • 并发读写怎样 latch/lock。
text
page table: page_id -> frame_id
free list / replacement candidates
frame metadata: pin_count, dirty, usage, io_state
frames: actual page bytes

page hit 只说明 page bytes 在内存,不代表 row 对当前 transaction 可见;MVCC visibility 仍需检查。

Pin、latch 与 transaction lock 不同 ​

  • pin 防止 frame 在使用时被 eviction;
  • latch 保护内存结构/page 的短临界区;
  • transaction lock 保护 logical data concurrency,生命周期可能跨 statement/transaction。

混用三者会得到错误的性能和 deadlock 模型。读取一个 page 可能短暂持 latch,却不一定获得业务 row lock。

Replacement 不是纯 LRU ​

精确 LRU 维护成本高,而且一次大 scan 可能驱逐 hot working set。engine 会采用 clock-sweep、LRU-K、2Q、scan-resistant admission、sequential-scan ring 等策略或变体。

策略目标也不只是 hit ratio:dirty write cost、scan behavior、prefetch、concurrent contention 和 page reuse distance 都会参与。

“buffer pool 设为物理内存固定百分比”不是通用公式。还要给 OS、connection memory、sort/hash、background process、filesystem cache 和其他服务留空间。PostgreSQL 与 InnoDB 的 caching path 也不同,应以目标版本官方文档和 workload measurement 配置。

Dirty page 与 durability ​

修改发生时,page 可先在 buffer pool 中变 dirty。write-ahead rule 约束的是:当 dirty data page 要写入 durable data file 前,能重做该修改的 WAL 必须先 durable。

这不等于“每改一个内存字节前都先同步写日志”。将 memory modification、WAL insertion、WAL flush、data-page flush 和 commit acknowledgement 分开,才能理解 group commit 与 checkpoint。

OS page cache 与 direct I/O ​

数据库可能通过 buffered I/O 利用 OS page cache,也可能在部分路径使用 direct I/O。double caching 是否浪费,取决于 engine 与 workload;OS cache 还承担 filesystem metadata、executable 和其他文件缓存。

内存中看见数据与 durable storage 接受 flush 是两件事。write() 返回、kernel cache、device volatile cache 与 power-loss persistence 不能混为一谈。

观测问题 ​

排查 I/O 时收集:

text
logical page reads vs physical reads
buffer hit/miss by relation/query
read/write latency distribution
dirty pages and checkpoint write rate
scan bytes, returned rows, filtered rows
temporary spill bytes
device queue and utilization
working-set change over time

高 buffer hit ratio 不一定等于快:所有 page 都命中时,CPU、latch、decompression 或 query plan 仍可成为瓶颈。低 hit ratio 也不一定是问题:大而低复用的 analytical scan 本就不应污染 hot cache。

本课小结 ​

页式存储把 row 管理转换为 page 管理;buffer pool 决定哪些 page 驻留、何时写回。顺序扫描、随机 lookup 和 cache hit 必须结合数据量与访问模式解释。下一课把 B+ 树放进这个 page model,分析 point lookup、range scan、split 和不同 engine 的 leaf payload。

官方资料入口 ​

Built with VitePress | Software Systems Atlas