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 回滚流程

我画了一张流程图,把整个切换和回滚的决策逻辑串起来。你看一眼就明白了。

A/B 系统切换与回滚流程图 A/B 系统切换与回滚决策流程 系统上电启动 读取 misc 分区状态 当前 slot 有效? 是 启动当前 slot 否 尝试另一个 slot 试运行 模式? 否 正常运行 是 启动看门狗 看门狗 复位? 否 试运行通过,确认切换 是 触发回滚 更新 misc,回滚到旧 slot 重启 说明 试运行次数耗尽或 用户手动触发回滚

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 系统就是最稳妥的选择。没有之一。