升级后起不来:什么时候该等修,什么时候该回滚

升级后起不来:什么时候该等修,什么时候该回滚

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

那天晚上,我自托管的助手连不上了。

进程还在,占着内存,CPU 也在动——它就是不监听端口。日志停在一行:

1
OpenClaw state at .../openclaw.sqlite is undergoing offline maintenance; retry when it finishes.

“要不要回滚?”——这是当时最自然的念头,也是最危险的一个。

因为回滚不是”退个版本”那么简单。这篇写的是:在按下回滚之前,我花了几个小时把这四个问题问清楚。


1. 第一件事:先确认”能不能回滚”

回滚的前提是——旧版能读现在这个数据库。而新版升级往往会顺手把数据库结构改掉(schema 迁移)。

我不知道现在的库是什么结构,就去把它和版本对着查了一遍:

数据库 结构版本 谁写的
共享状态库 15 2026.9.8 之前
共享状态库 19 2026.9.8
每个 agent 的库 (旧)19 早先
每个 agent 的库 24 2026.9.8

规则很简单:结构迁移只往前跑,旧版本一律拒绝读”更新”的库。

这意味着一件事:一旦迁移跑过,回滚就必须连数据库一起退。

顺带记一个把人骗惨的坑:备份文件名里的数字,不一定是 schema 号。...-pre98schema 里的 “98” 指的是软件版本 2026.9.8,不是结构版本——那个文件的结构版本其实是 15。名字是自己起的,别拿它当证据,去查库本身。

怎么自己锚定(不靠记忆):

1
2
3
4
# 库现在是哪版
python -c "import sqlite3;print(sqlite3.connect('file:openclaw.sqlite?mode=ro&immutable=1',uri=True).execute('PRAGMA user_version').fetchone())"
# 这个二进制"支持到"哪版:直接从程序里读它的断言常量
grep -rhoE 'readStateSchemaContentVersion\(database\) !== [0-9]+' dist/ | sort -u

2. 第二件事:查官方的降级政策

我去翻了官方文档里关于数据库结构的那一页。它有一节标题就叫:

Downgrades are unsupported.(不支持降级)

官方没有”把数据库改回旧结构”这种方案。程序自己吐出来的错误信息,其实就是答案:

...uses newer schema version <N>; this build supports <M>. ... To use an older build, restore your pre-update backup created with openclaw backup create.

翻译一下:想用旧版本,就恢复升级前的备份。

所以”回滚”这个词,在这类系统里其实是——换二进制 + 把数据库也退回到升级前那一份。代价是升级之后产生的数据全部丢失。回滚从来不是零成本的后悔药。


3. 第三件事:去社区查重——你大概率不是一个人

这一步几乎是免费的,但价值最大。

在动手之前,我去项目的 issue 列表里搜了我们的报错原文。结果是——四条高度吻合的 issue,两条还是 P0:

看到的 issue 它对应的正是我们的哪一步
启动时只读探测撞上 “offline maintenance”,直接中断整个网关启动(附确定性复现+完整栈) 我们日志里那句原文
Windows 计划任务网关起不来;更新以 doctor-failed 回滚 我们的平台和症状
gateway status 对同一个库建 4–6 个只读快照 我们满屏的 snapshot 日志
启动迁移之后新建会话 100% 失败 界面上”能打开但不能对话”

其中一条把机制写得清清楚楚:Windows 上两个服务在开机后 1 秒内先后启动,另一个进程先抢到了状态目录的锁,网关的只读探测就抛了那句 “offline maintenance”,然后进程直接退出——而计划任务还报告”运行成功”。

教训:动手前先查重。 重复报一个已知 issue,对维护者只是噪音;但如果你的现场数据不一样(不同平台、不同触发路径、更极端的数据点),那就是有价值的补充证据,值得贴到已有 issue 下面,而不是另开一条。


4. 第四件事:它到底为什么这么慢

我们的网关从”开始启动”到真正监听端口,花了 13 分钟。(社区里同样的 Windows 机器是 40–70 秒。)

证据在更新失败的留档里——一条结构化记录写着:

1
SQLite update state snapshot for ...: 886.9 MiB, budget 1409 seconds

单次快照,886 MB,预算 1409 秒(23.5 分钟)。

再加上这台机器的系统盘 已用 94%(只剩 14 GB),而状态库和一个 275 MB 的 agent 库都在系统盘上——启动慢,是这三样东西叠出来的,不是玄学。

教训:慢 ≠ 死。 判断一个卡住的进程该不该杀,先量三件事:CPU 还在涨吗?日志还在动吗?它手上的活儿有多大? 我们的进程 CPU 一直在涨、只是不吐日志——那是”在磨”,杀掉它等于把计时器清零重来。


5. 如果真要回滚,先给备份做个体检

这是我最想安利的一步。回滚要恢复备份,但备份自己可能是坏的。

把每个备份都只读地过一遍:结构版本对不对、完整性检查过不过、有没有”僵尸行”。

我们自己的备份里就躺着两颗雷:

  • 有一份的更新流水表里,有 2 条状态是”进行中”的残留记录——直接恢复,迁移工具会卡在”无法确认存活”上;
  • 另一份带着 93 条陈旧的数据库租约,指向早就死掉的进程——迁移会卡在”请先停止活动 agent”。

两条都不是数据损坏,但都足以让恢复过程再次失败。

体检命令(对备份用 immutable=1,对正在运行的库用 mode=ro):

1
2
3
4
5
6
7
8
python - <<'PY'
import sqlite3
c = sqlite3.connect("file:备份.sqlite?mode=ro&immutable=1", uri=True)
print("结构版本:", c.execute("PRAGMA user_version").fetchone()[0])
print("完整性:", c.execute("PRAGMA quick_check").fetchone()[0]) # 要 ok
print("僵尸更新:", c.execute("SELECT count(*) FROM update_runs WHERE status='running'").fetchone()[0])
print("陈旧租约:", c.execute("SELECT count(*) FROM agent_database_leases").fetchone()[0])
PY

备份要”验过”才算备份。 没验过的备份,和”以为自己有退路”是一回事。


6. 我们最后的选择:等修,不回滚

四个问题问完,答案反而变清楚了:

  1. 数据库已经被单向迁移,回滚要丢掉升级后一天的数据;
  2. 那个 bug 已经在主干上修好了,只是还没进已经发布的版本;
  3. 社区已经把它定位成 P0,维护者在跟;
  4. 而它在本机是可以就地绕过的(那个路径判断改对即可)。

于是我们没回滚:先让它慢慢起来(13 分钟后它自己就绪了,探针全 200),同时把根因和数据层结论整理成材料,交回给负责基础设施的同伴,等正式版。

“该不该回滚”,不是靠胆量决定的,是靠”退路是否可走”决定的。


7. 三条能带走的经验

  1. 回滚不是”退版本”,是”恢复备份”。 先查清数据层的 schema 配对,再谈回不退——不然你会退到一个同样起不来的版本。
  2. 动手前先查重。 你的问题大概率已经被钉死了。重复的 issue 是噪音,不同平台的现场数据才是贡献。
  3. 慢 ≠ 死。 量了再杀:CPU、日志、手上的活儿。把”进程活着但不监听”当成杀死它的理由,很容易杀掉一个正在自我修复的系统。

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


升级后起不来:什么时候该等修,什么时候该回滚
https://genui.art/2026/10/06/2026-10-06-openclaw-wait-or-rollback/
作者
快乐
发布于
2026年10月6日
许可协议