【SQLite 内核】Pager 与 Page Cache

💡 原文中文,约9900字,阅读约需24分钟。
📝

内容提要

本文介绍SQLite内核中Pager模块的职责:作为B-Tree与文件系统间的契约层,管理页面缓存、脏页生命周期及日志协作。Pager通过sqlite3PagerGet和sqlite3PagerWrite接口,确保事务原子性,支持回滚日志和WAL两种模式。与PG/InnoDB不同,SQLite页面缓存为连接私有,多进程通过变更计数器或wal-index检测失效。cache_size仅为建议值,非硬性限制。

🔎

延伸解读

Pager 的职责边界:不只是缓存

Pager 常被误解为简单的 LRU 缓存加文件描述符,但实际上它承担着保证事务原子性的关键职责。在页面被修改前,Pager 必须确保原始内容已安全记录到 rollback journal 或 WAL 中,否则进程崩溃可能导致数据库损坏。这种“改之前先记原样”的机制是 SQLite 实现“全改或不改”承诺的基础。

连接私有缓存与多进程失效检测

与 PostgreSQL 和 InnoDB 的跨进程共享缓冲池不同,SQLite 的 page cache 是每个连接私有的,多进程之间没有共享内存。为了感知其他进程对数据库文件的修改,SQLite 依赖 file change counter(rollback journal 模式)或 wal-index(WAL 模式)进行失效检测。这种设计换来了零跨进程同步开销,但代价是每个连接需要独立维护缓存,可能造成重复内存占用和磁盘读。

cache_size 是建议值,非硬性限制

PRAGMA cache_size 控制 page cache 的建议页数,但官方文档明确它是 advisory 而非硬性配额。Pager 可能在缓存压力下短暂超出该值,也可能因工作集小而用不到。因此,调大 cache_size 并不保证命中率提升,需结合具体工作集评估。本文未提供通用调优配方,建议读者针对自身场景进行实测。

Q&A

SQLite中Pager模块的主要职责是什么?

Pager是SQLite中B-Tree与操作系统文件之间的契约层,负责管理页面缓存、脏页生命周期以及与日志(rollback journal或WAL)的协作,确保事务的原子性。它向B-Tree提供取页(sqlite3PagerGet)和改页(sqlite3PagerWrite)的接口,并处理磁盘I/O。

SQLite的page cache命中与未命中有什么区别?

当page cache命中时,SQLite直接从内存返回页面,不触发任何系统调用;未命中时,需要调用pread()从磁盘读取页面,产生系统调用开销。即使OS page cache可能缓存了数据,未命中仍会发起系统调用,与命中时的零系统调用有本质区别。

SQLite中脏页是如何产生的,以及何时写回磁盘?

当B-Tree调用sqlite3PagerWrite()标记页面可写后,该页成为脏页。脏页不会立即写回,而是在事务提交时或缓存压力大时提前写回。写回前必须确保原始内容已记录到journal或WAL,以保证崩溃恢复。

SQLite的rollback journal和WAL模式在Pager层面有何不同?

在rollback journal模式下,修改前将原始页面拷贝到-journal文件,提交时先同步journal,再写回数据库文件;WAL模式下,修改后的页面追加到-wal文件,数据库文件在checkpoint前保持不变。Pager是两种模式的统一上层,切换模式不改变B-Tree的调用方式。

SQLite的page cache与PostgreSQL的shared_buffers有何本质区别?

SQLite的page cache是每个连接私有的堆内存,不跨进程共享;PostgreSQL的shared_buffers是共享内存,所有backend进程共享同一份缓存。SQLite通过file change counter或wal-index检测其他进程的修改,而PG通过共享内存直接看到最新数据。

PRAGMA cache_size是硬性限制吗?

不是。cache_size是建议值(advisory),不是硬性配额。Pager可能在缓存压力下短暂超出该值,也可能因工作集小而用不到。默认值-2000表示建议约2000KiB缓存,但实际使用量可能不同。

两个进程同时打开同一个SQLite数据库,会共享page cache吗?

不会。SQLite的page cache是连接私有的,每个进程维护自己的缓存,互不相通。多进程间通过file change counter(rollback模式)或wal-index(WAL模式)来检测文件被其他进程修改,从而失效本地缓存。

SQLite的page cache未命中时,是否一定触发磁盘I/O?

不一定。SQLite默认不绕过OS page cache,所以未命中时发出的pread()可能命中OS缓存而不真正读取磁盘,但仍是一次系统调用,有用户态/内核态切换开销,与SQLite page cache命中时的零系统调用不同。

🏷️

标签

➡️

继续阅读