数字花园
WAL 之殤:一次数据库的掘地自救
最深的故障从来不在错误码里,而在错误码脚下的土地里。
一、病象
“我的 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,可真正的病灶是两套文件系统语义的世仇——而它本可以在第一次报错时就被抓住,如果当时有人愿意多掘一层。
真实坐标
- 病象:
sqlite error 4618(SQLITE_IOERR_SHMOPEN);/api/status200 但一切数据库操作挂死;磁盘余 45G、文件俱在、fd 远未耗尽(9/104万) - 根因:SQLite WAL 模式 + Docker Desktop 9p/drvfs bind mount——strace 抓到
ftruncate(shm, 3)返回 EINVAL;new-api 二进制硬编码journal_mode(WAL),原目录无法绕开 - 三实验钉死:复制副本本地盘可开(库没坏)/ 挂载上新库正常(机制没坏)/ 仅现有三文件打不开
- 时间线:10:31 重启正常 → 15:32:46 长请求触发 WAL 重建 → 全挂;9/2 与 9/23 同类报错系重启糊弄
- 修复:备份 125MB → 优雅停机 → 容器本地盘 WAL checkpoint(1.9MB 并回,integrity ok)→ 迁入命名卷(WSL2 ext4)→ 同配置重建容器
- 复验:1 个用户(bg09577)/ 51938 条日志 / 53 个日志文件 / 后台任务周期正常 / 登录与真实 API 全通
- 原目录备份留存于
C:\Users\Min\new-api-data-backup-20261008
评论