Microduck的OTA不砖机:updaterd的签名验证、健康门控与自动回滚
2026/9/16 21:04:54 网站建设 项目流程

04-OTA不砖机:updaterd的签名验证、健康门控与自动回滚

引子:给机器人"刷机",为什么比给手机刷机难一万倍

大家好,我是黒漂技术佬。

上一篇我们认识了 7 个守护进程,其中有一个"管生杀大权"的家伙——updaterd,负责把新固件刷进 microduck。

给设备做 OTA(Over-The-Air 空中升级),对做过物联网的人来说是个熟悉的噩梦。但给机器人做 OTA,噩梦的等级还要再上三层,因为机器人有一个手机没有的致命特性:

手机刷机失败,最坏结果是变砖返厂。机器人固件更新失败,可能当场摔在地上——物理性的"砖"。

想象这个场景:鸭子正在桌上走,OTA 触发,新固件加载到一半,控制环卡顿了一下——失去平衡,“啪”,800 克的鸭子拍在桌面上。或者更糟:新固件装好了,但步态策略是坏的,一启动就原地转圈、撞墙、抽搐。

所以 microduck 的 OTA 设计有一个非常明确的北极星指标:

无论什么情况,都不能让机器人处于"不可恢复"的状态。宁可回滚到旧版,也绝不允许刷死。

这期文章我们就来拆解 updaterd 的完整设计:签名验证 → 原子切换 → 健康门控 → 自动回滚,一条完整的"刷机安全链"。


一、先看发布模型:整目录切换,而不是打补丁

microduck 的更新模型一句话就能说清:

swapped not patched(整体切换,不打补丁)。

新固件不是"在旧固件基础上改几个文件",而是一个完整的、自洽的新版本目录,整体替换旧版本。

/opt/robot/daemon/ ├── releases/ │ ├── v1.2.0/ # 旧版本完整目录 │ │ ├── robotd │ │ ├── updaterd │ │ ├── configd │ │ ├── btd │ │ ├── padd │ │ ├── mediad │ │ ├── tofd │ │ ├── policies/ # 神经网络策略文件 │ │ └── default-config.toml │ └── v1.3.0/ # 新版本完整目录(新装的) ├── current -> releases/v1.2.0 # ★ 软链接,指向当前生效版本 └── rollback/ # 回滚时把 current 指回这里

current是一个软链接。切换版本 = 改一个软链接指向,而不是搬动任何文件。这是整个 OTA 设计的基石,也是 Unix 哲学里"用指针做状态切换"的经典应用。

为什么"整体切换"优于"增量打补丁"?

维度增量补丁整体切换
升级包大小大(但嵌入式系统通常是整镜像,差距没那么大)
一致性麻烦——如果中途失败,系统处于"半新半旧"状态天然一致——目录要么完整存在,要么不存在
回滚难——要"撤销"补丁简单——软链接指回去就行
依赖管理组件间版本耦合容易出问题整版本自洽,不会出现"A 是新的、B 是旧的"

对于一套 7 个 daemon 的紧密耦合系统,“整体切换"几乎是唯一正确选择——它把"系统处于什么状态"这个问题简化成了"软链接指向哪个目录”。


二、升级流水线:从 GitHub 到鸭子落地

updaterd 的完整升级流程(这是整个设计的核心,值得画下来):

┌────────────┐ ┌──────────────┐ ┌──────────────────┐ │ GitHub │ │ updaterd │ │ robotd │ │ Release │ │ │ │ │ │ │ │ ① 下载+验签 │ │ │ │ (签名包) │────►│ ② 校验SHA-256 │ │ │ │ │ │ ③ 解压(zstd) │ │ │ │ │ │ ④ 原子落盘 │ │ │ │ │ │ ⑤ 切换current │ │ │ │ │ │ ⑥ 重启服务 │────►│ ⑦ 启动自检 │ │ │ │ │◄────│ ⑧ 回报health │ │ │ │ ⑨ 健康门控 │ │ │ │ │ │ 健康→保留 │ │ │ │ │ │ 不健康→回滚 │ │ │ └────────────┘ └──────────────┘ └──────────────────┘

