美团金融扫码付静态资源加载优化实践

扫码付项目是美团金融智能支付团队面向 C 端消费者推出的一款 H5 融合支付类的产品,消费者在商家消费之后,可使用多种 App 进行扫码支付,同时可对商家进行评价,支持美团、大众点评、微信、支付宝、美团钱包等多种 App,目前业务日均 PV 千万级。

如下图所示:

接入扫码付的商家大多数位于购物中心、写字楼等人口密集的室内空间。网络链路复杂、相对开阔的地区网络质量较差,为了减轻网络条件的影响,我们使用团队之前实现的模块加载器 ThunderJS。通过 字符级增量更新 减少文件传输大小,节省流量、提高页面成功率和加载速度。其中增量计算能力由美团平台的静态资源托管方案 Build Service 支持。

我们曾经在 《美团智能支付背后的前端工程师》介绍过我们的前端服务架构,如下图:

ThunderJS(团队内部实现的一款 CMD 模块加载器) 属于其中非常重要的一环,集成在脚手架中为井喷的业务发展提供了基础。相比业界其它模块加载器,ThunderJS 定制加强了与静态资源托管(公司自研的Build Service)结合的能力,能够让我们对静态资源的加载进行针对性的优化,而在 C 端项目中,静态资源的加载优化是我们尤为重视的。

扫码付项目中也使用了ThunderJS,随着业务规模的持续增长,ThunderJS 的方案也在不断优化,本文主要介绍基于 ThunderJS 和 Build Service 的产品优化方案,希望为大家优化项目的静态资源加载提供更多思路。

最初的方案

ThunderJS工作流程

ThunderJS 将页面的 JS 资源及版本信息存储在 LocalStorage 中。页面加载时通过线上版本和本地版本来判断是否需要更新,如果需要则会尝试进行 Diff 合并请求并 Patch 到本地资源。不需要更新则直接执行 LocalStorage 中缓存的数据,并且在合并请求失败的情况下会逐一加载单文件。

是否需要更新

判断是否需要更新的具体原则如下:

  1. 该文件名在线上版本和本地版本中都存在。
  2. 该文件的版本在线上和本地中一致。
  3. 该文件存在于LocalStorage中。

Diff 合并请求 与 Patch Diff 结果

流程图中的 Diff 合并请求 是指在一次请求中输出多个文件的增量计算结果,请求合并是一种常用的 Web 资源优化策略,拼接多个相同媒体类型的资源经由单个请求输出,可减少页面实际发起的网络请求数。请求合并需要 Web 资源加载器配合。

增量计算的输出是一个固定格式的 JSON,描述了 Patch Diff 结果时所遵循的规则,如下图:

例如:

以上数据结构表示原文件从第 0 个位置开始保留 33 个字符,连接 mn ,从第 34 个位置开始保留10个字符。

Patch Diff 结果就是利用增量更新的结果,结合原文件,将文件恢复至最新文件的过程。

Build Service 工作流程

Build Service 是美团平台的静态资源托管方案,提供静态资源部署、处理和分发能力,对接 CDN。

如图,用户请求达到 CDN 如果没有命中缓存,会一路回源至源站,源站检索并处理资源,经网关输出给 CDN。本文中提到的增量计算属于资源处理任务,由源站执行。

文本增量计算的工程选择

文本增量计算最初基于编辑距离原理实现,时间复杂度 O(N^2),与文本长度正相关,实际应用时性能较差。Build Service 选择 Myers 增量算法,有效降低单次增量计算的时间消耗。其时间复杂度由 O(N^2) 改变为 O(ND),与文本长度、差异长度正相关。Web 业务迭代频率高、单次迭代差异小、D 接近常数,使用 Myers 增量算法时间复杂度可接近 O(N)。

初步效果

根据扫码付的统计结果,增量更新相比全量请求,传输数据可减少多至99%,合并请求平均可减少请求数95%。

业务增长与计算瓶颈

