gtts的博客

QRN更新方案

August 10, 2018

名词解释

QP 包: qunar 自定义的一种文件格式,类似zip 包,会将静态资源(js,css,png等)打包成一个文件

vid:类似版本号,整型类型,随着客户端发版递增,用来做版本控制

背景

在大前端的环境下,跨平台和动态更新是两个绕不过去话题。2014年Qunar 在Hybrid 框架上开始投入研发,最终得到大面积的推广应用,得益于动态更新技术,前端做到了灵活发布,不再受制于客户端发版限制。 到了2015年Facebook开源了React Native(RN) ,Qunar 第一时间开始调研,认为RN 是Hybrid 的方案的一个补充,可以用在对性能要求更高的地方,随后我们在RN 的基础上做了大量改进,研发出了QRN 框架,而动态更新方案也同样适用于QRN,使得QRN 项目也可以做到灵活发版,快速迭代。

对RN 来说,在没有动态更新技术的情况下,也可以通过以下两种方式达到开发目的:

方案1:将js 资源打包到客户端项目里,跟随客户端发版;

方案2:将js 资源发到线上,每次打开客户端时,请求js 资源,再加载运行。

如果再进一步,对方案2加一个缓存策略,这算是最小可用的一个动态更新方案了。但是针对移动端复杂场景,还远远达不到我们的要求,经过不断迭代,Qunar 有了更完善的动态更新方案,下面我将和大家分享一下QRN 的更新机制。

最小化解决方案

动态更新主要解决了版本控制页面加载慢灵活发版的问题。

1、解决版本控制问题

版本控制问题归根结底就是指定版本的客户端能下载到合适的最新QP 包。如果做不好版本控制,可能会引起线上故障,想象一个场景,Native 在最新版本提供了一个新插件供RN 使用,在最近一次迭代时RN 项目使用了这个新插件,那么将这个RN 项目发出去之后,如果让老版本的客户端运行了这个最新的RN 项目代码,因兼容性问题将会引起不可预知的故障。

我们解决版本控制问题的思路是,配置一个客户端的vid 字段,然后通过这个配置打出的QP 包,能被所有比指定vid 大的客户端下载,当有新的QP 包更适合指定vid 客户端时,服务端会下发最新的QP 包。

vid配置如下所示:

iOS_vid : vid_10
android_vid : vid_10,com.qunar.module_1,com.qunar.module_2 

注:Android 大点的项目有native 模块的动态热发,所以比起iOS 多了组件版本项。

还有一种场景我们经常遇到,要求修复指定版本的bug 时,该如何处理呢?我们采取的解决方案是vid 字段可以配置区间,这样就只用vid 落到区间里的客户端能下载到bugfix 的QP 包。 Vid配置如下所示, 版本9和10可以下载到:

iOS_vid : vid_9-vid_10

2、解决页面加载慢问题

针对页面加载慢,我们可以通过缓存策略,将本来应该从线上请求的资源,在本地存在的情况下直接从本地获取,本地不存在再去线上请求。如下图所示:

image

步骤1先发起一个网络请求没有直接从服务端获取数据,而是通过步骤2经过了“网络请求拦截器”,再经过“拦截过滤器”进行一次摔选,保证本地有资源的情况下,直接从本地拿数据,在本地构造一个Response 完成网络请求,本地没有数据的情况下,通过步骤7从线上获取资源,这样就在数据获取方面做到了最快。

3、解决发版灵活问题

针对灵活发版,我们先给出一个最小化解决方案。 我们的业务代码一般放在gitlab,当代码开发完成之后接着就是打包,生成一个可供客户端下载的包(即QP 包),这个包需要一个服务端来做版本控制,我们暂且叫QP Server,最后客户端通过一定的更新策略,就可以更新到合适的QP 包,当本地有包了,就按前面介绍的缓存策略来加载资源,如下图所示。

image

仅通过上图所示方式,当QP Server故障或者网络慢时,客户端将下载不到QP 包,这时应该考虑一个应对这种情况的方案,当本地获取不了资源时,会从线上获取,这个在“解决加载慢问题”的图片中也是可以看出来的。在上图方案的基础上,我们在打包环节加一些步骤,先将qrn 代码打包成jsbundle 上传到jsbundle Server, 接着再打出QP 包,这样就可以保证客户端在下载不到QP 包时,可以通过jsbundle Server 直接加载js。如下图所示:

image

做到这些,我们已经有了最小可用方案了,但是在实践过程中,总会有各种各样的情况出现,比如:

  • 安全性如何保证,QP 包被人篡改了会怎么样?
  • 如果线上出了故障,怎么快速解决?
  • 发了新版本更新率低,想提高搞更新率怎么办?
  • 想要灰度发布,怎么办?
  • ……

面对这一系列的问题,我们不断改进更新机制,下面我会挑重点进行分享。

QP 包如何保证安全

QP 包的安全问题主要表现在传输安全、存储安全和包完整性上, 若被中间人攻击替换代码,会造成较大的危害,传输方面,我们采用HTTPS 以及客户端到服务端请求参数加密的形式来保证安全。在存储安全和完整性校验上,我们采用了RSA 校验方式,可以参考下图。

image

  • 1、请求url
  • 2、job打QP 包
  • 3、通过私钥加密QP
  • 4、生成加密后的md5
  • 5、将QP和md5等上传到更新服务器
  • 6、客户端请求
  • 7、客户端计算QP 包的md5
  • 8、客户端通过公钥解密从服务端拿到的加密的md5
  • 9、对比7和8两步的MD5 值, 若相等,则校验通过