第 1 步:签名验证(minisign)

升级包在 CI 构建时就用私钥签了名。updaterd 下载后第一件事是验签——用内置的公钥验证包的签名是否合法。这保证了:

  • 包确实来自官方 CI(不是中间人篡改的);
  • 包没有被任何人在传输过程中动过手脚。

使用的签名工具是minisign——一个比 GPG 更轻量、更现代的签名方案。嵌入式场景选 minisign 而不是 GPG,理由和选 JSON-RPC 而不是 D-Bus 一样:够用、轻、现代

第 2~3 步:完整性校验 + 解压

验签通过后,再校验 SHA-256 哈希,然后解压(格式是 zstd + tar——zstd 压缩率高且解压速度快,适合嵌入式)。

注意一个实现细节:文档提到"递归删除树用spawn_blocking同步执行"——因为删目录树是 CPU/IO 密集操作,不能阻塞 tokio 的异步运行时。异步世界里,CPU 密集操作要交给专门的阻塞线程池——这是 Rust 异步编程的经典正确姿势,细节见真章。

第 4~5 步:原子落盘 + 切换

新版本先完整落盘到releases/v1.3.0/(这个过程失败不影响当前版本——因为 current 还指向旧版),落盘成功后原子地current软链接切换到新版本。

“原子"的意思是:切换操作要么成功要么失败,不存在中间状态。哪怕切换的瞬间断电,重启后系统依然是一个完整可用的状态(最多是 old 或 new 其中之一,绝不会是"半新半旧”)。

第 6~8 步:重启 + 健康门控

这是整个设计最精彩的部分。

服务重启后,updaterd 不会立刻宣布"升级成功"。它会去问 robotd 的robot.health接口:“新固件跑起来了吗?状态健康吗?”

  • 健康→ 升级确认,保留新版本;
  • 不健康(进程起不来、控制环异常、状态机报错)→自动回滚到旧版本。
升级完成 → 重启 → 问 robot.health ├── healthy → ✅ 保留新版本 └── unhealthy → 🔄 自动回滚旧版本

这个"健康门控"(health gate)的价值怎么强调都不过分:它把"升级是否成功"的判定从"安装完成"推迟到了"运行良好"。很多 OTA 系统的 bug 恰恰在于把"装上了"当成了"成功了"——装上不等于能跑。

第 9 步:boot counter 兜底

还有一个更深的保险:boot counter(启动计数器)

假设新固件装好后能启动,但启动后 30 秒才崩溃(比如某个延迟初始化的模块炸了)——此时健康门控可能已经确认过健康了,来不及回滚。怎么办?

boot counter 的思路:每次开机启动计数器 +1,只有健康运行超过一定时长后才清零。如果系统反复崩溃重启,计数器持续增长到阈值,引导程序就判定"这个版本有毒",自动回退到上一个版本。

这和我们熟悉的 Android Recovery 模式、路由器双分区(dual-bank)设计是同一个思想,只不过实现更轻。“用计数器识别 crash-loop”是嵌入式可靠性设计的经典手段,值得所有做设备的团队抄作业。


三、日志:刷机过程也要可审计

升级是高风险操作,所以 updaterd 对日志的执念也写进了设计:

更新历史记录在/var/lib/robot/updater/update-log.jsonl,fsync 追加、原子重写、保留 200 条、跨断电存活。

拆开看每一条都是讲究:

  • fsync 追加:写完必须刷盘——防止断电丢日志(升级时最容易断电);
  • 原子重写:日志文件结构不被写坏;
  • 保留 200 条:嵌入式存储有限,日志也设上限;
  • 跨断电存活:任何时刻掉电,日志记录都不损坏。

为什么要这么较真?因为现场排障时,最需要的就是"上次升级到底发生了什么"的完整记录。设备厂商最怕的售后场景是:用户说"升完级就坏了",但你既没有升级日志、也没有版本记录,只能"嗯嗯我们查一下"然后不了了之。审计日志是升级系统的安全气囊。

