• 欢迎访问少将全栈,学会感恩,乐于付出,珍惜缘份,成就彼此、推荐使用最新版火狐浏览器和Chrome浏览器访问本网站。
  • 吐槽,投稿,删稿,交个朋友
  • 如果您觉得本站非常有看点,那么赶紧使用Ctrl+D 收藏少将全栈吧

Microduck 是怎么做到”绝不砖机”的:OTA 更新系统的 5 层保险

Build in Public admin 40分钟前 8次浏览 已收录 扫描二维码

复刻 Microduck 的人,九成精力花在步态和舵机上,很少有人去细看那套更新系统。但恰恰是它,决定了一个 399 美元的玩具机器人能不能真的卖给不懂技术的客户。

Pollen Robotics 在 updater-design.md 里把话说得很直:机器人会送到非开发者手里,更新必须能在手机上点几下就完成。可一台会走的机器人,装坏一次固件就可能变成一块”砖”。所以这套更新系统的核心命题只有一个——怎么在客户手里安全地装新软件,还绝不砖机。

这篇不讲步态,专门拆 updaterd:它为什么不用 A/B 镜像、签名怎么验、回滚靠什么触发、开机恢复网又兜了什么底。

第一层:更新粒度——为什么不用 A/B 分区镜像

做 OTA 的人第一反应通常是 RAUC 或 Mender 那套 A/B 双分区:两个完整系统镜像,一个跑一个备用,坏了切回去。Microduck 明确拒绝了这个方案,原因朴素:它动的东西太少。

每次发布真正变化的只有两样——daemon 二进制(robotd/mediad/btd)和 ML 步态模型 + 配置。操作系统、内核、驱动层是静态的,压根不参与 OTA。为一个只变 app 层的场景去做整机双镜像,官方直接判了四个字:over-engineering(过度工程)。

于是它退回到更轻的方案——应用级更新。这带来的好处是回滚不需要分区切换,靠三样东西就够了:

  • 版本化目录:每个 release 一个独立目录,旧版本不删
  • 原子符号链接切换:current 指向谁由一条 symlink 决定,切换是原子的
  • 健康门控:切过去之后,新的 robotd 起不来看起来健不健康,不健康就切回来

没有分区,就没有”切坏了要重新刷整机”这种高成本动作。这就是第一层保险的设计哲学:能少动就少动,能不动就不动。

第二层:签名——未验签的字节绝不允许执行

更新既然是拉取二进制,就绕不开”这个包是不是官方发的”。Microduck 用 minisign 签名,但真正的讲究在后面。

机器人出厂时烧进去的不是一把公钥,而是一组受信公钥,放在 /etc/robot/trusted_keys/。签名只要匹配任意一把就算有效。为什么要一组而不是一把?为了密钥轮换和丢失可恢复:如果某把发布密钥泄露或丢了,你可以用现存密钥签一个更新,把新密钥加进去,再把旧的淘汰掉。要是只烧一把,密钥一丢,全世界所有机器都再也没法更新——只能手工刷机。这把”备用钥匙必须从第一版就烧进去”的经验,藏在表格里一行,实操里是救命的。

验签顺序也是硬约束,官方写得很死:

先验 manifest 签名 → 再下载 artifact → 验 sha256 → 最后验 artifact 签名。 并且强调了一句话:任何未验签的字节,绝不允许被执行或解压到活路径。这是第二层。

密钥托管也讲究:release-1 的私钥放在密码管理器和 CI,release-2 换个口令、只放密码管理器、绝不进 CI,release-3 离线存放、最好一辈子不上联网机器。三把钥匙三种暴露面,谁都补不了另一把的位——它的设计目标不是”好用”,是”其中一把被攻破时系统还能活”。

第三层:回滚靠什么触发——启动计数器和健康门控

光有签名还不够,得在运行时判断”这次更新到底成没成”。这里有两个机制配合。

健康门控(health gate):on_apply 会重启 release 携带的所有服务,然后检查新的 robotd 是否正常起来并报告 healthy。注意这里有个退化语义:如果 robotd 因为舵机没供电起不来,健康门控要能区分”不健康”(drop 的 release 是坏的)和”降级”(硬件本身的问题)——前者回滚,后者回滚也没用,因为 golden release 一样起不来。

启动计数器(boot counter):每次”武装”的 trial 启动会累加计数,配合 recover_on_start 逻辑,如果连续几次都是坏 release,就自动回滚到上一个可用版本。

