Skip to content

LV031-发布与量产运维 ​

一、 服务器端职责与架构 ​

1. 组件构成 ​

服务器端不一定是复杂的云平台。小型项目里,"一个 HTTP 文件服务 + 一份版本描述 JSON"就已经够用;规模化之后才需要演进出下面这些组件:

组件职责最小实现
版本仓库存放各版本的整包与差分包,记录哈希、适用型号对象存储 / 目录
版本描述服务设备查询接口,返回可用版本与下载地址静态 JSON 或一个 HTTP 接口
策略引擎按型号、区域、灰度批次决定"谁能升"配置文件规则
签名服务用私钥对升级包签名(私钥不落盘到应用服务器)独立签名机
差分包生成按历史版本批量生成补丁CI 中的构建任务
上报与监控收集升级结果、成功率、失败原因日志表 + 看板

2. 版本描述文件设计 ​

设备查询更新的入口,字段设计要考虑"防呆"与"可运营":

json
{
  "device_model": "sensor-gw-100",
  "hardware_rev": "1.1",
  "current_version": "2.0.3",
  "latest_version": "2.1.0",
  "release_notes": "修复 4G 断线重连后无法上报的问题",
  "mandatory": false,
  "min_battery": 30,
  "rollout": {
    "batch": "b2",
    "percentage": 10,
    "whitelist": ["SN202600001", "SN202600042"]
  },
  "package": {
    "type": "delta",
    "src_version": "2.0.3",
    "url": "https://ota.example.com/pkg/2.0.3_to_2.1.0.delta",
    "size": 184320,
    "sha256": "9f2c...",
    "signature": "MEUCIQ..."
  },
  "fallback_package": {
    "type": "full",
    "url": "https://ota.example.com/pkg/2.1.0_full.bin",
    "size": 1048576,
    "sha256": "3ab7...",
    "signature": "MEQCIF..."
  }
}

【设计要点】

(1)device_model + hardware_rev 双重匹配:避免把 B 型号固件推给 A 型号

(2)mandatory + min_battery:强制升级也要有前置条件,电量不足时延迟而非放弃

(3)package.type 与 src_version:差分包必须匹配源版本,字段缺失就是线上事故的源头

(4)fallback_package:差分失败时设备可直接切整包,无需再次请求服务器

3. 版本规则 ​

  • 版本号规范:语义化版本 major.minor.patch,只有 major 变化允许破坏性接口调整
  • 兼容性矩阵:明确每个版本支持哪些硬件版本,把它做成机器可校验的声明而不是文档
  • 不可变制品:一个版本号对应唯一的二进制与哈希,永远不要覆盖已发布版本的文件

二、 发布流水线 ​

1. 从提交到制品 ​

OTA 发布流水线与灰度策略

流水线中的每个环节都应可复现、可追溯:

(1)构建:编译环境容器化,构建产物带版本号与 Git commit hash

(2)测试:单元测试 + 硬件在环测试(HIL);关键版本补充内存/存储占用基线检查

(3)签名:只把镜像摘要送进签名服务,签名结果写回包内。签名服务器与构建机网络隔离

(4)差分生成:对"仍在网上的所有历史版本"各生成一个补丁——这是耗时最长的步骤,适合并行执行,并按需淘汰过老版本

(5)入库:包与元数据一起入库,记录哈希、签名、生成时间、适用的源版本列表

2. 差分包生成的取舍 ​

版本碎片化会带来组合爆炸。假设线上有 10 个版本,每次发布就要生成 10 个补丁。实践中的策略:

  • 只对"最近 N 个版本"(如 N = 3)生成补丁,更老的设备直接发整包
  • 补丁生成失败(版本跨越过大、算法不适应)时自动降级为整包
  • 在版本描述里明确告知设备本次是差分还是整包,设备端逻辑保持一致
  • 算法与压缩的选择(bsdiff / HDiffPatch、zstd)见 LV010-差分算法对比与嵌入式移植,其中的质量指标(补丁体积比、降级率)应纳入发布监控

3. 构建与发布的分离 ​

绝不把"构建成功"等同于"可以发布"。制品入库后,发布是独立动作:

  • 发布需要人工审批(或至少需要明确的发布指令)
  • 灰度批次与放量节奏在发布阶段决定,而不是在构建阶段写死
  • 支持"暂停发布"与"撤回"(撤回 = 把版本描述里的推荐版本改回旧版本)

三、 分发与灰度 ​

1. 灰度分批 ​

典型的放量节奏:

批次范围观察窗口晋级条件
内部验证实验室设备1 天功能符合预期
白名单内部员工 / 种子用户2 天成功率 ≥ 99%,无严重问题
灰度 1%随机抽样1 天成功率与崩溃率无异常
灰度 10%随机抽样2 天同上,且关注长尾异常
全量 100%剩余设备—持续观察

晋级的判据要可量化:升级成功率、升级后重启次数、关键业务指标(如上报成功率)与基线对比不劣化。

2. 分组与设备标识 ​

  • 按 device_model + hardware_rev 分组是最小要求
  • 按地域/客户分组,可避免同一客户全部设备同时升级
  • 分批要稳定:同一设备在多轮询中应落在同一批次(用设备 ID 哈希取模,而不是随机数),否则会出现"通知了但没升"的错觉

3. 强制升级与限速 ​

  • 强制升级只用于安全补丁或严重缺陷修复,并明确降级路径
  • 限速:服务器侧限制并发下载量,或让设备端随机延时(如 0~30 分钟)后开始下载,避免整点拥塞
  • 低功耗设备特殊处理:电量低于阈值时延迟;不要在设备业务高峰时段重启

