2. 恢复机制设计原则:原子性、幂等性、回滚能力在OTA升级中的体现

做OTA升级,最怕什么?

怕升级到一半断电了。怕网络断了。怕新固件有bug,设备变砖了。

我早年做智能家居网关的时候,就遇到过这么一次事故。远程升级到90%,啪,断电了。再上电,设备起不来了。用户投诉电话打爆了。那次之后,我花了整整两周重构升级流程,核心就是今天要讲的三个原则:原子性、幂等性、回滚能力。

2.1 原子性:要么全做,要么全不做

原子性这个概念,搞数据库的同学很熟。但在OTA里,它说的是:一次升级操作,要么完整成功,要么对系统没有任何影响。

说白了,不能出现“固件写了一半,系统挂了”这种中间状态。

核心思路:把升级过程拆成“准备阶段”和“提交阶段”。准备阶段可以失败,失败了就回滚。提交阶段一旦开始,必须完成。

我习惯的做法是这样的:

  1. 下载校验:先下载完整固件包,做完整性校验(MD5/SHA256)。校验不过,直接丢弃,不碰系统分区。
  2. 写入备用分区:把新固件写到备用分区(A/B分区中的B分区),不影响当前运行的系统。
  3. 切换标记:写入完成后,设置一个“切换标记”。这个标记才是真正的“提交”。
  4. 重启生效:重启后,bootloader检查标记,从新分区启动。

你看,真正的“原子操作”其实就是第3步——设置标记。前面所有步骤都可以重来,只有这一步是“一锤子买卖”。

我的经验:标记最好写两次。第一次写“准备切换”,第二次写“切换确认”。如果重启后发现只有第一次标记,说明切换过程异常,自动回滚。这叫“两阶段提交”,我在工业设备上用了很多年,很稳。

2.2 幂等性:重复执行也不怕

幂等性,听起来很学术。其实意思很简单:同一个操作执行一次和执行多次,结果是一样的。

为什么会需要这个?你想想看,OTA升级过程中,网络随时可能断。断了怎么办?重试啊。但如果重试的时候,系统已经写了一半固件,再写一次会不会乱套?

嗯,这就是幂等性要解决的问题。

我在项目中遇到过最典型的场景:设备收到升级指令,开始下载。下载到一半,网络断了。设备重连后,又收到同样的升级指令。如果代码没做幂等处理,就会重新下载,覆盖之前已经下载的部分,甚至可能同时开两个下载线程,把存储写坏。

怎么设计幂等的升级流程?我总结了几个要点:

场景 非幂等做法 幂等做法
重复下载 每次重新下载,覆盖已有数据 检查本地已有块,只下载缺失的
重复写入 每次从头写分区,破坏已有内容 按块写入,已写的块跳过
重复切换 反复设置切换标记 标记已存在则忽略
重复重启 每次重启都执行升级流程 检查升级状态,已完成则正常启动

避坑指南:我曾经在一个项目里,下载模块没做幂等。设备每次重连都重新下载,结果同一个固件包下载了7次,Flash磨损不说,还把备用分区的元数据写坏了。从那以后,我所有下载模块都加了一个“块位图”,记录哪些块已经下载成功。

2.3 回滚能力:给自己留条后路

回滚能力,说白了就是:新固件跑不起来,我能退回去。

这个能力有多重要?我举个例子。有一次我们给一批路灯控制器升级,新固件有个内存泄漏的bug,运行48小时后系统崩溃。如果没有回滚能力,那48小时后全城的路灯都得手动去修。有了回滚,设备检测到崩溃,自动切回旧固件,虽然功能少了点,但至少灯还能亮。

回滚能力的实现,通常有三种方案:

  • A/B分区(双分区):系统有两个分区,一个跑当前固件,一个写新固件。新固件有问题,切回旧分区就行。这是最可靠的方式,但需要双倍存储空间。
  • 备份+恢复:升级前把当前固件完整备份到另一个区域。升级失败时,把备份写回来。空间占用少,但回滚时间长。
  • 增量回滚:只备份被修改的部分。适合存储极度受限的设备,但实现复杂。

我个人最推荐A/B分区方案。虽然成本高一点,但可靠性是最好的。你看手机厂商,基本都是这个方案。

回滚触发条件(我常用的):

  • 新系统启动后,看门狗超时(系统卡死)
  • 关键服务注册失败(比如网络服务起不来)
  • 用户手动触发回滚(通过物理按键或远程指令)
  • 新系统运行一段时间后,健康检查发现异常指标

2.4 三个原则的关系

这三个原则不是孤立的。它们互相配合,才能构建一个可靠的升级系统。

我画了一张图,帮你理清它们的关系:

OTA升级三大原则关系图 OTA升级三大原则关系图 OTA升级 原子性 要么全做,要么不做 幂等性 重复执行结果一致 回滚能力 失败能退回去 原子性体现 • 两阶段提交 • 备用分区写入 • 切换标记原子化 幂等性体现 • 块位图去重 • 状态机防重复 • 标记幂等写入 回滚能力体现 • A/B分区切换 • 看门狗触发回滚 • 健康检查回退 三者配合 → 可靠的OTA升级系统 原子性保证不破坏,幂等性保证可重试,回滚保证有退路

从这张图你能看出来:

  • 原子性是基础,保证升级过程不会把系统搞成“半死不活”的状态。
  • 幂等性是保障,让网络不稳定、设备重启这些意外不会导致升级失败。
  • 回滚能力是最后一道防线,新固件有问题,还能退回去。

我的建议:设计升级系统时,先把原子性和回滚能力做扎实。这两个是“保底”的。幂等性可以后续优化,但前两个必须在第一版就实现。我见过太多项目,先上线再补回滚,结果补的时候发现分区布局不支持,只能大改硬件——那代价就大了。

2.5 一个实际案例

最后,我分享一个实际项目的代码片段。这是一个简化的升级状态机,体现了三个原则:

// 升级状态机 - 体现原子性、幂等性、回滚能力
typedef enum {
    STATE_IDLE,           // 空闲
    STATE_DOWNLOADING,    // 下载中(幂等:支持断点续传)
    STATE_VERIFYING,      // 校验中(原子:校验失败不提交)
    STATE_WRITING,        // 写入备用分区(幂等:按块写入,已写跳过)
    STATE_COMMITTING,     // 提交切换标记(原子:两阶段提交)
    STATE_UPDATING,       // 重启更新中
    STATE_ROLLBACK,       // 回滚中(回滚能力)
    STATE_COMPLETE        // 完成
} ota_state_t;

int ota_process(ota_state_t current_state) {
    switch (current_state) {
        case STATE_DOWNLOADING:
            // 幂等处理:检查已下载的块
            resume_download(check_block_bitmap());
            break;
            
        case STATE_COMMITTING:
            // 原子操作:两阶段提交
            write_marker(MARKER_PREPARE);   // 第一阶段
            sync_flush();                    // 确保写入
            write_marker(MARKER_CONFIRM);   // 第二阶段
            break;
            
        case STATE_ROLLBACK:
            // 回滚:切回旧分区
            switch_partition(OLD_PARTITION);
            clear_marker(MARKER_CONFIRM);
            break;
    }
    return 0;
}

这段代码看着简单,但每个状态的处理都对应了今天讲的原则。你写代码的时候,也可以按这个思路来设计。

好了,这一章就到这里。记住这三个原则,你的OTA升级系统至少不会出大问题。