手机厂商每次推送系统升级,都在跟包体体积和发布风险较劲。全量ROM包动辄2GB以上,用户下载等待时间长,CDN流量成本也居高不下;如果新版本存在兼容性问题,直接全量推送会让所有用户同时暴露在风险中,售后和客服压力瞬间上升。解决这两个问题的组合拳,是把差分更新和灰度发布放进同一条发布链路里,并让CDN承担高效分发的角色。差分更新可以把升级包从几GB压缩到几十MB,灰度发布则让新版本先在小范围用户中验证,确认稳定后再逐步放量。

一、差分更新:从全量包到增量包的演进
全量升级包包含完整的系统分区镜像,无论用户当前版本是什么,下载的都是同一个大文件。差分更新则只生成当前版本与目标版本之间的差异数据,客户端把这个补丁应用到本地旧镜像上,得到新镜像。这样做的好处非常直接:下载体积大幅缩小,升级时间缩短,CDN带宽消耗下降。对手机厂商来说,一个覆盖多个历史版本的升级活动,如果为每个旧版本都生成差分包,用户只需要拉取几十MB的数据,而不用下载完整ROM。
差分算法大致分为两类:基于文件列表的差分和基于二进制块的差分。基于文件列表的方式先比较两个版本的文件树,只把新增、修改、删除的文件打包,适合文件数量多但单个文件变化小的场景。基于二进制块的差分则不考虑文件边界,直接在字节流上查找匹配块,常用的工具有bsdiff、xdelta3、hdiffpatch等。手机ROM中的system.new.dat.br、vendor.new.dat.br这类镜像通常经过压缩和稀疏化处理,直接对压缩后的文件做差分效果不稳定。一般先在构建流程中把它们转换成可挂载的ext4或erofs镜像,对原始块设备做差分,或者对解压后的dat文件做块级差分。
# 使用bsdiff生成差分补丁 bsdiff old_system.img new_system.img update/patch_system.bin # 客户端应用补丁 bspatch old_system.img new_system_patched.img update/patch_system.bin
bsdiff的内存占用和文件大小相关,处理4GB以上的超大镜像时可能耗尽构建机内存。工程上会把镜像按固定大小切成多个块,例如每块256MB,分别生成补丁。这样既降低单次差分的内存峰值,也能在客户端并行应用补丁。另一个常见做法是针对不同历史版本建立升级路径矩阵,例如支持从12.0、12.5、13.0直接升到14.0,如果旧版本过多,可以只保留最近三个版本的差分包,更老的用户先升到中间版本,再继续升级。这个策略需要在服务端维护版本映射,避免全量包和差分包之间出现路径混乱。
二、灰度发布:分批放量与实时监控
灰度发布的核心不是简单设置一个百分比,而是让更新策略具备可观察、可干预、可回滚的能力。手机ROM的灰度维度通常比普通App更丰富,因为设备型号、硬件配置、运营商定制版本、区域推送策略都会影响升级结果。常见的分组方式包括:按设备识别号做哈希取模,例如取IMEI或Android ID的MD5后两位,映射到0到99的桶;按型号和版本组合定向推送;按销售区域或运营商代码控制;也可以结合内测用户白名单和设备注册状态。
一次典型的灰度发布会从1%或5%开始,观察6到24小时。监控指标至少包括:升级请求成功率、补丁校验失败率、写入recovery或A/B分区的失败率、首次启动成功率、启动后崩溃率、特定硬件驱动的异常日志。如果这些指标在灰度桶内没有明显劣化,再逐步提升到10%、30%、100%。灰度过程中一旦发现严重问题,可以立即暂停推送并修改服务端策略,已经下载了补丁但未重启的用户可以在下次检查更新时收到暂停指令,避免继续安装。
{
"gray_strategy": "imei_hash",
"bucket_total": 100,
"gray_percent": 5,
"target_version": "14.2.0",
"from_versions": ["13.2.0", "13.5.0"],
"pause": false,
"rollback": false
}
灰度配置不要直接写死在客户端,而是由升级服务下发的元数据控制。客户端只负责上报设备信息、当前版本和升级请求,服务端根据灰度策略计算该设备是否命中灰度,并返回对应的差分包下载地址。这样做的好处是调整灰度比例时不需要发版客户端,只需要修改服务端配置。对于已经全量发布的版本,如果发现新的兼容性问题,还可以通过相同通道发布回滚补丁,把用户设备从新版本降回旧版本。需要注意回滚补丁同样可以走差分,只要构建新版本到旧版本的反向补丁即可。
三、CDN在ROM升级链路中的缓存与回源设计
当大量用户同时触发升级时,升级服务本身可能扛不住下载流量,所以下载请求通常会重定向到CDN节点。CDN在这里解决两个问题:一是把差分包缓存到靠近用户的边缘节点,降低下载延迟;二是分散源站压力,避免对象存储带宽被打满。差分包URL需要包含足够的信息来区分不同升级路径,例如/rom/mars/14.2.0/patch_13.2.0_to_14.2.0_diff.bin,其中mars是设备代号,13.2.0和14.2.0分别是源版本和目标版本。
CDN缓存键的设计要避免把不必要的参数带入缓存。如果URL中带签名参数,而且签名随时间变化,缓存键就不能包含签名字段,否则每次请求都会回源。常见做法是把签名放在查询参数中,但CDN缓存键只使用URI和除签名外的固定参数。缓存有效期可以根据升级包是否可变更来设置。灰度期间的差分包可能因为修复问题重新上传,建议设置较短的缓存时间,例如300秒到3600秒;全量稳定包则可以设置7天或更久。回源配置上,需要开启range请求支持,因为部分下载工具会分段拉取大文件。
location /rom/ {
proxy_cache rom_cache;
proxy_cache_key "$uri$is_args$args";
proxy_cache_valid 200 302 10m;
proxy_cache_bypass $arg_nocache;
add_header X-Cache-Status $upstream_cache_status;
proxy_pass http://rom_origin;
}
如果差分包做了动态签名,Nginx这类边缘节点通常需要配合Lua或网关模块剥离签名参数后再计算缓存键。也可以把签名校验放在源站或鉴权服务上,CDN只负责缓存。另一个容易被忽略的问题是命中率。当同一个型号存在多个可升级版本时,差分包数量会成对增长,CDN存储和缓存热度都会受到影响。此时可以按设备型号的历史版本分布做数据统计,优先把用户量最大的升级路径的差分包预热到边缘节点,冷门路径则让用户回源,避免缓存空间浪费。
四、差分更新与灰度发布结合的实现流程
把这两个机制串联起来,完整流程可以分成构建、上传、放量、下载、应用、上报、调节七个步骤。构建阶段,CI系统会对每个目标版本和需要支持的源版本分别生成差分包,并计算SHA256摘要。上传阶段,差分包和元数据一起写入对象存储,同时同步给升级服务。放量阶段,运营人员在升级后台配置灰度比例,升级服务开始向命中灰度的设备返回差分包地址。下载阶段,客户端请求CDN节点,CDN缓存差分包并返回文件。应用阶段,客户端校验摘要后把补丁应用到本地分区,然后重启进入新系统。上报阶段,客户端在重启后把升级结果和异常日志上报给数据平台。调节阶段,数据平台按灰度桶聚合成功率,达到阈值后自动或人工提升比例。
服务端判断设备是否命中灰度的逻辑不能只用随机数,否则同一个设备每次检查可能得到不同结果,导致用户先命中灰度后又回退到稳定版。应该使用设备唯一标识做哈希取模,让判断结果稳定。举个例子,把设备IMEI做MD5,取前8位十六进制转换为整数后对100取模,如果结果小于当前灰度百分比,则命中灰度。这个逻辑放在服务端,客户端不参与命中判断。
import hashlib
def in_gray(device_id: str, percent: int) -> bool:
digest = hashlib.md5(device_id.encode()).hexdigest()
bucket = int(digest[:8], 16) % 100
return bucket < percent
def select_patch(device_id: str, from_ver: str, config: dict):
if in_gray(device_id, config["gray_percent"]):
return config["patches"][from_ver]["gray"]
return config["patches"][from_ver]["stable"]
代码中的percent参数来自升级后台配置,修改这个值就可以控制放量范围。需要强调的是,灰度桶的稳定性依赖设备唯一标识的可靠性。如果设备ID在恢复出厂设置后变化,或者用户通过三方工具修改了标识,哈希分桶结果会漂移。实际工程中可以使用设备的硬件序列号或厂商私有ID,并做好异常标识的降级处理。
另外,差分包本身需要包含版本兼容性元数据。客户端在应用补丁前必须再次校验当前系统版本是否与补丁声明的源版本一致,防止用户手动刷入其他版本后错误应用补丁。升级服务返回的响应中还应携带补丁大小、SHA256、目标版本号、是否需要双清等信息。这样客户端即使下载过程中网络中断,也可以通过断点续传完成下载,并在本地做完整性校验后再进入升级流程。整个链路中的CDN日志、客户端上报、服务端策略记录要统一到同一个追踪ID下,方便灰度期间排查某个特定设备为什么没有收到更新,或者为什么升级失败。