随着业务的增长,PV 很快就在2017年4月份达到了百万级。扫码付业务采用细粒度模块化的设计,业务不断迭代,文件数越来越多,单次合并请求的文件数超过 30个。需要进行增量计算的版本组合也越来越多,跨越多个版本的增量计算开始出现,增量计算耗时增加,Build Service 遇到了计算能力的瓶颈。我们发现3s超时时间条件下,合并请求的失败率超过50%,于是着手开始优化。

Build Service优化策略

服务拆分与隔离

Build Service 最初直接通过公共集群提供文本增量计算服务。公共集群同时还承载着其他计算任务,如文件压缩、引用计算等。增量计算与其他任务相比,计算规模差异巨大,消耗了集群大多数算力,导致其他计算任务延迟大幅升高。为了避免公共集群不可用对公司其他业务产生影响,Build Service 紧急拆分上线 Build Service Mixer 服务(以下简称 Mixer 服务),将请求合并和增量计算独立出来单独搭设集群,实现业务隔离。

Mixer 服务上线后,隔离了增量计算对其他业务的影响,争取了一些时间优化整个方案。

持久化计算缓存

合并请求的各个资源文件是互相独立的。Mixer 收到一次请求,会分别缓存每个资源文件的计算任务输出。不同的资源合并请求可以复用结果片段,减少不必要的计算。上线后,Mixer 服务的计算能力显著增强,日可用性一度达到100%,计算成功的增量片段再输出的时间消耗稳定在50毫秒以内。

超时自动重启机制

Myers 增量算法大多数情况下性能提升显著,但是当文本差异较大时,计算耗时会显著增加。最不理想的情况下时间复杂度会退化到O(N^2)。Mixer 服务使用 Node 开发,计算增量与输出资源在一个进程,为了避免计算任务阻塞请求响应,我们将计算改为了进程内异步。

有时业务会上线差异较大的增量片段,在一个很短的时间窗口内,许多相似的用户请求会同时分摊给所有 Mixer 进程,宿主机的所有 CPU 核心被占用处理同一个慢的增量计算,导致 Mixer 服务输出能力下降,请求积压。为了临时解决这个现象,我们采用了简单粗暴的自动重启,如果计算超时判定为慢计算,服务自杀由 PM2 重新拉起。服务重启后慢计算立即失败,用户侧降级到单资源请求,Mixer 有概率可以分配到快计算。时间窗口通过后不再出现慢计算时,Mixer 服务算力恢复。

这个机制一定程度上缓解了我们的计算瓶颈,但是没有完全解决问题。

ThunderJS 优化策略

限制合并请求文件数

实际业务使用中,我们发现由于没有对合并请求的文件数做限制,一次合并请求会合并过多的请求,特别是在扫码付这个项目中,导致一次请求的计算量过大,造成比较严重的超时问题。

正逢 Mixer 瓶颈阶段,为了降低 Mixer 的输出压力,我们需要使一次合并请求能够使用更少内存更快地完成。综合考虑后,我们降低了单次请求合并的资源数量上限,从最初的不设限改为限制最多 10个资源,这样由原本一次请求30个文件的增量结果,改成并发3个请求,每次请求10个文件,同时 Mixer 配合参数调优,一定程度上缓解了超时问题。

业务降级机制

合并请求失败后的单文件加载缓存

正如前文所说,在实际情况中,Mixer 计算服务会不可避免的遇到超时的问题,为了避免超时后导致无法加载相应的静态资源,我们有针对性的设计了降级机制。

在最初的 ThunderJS 中,如果遇到超时,会重新使用 createElement 方式将合并请求中的资源单独加载(直接请求文件,而不是请求文件两个版本的增量结果)。但是在实际业务中,我们注意到,如果能将单独加载的文件也做缓存,那在超时比较严重的时段,能有效避免老用户重复进行请求,因此我们将 createElement 方式换成 XHR ,将请求响应的文件内容存入 LocalStorage,实现了在降级机制下增强缓存的效果。

弱网优先使用缓存文件

