• 对离线包服务整体流程治理和优化
  • 发布于 2个月前
  • 214 热度
    0 评论
早在三年前转转已经有了 h5 离线包服务的工程化能力,并且服务于集团内部若干个线上项目,同时带来的收益也是显著的。

什么是离线包
离线包是将包括 HTML、JavaScript、CSS 等页面内静态资源打包到一个压缩包内。预先下载该离线包到本地,然后通过客户端打开,直接从本地加载离线资源,从而最大程度地摆脱网络环境对 H5 页面的影响。离线包技术不仅提升用户体验,也可以实现动态更新,节约用户流量(一次下载多次使用)。传统的 H5 技术容易受到网络环境影响,因而降低 H5 页面的性能。通过使用离线包技术,可以有效的解决该问题,同时保留 H5 的优点。

转转离线包实现原理
由于 IOS 与 安卓客户端系统能力差异性,并且 IOS 请求拦截时系统本身的一些策略导致,因此转转针对不同端采用了不同端实现技术方案。
安卓侧:安卓侧实现相对简单一些,主要利用 shouldInterceptRequest api 能力进行请求拦截,对于静态资源,先匹配服务端拦截路径前缀如果与本地离线资源匹配则走本地离线没有则走线上 CDN,对于 ajax 请求直接走线上。该方案实现相对简单。
IOS 侧:由于 IOS 客户端能力限制,当请求拦截时,http(s) 的 post 请求则丢失 header、 body,因此在方案的实现上比安卓复杂。

转转内部探索了自己的实现方案,具体原理就是,通过 NSURLProtocol,注册 webview 并设置拦截 Scheme 为 "http"。在 canInitWithRequest 方法拦截所有 scheme 为 "http" 的 url,通过特定规则映射本地路径,如果找到本地资源,则加载本地资源,否则还原 scheme 为"https" 并请求远端数据。

需要制定一套特定的匹配规则方案进行实现。


离线包的更新策略
如何做到及时的更新是一个必须要面临的难题。转转的方案是客户端主动拉取服务离线包版本 + 差分包策略,及时对用户侧的离线包进行更新。

主动拉取策略是 1. 用户打开 app 2. 用户打开 webview。这两种时机触发拉取离线包服务端版本进行与本地版本 diff 来实现及时的更新。

面临的新问题
但是,随着时间的推移,业务项目不断地扩张,工程化体系的演进愈来愈错综复杂,各种因素造成离线包整体服务本身也积攒了较多问题没有较及时的彻底的解决。因此决定对离线包服务现有的问题进行一次相对较彻底的治理与升级。

经过对现有离线包服务、项目情况以及整体使用流程进行了调查,发现了以下几个主要问题。
1.现有离线包平台使用成本较高,离线包管理与项目管理处于并行管理状态;项目离线包的开启与否取决多个人工操作步骤;且没有下载优先级概念。
2.现有离线包平台对离线包的开启率、端内页面整体命中率暂未统计,无法进行整体数据查看与分析
3.现有离线包项目开启率很低,经过统计大概只有 15% 的业务项目接入并开启了离线包功能(开启不代表端会下载)
4.某些没有 pv 的旧项目开启了离线包,并且造成了客户端进行无效的下载,浪费了流量和空间;
5.集团内的魔方(转转内部的活动页搭建平台)项目未接入离线包,某些业务会直接采用的魔方页面作为主入口页面。
当然了,以上背景是基于现象初步分析得出来的一些结轮,只了解现象还远远不够,还需要进一步进行刨根问底。

比如,为什么离线包开启率只有 15%,如何才能做到开启率达到 90%+呢?针对开启率低的问题,经过进一步的相关人员咨询和对离线包整个体系的原因分析,发现主要因素来源于:
1.项目回滚会直接关闭该项目的离线包功能(也就是离线包服务不支持业务项目的回滚)
2.由于某些项目在 IOS 端内请求会挂掉导致临时手动关闭该项目离线包
3.某些项目名称重复冲突 会引起项目发版本时造成离线包更新异常,因此临时关闭离线包
4.项目发版本时如果离线包服务与 ftp 服务通信异常时,会造成离线包服务 mongo 表被创建而实际上没有离线包数据的异常情况,因此临时关闭离线包
5.上述某些原因同时也导致了有些项目负责人的心理负担因此不敢开离线包
经过分析大概了解了最终导致了开启率低的现象的根本原因,那么,应该如何进行对开启率优化和治理呢,具体解决方案是什么呢?下面的内容会进行揭晓。