从图中可以看出,我们用到了差分(bsdiff)更新方案,客户端本地存在老版本QP 包时,只需要下载新版本和老版本的差分数据,在客户端把差分数据和老版本做一个patch,就可以得到新版本,同时通过MD5校验,可以检验合并是否成功,如果不成功,再去下载全量的新版本QP 包。

更新机制的完善

经过不断完善,有很多细节上面的优化,从大的方面来看QRN 的更新机制主要有:

  • WiFi 下的全量更新
  • 任意网络下的单项更新
  • 强制更新
  • 下线回滚等

1、WiFi下的全量更新

目前系统有接近100个QP 包,每个1M 左右,在考虑到为用户省流量的情况下,我们不能在任意网络情况下都去下载QP 包,所以只在WiFi环境下进行所有QP 包的下载,逻辑如下图所示。

image

当下载完QP 包之后,并不能直接替换,因为当一个QP 包被使用过之后,如果马上替换,可能会出现第一个页面和第二个页面使用了不同版本QP 包的情况,在两个版本不兼容的情况下,将会引起线上故障,所以替换需要定一个时机。

  • 当QP 包未被使用过,则下载完就直接替换达到可使用状态。
  • 当QP 包已经被使用过了,下载完之后先放到缓存目录,等替换时机(图上有)出现了,再替换。

2、任意网络下的单项更新

任意网络下的单项更新,从字面意思就比较好理解,唯一需要明确的是,在打开一个项目的时候,我怎么知道要更新哪个呢?我们的解决方案是,在请求的url 上,加hybridId 参数,通过hybridId 参数就可以知道要单独更新哪个包。 例如:

https://ued.qunar.com?hybridId=test

单项更新将不会区分网络,在任意网络都会进行,具体如下图所示:

image

上图红框圈住的地方,将会有页面的加载逻辑,包括强制更新以及是否下线的判断,后续再进一步说明,我们先看下在正常情况下的流程,如下图所示。

image

3、强制更新

强制更新是为了解决出了故障或者希望某个版本(业务做活动)的QP 包能快速被更新到而设计的功能,使用起来也相当简单,只需要在发布QP 包的时候,选中强制更新选项即可。

原理其实就是对当前发布版本的QP 包做一个强更的标记,客户端在检查更新的时候,获取到这个标记之后,会缓存下来,直到打开业务页面的时候,查一下缓存,如果是强制更新,就先更新完再进业务,打开业务页面时的流程,如下图所示。

image

我们为了保证页面打开的速度,没有在每次打开页面都去检查是否强更,而是通过一个论循的方式,每隔5分钟全量检查一次更新,然后将检查结果进行缓存。这样做的目的是因为,检查更新需要耗时,而我们期望更快的页面打开速度,不想因为少部分的更新情况而让所有用户所有时间都去先检查一次更新,所以我们的强更方案是牺牲部分更新率的,如下图所示。

image

从数据统计情况来看,强更的效果也是相当明显。

  • 普通更新,会比较慢,每个项目情况不同,长尾效应也比较长,统计如下图所示。

image

  • 强制更新,不管是iOS 和Adr 在2小时内达到了90%以上的更新率,这比起普通的更新实在快太多了,统计如下图所示。

image

从上图也可以强制更新并没有达到100%的更新率,我们基于性能以及用户体验考虑,只能无限接近100%的。

4、下线回滚

强更解决了更新率低的问题,但是当遇到线上故障,我们还没有新版本能发布的情况下,我们希望能下线出问题的版本,同时能回滚到可用的版本。早期我们采用的方式是,回滚代码,重新发布QP 包,用新的版本去覆盖老版本,这种策略执行起来不但耗时,而且因为更新率不能达到100%,导致问题版本一直存在。

要做好下线回滚,不能只从服务端做,而需要客户端配合一起实现,这样才能做到客户端和服务端同时下线。

下面举例说明,假设一个项目hybridId 是hybridIdTest,本地QP 包的版本号为99,当客户端客户端发起检查更新请求时,参数如下:

{
    hybridId: "hybridIdTest",
    QPVersion: 99
}

请求结果如下:

{
    status: 0,
    message: "成功",
    data: {
        hlist: [
            {
                hybridId: "hybridIdTest",
                patchUrl: "差分包url",
                md5: "rsa加密的md5值",
                url: "全量包url",
                version: 100,
            }
        ],
        offlineHlist:[
            {
                hybridId: "hybridIdTest",
                version: 99
            }
        ]
    }
}

offlineHlist字段中,包含了hybridId=hybridIdTest,version=99的条目,说明99版本已经在服务端下线了。数据hlist部分给出了版本号100的下载地址。当客户端要加载页面时,如果本地版本号还是99,那么就要将版本99的QP 包标记为失效,然后下载版本100的QP 包。

总之就是当后台对一个项目的某个版本下线之后,客户端在发起请求的时候,会拿到offlineHlist 字段,在加载页面的时候,会采取对下线版本失效的处理。这样就达到了服务端和客户端同时下线的目的。

最后再看下如何回滚,当客户端能接收比本地版本低的新包,这样服务端可以下发合适当前客户端版本的没有下线的最新QP 包,从而达到回滚的目的。

总结

本文分享了QRN 的更新方案,从最小可用方案到遇到问题衍生出一套完善的更新机制,并且得到了大量实践检验。限于篇幅所限,并没有进一步深入细节,其实要让整个方案可靠且方便使用,还有更多的环节,就QRN 更新方案来说,我们在调试工具,发布系统,打包脚本等等环节都有大量投入。

最后,希望本文可以抛砖引玉,带来更多思考以及好问题。


gtts

人生没有白走的路,每一步都算数
我的github