升级包结构设计:升级包的组成与分片策略
好,咱们进入第二章。上一章聊了断点续传的整体架构,这一章咱们得把升级包本身拆开看看。
说白了,升级包就是你要发给车机的那坨数据。但怎么组织这坨数据,直接决定了断点续传能不能续得上、续得快。我在项目里见过不少团队,上来就写业务逻辑,结果升级包结构一团糟,断点续传根本没法做——嗯,咱们今天就把这事聊透。
升级包的三大组成部分
一个标准的车载OTA升级包,我习惯把它分成三块:元数据、分片数据、校验信息。缺一不可。
| 组成部分 | 核心内容 | 作用 |
|---|---|---|
| 元数据 | 包版本、目标ECU列表、分片总数、分片大小、哈希算法、签名 | 描述升级包“是什么、给谁用、怎么拼” |
| 分片数据 | 按策略切割后的二进制块,每个分片有独立序号 | 实际传输和存储的最小单元 |
| 校验信息 | 每个分片的哈希值、整体包的哈希值、数字签名 | 保证数据完整性,防止篡改 |
关键点:元数据必须在升级包的最前面,而且不能被分片。车机拿到升级包的第一件事,就是解析元数据。元数据坏了,整个包就废了。
元数据设计:升级包的“身份证”
元数据我一般用JSON或Protocol Buffers来定义。JSON可读性好,调试方便;Protobuf体积小,解析快。车载场景我倾向于Protobuf,毕竟带宽和存储都金贵。
一个典型的元数据长这样:
message OtaPackageMeta {
string package_version = 1; // 升级包版本号
string target_ecu_id = 2; // 目标ECU标识
uint32 total_chunks = 3; // 总分片数
uint32 chunk_size = 4; // 每个分片的大小(字节)
string hash_algorithm = 5; // 哈希算法,如"sha256"
bytes package_hash = 6; // 整个包的哈希值
bytes signature = 7; // 数字签名
uint64 package_size = 8; // 原始包总大小
repeated EcuTarget targets = 9; // 多ECU场景下的目标列表
}
你可能会问:为什么要把total_chunks和chunk_size放在元数据里?
原因很简单——车机拿到元数据后,就能算出自己需要收多少个分片。如果之前已经收了30个分片,发现总分片是100个,那它就知道还要收70个。这就是断点续传的起点。
我的习惯:元数据里再加一个min_supported_version字段。有一次我在项目中遇到旧版车机不兼容新版升级包格式,差点导致远程升级大面积失败。从那以后,我都在元数据里带上兼容性信息。
分片策略:切多大才合适?
分片粒度是升级包设计的核心问题。切得太小,元数据膨胀,网络开销大;切得太大,断点续传的粒度太粗,重传代价高。
我经历过一个项目,一开始用了1MB的分片,觉得挺合理。结果车机网络环境差,经常传一半断掉,每次重传都要丢将近1MB的数据。后来改成256KB,重传效率提升了4倍。
分片大小的选择,主要看这几个因素:
- 网络稳定性:网络越差,分片应该越小。4G环境下我常用256KB~512KB;Wi-Fi环境下可以用1MB~2MB。
- 存储限制:车机MCU的RAM通常很小,分片太大可能放不下。我曾经在某个MCU上只能用64KB的分片,因为它的RAM只有128KB。
- 校验开销:每个分片都要算哈希,分片太多会增加CPU负担。但现代车机芯片算个SHA256也就毫秒级,这个不是瓶颈。
- 传输协议限制:有些HTTP服务器对单个请求的大小有限制,比如Nginx默认的client_max_body_size是1MB。
| 场景 | 推荐分片大小 | 理由 |
|---|---|---|
| 4G蜂窝网络(信号一般) | 256KB ~ 512KB | 断线重传代价小,适应波动 |
| Wi-Fi / 以太网 | 1MB ~ 4MB | 带宽充足,减少分片数量 |
| MCU(RAM < 256KB) | 64KB ~ 128KB | 内存限制,必须小分片 |
| 卫星/窄带物联网 | 16KB ~ 64KB | 极低带宽,极小分片 |
注意:分片大小一旦确定,整个升级过程中不能改变。否则车机算不清自己到底收了哪些分片,断点续传的逻辑会乱掉。我曾经见过一个团队在升级中途动态调整分片大小,结果车机端的分片索引全对不上了——那叫一个惨。
校验和设计:每一片都不能错
校验和是升级包的“安全锁”。我见过有些方案只在包级别做校验,分片级别不做。这其实有风险——如果一个分片传坏了,你得等到所有分片收完、合并成完整包之后才能发现。那时候再重传,代价太大了。
我的做法是:每个分片独立校验,整体包再做一次校验。
分片校验用SHA256就够了。整体包校验除了哈希,还要加数字签名,防止升级包被篡改。
// 分片校验信息结构
struct ChunkChecksum {
uint32_t chunk_index; // 分片序号
uint8_t hash[32]; // SHA256哈希值
uint64_t chunk_offset; // 在原始包中的偏移
uint32_t chunk_size; // 实际大小(最后一个分片可能不满)
};
// 整体包校验信息
struct PackageChecksum {
uint8_t package_hash[32]; // 整个包的SHA256
uint8_t signature[256]; // RSA-2048签名
};
车机收到每个分片后,先算哈希,跟元数据里的比对。对不上就直接请求重传这个分片,不用等全部收完。这就是断点续传的“精准打击”。
升级包整体结构图
下面这张图把升级包的组成和分片流程串起来了。你看一眼就能明白整体逻辑。
分片索引与存储策略
分片收下来之后,怎么存?我推荐两种方式:
- 文件系统方式:每个分片存成一个独立文件,文件名包含分片序号。优点是简单直观,缺点是文件数太多时inode可能不够用。
- 稀疏文件方式:预先分配一个跟原始包等大的文件,每个分片写入对应的偏移位置。优点是只有一个文件,管理方便;缺点是需要文件系统支持稀疏文件(ext4、btrfs都支持)。
我个人偏好稀疏文件方式。在项目中用过一次文件系统方式,结果升级包分了2000多个小文件,车机的flash文件系统差点撑爆。换成稀疏文件后,一个文件搞定,清爽多了。
小技巧:用稀疏文件时,记得在元数据里记录每个分片的chunk_offset。这样车机收到分片后,直接seek到对应位置写入,不需要额外维护映射表。
避坑指南:我踩过的三个坑
- 坑一:分片大小跟文件系统块大小不对齐。我曾经用511KB的分片大小,结果每个分片跨了两个文件系统块,写入性能直接腰斩。后来改成512KB(对齐到1MB边界),问题解决。
- 坑二:元数据里没留扩展字段。有一次需要增加一个“升级后需要重启”的标记,结果元数据格式已经冻结了,只能打补丁。从那以后,我都在元数据里留一个
map<string, string> extra字段。 - 坑三:分片序号从0开始还是从1开始?看似小事,但团队里有人用0有人用1,对接时出了bug。我统一规定从0开始,跟数组下标一致,减少心智负担。