Microduck 的更新系统:一只鸭子怎么在不摔的情况下换大脑
搞机器人部署的人都知道一个噩梦:你给机器人推了个软件更新,更新有 bug,机器人启动不了了。用户手里拿的是一块砖。你要么寄回去修,要么远程指导一个非技术用户进 recovery 模式刷固件。两个选项都很糟。
Microduck 的更新系统设计就是冲着这个问题来的。它的核心思路不复杂——原子替换、签名验证、健康检查、自动回滚——但魔鬼在细节里。今天拆开看 Pollen Robotics 怎么让一只鸭子在不摔的情况下换大脑。
为什么不用 A/B 分区
嵌入式设备更新有两种主流方案。一种是 A/B 分区——两套完整系统镜像,交替启动,新的不行就回旧的。RAUC 和 Mender 是这套方案的代表工具。另一种是应用层更新——只换应用程序,操作系统不动,出了问题回滚应用就行。
Microduck 选的是后者。原因很直接:需要更新的只有 daemon 二进制和 ML 策略模型,操作系统和驱动是静态的。A/B 分区要切两份根文件系统,对 SD 卡存储是不小的开销,而且每次更新都要写完整镜像,几百 MB 起步。Microduck 的 daemon 包只有几 MB,策略模型更小,全量替换几秒就完事。
文档里写了一句很清醒的话:”A/B would be over-engineering”——过度工程。需要换的东西少,就别搬整套备份基础设施。
不过文档也提到了一个补充:Yocto 镜像层面有独立的 A/B slots 和自己的更新器。这是 OS 层的保障,和应用层更新不冲突。两层各管各的,应用层更新管 daemon 和模型,OS 层 A/B 管内核和驱动。
更新粒度:daemon 和模型独立版本
Microduck 的更新不是一个大包。它分两个独立通道:
daemon 通道: Rust 编译的二进制,包含控制循环、安全层、IPC、kinematics。来源是 GitHub Releases。发布节奏前期每 2-3 周一次,稳定后每 2-3 个月一次。
model 通道: ONNX 策略文件,走路、站立、坐下站起、前滚翻等各一个。来源是 Hugging Face Hub。模型可以独立更新,不用重启 daemon——加载新模型就行。
这两个通道的版本号完全独立。daemon v5 配 walk model v3 完全合法,只要 daemon 声明了它支持的模型版本范围。这个设计的关键在于:策略迭代比 daemon 快得多。你训了一个新步态,导出 ONNX,推到 HF Hub,鸭子就能用上新策略,不需要等下一次 daemon 发布。
签名:minisign,不是自签证书
更新包的签名验证用 minisign。这不是什么时髦方案——它是一个极简的文件签名工具,格式就几行文本,验证库几十 KB。
为什么不用 X.509 证书链?因为那套东西是给有 CA 层级、有证书撤销列表、有过期管理的场景设计的。Microduck 的场景简单得多:CI 用一个私钥签名,机器人内置对应的公钥验证。没有中间人,没有证书过期,没有撤销问题。minisign 的密钥管理就是两个文件——一个私钥一个公钥,私钥放在 CI 里,公钥烧进机器人。
验证失败的包直接拒绝,不会落盘。这意味着即使有人攻破了 CDN 或者 GitHub Release 被篡改,机器人也不会安装假包。当然,如果私钥泄漏了那就全完了——但这跟用什么签名方案无关。
原子替换:符号链接切换
更新的核心操作听起来很朴素:下载、解压、验证签名、移动到目标目录、切换符号链接、重启服务。
具体来说,每个版本装在 /opt/robot/daemon/releases/<version>/ 目录下。current 是一个符号链接,指向当前活跃的版本。更新时:
- 下载新版本到临时目录
- 用 minisign 验证签名
- 解压到
/opt/robot/daemon/releases/<new_version>/ - 原子切换
current符号链接(symlink rename 是原子的) - 重启相关服务
符号链接的 rename 操作在 Linux 上是原子的——要么成功指向新目录,要么还是指向旧目录,不存在中间状态。这比”先删旧文件再拷新文件”安全得多。
但有个微妙的问题:updaterd 和 btd 自己也在 daemon 包里。如果更新后重启所有服务,updaterd 会被杀掉——而它正在执行更新流程的中途。这就是文档里强调的”structural rule”:更新器必须独立于被更新的东西。
updaterd 的自我排除:一个踩过坑的设计
updaterd 和 btd 在更新后不能立即重启自己。原始设计是延迟到下次重启,但这个”延迟”变成了”永远不重启”——两个进程一直跑着旧二进制,直到有人手动重启。
这个 bug 的症状很隐蔽。updaterd 拒绝了一个新版 robotctl 的连接,报错”client speaks API v4, daemon speaks v3″——看起来像是版本不兼容,实际上 updaterd 自己在跑旧版本。btd 的修复也没法测试,因为测试时 btd 从来没被重启过。
修复方案是 RESTART_AFTER_REPLYING:更新完成后,先回复 BLE 客户端”更新成功了”,然后通过 systemd transient timer 延迟 5 秒重启自己。5 秒够把回复发出去,也不会让用户等太久。
这个排除列表写死在代码里(NEVER_RESTART),不放在配置文件。原因文档说得很明白:这两个进程的排除是”它们是什么”的属性,不是操作者应该能搞错的配置项。
健康检查:更新后鸭子得能站起来
原子替换保证了文件系统不会坏,但不保证新版本能正常工作。新 daemon 可能有 bug,新策略可能跟硬件不匹配。这就是健康检查存在的理由。
更新流程的最后一步是问 robotd:”你健康吗?”这个检查不是模糊的”进程在不在”,是具体的:控制循环频率达标没有(50Hz 不能掉到 40Hz 以下)、舵机总线通了没有、IMU 在不在更新、策略文件加载了没有。
如果健康检查失败,updaterd 自动把 current 符号链接指回旧版本,重启服务。整个过程不需要人工介入。文档里说自动回滚默认是关掉的——大概是怕误判频繁回滚导致更新永远装不上——但机制是有的,可以打开。
还有一个 boot counter 兜底。如果新版本导致系统反复重启,boot 计数器超过阈值后 systemd 会标记为失败,触发回滚。这是最后一道防线——即使 updaterd 自己挂了,boot counter 也能兜住。
离线恢复:没有网络也能回滚
一个很实际的场景:用户的鸭子连不上 WiFi 了,但之前装了个有问题的更新。怎么办?
文档的设计是:回滚不需要网络。rollback 和 reset-to-golden 操作只动本地已有的版本目录,不下载任何东西,不走网络。这些操作通过 BLE 就能触发——btd 在 robotd 挂了的情况下也能服务它们。
这意味着一只没有 WiFi 的鸭子,只要还能 BLE 配对,就能回滚到上一个版本。文档的说法是”an unprovisioned or offline robot is therefore recoverable, just not upgradeable”——能恢复,只是不能升级。这是一个很正确的设计权衡:让失败的那一面落在安全侧。
反过来说,BLE 的吞吐是 10-30 KB/s,传一个几 MB 的 daemon 包要几十分钟,传模型要几小时。所以大文件不走 BLE,只走 WiFi。BLE 只传控制指令和状态汇报。
staging 和 stable 双通道
内部迭代和对外发布节奏完全不同。文档提到原型阶段 3 天打了 8 个 tag——这种速度下,stable 通道根本不够用。
解决方案是 staging 和 stable 双通道。内部开发用 staging,随时推、随时拉、随时回滚。对外发布用 stable,经过测试后才从 staging 提升过来。robotctl update 默认拉 stable,加参数可以指定 staging。
这个设计让内部团队可以快速迭代而不污染用户通道。同时,staging 通道的回滚操作是”load-bearing”的——不是可选的锦上添花,是日常开发依赖的核心功能。如果回滚不好使,开发流程就卡死了。
强制升级:最低版本地板
有时候一个版本有严重 bug,需要强制所有用户升级。Microduck 用一个 minimum-version floor 机制:每个发布通道有一个最低版本号,低于这个版本的机器人必须升级,不能选择跳过。
这个机制防止了一个常见问题:用户卡在一个已知有 bug 的旧版本上不升级,然后报 bug。最低版本地板把”你必须升级”这件事从用户选择变成了系统强制。
对比主流 IoT 更新方案
Microduck 的更新系统不是最复杂的,但可能是最合适的。
RAUC/Mender 的 A/B 方案适合更新整个系统镜像,包括内核和驱动。但 Microduck 不需要——OS 是静态的,只有应用层在变。用 A/B 就是用大炮打蚊子。
Android 的 OTA 更新有 delta diff、分块验证、A/B 槽位,但那是一个有完整 fleet 管理后台、几亿设备规模的系统。Microduck 的模式是”latest version, all robots”——没有灰度发布、没有分设备推送、没有回滚率监控。简单,但对一个几千台设备的开源机器人项目来说够用。
Mender 的优势在 fleet management——设备分组、灰度推送、远程诊断。Microduck 明确说了这是 non-goal。如果你想给自己的鸭子群做灰度,你得自己在外层搞。
实际使用流程
从用户视角看,更新一只鸭子的流程是这样的:
- 手机 App 通过 BLE 发送更新指令给鸭子
btd接收指令,转发给updaterdupdaterd通过 WiFi 从 GitHub Releases 下载 daemon 包(或从 HF Hub 下载模型)- minisign 验证签名
- 解压到版本目录
- 原子切换
current符号链接 - 重启服务(排除
updaterd和btd) - 健康检查:问
robotd状态 - 健康 → 更新成功,回复 App
- 不健康 → 自动回滚,回复 App “更新失败已回滚”
整个过程用户只点了一下”更新”按钮。如果出了问题,鸭子自己回到旧版本,用户甚至不一定知道发生过什么。
这就是更新系统该有的样子。不是最炫的,不是最全的,但它在”不让鸭子变砖”这个核心目标上做对了。原子替换保证文件系统安全,签名验证保证来源可信,健康检查保证新版本可用,自动回滚保证失败可恢复,离线恢复保证最坏情况能救。每一环都有具体的实现,没有一笔带过的”后续完善”。
数据来源:pollen-robotics/microduck 官方仓库,docs/design/updater-design.md 和 docs/design/architecture.md。