优化的目标
基于以上列出的背景以及问题,综合考虑之后制定了以下目标:
1.梳理现有离线包整体方案、优化整体工程化发布服务流程,支持版本回退,降低离线包服务异常率,提高稳定性
2.引入自适应策略自动调整项目离线包下载优先级、项目开启状态等算法,减少手动成本提高维护效率

3.离线包整体开启率、命中率的数据统计展示,并提升该指标,方便为后续以数据驱动做准备


主要内容
先放一张优化升级后离线包服务整体架构图。这张图里面包含了这次离线包服务侧升级后的所有内容

离线包构建过程优化
首先对离线包构建发布的整体流程进行了梳理。梳理流程最简洁快速的方式就是画流程图,对流程存在等问题进一步剖析。

对原有的项目构建打包过程进行了分析,发现项目进行 webpack 打包时存在的问题(与现有离线包加载策略不完全匹配):
1.只对打包后离线包文件夹中的 manifest.js 文件进行了 https 的降级处理,导致 ios 在加载资源时,部分没作降级的静态资源请求会走线上,导致离线包资源利用不充分。
2.多页面应用打包之后不存在或不只存在 index.html,构建时候没兼容这种情况,导致 ios 与本地路径的 html 策略匹配异常,导致离线包失效。

3.离线包项目的 ajax hook, 只对正则匹配到 http:// 的 ajax 请求 scheme 做了拦截替换, 没有对 // 进行匹配处理,导致请求 以 http 发出当作静态资源处理 导致了 ajax 请求的 cookie / body 丢失。


