尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

IoT设备管理实战:用OTA与容器技术搭建远程升级链路

IoT设备管理实战:用OTA与容器技术搭建远程升级链路 做物联网设备这一行我最大的感受是设备卖出去之后运维才是真正的开始。早期我们做网关产品系统是Yocto自己编的rootfs应用直接躺在文件系统里每次升级都要有人跑到现场刷机。后来设备数量从几十台涨到几千台这种模式彻底撑不住了。碰巧当时团队要同时维护两种设备——一边是跑Linux的网关盒子一边是跑Zephyr的MCU控制板——两个系统风格完全不同最后我们决定把“设备管理”这件事整体托付给托管式Linux和Zephyr发行版用OTA和容器技术作为基础设施来设计。这篇文章把我从零搭建设备管理链路的完整过程写出来包括技术选型、分区策略、签名校验、容器运行时选型以及实际部署时踩到的各种坑给正在做IoT设备管理平台的兄弟一点参考。1. 项目解读托管式发行版到底是什么意思1.1 从“自己折腾”到“有人管”的转变传统嵌入式开发者的习惯是打开Yocto或者Buildroot自己编一个rootfs出来。这种玩法很灵活想怎么裁剪就怎么裁剪但代价是你要长期维护一整套系统内核补丁、驱动适配、包管理、安全更新哪一个环节出问题都只能自己扛。在设备量小的时候没问题设备一多光是收集“哪些设备还跑着老内核”这件事就够你喝一壶。托管式发行版的核心逻辑正好反过来。系统层由发行方持续维护设备上跑着一个和云端对接的Agent云端能统一管理版本、下发升级任务应用交付则走容器。你不再需要关心“某个内核CVE有没有打补丁”那是发行方的事你只需要关心“我的容器应用跑得对不对”。说白了操作系统本身变成了一种服务设备只是运行这个服务的载体。这个转变不是简单的工具替换而是运维思路的根本变化。以前是“自己造车自己修”现在是“开租来的车保养交给车队”。优点是你终于能把精力腾出来做业务缺点是你就不能随意折腾内核了系统层的定制空间变小。如果你的产品是通用型物联网设备对内核没有重度定制需求托管式发行版基本是稳赚不赔的买卖。1.2 Linux 和 Zephyr 各管什么“Linux和Zephyr”这两个名字放在一起容易让人误会是要二选一。实际上一台典型的物联网设备里往往两个都要各自管不同层次的事情。Linux侧负责的是计算密集型和生态依赖型的任务运行容器、处理网络协议栈、对接云端、跑数据库和消息队列、做人机交互界面。它的优势是内存管理完善有完整的MMU进程隔离做得好生态丰富你能想到的中间件基本都有现成版本。Zephyr侧负责的是硬实时和低功耗任务传感器采集、电机控制、电源管理、GPIO中断响应、低功耗唤醒。它不是一个传统意义上的发行版而是一个RTOS实时操作系统但搭配MCUboot引导区后可以把它看作一种托管式的固件平台。设备里的两个处理器各干各的但都需要被管理、被升级所以“托管”这件事必须覆盖两层。实际项目中最常见的问题是Linux工程师和嵌入式工程师各管各的升级链路也各做各的最后变成两条互不相关的通道。这种割裂在研发阶段问题不大到了运维阶段就会很痛苦——网关升级了而控制板没升或者反过来版本组合不匹配设备行为异常。1.3 为什么OTA和容器是这套组合的核心回到标题里的三个关键词OTA、Container、IoT。把这三个词放一起就是一套现代IoT设备管理的基本逻辑。OTA是设备部署到现场后远程更新固件和系统的唯一通道。没有OTA设备卖出去之后一切迭代都靠人工成本不可控。容器则是把应用更新和系统更新解耦的关键。一个物联网设备通常有这两类更新需求系统层半年升级一次主要打安全补丁、更新内核应用层可能一周发好几个版本比如业务逻辑调整、告警规则修改。如果这两类更新用同一条链路管理升级频率低的系统会把升级频率高的应用拖死。容器的作用就是让应用层更新完全不占用系统升级通道。系统升级管“底包”容器升级管“业务”两条路分开走互不干扰。这也是为什么“托管式Linux发行版 容器”在工业物联网、边缘计算领域越来越流行——它是真正能支撑大规模设备运营的架构形态。2. 技术架构设备端、云端和升级链路怎么搭2.1 硬件分层MCU 跑 ZephyrMPU 跑 Linux我们做的边缘网关核心板是Rockchip RK3568带一个Cortex-A55四核处理器板子上还挂了一颗STM32G0用来做电源管理和风扇控制。这就是典型的“MCU MPU”双脑架构MPU跑Linux负责主业务和通信MCU跑Zephyr负责实时控制和低功耗场景。两块芯片之间通过UART通信心跳检测由Linux侧主控统一管理。在方案选型上Zephyr跑MCU有几个明显优势支持设备树、内核驱动模型清晰、支持MCUbootOTA升级链路可以复用MCU领域成熟的A/B分区方案。更重要的是Zephyr的内存占用很小一个完整的固件可能只有几十KB升级包传起来毫无压力。有意思的是Zephyr本身并不支持容器。容器技术依赖Linux内核的命名空间、cgroups、OverlayFS这些特性Zephyr作为RTOS没有这些能力。所以正确的架构理解是容器跑在Linux侧Zephyr侧的“容器”实际上应该理解为一种模块化固件组件的动态加载机制。如果你真的要在资源极受限的设备上做动态应用加载行业里通常会用WebAssembly比如WAMR来实现而不是容器。2.2 升级链路的双层设计因为一个设备里同时有Zephyr和Linux我们把升级链路设计成了两层互相不干扰但又有一个统一的调度入口。第一层是MCU固件升级。Zephyr固件通过MCUboot引导MCUboot支持A/B分区模式可以做双镜像备份升级失败自动回滚。升级包由云端下发到Linux侧Linux侧Agent校验签名后通过UART把固件内容推给MCU的接收线程写入另一个分区然后触发重启切换。第二层是Linux系统升级。Linux侧采用A/B根文件系统切换方案。系统分区分为A和B两个根文件系统升级时往非活动分区写入新系统写完后修改BootLoader环境变量重启后切换到新分区。如果启动失败BootLoader因为“尝试计数”机制自动回滚到上一个可用分区。这两层各自的回滚机制组合起来就构成了一套完整的保护网。我们的经验是MCU固件升级尽量通过Linux中转不要直接暴露在公网。因为MCU的通信能力弱直接把升级包发给它容易出错而Linux侧可以做好断点续传、验签、超时重试这些复杂逻辑再把干净的固件数据转发给MCU。2.3 容器应用的交付链路容器应用交付链路相对独立。我们在云端自建了一个镜像仓库设备端Agent启动时向云端注册拿到自己的设备证书。需要更新应用时云端向设备下发一个新版本镜像的下载地址设备用HTTPS拉取镜像然后通过本地的容器运行时完成镜像加载和容器切换。这套链路里最关键的是设备认证。容器镜像里可能包含业务逻辑和数据不能随便让一台陌生设备拉走。我们给每台设备烧录了唯一的X.509证书私钥存在设备的安全存储区里Agent启动和拉取镜像时都带着证书服务端校验通过才给数据。当初图省事用token认证结果一个token泄露差点被刷走几百GB的镜像流量后来老老实实换成了证书体系。容器运行时的选择方面如果设备内存小于512MB我建议用containerd而不是Docker。Docker的守护进程加上完整的管理工具链内存占用比较可观。containerd是Docker底层的容器运行时自带ctr命令功能够用资源开销小很多。后面章节我会详细对比。3. OTA 升级的完整实现从打包到回滚3.1 镜像打包与签名MCUboot 与 imgtool 实操OTA升级的第一个环节就是打包。Zephyr固件用MCUboot的imgtool工具签名Linux系统镜像用我们自己的构建脚本生成A/B分区镜像。签名不是可选项而是必须项——设备在公网环境下任何未签名的固件都可能被中间人替换。以Zephyr侧为例签名命令大致长这样# 使用 RSA 私钥对 Zephyr 固件签名 imgtool sign \ --key root-rsa-2048.pem \ --align 8 \ --version 1.0.0 \ --header-size 0x200 \ --slot-size 0x100000 \ --pad-header \ build/zephyr/zephyr.bin \ signed.bin这里几个参数值得多说一句。--align 8表示镜像按8字节对齐这个必须和Flash的擦写粒度匹配--slot-size是每个分区的空间大小签名工具会把镜像补齐到这个大小--pad-header是为了让镜像在Flash上的地址偏移对齐。刚开始我们没加--pad-header导致固件烧进去后BootLoader始终找不到镜像头排查了一整天才发现是对齐问题。Linux侧的OTA包与MCU固件类似我们会打包成一个tar归档里面包含根文件系统镜像、内核镜像、设备树、版本信息和文件校验和。整个包先用发布密钥签名设备端用公钥验签验签通过才会开始烧写。3.2 A/B 分区切换与版本策略A/B分区的概念今天说起来很顺但真正的踩坑点是“切换时机”。BootLoader在决定切到新分区之前要先做“尝试计数”和“结果确认”。以MCUboot为例它有一个image状态的标记新固件启动后应用主动调用boot_set_confirmed()告诉BootLoader本次启动成功之后这个分区才被设为confirmed状态下次正常启动时继续使用。如果应用没有调用BootLoader会在尝试计数耗尽后自动回滚到旧分区。这个机制非常关键它意味着“升级成功”不是指新固件能启动而是指新固件能稳定运行一段时间后主动确认。我们后来在应用里加了开机自检逻辑确认所有驱动都正常初始化了才做确认操作。曾经有一次升级固件能启动但Wi-Fi模块始终初始化失败因为应用没有做确认BootLoader自动回了滚设备保持了可用的旧版本。Linux侧的逻辑类似。我们用systemd管理一个名为update-status的服务新系统启动后延迟5分钟执行健康检查检查网络连接、容器运行时、关键业务进程。全部正常就写一个/data/update-ok标记文件BootLoader看到这个文件才算新系统成功。这套机制唯一的代价是升级后首次启动会多一个“健康检查确认期”但换来的是非常高的升级成功率。3.3 增量升级差分算法与生产流程全量升级包虽然简单但云端分发的流量成本高。以一个120MB的Linux根文件系统镜像为例如果所有设备都走全量升级一次更新可能要消耗几十GB的云端流量。所以我们实现了增量升级用差分算法对比新旧两个镜像只下发差异部分。Zephyr固件因为体积小我们直接用全量包。Linux侧则用开源工具bsdiff和bspatch发布系统时自动构建机用bsdiff生成旧版本到新版本的差分补丁设备端下载补丁后用bspatch和本地的旧镜像合成新镜像再写入非活动分区。但这里有个很隐蔽的坑差分补丁依赖“设备本地当前镜像”和发布时基准镜像一致。如果设备曾被人为改过文件差分可能失败或生成损坏镜像。所以我们对做增量升级的设备有一个强制校验条件设备上报的当前系统SHA-256必须和发布基准完全一致否则云端直接回退到全量包下发。这个策略牺牲了一点云端流量但大幅降低了升级失败率。除了bsdiff还有更现代的方案如zchunk它把文件按块做哈希支持HTTP Range请求可以只下载变化的数据块。我们计划下个版本试点zchunk主要是因为它对弱网环境的断点续传支持更好。3.4 断点续传、灰度发布与带宽控制OTA升级到了现场最现实的挑战是网络不好。工业现场经常是2G/3G信号或者Wi-Fi信号弱一个几十MB的包很容易下载到一半断掉。我们最终实现的是基于HTTP Range的断点续传Agent下载时把进度记录在本地断网后重启续传不需要从头开始。云端侧做了一个简单的升级调度服务逻辑不复杂设备上线报告版本号服务端返回是否有新版本有则返回下载地址、包哈希、签名。灰度发布就靠一个“批次”字段控制第一批放5%的设备观察一天没有问题再逐步放开到30%、100%。一旦发现某批次设备升级后异常率上升立即暂停该批次的发布并支持远程下发回滚指令。带宽控制也很重要。当时我们所有设备同时启动升级结果云端出口带宽被打满连正常的业务API都慢了。后来加了限速策略Agent下载时限制到1MB/s设备端在夜间低峰时段主动升级没有硬性的时间窗口而是每组设备随机延迟5到30分钟再启动下载避免“惊群效应”。4. 容器技术在受限设备上的落地经验4.1 设备端容器的三个优势与三个代价把容器搬到IoT设备端很多人第一反应是“资源不够”。但对比完裸机部署和容器部署之后我认为容器的收益是值得的。三个优势很明确第一环境一致性。开发环境、CI、设备端用的是同一个镜像不存在“我本地跑得好好的”这类问题。第二隔离性。业务容器和系统组件隔离应用崩了不会把系统拖垮容器的重启策略可以自动拉起。第三滚动更新。应用更新时先起一个新容器健康检查通过再停旧容器业务中断时间极短。三个代价也很实在第一资源开销。容器运行时本身要占内存和磁盘。我们的经验是256MB内存的设备跑containerd比较勉强512MB以上才舒服。第二镜像体积。基础镜像加业务代码动辄几百MB拉取时对网络要求高。第三运维复杂度。镜像仓库、证书管理、运行时升级这些都需要新的运维能力。4.2 容器运行时选型containerd、Docker、Podman、K3s针对不同的设备形态容器方案不能一刀切。如果设备内存大于1GBDocker或者containerd都可以。Docker的好处是生态好调试工具多docker compose对多容器编排很友好。坏处是守护进程占内存群晖和NAS上的开发者体验过IoT设备也一样。我们最终在RK3568网关上用的是containerd配nerdctl作为命令行工具日常操作足够。如果设备只有256MB内存我建议你考虑Podman。Podman是无守护进程架构内存占用比Docker小不少。不过Podman网络配置稍微繁琐对内核特性要求也高。如果设备是一台比较强的边缘服务器比如8核16G内存跑K3s是合理的。K3s是专为边缘场景裁剪的Kubernetes发行版能让你用k8s的声明式API管理节点上的容器适合多设备集群场景。我们的RN 3568网关最终用的是containerd nerdctl没有上K8s。原因是设备数量不多组网复杂K3s带来的收益不明显反而增加了运维负担。4.3 镜像瘦身与资源限制镜像瘦身在IoT领域不是优化项而是必修课。我们的一个业务镜像起初用python:3.11-slim作为基础镜像加上依赖大概500MB结果客户现场2G流量套餐的设备拉镜像慢得要命。后来做了两件事换成python:3.11-alpine再用多阶段构建把编译工具链剥掉最终镜像压缩到180MB。写Dockerfile时遵循几个原则基本就能控制住体积# 阶段一构建 FROM python:3.11-alpine AS builder WORKDIR /app COPY requirements.txt . RUN pip install --prefix/install -r requirements.txt # 阶段二精简运行镜像 FROM python:3.11-alpine COPY --frombuilder /install /usr/local COPY app.py . CMD [python, app.py]多阶段构建的好处是最终镜像里不会残留编译工具和依赖缓存。如果业务代码是Go或Rust写的更简单直接编译成静态二进制基础镜像可以换成scratch或distroless镜像体积能压到30MB以内。资源限制方面每个容器都要配置--memory和--cpus参数。我们给MQTT客户端容器限制的是128MB内存和0.5个CPU核心防止异常业务把整个网关照死。4.4 容器升级与系统升级的配合容器升级和系统升级之间要有一个明确的协同关系。我们在实践中总结出一条原则容器升级不重启系统系统升级不碰业务数据。容器升级走标准流程拉镜像、停旧容器、起新容器、健康检查整个过程业务几乎无感。系统升级则必须考虑容器兼容性。系统升级前Agent会先检查当前正在运行的容器版本是否在“兼容清单”里如果某个容器版本和即将升级的系统内核存在已知冲突升级会被阻塞直到容器先升级到兼容版本。这个逻辑很朴素但执行起来需要云端和设备端双向配合。云端在发布系统版本时必须填写“兼容容器版本范围”设备端在做系统升级前先向云端查询一个“升级预检”接口返回是否允许升级。当初我们不搞这套直接让系统升级重启了结果一个老版本容器依赖的内核模块在新内核里被移除直接起不来花了很大力气才救回那批设备。5. 从零搭建一套最小可用的 IoT 设备管理链路5.1 硬件准备与系统安装我们用来做验证的开发板是树莓派4B模拟Linux网关加一块STM32F429模拟MCU控制板。这个组合虽然性能比真实工业设备强但用来验证OTA和容器链路完全够用。Linux侧安装的是Ubuntu Server 22.04 LTS因为Ubuntu对树莓派的支持最成熟。这里有个提示系统安装时手动分区磁盘分成/boot、/和/data三个区其中/建议将来用A/B模式但前期验证可以先用单分区先把容器和OTA链路跑通再说。我们初期不搞双分区是为了减少变量先把Agent、容器、云端打通再逐步引入A/B切换逻辑。STM32这边用Zephyr的smp_svr示例工程通过UART和树莓派对接。Zephyr SDK编译固件MCUboot作为BootLoader烧到起始地址应用镜像通过MCUboot的imgtool签名后再烧写。5.2 设备端 Agent 的骨架实现设备端Agent是整个链路的中枢我写了一个Python骨架来验证流程。Agent启动后做四件事注册、上报版本、订阅升级任务、执行升级。# agent.py 简化版 import paho.mqtt.client as mqtt import requests, hashlib, subprocess DEVICE_ID gw-001 VERSION_FILE /data/version.json def on_message(client, userdata, msg): task json.loads(msg.payload) if task[type] system: download_and_apply_system(task[url], task[sha256]) elif task[type] container: pull_and_restart_container(task[image], task[ver]) client mqtt.Client() client.username_pw_set(device, device-token) client.on_message on_message client.connect(wss://your-broker.example.com, 443) client.subscribe(f/ota/{DEVICE_ID}/task) client.loop_forever()真实项目的Agent会用Go或Rust写内存和性能更好但逻辑骨架是一致的MQTT订阅任务、HTTPS下载、校验、执行、回执。5.3 云端升级调度与镜像仓库云端我们用了一套极简的组合避免引入过重的平台EMQX作为MQTT BrokerMinIO作为对象存储放升级包和容器镜像一个简单的Python服务管理设备版本和升级任务。这套组合的好处是组件少、容易自托管、协议标准。升级任务的调度逻辑是这样设备上线时上报版本服务端查数据库判断该设备的版本是否最新如果不是返回升级指令。指令里包含下载地址和SHA-256校验值。设备下载并升级成功后再次上报版本服务端确认版本更新。另一个重要组件是镜像仓库。我们用的是Harbor支持镜像签名和安全扫描。设备端拉镜像时containerd会校验镜像签名并只允许拉取被标记为trusted的镜像。5.4 联调流程一次完整的升级验证整个链路联调时我建议从最简单的场景开始不搞A/B切换、不搞灰度先验证“旧版本被新版本替换”这个核心流程。具体步骤是先构建一个v1版本的容器镜像和一个v1版本的Zephyr固件分别部署到设备和MCU上。然后构建v2版本上传到MinIO和Harbor。设备在线后云端手动触发升级任务观察Agent的日志下载、验签、停止容器、写新固件、重启、健康检查、版本上报。我印象最深的是第一次做MCU固件升级时下载、验签都成功了但MCU重启后始终跑不起来。查日志发现问题出在Zephyr固件的--version字段没有递增MCUboot认为这是同一个版本的镜像拒绝了这次切换。这让我养成了一个习惯每次发布前先核对版本号确认无误再签名打包。6. 常见问题排查与工程原则6.1 问题速查表这里把遇到的高频问题整理成一个表格方便大家快速定位。问题现象可能原因排查思路与解决办法固件烧写后BootLoader找不到镜像镜像未按Flash对齐头信息偏移错误确认--align与Flash擦除块对齐补上--pad-header升级后系统不断回滚旧版本新系统未执行健康检查确认确认确认标记文件在预期路径检查系统启动后是否有写权限差分补丁应用失败设备本地镜像与发布基准不一致升级前强制校验本地SHA-256不一致改走全量升级容器拉取镜像超时网络带宽不足或镜像过大优化镜像体积配置Agent限速和断点续传错峰拉取设备升级后业务容器起不来容器版本与系统内核不兼容升级前做“兼容容器版本”预检确保容器可回退到旧版本MCU升级后不生效固件版本号未递增检查Zephyr镜像版本号MCUboot确认镜像状态标志MQTT设备反复掉线证书过期或心跳设置太短检查证书有效期确认Broker的心跳超时配置还有一个容易被忽略的问题设备时间是错的。很多设备断电后RTC不准导致TLS证书校验失败Agent无法连接云端。处理办法很简单在Agent启动时先做NTP时间同步如果NTP不可达至少保证证书有效期在设备本地时间上宽裕几天。6.2 三个值得坚持的工程原则第一升级必须可回滚。任何升级方案只要没有回滚保护本质上都是在赌运气。哪怕是简单的容器升级也要把旧镜像留在本地方便快速回退。第二所有升级必须验签。设备端再缺资源也要留出验签的算力。没有签名的OTA就相当于把设备的控制权交给了网络上的任何一个人。第三升级过程要有状态记录。设备端要把每次升级的记录写进Flash或磁盘包括时间、版本、结果。没有这些数据排查问题时你会抓瞎。这套链路从设计到落地大概花了一个多月中间踩了不少坑。但跑通之后后面所有的设备迭代都轻松了很多。我们把OTA和容器当作基础设施来建设而不是某个项目的临时功能这是我认为最值得的投资。如果你正在规划物联网设备的管理平台建议一开始就把这两件事纳入架构别等设备上了线再补那就不只是技术问题了。
返回列表