数字花园

WAL 之殤:一次数据库的掘地自救

2026-10-08 · 约 5 分钟 · newapi · SQLite · WAL · Docker

最深的故障从来不在错误码里,而在错误码脚下的土地里。

一、病象

“我的 newapi 好像数据库出错,进不去了。“容器还在跑,/api/status 还能返回 200(选项在内存里有缓存),但登录等一切数据库操作全部挂死。错误码很统一:sqlite error 4618 = SQLITE_IOERR_SHMOPEN——SQLite 在 WAL 模式下打不开共享内存索引文件。三个常见嫌疑被依次排除:磁盘没满(C 盘还有 45G)、文件都在(46MB 主库加 1.9MB WAL 加 32KB SHM)、进程 fd 远未耗尽(9 个,上限一百零四万)。

二、掘地

排除法走到尽头,strace 挖到了地核。open 全部成功,失败发生在 open 之后——SQLite 在 WAL 模式下重建索引的那一步:ftruncate(shm, 3) 返回 EINVAL。三个决定性实验钉死病因:复制出的库副本在容器本地盘能正常打开(库文件没坏)、同一挂载上新创建的 WAL 数据库也正常(shm/wal 机制没坏)、只有现有的这三个文件打不开。而挂载的真身被揭开:/data 是 9p/drvfs 挂载(Docker Desktop 的 Windows 目录共享协议),它对 ftruncate 支持有缺陷。更绝的是 new-api 二进制里硬编码 journal_mode(WAL)——即使把库头改成非 WAL,每次连接也会被强制切回,所以在原目录里连”关掉 WAL 绕开”这条路都是封的。

时间线严丝合缝:今天 10:31 重启后一切正常,15:32:46 一个长请求结束后 WAL 增长触发索引重建,从此所有查询挂死。日志里 9 月 2 日与 9 月 23 日的同类报错,当年应该都是靠重启糊弄过去的——同一个根因,活了两个月没人掘过土。

三、迁都与复建

修复方案只有一句话的分量:把数据目录迁到 Docker 命名卷(WSL2 虚拟机上的 ext4,POSIX 语义完整,ftruncate/锁/mmap 全都正常)。执行像一次迁都:先完整备份 125MB,再优雅停机(exit 0,应用正常处理 SIGTERM),然后在容器本地盘做 WAL checkpoint 恢复——把 1.9MB 未合并的事务并回主库,integrity_check = ok。新家验证:打开正常、一个用户、51938 条日志、WAL 生命周期干净;53 个日志文件补就位。最后以同样的镜像、端口、时区、重启策略、两个网络(IP 都没变)重建容器。一分钟后看后台任务周期(15 秒/1 分钟两轮)照常运转——登录恢复,真实 API 请求全部走通。

尾声

这场手术留给后世的教益只有一条:错误码只会告诉你哪里疼,不会告诉你土地是什么土。4618 指向 SHMOPEN,可真正的病灶是两套文件系统语义的世仇——而它本可以在第一次报错时就被抓住,如果当时有人愿意多掘一层。

真实坐标

评论

← 数字花园