← 中文资源

简体中文版本

SQLite — WAL 文件无限增长且检查点停滞

诊断不断增长的 SQLite WAL 文件,找到阻塞检查点的长事务读者,并用超时与读者纪律恢复检查点推进。

阅读英文原文

症状

  数据库的磁盘足迹与内容自相矛盾:主库文件 12 MB,-wal 伴随文件却长到 2.7 GB。PASSIVE 检查点的返回结果道出原因:busy=1 表示存在被最老读者快照禁止回收的帧。自该读者开始之后的每次写入都追加在被钉住的标记之后,文件因此无限增长。磁盘告警触发在一个逻辑体积只占零头的库上,是这种形态的常见入口。

诊断思路

  先执行 PRAGMA wal_checkpoint(PASSIVE) 读 busy 标志,等于 1 即有读者钉住了 read mark;再用 lsof 列出持有库文件的连接,长驻 PID 是嫌疑人;最后做因果测试:停掉长读者后执行 TRUNCATE,文件缩到零即确认阻塞者。隐式快照持有(打开游标不关、连接贯穿应用生命周期)、未 finalize 的语句、持续写入叠加默认被动检查点追不上帧尾,都是常见成因。

根因

  WAL 检查点只能回收比最老活跃读快照更早的帧。长驻读者钉住 read mark,之后的每次写都在标记后追加新帧,检查点在结构上无法越过被钉住的标记,WAL 于是无界增长。

修复与验证

  先修读者纪律:读事务保持短小、及时关闭连接,Python 里用 contextlib.closing 包游标、请求级连接,报表类长查询放只读连接并尽快关闭或走快照副本,避免钉住 OLTP 的读标记。再调机器:journal_size_limit 封顶文件体积、wal_autocheckpoint 设小页阈值让检查点高频小步前进、busy_timeout 让写者与检查点优雅竞争而不是 busy 失败。验证时跑含报表负载的代表性流量:WAL 在有界区间震荡、检查点 busy=0、文件被 limit 封顶;再做因果实验——启动长读者看 busy 翻 1、文件爬升,停掉它看 TRUNCATE 立即回收空间。这种可逆性证明修复针对的是机制而非症状。不要在写负载下激进手工 TRUNCATE,写者会排在检查点后面挨饿,调 autocheckpoint 阈值才是正解。

代码示例

$ ls -lh app.db*
-rw-r--r--  12M  app.db
-rw-r--r--  2.7G  app.db-wal   <- the real tenant
sqlite> PRAGMA wal_checkpoint(PASSIVE);
0|1430|1430        <- busy=1: a reader blocks recycling
# bad: implicit snapshot held for the app lifetime
cur = conn.cursor()
rows = cur.execute("SELECT ...").fetchall()  # snapshot stays open

# good: read, then release
with contextlib.closing(conn.cursor()) as cur:
    rows = cur.execute("SELECT ...").fetchall()
conn.close()  # or per-request connections
PRAGMA journal_size_limit = 67108864;    -- cap at 64 MB
PRAGMA wal_autocheckpoint = 1000;        -- checkpoint every 1000 pages
PRAGMA busy_timeout = 5000;              -- contend gracefully