针对以上问题,主要进行了代码细节上的兼容与优化,部分代码如下:
// 堆代码 duidaima.com
// offline-plugin.js
const pathAllsJS = globby.sync(zipFileName, {
    expandDirectories: {
      files: ['*.js'],
    }
  });
  // 需要匹配的js资源前缀
  const assetsPath = (offlineConfig.assetsPublicPath || '').replace(/^(https?:)?\/\//, '');
  if(!assetsPath) throw Error('please sure offlineConfig.assetsPublicPath')
  const pathsJS = pathAllsJS.filter((item) => {
    const fileStr = fs.readFileSync(item, 'utf-8');
    return fileStr.includes(assetsPath);
  });

  pathsJS.forEach(jsPath => {
    handlerURLProtocolJS(jsPath, assetsPath);
  });
// xhr-proxy-script.js
window['$ajax_proxy'].$registerInterceptConfig(function (config) {
  if(/^\/\/[\S\s]+/.test(config.url)) {
    config.url = 'https:' + config.url;
  } else if(/^http:\/\/[\S\s]+/.test(config.url)) {
    config.url = config.url.replace('http://', 'https://');
  }
  return config;
})
离线包发布流程优化并支持支持版本回退
经过对整体流程梳理和分析发现如下问题:
1.由于早期没有一个统一项目 name 规范,项目进行构建发布到离线包服务时,会以项目 package.json name 作为离线包项目的唯一名称, 然后发布到离线包服务平台,但是某些项目命名不规范导致两个项目名字重复。离线包服务以 项目 name 作为项目唯一性校验并生成对应 name 的离线包基础包/差分包,如果 name 一致就代表同一个项目,但是两个 name 一样但项目实际不一样的情况会导致 zip 被异常覆盖。并且离线包发布时候会先进行发布版本校验,如果项目进行回滚操作,则认为版本存在导致服务异常,直接关闭该项目离线包。

2.项目构建发布到离线包服务时候,离线包的表信息入库时序有问题,会在离线包的差分包生成之前进行入库并且没有对表数据进行复原,导致如果出现了 ftp 服务连接异常情况表记录有而差分包信息找不到的异常情况。

3.原有的离线包服务表设计有些缺陷或不足,项目的离线包表结构没有维护基础包的字段 baseVersion, 而是放在项目表结构中,导致 baseVersion 发生变化时,历史 baseVersion 直接被新的覆盖掉,这也导致了不支持项目回滚的根本原因,因为基础包没有历史记录,回退时候找不到对应基础包,当下发给客户端时会出现合包异常。

综上所述,如果要避免离线包名称冲突覆盖问题、支持版本回退能力问题等必须要对离线包表结构进行重构,另外需要与工程化体系 SIC 平台打通并提供唯一的项目 name(SIC 即内部统一规范化项目管理平台)。

经过综合考虑,将业务项目表和离线包历史记录表分表进行管理,离线包记录表只记录离线包的一系列信息,项目表通过 bizid 和 metaid 与离线包表进行关联,后向或者前向回滚时将 curMetaid 指向对应 version 的 metaid 即可。

重构之后离线包表结构如下:

流程上的优化主要是项目名称字段唯一性、流程上时序的调整优化等业务相关的工作,事情本身难度并不大这里就不展开了。最后,放一张整体构建发布流程图。

支持根据策略的自动开启及优先级自适应算法
之前面临的问题是,项目的离线包开启与否需要项目负责人手动进行管理(比如项目没有 pv 了,但是不应该在离线包下载列表中),并且没有下载优先级的概念,某些重要的业务需要优先进行下载更新的需求等等。

因此面对这类问题,提出了根据策略进行自适应优先级调整的算法,降低人工维护的成本。策略的主要影响因子主要包括,项目近期 pv、包体积大小、下载列表最大数据等等。

在设计方案时候,遇到两个相对麻烦的问题。

第一个问题是,如何定义优先级,怎么定义策略的规则?
结合实际业务情况的综合分析,将优先级分配策略定义如下:
1.昨日 pv top-50 且 pv 大于 3000,为高优先级,可加入下载队列
2.新发布的项目,默认设置为高优先级队列末尾
3.高优先级离线包的包体积大于 1M 则放队列末尾,且最大限制为 3M
4.年度大促项目为最高优先级,可手动设置最高优
5.人工分配的权重大于策略的分配,即置为负数
另外,对于项目优先级具体系数的设定准则,参考了参考 UNIX 系统进程调度规则中策略,优先级系数设定规则。即从负数到正数,优先级越低,方便进行随时调整和排序,方便设定高优先级任务队列,如设定 -1。

优先级系数根据策略自动设定的部分代码如下:

当然了,也可以在离线包管理平台手动调整项目优先级。

第二个问题是, 项目的 pv 信息不能直接获取的问题。
由于离线包的项目名称与集团内部性能平台名称没有一个统一的映射关系,也就是通过 SIC 项目名称去查询对应项目的 pv 数据是查不了的,因此,需要建立映射关系表将两者打通。然而有 pv 的 FE 项目将近 110 个项目,如果人肉去维护一个 110 个的映射表需要的时间成本太大了。因此,需要寻找一个相对快捷的方式去实现这个关系映射表带建立。

经过分析发现,性能平台的名称与 lego 上报参数强关联,上报参数与项目的 html 部署路径有一定的关联,部分项目与 package.json 名称有关联。因此,发现可以通过数学集合中一系列的的 "并" "交" "补" 运算就可得到一个初步的映射表。然后基于这个初步的映射表再人肉去添加那些没统计到的即可。

最后,有将近 70 个项目通过集合运算的方式得到了映射关系,因此减少了三分二的人肉工作量。另外,还有一个需要注意的点是,新建项目发布时也需要支持自动更新该映射表,解决起来很简单,让工程效率组提供一个基础信息查询的接口,把对应部署路径信息与名称信息新增到映射表即可。

最终映射表效果如图所示,并支持手动修改和添加映射条目。

整体开启率与命中率的统计
在进行开启率和命中率之前,先明确一下怎么定义这两个概念的。

开启率,即统计项目接入离线包平台之后是否用户主动关闭了,如果关闭了则定义为未开启,即开启率为 未主动关闭的项目数量/接入离线包服务的总项目数量。并且该命中率未统计魔方搭建的活动页面,因为该类型页面生命周期较短并且数量极其庞大,计算入开启率意义不大。由于默认接入开启的因此,在不主动关闭的情况下开启率可以达到 100%。当然了这个开启不代表就会添加到下载队列,只有高优先级项目才会添加到下载队列中。

命中率,即用户打开 app 后访问 webview H5 页面时命中离线包的比例,即打开的页面命中了离线包的次数/打开页面的总次数。因此该数据不可能是 100%。因为业务有近百个项目,所有项目都离线下来对 app 来说是灾难性的是不可接受的(空间和流量的占据过大)。

开启率通过离线包服务的定时任务模块每日进行统计即可,但是命中率需要根据客户端上报的埋点信息来计算,通过离线包服务当时拉取大数据平台的每日埋点统计数据即可。

在做定时任务统计的时候,遇到一个很棘手的问题就是,由于离线包服务部署了 4 台机器,egg-schedule 定时任务会跑四次,任务完成后统计数据入库时相当于数据有三条是垃圾数据。当时尝试了各种方案,redis 原子锁 / 任务调度器等等。最后发现 agenda 这个分布式任务调度器很好用,适合业务场景。因此基于 egg-schedule 和 agenda 封装了一个支持集群定时任务的 egg 插件 egg-cluster-schedule。

最终,开启率与命中率的统计如图所示。开启率基本在 90%+,命中率平均在 50% 左右。

魔方项目接入离线包平台
首先介绍一下魔方是什么,魔方是转转内部快速搭建活动运营 h5 页面的平台,集成了区块组件的拖住、编辑、构建、一键发布等一体化的活动页搭建平台。魔方系统是一个快速搭建活动页面的可视化平台,供转转、找靓机、采货侠各业务线运营人员配置使用。支持手机客户端页面的搭建,配置好的活动,需要获取专题链接后自行推广。可作为大促活动页,也可展示规则或协议、用于日常页面承接等。

在分析提升命中率问题时,经过排查发现某些魔方活动页 pv 量很大,但没有接入离线包到工程化体系内。

魔方项目主要用于活动运营页面的搭建发布,但是在端内某些业务的主入口也是嵌入的魔方页面。另外,魔方项目的构建发布工程化流程是独立的,是在单独的一台机器进行的,那么,面临的问题就是如何简洁快速的接入现有的离线包工程化服务体系。

通过对接入流程的梳理,发现主要改动如下:
1.魔方构建侧安装离线包平台所需字段进行接口上报,唯一项目名称,已魔方已有 hash 值作为项目名称即可
2.离线包服务支持魔方项目的提供一个专门的项目类型标识字段,新增 type 为 magic
3.魔方项目的开启与自适应优先级策略与现有的策略融合,即 项目 pv 列表、优先级列表的融合
4.客户端兼容魔方离线资源的路径规则
原有项目的匹配路径为:
// 普通项目 html 路径示例
https://m.zhuanzhuan.com/
  - open/
    - ZZBook/
      - index.html
魔放项目的匹配路径为:
// 魔方项目 html 路径
m.zhuanzhuan.com/
  - Mzhuanzhuan/
    - zhuanzhuan/
      - zzactivity/
        - magic/
          - 6241a3d76e6308005fed810d/
            - index.html
因此需要客户端对 html 以及静态资源的匹配路径进行兼容处理。经过与相关负责人讨论和分析后,发现整个接入过程并不难并且客户端兼容成本也比预想要低。具体流程与上面构建发布流程图相似。最终实现效果如下,可以看到魔方项目资源路径都可以正常命中。

离线包异常监控
能够测试到的功能性的问题基本上得到了解决,但是用户的使用场景往往复杂多变,因此,想要监控每一个用户使用时的稳定性问题,还需要将此接入监控或报警平台。在离线包的监控方面,主要通过埋点上报和 sentry 异常报警方式进行。lego 埋点 主要上报一些问题排查的辅助信息,sentry 主要上报一些该技术在用户使用时遇到的较严重的错误信息,比如下载异常、解压异常、差分包合包异常等会影响到正常使用离线包的异常。

根据上报到 sentry 平台的异常信息可以很方便的定位和排查问题。

总结及展望

经过这次对离线包服务整体流程治理和优化,集团内 app 主入口的页面都可以命中离线包资源。自己对 Hybrid 体系下的离线包技术也有了新的认知,因为该技术涉及到了集团内整个 hybrid 工程化体系的各个方面。


这次优化其实并没有达到自己设定的预期目标(所有 App 内离线包整体命中率达到 60%+),主要原因是业务项目数量庞大,考虑到用户终端影响,只能下发核心高优先级的项目供给客户端下载,这也说明特定场景下并不是数值越高就一定越好,需要一个综合的衡量结果。另外,业务项目对依赖插件的升级覆盖过程比较漫长也导致某些项目暂时没开启生效从而影响到了整体命中率,其实这也是基础设施升级后需要面临和重视的一个问题。


另外,由于某些原因,虽然做了一些工作,但是还有很多要改进的地方,比如项目更新的策略有主动 pull 能不能升级为被动 push, 这样更新的会更加及时。IOS 侧整体方案优化,现方案虽然能正常运行,但是其维护成本较大,扩展性也较差,需要发掘新的简洁方案。分析命中数据、页面详细性能数据等,发掘离线包体系还有哪些优化的空间。现有的命中率统计方案 没有统计到 SSR 项目,后续需要支持优化等等。这些都需要不断去探索去优化。道阻且长。
用户评论