不管请求有多快,终究还是需要发起网络请求,最好的方式就是不需要网络请求即可使用,我们将网络状况分为 WiFi、4G、3G、2G、unknown ,其中 2G和unknown 被我们认为是 弱网,大概占比在10.35%,对于这部分用户,我们选择优先执行缓存中文件,没有相关文件则进行单文件请求。

优先执行缓存的出发点在于弱网下加载文件成本较高,我们需要优先保

证支付流程的完善,即使这样无法给用户带来最新的用户体验。

完善降级机制后的流程图如下所示:

实践证明,降级机制起到了非常大的作用,在前期超时问题比较严重的情况下(超时率50%+),降级机制甚至承担了主要角色,在后期,降级机制的存在也是根本解决计算瓶颈的方案之所以能实施的前提。

计算瓶颈的根本解决方案

扫码付业务持续增长,增量计算服务的瓶颈依然存在。根据公司的基础监控服务的数据,Mixer 服务周期出现的请求积压越来越频繁,CLOSE_WAIT 数会快速增长。CLOSE_WAIT 是一种连接状态,在服务端响应未完成的请求前,连接被请求方关闭时可能出现。这个指标的快速增长意味着大量的请求不能在超时区间内处理,直接表征 Mixer 服务算力不足。

前文中我们讨论过增量计算时间复杂度高,即便使用 Myers 增量算法,也不会快到没有耗时。除非文本增量计算有重大理论突破,否则静态资源的文本增量计算的固有耗时是不可能降低的。

Mixer 增加超时重启机制后,提高快计算被分配到的概率,但并未达到 100%。更糟糕的是,计算完成后写入本地持久化缓存的过程是异步的,服务遇到慢计算后重启,上一个写入可能并未完成。这样下次请求到达后,缓存不可用,快计算也需要重新计算。由于 Mixer 服务设计为各节点完全等价,无论扩容多少个节点,当业务请求窗口到达时,慢计算都会出现在所有节点。

计算结果不可在节点间复用、慢计算导致服务反复重启、计算结果不能确保持久化缓存,浪费了整个服务的算力。

Bulid Serivce 异步计算服务

我们重新设计了静态资源服务的架构,将计算服务(Build Service Brain,以下简称 Brain 服务)和分发服务分离开来。

Brain 服务使用 MySQL 存储计算的唯一标识(我们称为 trace 信息)。每个 trace 可以唯一指代一个计算,每个计算仅允许一个节点执行。当计算任务到达 Brain 服务的随机一个节点后,Brain 服务首先检查是否已经被分配,如果已经分配立即返回状态信息;如果计算任务完成直接路由到对应节点输出结果。这个设计使 Brain 服务成为可水平扩展算力的分布式计算和存储服务。计算任务本身改为另起进程,完全避免计算任务和网络服务进程抢占资源的问题。只要部署节点数达到一定数量,集群就可以避免整体被某个慢计算挂起。

Brain 服务上线后,Mixer 服务不再负责计算,可用性提升至稳定 99.99%。整体后端响应时间(TP90)从 5800 ms 提升至 90 ms。Brain 服务在一个月时间内完成了 10W+ 计算。根据业务的统计数据,Diff 合并请求成功率提升至少 50%。

线上发版前的预热方案

经过以上 ThunderJS 和 BuildService 的优化 ,我们的超时率降到 3%,收益非常显著。此时的流程是:

当一个计算任务的固有耗时无法减少时,可以通过提前计算来避免用时压力。以前的 Build Service 架构不能支持我们任意预热,但是新架构的设计是允许预热的。所以我们进一步实施了预热方案。

实施预热首先要考虑的点就是哪两个版本之间的预热,在 ThunderJS 的设计中,文件版本号取自 Git 的 CommitId,每次提交后,即使文件内容没有变化版本号也会递进,导致需要进行不必要的合并请求。这一点在之前我们优化超时问题时,被认为是 ThunderJS 的一个待优化点,而在预热阶段,CommitId 比文件内容的 Hash 值更有价值,通过追踪 Git 提交历史,我们可以很容易的找到所有文件的线上版本;如果使用文件内容的 Hash 值作为版本,不能描述版本先后关系,无法明确找到文件增量计算的前后版本,预热也就无从谈起了。

