干货 | 携程无线离线包增量更新方案实践
赵辛贵,携程技术中心基础业务研发部无线研发总监。 2013年加入携程,主要负责App基础框架研发相关工作,主要关注App开发框架、性能、质量、效率和新技术。
先后负责和参与携程Native、Hybrid和React Native框架设计、工程模块化拆分解耦、Android插件化动态加载、无线持续交付平台等项目。目前重心主要在React Native框架在公司的推广和研发支持、以及公司内部其它独立App的框架和工程架构升级。
携程旅行App中近半数业务页面使用H5 Hybrid和RN技术开发,为了提高页面加载速度和成功率,我们在开发Hybird技术之初就采用了离线包方式,即将RH5 Hybrid或者RN开发的业务代码打包到App中,直接通过应用商店分发到用户终端。如果有业务功能有变更,就通过我们的无线发布系统,将新的业务离线包更新到App中,从而做到随时发布,动态更新。
当然如果都是全量发布,App在启动时就需要下载更大的离线包,增加用户流量的同时加大了下载失败的概率,因此需要考虑好增量更新的方案。
离线包增量更新方案
下面这张简图,介绍了我们是如何设计离线包增量更新方案的:
从客户端的角度,整个流程分为2部分,离线包下载列表获取和离线包文件下载。
现在以一个新的业务模块上线为例,说明下整个流程:
1、创建业务模块
在离线包管理系统里面,新增业务模块,并配置生效的App、版本和环境(开发/测试/生产)。
2、发布业务模块
在离线包发布系统,选择业务代码仓库分支,然后Build,发布。
3、App打包,获取最新基准包
打包系统中,嵌入下载最新离线包功能的脚本,确保每次打包都能将最新的离线包打包到App中,并将每个包的版本信息,一并打包到App中。
4、App启动,获取最新离线包列表
App启动之后,发送本地App中离线包的版本号,以及App的ID到服务端,服务端返回最新离线包的下载路径。
5、App根据离线包列表下载离线包
获取到离线包列表之后,在后台线程中按顺序逐个下载。
6、离线包安装
下载完成,解压,合并,安装;
其中2个系统的功能简单介绍下。
1、离线包管理系统主要负责以下功能:
a、离线包元数据信息管理
元数据包括唯一包名、适用的平台、优先级、负责人、以及业务频道描述。
b、离线包对应的App关系维护
所有的离线包,最终都要打包到App中,考虑到灵活性和扩展性,需要维护离线包和App+环境的关系。
c、离线包的启停用控制
离线包在某个App版本中不在使用之后,可以修改相关配置。
d、App+版本+环境的最新离线包查询列表
打包App的时候,需要将最新的离线包打包进去,这个时候,需要离线包管理系统提供查询最新离线包列表的API。
2、离线包发布系统包含以下功能:
a、拉取选择仓库分支的代码,然后Build
b、发布Build完成的包 (修改数据库中该离线包版本,修改离线包管理系统中最新包的版本)
c、灰度发布、回滚、停用支持
灰度切分流量下发离线包,发现有问题及时回滚,可以随时停止发布,避免影响更多用户。
d、发布数据查询与监控
发布效果监控,可以查看升级百分比,也支持特定用户的对某个发布的结果查询。
工程实践中的问题和解决方案
上面介绍了离线包增量更新方案,但在实际工程实践中还是会遇到了诸多问题,接下来逐个分析。
包依赖管理
携程旅行App有超过100个离线包,每个离线包,都是一个独立的功能或者业务模块,这么多的业务之间必定有相互依赖的问题,要严谨的解决该问题,需要引入类似node的包管理机制,但这样的解决方案对我们来说太重。
因此我们设计了一套简单的依赖管理规则,来解决这个问题:
1、使用数字标识离线包的优先级,数字越小,优先级越高;
2、优先级越高的包,先下载安装;
3、优先级相同的离线包,下载顺序和发布顺序一致;
实际使用过程中,我们只定义了2个优先级0和100。0为框架类,公共业务类的离线包,100为业务功能的离线包。业务依赖框架正常,极少有业务之间的强依赖,偶尔有的时候,业务之间协调好发布顺序即可。
动态差分
为了让用户能够尽快下载离线包,我们需要尽可能的减小每个离线包的大小。
这个时候,就需要采取差分算法,计算最新发布的包和原始打包到app中的基准包之间的差量,然后下发给App。我们的方案是使用bsdiff做差分:
1、服务端拿最新包和打包到App里面的基准包计算差量,生成patch;
2、客户端下载到该patch文件后,和打包到app里面的原始文件merge,生成最新包;
看起来很完美的方案,并且业内大多做离线包的差分都是采取这种成熟的方案。但是实际效果并不完美,我们发现偶尔会出现300多KB大小的离线包在差分之后,生成的包有100多KB。
经过反复测试,我们发现zip文件解压之后比较里面的变化文件,生成diff文件,然后将diff文件生成一个zip包,比直接bsdiff计算2个zip包生成的diff,会小很多。基于这个测试,我们对bspatch做了一些改进:
上图可以看到,生成patch包的时候,只zip进去变更过和新增的文件,同时,对每个变化过的文件,生成了一个hash文件,这样可以确保客户端将diff文件patch到原始文件之后,能验证文件是否完整, 这一点非常重要,因为bspatch执行的时候,不会因为文件内容合并不正确而返回patch失败。
下图是某个版本中发布的4个差分包,传统bsdiff方案和我们的优化方案使用后,最终实际下载包的大小对比,可以看出优化效果非常明显,
另外,因为打包到不同App定制/渠道包里面的各个离线包版本不同,因此差分包需要动态生成。客户端获取到最新离线包列表之后,会先通到CDN下载,如果CDN没有,再回源到源站服务器,这个时候触发动态差分包生成,生成完成之后,再推到CDN上。
App端离线包下载
主要由以下机制进行保障:
1、重试机制
离线包下载在网络状态不好时,会有下载失败的情况。为了减少网络因素导致的失败,需要增加重试策略,比如最多下载3次,第一次失败,隔15s重试一次,第二次2次失败,隔30s再试一次,3次失败则会终止继续下载。
2、签名校验
文件下载完成之后,需要检查文件是否被篡改过,因为离线包里面都是代码,必须保证代码的正确性,建议在下发离线包列表的时候,下发该文件的签名,下载完成之后,校验签名是否正确。
3、定时轮循
最初我们的离线包列表是在App启动之后,会获取一次,然后下载,当时经常会有反馈离线包下载不及时,不重启就下载不到最新版本。为了解决这个问题,也考虑过使用服务器推送的方案,但是成本较高。因此简单为第一次离线包下载完成之后,每隔10分钟再去服务器查询一次,是否有最新离线包列表,如果有,继续下载,这样保证了发布之后,用户网络正常情况下,最多间隔10分钟左右,离线包就可以被更新。
离线包的使用、安装和加载
离线包是打包到App中,发布到应用市场,用户下载安装的,因此在本地使用之前,需要先解压安装。从服务器端下载到的新离线包版本,也需要解压安装才能使用。下图是进入某个离线包业务的流程:
以业务A为例,简单说明离线包的下载安装过程:
1、业务A的离线包下载成功之后开启子线程合并下发的离线包Anew.7z和App包中的原始包Abase.7z,合并成功之后保存在离线包的工作目录,例如A的工作目录为Awork,将成功合并的目录命名为Abak,不可直接覆盖该离线包的工作目录A_work,因为A业务可能正在被使用;
2、进入A业务,如果发现有合并成功的离线包文件Abak,先使用该文件覆盖Awork, 覆盖成功之后,加载页面时候,需要清空缓存,reload页面,加载失败回滚A_work目录。
发布控制
灰度发布:离线包发布直接到达用户终端,为了确认发布的功能对用户带来的影响线,需要先观察一部分用户行为和数据。这样发布系统就需提供灰度发布功能。我们采用的默认设定规则是10分钟10%,30分钟50% 1小时后达到100%。
发布回滚:发布的包如果有问题,可以回滚到先前版本的包。
停止发布:发布的包如果没有生效,也没有副作用,为了尽可能的减少影响,可以直接停止这个包的发布。
端到端监控
离线包的下载、安装是一个复杂的过程,并且都是在App运行后台进行,用户并无感知,为了能了解发布的状态和结果,需要完整的数据采集和监控。
对于生产环境:可以以一次下载为起点,安装完成为终点,当做一个事务。每次事务完成,都记录一条日志。这些日志上报后,后端就可以根据这些日志进行监控,实现对端到端的离线包更新效果进行监控或告警。
对于测试环境:可以在测试包中保持完善的文件日志,记录离线包下载更新的每一步。另外,可以提供Debug工具,查看每个离线包业务的版本号,例如下图:
小结
通过持续的迭代优化,这样一套无线离线包更新方案,已经稳定的运行在携程旅行主App和集团其它子App中。从目前生产统计数据看,我们的离线包在iOS和Android平台分别达到99.8%和99.5%的下载安装成功率,希望我们的方案可以对读者的类似业务有所参考。
【推荐阅读】