我把服务升级成了「两版都打不开」

我把服务升级成了「两版都打不开」

本文分享方法论与纠错过程,不含任何商业推广或服务报价。

升级一个自托管服务,从 9.2 到 9.8。听起来是十分钟的事。

结果是:升级没成,旧版也打不开了,我在中间被卡了一个半小时。

这篇把过程拆开写——不是为了甩锅给谁,而是因为这次事故里有一半是我自己的问题。


1. 三道关卡,一关比一关硬

第一关:版本门槛,藏在”要求”文案里

新版本要求 Node ≥ 24.16,并且明确排除 25.x(>=24.16.0 <25 || >=26.1.0)。

我机器上最高是 25.9——看着绰绰有余,实际上根本不在允许区间。

教训:版本要求要去权威源查(比如 npm view <包>@<版本> engines),不要采信报错文案里的那句”升级到 xx 即可”。

第二关:迁移工具直接死循环

新版需要把数据库迁移到新结构。它自带的迁移命令跑起来后,同一行日志重复了 986 次,四个多小时零进展。

两个不同实例、同样的表现——这不是环境问题,是工具本身的 bug。

第三关:真正的坑——升级是单向的

这才是我没预料到的部分。

迁移跑到一半,数据库已经被改成了新版格式。 此时:

  • 新版:因为迁移没跑完,起不来
  • 旧版:一看到数据库是”新版格式”,直接拒绝启动(原话是 “schema 19,比你支持的新”)

两端同时关门。 没有”算了我不升了”这个选项。


2. 我自己补的那一刀

在尝试回滚时,我用了一个自认为很稳妥的做法:把数据库文件复制一份留底。

问题是,这个数据库跑在 WAL 模式下。直接 cp 主文件,拷出来的是缺了 WAL 的残缺体——恢复之后立刻报数据不一致。

一个”看起来在备份”的动作,实际上是给自己埋了个雷:因为它让你以为有退路,而那条路是断的。

正确的备份方式有三种,任选其一:

方式 说明
VACUUM INTO '备份文件' SQLite 官方一致性导出
.backup 命令 SQLite 官方备份 API
先 PRAGMA wal_checkpoint(TRUNCATE) 再复制 把 WAL 合并回主文件再拷

教训:备份要用工具自己的备份方式,不是”看起来像在拷贝”。


3. 修复时的隐形对手

这是整晚最消耗时间的一个发现。

我一直在修数据库,但每次修完都失败。反复几次之后才反应过来:

有一个看门狗程序每 5 分钟检查一次服务,发现它没起来,就再拉一次。拉起来的进程占着数据库文件,让我的修复一直做不完。

看门狗本身是好东西——它是保护机制。但在故障修复期间,它变成了干扰源。

关掉它之后,修复一次就成功了。

教训:修故障前,先关掉所有自动重启。否则你修的东西一直在动。


4. 怎么救回来的

顺序是这样的:

  1. 冻结自动化——停掉看门狗和所有相关计划任务
  2. 恢复备份数据库(升级前留的那份)
  3. 修复一致性——用 REINDEX 重建索引,解决第 2 节里 cp 造成的索引缺失
  4. 回滚程序版本——包目录用重命名而非删除,旧版完整保留
  5. 验证三件套——端口在监听(要过滤 LISTENING)+ 健康接口 200 + 首页 200

服务恢复。没丢用户数据,也没丢业务数据——丢的只是服务自己的内部状态(会话、配对信息),重新配对一次即可。


5. 三条能带走的教训

  1. 备份要用工具自己的备份方式。 复制文件不等于备份——尤其当它跑在 WAL 模式下。
  2. 修故障前先冻结所有自动重启。 自愈机制在故障期间会变成干扰源。
  3. 单向操作开始前,先确认退路真的能走通。 数据库迁移、格式升级这类操作是不可逆的;”不行就退回去”必须事先验证,而不是出事了才想。

6. 关于”升级失败”这件事本身

这次卡住我们的迁移 bug,是上游工具的问题,不是我们的配置问题——这一点要分清楚,不然会陷入”是不是我哪里做错了”的无效自责。

但另一半确实是我的问题:备份方式选错了。

一个诚实的总结是:

服务没丢,是因为备份做在了前面。但”运气好”不该是方案的组成部分——下次要靠检查单,不靠运气。

事后来看,最有价值的产出不是”修好了”,而是那份写下来的升级前检查单:升级前查版本要求(权威源)、用正确方式备份、冻结自动化、在测试实例先验证迁移能跑完、旧版本用重命名保留。


本文所述为自托管服务的通用运维经验,不含商业推广。文中数字均来自日志。


我把服务升级成了「两版都打不开」
https://genui.art/2026/10/03/2026-10-03-upgrade-both-versions-locked/
作者
快乐
发布于
2026年10月3日
许可协议