4、双备份(A/B)系统架构:A/B分区原理、切换策略、回滚机制
双备份架构,圈里人常叫它 A/B 系统,或者无缝升级。说白了,就是给固件准备了两套完整的运行环境。一套在跑,另一套闲着。升级的时候,直接往闲的那套里写,写完了重启切换过去。
我最早接触这个方案是在做车载信息娱乐系统的时候。那会儿客户要求升级过程中不能黑屏,不能重启超过 5 秒。嗯,不用 A/B 系统根本搞不定。今天咱们就把这套架构掰开揉碎了讲清楚。
4.1 A/B 分区原理
先看分区布局。Flash 里至少有两个完全独立的固件分区,我习惯叫它们 slot_a 和 slot_b。每个 slot 都包含完整的 bootloader、kernel、rootfs 和 app 分区。结构上完全对称。
典型 A/B 分区表(eMMC / NAND Flash)
------------------------------------------------
| 分区名 | 大小 | 用途 |
|---------------|---------|-------------------|
| bootloader | 2 MB | 引导加载程序 |
| slot_a_boot | 32 MB | A 系统内核 |
| slot_a_rootfs | 256 MB | A 系统根文件系统 |
| slot_a_app | 128 MB | A 应用分区 |
| slot_b_boot | 32 MB | B 系统内核 |
| slot_b_rootfs | 256 MB | B 系统根文件系统 |
| slot_b_app | 128 MB | B 应用分区 |
| misc | 16 KB | 元数据/状态存储 |
| userdata | 剩余 | 用户数据 |
------------------------------------------------
这里有个关键点——misc 分区。它很小,但存着当前激活哪个 slot、升级状态、回滚次数这些命根子数据。我见过不少团队把状态存到某个 slot 的文件系统里,结果升级一半文件系统坏了,状态全丢。千万别这么干。
核心原则:misc 分区必须使用独立物理块,不能依赖任何文件系统。每次写入后做 CRC 校验,防止写一半掉电导致状态损坏。
4.2 切换策略
切换策略,就是决定什么时候从 A 切到 B,或者从 B 切回 A。我把它分成三种模式。
4.2.1 立即切换
升级完成后,重启直接切到新 slot。简单粗暴,适合对可靠性要求不高的场景。但风险也大——新固件如果起不来,设备就变砖了。
4.2.2 延迟切换
升级完成后,先不切。等下次用户主动重启,或者系统空闲时再切。我在智能家居网关项目里用过这个策略。用户半夜不用设备,系统自动切过去,切成功了再通知用户。体验好很多。
4.2.3 条件切换
这是最稳妥的做法。升级完成后,先切到新 slot 试运行一段时间。试运行期间如果出现严重错误(比如看门狗超时、关键服务崩溃),自动回滚。只有试运行通过,才正式确认切换。
我的习惯:条件切换里,试运行次数我一般设 3 次。为什么是 3 次?因为有些硬件故障是间歇性的,跑一次不一定能复现。3 次能覆盖大部分偶发问题。
4.3 回滚机制
回滚是 A/B 系统的灵魂。没有回滚,双备份就只是个双倍浪费 Flash 的方案。
4.3.1 回滚触发条件
- 启动失败:bootloader 检测到新 slot 的内核无法加载,直接回滚。
- 看门狗复位:新系统运行后,看门狗连续复位超过阈值(我一般设 3 次),触发回滚。
- 用户手动触发:用户按住某个按键上电,强制回滚到旧版本。
- 升级中断:升级过程中掉电或通信中断,下次启动时自动回滚。
4.3.2 回滚流程
我画了一张流程图,把整个切换和回滚的决策逻辑串起来。你看一眼就明白了。
4.3.3 回滚计数与熔断
这里有个坑,我踩过。如果新固件有严重 bug,一启动就崩溃,看门狗复位,系统回滚。但回滚后如果 bootloader 又尝试切到新 slot,就会陷入「启动→崩溃→回滚→再启动→再崩溃」的死循环。
解决办法是回滚计数。每次回滚,misc 分区里的回滚计数器加 1。当计数器超过阈值(我一般设 5 次),bootloader 就彻底放弃新 slot,永远从旧 slot 启动。这叫熔断机制。
注意:熔断后,新 slot 不能再被自动选中。必须通过 OTA 服务器下发强制升级指令,或者用户手动进入 recovery 模式才能重新刷写。这是为了防止设备在野外反复变砖。
4.4 升级过程中的中断恢复
升级写到一半掉电了,怎么办?A/B 系统的优势就在这里体现出来了。
假设当前在 slot_a 运行,往 slot_b 写新固件。写到 60% 的时候掉电了。下次上电,bootloader 检查 misc 分区,发现 slot_b 的升级状态是「未完成」。它不会去启动 slot_b,而是直接启动 slot_a。设备正常运行,就像什么都没发生过。
等下次 OTA 触发,系统会重新从 0% 开始写 slot_b。之前写了一半的脏数据?直接覆盖掉就行。
| 场景 | 升级前状态 | 中断点 | 恢复行为 |
|---|---|---|---|
| 写入 slot_b 时掉电 | slot_a 运行 | slot_b 写入 60% | 下次启动 slot_a,重新升级 slot_b |
| 写入 misc 时掉电 | slot_a 运行 | misc 写入 50% | CRC 校验失败,使用旧状态,启动 slot_a |
| 切换后首次启动崩溃 | slot_b 运行 | 内核 panic | 看门狗复位,回滚到 slot_a |
| 试运行期间掉电 | slot_b 试运行 | 掉电 | 下次启动继续试运行,计数不重置 |
4.5 实际项目中的避坑指南
做了这么多年 A/B 系统,有些坑我帮你们提前踩了。
我曾经在一个项目里,misc 分区和 slot_a 共用了一个擦除块。结果升级 slot_a 的时候,擦除操作把 misc 也抹了。系统直接失忆,不知道该从哪个 slot 启动。从那以后,我要求 misc 分区必须独占一个物理擦除块,并且放在 Flash 的最开头或者最末尾,远离其他分区。
还有一次,客户反馈设备升级后偶尔会回滚。查了很久发现是看门狗喂狗线程优先级太低,被高优先级任务抢占了 CPU,导致看门狗超时。解决方案很简单——把喂狗线程提到最高优先级,并且在内核里加一个硬件看门狗,跟软件看门狗做双保险。
嗯,最后说一句。A/B 系统不是银弹。它需要双倍 Flash 空间,升级流程也更复杂。但如果你的产品对可靠性要求高,比如医疗设备、汽车电子、工业控制器,那 A/B 系统就是最稳妥的选择。没有之一。