但这里有个极其微妙的坑,官方专门写了一大段。看这一段:

> updaterd 和 btd 都打包在 daemon artifact 里,一个朴素的”restart everything”会在交换或健康检查进行到一半时把执行器自己干掉。

更新系统不能重启自己。updaterd 和 btd 必须从重启集合里排除。但”排除”不等于”跳过”——早期版本就是理解错了这句话:把这两者推迟到下次开机,结果旧二进制一直跑着,以至于新旧 API 版本对不上、btd 的修复代码根本没被跑过。正确做法是 RESTART_AFTER_REPLYING:等结果已经发回给客户端之后,用 systemd transient timer 延迟 5 秒再各自重启。这样既保护了进行中的更新,又没有让排除变成永久的跳过。

第四层:自测——提交前先跑一遍新二进制

签名、门控、计数器之外,还有一个更前端的保险:self_test_updaterd。

在 on_apply 是重启的场景下,新的二进制装好、unit 重启完、健康门控之前,先跑一遍新 updaterd 的只读模式——加载配置、构造引擎、在进入恢复逻辑前退出。这一步能抓住什么?架构装错、缺库、立即 panic,以及最可能的那个:新 updaterd 拒绝板上现有的 updater.toml(这是操作员的文件,跨安装保留的)。

而且这里有个设计细节值得记:--check-only 不是探测用的。它执行 recover_on_start 动作之后才尊重那个 flag,所以它会真的累加启动计数、可能真的回滚。官方明确警告它是操作员工具,不是探针——在中途用它会出第二个引擎破坏第一个引擎正在操作的数据。

补充一句流式细节:restart 必须用 systemd transient unit 而不是 child process,因为子进程会待在 updaterd 的 cgroup 里,在重启自己父进程的过程中被打断;transient unit 不会。同时更新锁要在 spawn 之前释放——因为 fork 会复制进程里所有打开的 fd,包括别的引擎持有的锁,会莫名让无关操作报 Busy。

第五层:开机恢复网——兜住”连 updaterd 都起不来”的最坏情况

前面所有机制都依赖 updaterd 能跑。那万一坏 release 把 updaterd 自己也带崩了呢?

这正是第五层的活,而且它必须住在 updaterd 之外、甚至住在 release 之外:开机后 3 分钟的 timer 问一句”这次 release 把 daemon 都带起来了吗?” 如果没有,一个 /bin/sh 的 rescue 脚本直接把 current 切回 golden release。

为什么必须放在 release 外面?因为它在等一个失败场景,是”updaterd 根本起不来”——这时候 recover_on_start 和启动计数器都够不着了。它不能修硬件:舵机没供电导致的 robotd 起不来,在 golden release 上一样起不来,这就是健康门控区分 unhealthy 和 degraded 的原因。

给复刻者的启示

复盘这套系统的价值,不在”多高级”,而在每层都在回答同一个问题:最坏情况下,我怎么把鸭子救回来?

  • 更新粒度定小,回滚成本就低
  • 签名用多密钥组,密钥丢失就有救
  • 健康门控和启动计数器,让回滚有客观触发点而不是看运气
  • 排除自身 + 延迟重启,保护进行中的更新
  • 开机恢复网在外层兜底,updaterd 挂了还能救

对复刻者最实用的一条是:把”自救路径”独立出来,别和主逻辑耦合。 从 9/22 拆的七守护进程到今天这套更新系统,Pollen Robotics 一以贯之的做法,就是让救援能力永远挂在主功能之外、能独立活着。你的复刻项目也一样——舵机和步态再重要,也得先保证”坏了能重来”这条路是通的。这条路不通,另外九成工程都危。

数据来源:

  • Pollen Robotics. microduck 仓库. docs/design/updater-design.md(更新粒度、minisign 签名与密钥托管、self_test_updaterd、RESTART_AFTER_REPLYING、健康门控、boot-recovery-net)。GitHub.
  • Pollen Robotics. microduck 仓库. docs/design/boot-recovery-net.md(开机恢复网机制)。GitHub.
  • Pollen Robotics. microduck 仓库. README(系统概览、进程分工)。GitHub.
  • 复刻项目进度记录: duck.whatled.com.
喜欢 (0)赏
[🍬谢谢你请我吃糖果🍬🍬~]
分享 (0)
关于作者:
少将,关注Web全栈开发、项目管理,持续不断的学习、努力成为一个更棒的开发,做最好的自己,让世界因你不同。