通过我们埋点计算,线上发版之前预热 5 个版本(分别计算最近 5 个版本到最新版本的增量)能将超时率降到1.5%,预热 10 个版本能将超时率降至 1.1%。

理论上,预热更多版本可以进一步降低超时率,预热所有版本可以使超时消失,但是预热所需时间也会大幅增加。在实际情况中,我们需要在预热效果和预热成本之间折衷选择。

总结

项目发展至今,ThunderJS 增量更新方案在扫码付项目中取得了非常好的收益。

扫码付项目的所有请求中,有90%来自于移动网络,10%来自于 WiFi,通过缓存平均每天节约流量 49.37GB,通过增量更新平均每天节约流量 33.41GB。对访问量大,网络环境要求严苛的 C 端产品来说,节约的流量和网络请求时间消耗都是我们为用户带来的价值。

原文发布于微信公众号 - 美团点评技术团队(meituantech)

原文发表时间:2017-12-14

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏技术翻译

优化MongoDB的4个技巧

你有没有MongoDB数据库的性能问题?常见的情况是运行查询时突然出现性能问题。显而易见的第一个解决方案是,“让我们创建一个索引!” 虽然这在某些情况下有效,但...

2001
来自专栏智能计算时代

「大数据系列」Apache NIFI:大数据处理和分发系统

简单地说,NiFi就是为了实现系统间数据流的自动化而构建的。虽然术语“数据流”用于各种上下文,但我们在此处使用它来表示系统之间的自动和管理信息流。这个问题空间一...

1823
来自专栏noteless

3.操作系统简单介绍 操作系统发展历史 批处理分时系统 操作系统是什么 操作系统对文件的抽象 进程 虚拟内存是什么 操作系统作用 操作系统功能

它是在人们使用计算机的过程中,为了管理硬件资源,提高性能提高资源利用率,而逐步地形成和完善起来的。

1203
来自专栏腾讯DevOps

Git Code Review设置与使用

自从转到git上之后,已经一年多没有code review了(哈哈,捂住无辜的小脸)。但是坦白来说,code review绝对是利大于弊的。不仅可以让自己把控代...

5853
来自专栏腾讯移动品质中心TMQ的专栏

精准测试新玩法の基于犯罪心理学挖掘代码风险

前言 犯罪心理学还能用于挖掘代码风险? 挖掘出来的东西是什么? 挖掘出来的东东长什么样子? 挖掘出来能用来做什么? 具体怎么样挖掘呢? 这是本文的主要探讨的内容...

1836
来自专栏数据和云

PayPal Oracle数据库的隐含参数

在这篇小文中,我和大家分享一下PayPal数据库的隐含参数,注意,本文参考资料来自网络OOW公开文档,只是我也是首次注意到这些设置。 在图中架构,可以看到Pay...

2948
来自专栏性能与架构

Kafka是否适合我们?

2324
来自专栏服务端技术杂谈

当我们准备做前后端分离项目时,我们在考虑什么?

几年前做前后端分离项目的原因,是node刚刚横空出世,业界开始考虑如何真正的用js去写后端服务,于是就借鉴阿里中途岛项目去尝试,主要还是用到了node的密集io...

4325
来自专栏ThoughtWorks

组件测试:改建遗留系统的起点 | 洞见

在遗留系统中工作,无论是开发新功能,还是对旧功能进行修改,抑或是通过重构以期重拾其往日的雄风,都会面临大量的挑战。这些挑战主要来自于流失的业务知识、失传的技术和...

1253
来自专栏pangguoming

unity htc vive使用

本文介绍如何在Unity中使用HTC vive设备,当前VR作为市场比较火热的热点,HTC VIVE设备作为三大供应商之一,许多人购买了该设备,却不知道如何使用...

3606

扫码关注云+社区