四、 下载失败的处理与上报 ​

传输层的断点续传协议、分块哈希与断点记录存放位置见 LV007-升级包格式与传输机制。本节讲的是这些机制在运营侧要怎么配、失败了怎么收场。

1. 服务器侧的三个配置 ​

  • 下载走标准 HTTP,服务器只需支持 Range 请求即可让设备续传,零定制
  • 静态包放 CDN,既省源站带宽也提高弱网设备的就近下载成功率
  • 包的体积与分块粒度在发布时确定,设备侧据此精确重传坏块而不是整包重下

2. 失败重试与退避 ​

设备侧的下载失败不该立即重试,而应指数退避:

c
// ota_retry.c —— 指数退避 + 抖动,避免设备群同时重试打垮服务器
#include <stdint.h>

#define RETRY_BASE_SEC   60u      /* 首次重试间隔 1 分钟 */
#define RETRY_MAX_SEC    3600u    /* 上限 1 小时 */

uint32_t ota_next_retry_delay(uint32_t attempt, uint32_t device_id)
{
    uint32_t delay = RETRY_BASE_SEC;
    for (uint32_t i = 0; i < attempt && delay < RETRY_MAX_SEC; i++) {
        delay *= 2u;
    }
    if (delay > RETRY_MAX_SEC) {
        delay = RETRY_MAX_SEC;
    }
    /* 抖动:±10%,把重试时间打散,避免惊群 */
    uint32_t jitter = (device_id % 21u) * (delay / 100u);
    return (delay / 10u) * 9u + jitter;
}

同时限制单位时间内的重试次数(如 24 小时内不超过 10 次),避免一台异常设备反复消耗带宽。

3. 上报的内容 ​

设备上报应包含足以定位问题的信息:设备型号、硬件版本、源版本、目标版本、包类型(整包/差分)、失败阶段(下载/校验/写槽/启动/自检)、错误码、已重试次数、下载耗时、是否曾回滚。

失败阶段字段最关键——它直接告诉运维该去看哪一段代码,而不是面对一句"升级失败"。

五、 测试与验证 ​

1. 异常注入测试 ​

升级路径的可靠性只能靠"故意破坏"来验证。必测清单:

注入时机注入方式期望结果
下载 50%断网恢复后续传,不重头开始
下载 50%断电重启依断点记录续传
校验前篡改包内一个字节校验失败,丢弃升级包,继续运行旧固件
写暂存/非活动槽 50%断电重启后仍启动旧固件,可重新升级
置 PENDING 标志时断电双副本至少一份有效,行为确定
新固件首次启动断电试用计数已递增,重试后回滚
新固件自检人为让自检失败超限后自动回滚到旧固件
mark good 写标志时断电仍处于待确认状态,再走一次确认流程
两槽镜像都改成无效数据Bootloader 进入恢复模式,不跳飞

【本节与机制推演的分工】

LV016-选槽与回滚机制 第六节从设计出发论证"为什么每个掉电点都不会砖",本表是同一组场景的可执行版本:给出注入方式与期望结果,能直接排进测试计划。两表按升级生命周期逐行对应,改动机制时两处都要同步。

2. 自动化验证 ​

  • 版本矩阵测试:每个支持的"源版本 → 目标版本"组合都跑一遍(n×m 组合,用脚本自动化)
  • 回滚演练:定期在实验室设备上主动触发回滚,确认旧固件能正常接管
  • 长稳测试:连续升级 100 次以上,观察 Flash 磨损与参数区写入是否正常

3. 灰度期的观测指标 ​

上线后 24 小时内重点盯:升级成功率、失败阶段分布、升级后 1 小时内重启次数、关键业务指标与基线差值。

六、 量产与运维 ​

1. 出厂固件与首个 OTA 通道 ​

最容易翻车的环节是"第一批出厂设备无法升级":

(1)出厂固件必须自带完整的 OTA 能力:Bootloader、参数区、升级代理、签名公钥一个都不能少

(2)出厂版本号与服务器基线一致:否则设备一上电就发现自己"需要升级"

(3)产线验证环节:产测时实际跑一次"从服务器拉取并安装当前版本"的全流程(可用同版本的假升级包),验证网络、签名、参数区写入全部通畅

(4)应急通道:保留本地升级方式(U 盘 / 串口 / USB),专供首批设备救急

2. 常见故障 FAQ ​

现象可能原因排查方向
反复下载同一版本版本号未真正更新(固件里写死或写入失败)检查参数区与版本上报
升级后不断重启并回滚自检条件过严或依赖未就绪(如网络慢)放宽自检时限,加入重试
校验总是失败服务器文件与描述哈希不一致;传输被截断比对服务端哈希,检查 CDN 配置
差分包全部失败设备当前版本与补丁源版本不匹配核对 src_version,补兜底整包
部分设备卡在恢复模式两槽镜像均损坏(写入时掉电且无兜底)检查写槽的原子性,加恢复通道
Flash 写入报错页未擦除、写地址越界、供电不稳检查擦写流程与电源设计
升级后功能异常但不回滚自检通过但业务仍有缺陷完善自检覆盖,考虑业务级灰度回退

3. 长期演进方向 ​

  • 分级自愈:从"回滚到旧版本"演进到"自动重试若干次 → 回滚 → 上报并等待云端指令"
  • A/B + 差分的组合:整包兜底、差分常态,兼顾可靠性与流量
  • 可观测性:给每次升级分配唯一 trace id,串联服务端下发、设备下载、安装、启动、自检的完整链路

本文档由 markdowncli 技能辅助生成