我把服务升级成了「两版都打不开」
我把服务升级成了「两版都打不开」
本文分享方法论与纠错过程,不含任何商业推广或服务报价。
升级一个自托管服务,从 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. 怎么救回来的
顺序是这样的:
- 冻结自动化——停掉看门狗和所有相关计划任务
- 恢复备份数据库(升级前留的那份)
- 修复一致性——用
REINDEX重建索引,解决第 2 节里cp造成的索引缺失 - 回滚程序版本——包目录用重命名而非删除,旧版完整保留
- 验证三件套——端口在监听(要过滤
LISTENING)+ 健康接口 200 + 首页 200
服务恢复。没丢用户数据,也没丢业务数据——丢的只是服务自己的内部状态(会话、配对信息),重新配对一次即可。
5. 三条能带走的教训
- 备份要用工具自己的备份方式。 复制文件不等于备份——尤其当它跑在 WAL 模式下。
- 修故障前先冻结所有自动重启。 自愈机制在故障期间会变成干扰源。
- 单向操作开始前,先确认退路真的能走通。 数据库迁移、格式升级这类操作是不可逆的;”不行就退回去”必须事先验证,而不是出事了才想。
6. 关于”升级失败”这件事本身
这次卡住我们的迁移 bug,是上游工具的问题,不是我们的配置问题——这一点要分清楚,不然会陷入”是不是我哪里做错了”的无效自责。
但另一半确实是我的问题:备份方式选错了。
一个诚实的总结是:
服务没丢,是因为备份做在了前面。但”运气好”不该是方案的组成部分——下次要靠检查单,不靠运气。
事后来看,最有价值的产出不是”修好了”,而是那份写下来的升级前检查单:升级前查版本要求(权威源)、用正确方式备份、冻结自动化、在测试实例先验证迁移能跑完、旧版本用重命名保留。
本文所述为自托管服务的通用运维经验,不含商业推广。文中数字均来自日志。