另外还有一个贴心的设计:robotctl version会报告四维版本信息——系统版、安装版、运行版、回滚版——让运维一眼看出"当前装的版本"和"实际跑的版本"是否一致。这个"版本分歧可视化"在排查 OTA 半成功状态时极其有用。


四、工程哲学:为什么这套设计"高级"?

技术细节讲完了,我想聊聊更本质的东西——这套 OTA 设计背后的工程哲学。

哲学一:把"失败"当作默认假设

整个 updaterd 的设计,从签名、原子切换、健康门控到 boot counter,本质上是一句话:

"升级会失败"不是异常,而是常态。系统设计的目标不是防止失败,而是保证失败后可恢复。

  • 验签失败?放弃安装,旧版继续跑;
  • 解压失败?目录不完整,current 不切换;
  • 启动不健康?自动回滚;
  • 崩溃循环?boot counter 兜底。

每一层失败都有对应的恢复路径,没有任何一层失败会导致不可恢复。这就是"面向失败设计"(design for failure)的完整实践。

哲学二:恢复路径要"越来越简单"

注意一个趋势:这套系统的恢复手段是分层的——

第一层:健康门控回滚(软件,自动) 第二层:boot counter 回退(引导程序,自动) 第三层:SSH / 重新刷镜像(人工,最后的保险)

越往下,手段越原始、越可靠、越不依赖出故障的系统本身。最底层的"重新刷镜像"甚至不依赖鸭子上的任何软件——只要硬件没坏,总能救回来。永远保留一条不依赖系统自身的逃生通道,这是嵌入式系统的生存法则。

哲学三:升级决策权和升级操作权分离

细心的读者可能已经发现:触发升级的指令来自客户端(robotctl update apply),但执行升级的是独立的 updaterd,验证健康的是 robotd,而这些都是互不信任的独立进程。

  • updaterd 不会因为"客户端说升级"就盲从——它要自己验签;
  • robotd 不会因为"updaterd 说健康"就放行——健康是它自己报告的;
  • 就算 updaterd 被攻破,它也只能改固件文件,改不了 robotd 的实时控制(进程隔离)。

权力分散 + 接口互相校验,这套思路在安全系统里叫"最小信任"(least trust)——每个组件只信任它该信任的最小集合。


五、我们能学到什么?

把这篇的干货提炼成给读者的四句话:

  1. 整体切换优于增量补丁。用软链接做版本切换,天然一致、回滚简单。你的设备不管有没有 OTA,版本管理都该这么设计。
  2. “装上了"≠"成功了”。升级确认必须等"运行健康"而不是"安装完成"。健康门控(health gate)是 OTA 系统最值得抄的设计。
  3. 面向失败设计。每一层失败都要有恢复路径,而且恢复手段要"越往下越简单、越不依赖系统自身"。
  4. 日志要能跨断电。fsync + 原子写 + 上限管理。升级日志是售后的安全气囊,平时看不见,出事就是救命稻草。

小结

updaterd 的 OTA 设计,本质上是一套**"把失败当常态"的可靠性工程**:签名验证挡外敌,原子切换保一致,健康门控验真相,boot counter 兜崩溃,审计日志留证据。每一环单独看都不算新奇,但串成一条完整的安全链,就成了教科书。

这种设计最打动我的地方在于:它所有机制的目标不是"升级成功",而是**“任何时候系统都可恢复”**。这个目标设定,决定了整个系统的气质。


到这里,microduck 的"运维三件套"(控制、通信、升级)就拆完了。但还差最后一块拼图,也是这只鸭子最神奇的地方——它那些会走路、会站起来的"本领",不是程序员一行行写出来的,而是在仿真里用强化学习自己"练"出来的。

下一篇,我们聊聊 sim2real:从 MuJoCo 仿真到真鸭子落地,PPO 策略是怎么穿越"仿真与现实的鸿沟"的。

我是黒漂技术佬,咱们下篇见。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询