2. 恢复机制设计原则:原子性、幂等性、回滚能力在OTA升级中的体现
做OTA升级,最怕什么?
怕升级到一半断电了。怕网络断了。怕新固件有bug,设备变砖了。
我早年做智能家居网关的时候,就遇到过这么一次事故。远程升级到90%,啪,断电了。再上电,设备起不来了。用户投诉电话打爆了。那次之后,我花了整整两周重构升级流程,核心就是今天要讲的三个原则:原子性、幂等性、回滚能力。
2.1 原子性:要么全做,要么全不做
原子性这个概念,搞数据库的同学很熟。但在OTA里,它说的是:一次升级操作,要么完整成功,要么对系统没有任何影响。
说白了,不能出现“固件写了一半,系统挂了”这种中间状态。
核心思路:把升级过程拆成“准备阶段”和“提交阶段”。准备阶段可以失败,失败了就回滚。提交阶段一旦开始,必须完成。
我习惯的做法是这样的:
- 下载校验:先下载完整固件包,做完整性校验(MD5/SHA256)。校验不过,直接丢弃,不碰系统分区。
- 写入备用分区:把新固件写到备用分区(A/B分区中的B分区),不影响当前运行的系统。
- 切换标记:写入完成后,设置一个“切换标记”。这个标记才是真正的“提交”。
- 重启生效:重启后,bootloader检查标记,从新分区启动。
你看,真正的“原子操作”其实就是第3步——设置标记。前面所有步骤都可以重来,只有这一步是“一锤子买卖”。
我的经验:标记最好写两次。第一次写“准备切换”,第二次写“切换确认”。如果重启后发现只有第一次标记,说明切换过程异常,自动回滚。这叫“两阶段提交”,我在工业设备上用了很多年,很稳。
2.2 幂等性:重复执行也不怕
幂等性,听起来很学术。其实意思很简单:同一个操作执行一次和执行多次,结果是一样的。
为什么会需要这个?你想想看,OTA升级过程中,网络随时可能断。断了怎么办?重试啊。但如果重试的时候,系统已经写了一半固件,再写一次会不会乱套?
嗯,这就是幂等性要解决的问题。
我在项目中遇到过最典型的场景:设备收到升级指令,开始下载。下载到一半,网络断了。设备重连后,又收到同样的升级指令。如果代码没做幂等处理,就会重新下载,覆盖之前已经下载的部分,甚至可能同时开两个下载线程,把存储写坏。
怎么设计幂等的升级流程?我总结了几个要点:
| 场景 | 非幂等做法 | 幂等做法 |
|---|---|---|
| 重复下载 | 每次重新下载,覆盖已有数据 | 检查本地已有块,只下载缺失的 |
| 重复写入 | 每次从头写分区,破坏已有内容 | 按块写入,已写的块跳过 |
| 重复切换 | 反复设置切换标记 | 标记已存在则忽略 |
| 重复重启 | 每次重启都执行升级流程 | 检查升级状态,已完成则正常启动 |
避坑指南:我曾经在一个项目里,下载模块没做幂等。设备每次重连都重新下载,结果同一个固件包下载了7次,Flash磨损不说,还把备用分区的元数据写坏了。从那以后,我所有下载模块都加了一个“块位图”,记录哪些块已经下载成功。
2.3 回滚能力:给自己留条后路
回滚能力,说白了就是:新固件跑不起来,我能退回去。
这个能力有多重要?我举个例子。有一次我们给一批路灯控制器升级,新固件有个内存泄漏的bug,运行48小时后系统崩溃。如果没有回滚能力,那48小时后全城的路灯都得手动去修。有了回滚,设备检测到崩溃,自动切回旧固件,虽然功能少了点,但至少灯还能亮。
回滚能力的实现,通常有三种方案:
- A/B分区(双分区):系统有两个分区,一个跑当前固件,一个写新固件。新固件有问题,切回旧分区就行。这是最可靠的方式,但需要双倍存储空间。
- 备份+恢复:升级前把当前固件完整备份到另一个区域。升级失败时,把备份写回来。空间占用少,但回滚时间长。
- 增量回滚:只备份被修改的部分。适合存储极度受限的设备,但实现复杂。
我个人最推荐A/B分区方案。虽然成本高一点,但可靠性是最好的。你看手机厂商,基本都是这个方案。
回滚触发条件(我常用的):
- 新系统启动后,看门狗超时(系统卡死)
- 关键服务注册失败(比如网络服务起不来)
- 用户手动触发回滚(通过物理按键或远程指令)
- 新系统运行一段时间后,健康检查发现异常指标
2.4 三个原则的关系
这三个原则不是孤立的。它们互相配合,才能构建一个可靠的升级系统。
我画了一张图,帮你理清它们的关系:
从这张图你能看出来:
- 原子性是基础,保证升级过程不会把系统搞成“半死不活”的状态。
- 幂等性是保障,让网络不稳定、设备重启这些意外不会导致升级失败。
- 回滚能力是最后一道防线,新固件有问题,还能退回去。
我的建议:设计升级系统时,先把原子性和回滚能力做扎实。这两个是“保底”的。幂等性可以后续优化,但前两个必须在第一版就实现。我见过太多项目,先上线再补回滚,结果补的时候发现分区布局不支持,只能大改硬件——那代价就大了。
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升级系统至少不会出大问题。