多 “维” 优化——前端高并发策略的更深层思考
WeTest 导读
一项指标的变好,总少不了相应优化策略的实施。优化并不是简单的一蹴而就,而是个不断迭代与推翻的过程。更深层的优化方案,往往是在某种思维策略之下,对问题场景和基本策略优缺的深刻理解后做出的当下最优的权衡结果。本文笔者从前端高并发优化这一具体点出发,逐步向大家阐述笔者在优化的“术”之上思维层面的一些思考。希望能给各位带来共鸣和感悟。
背景:
之所以会以前端高并发这一主题入手,一来是本人曾负责过一些超高并发量的业务(手Q红包),在这方面算是有些经验。二来是相对于业务功能优化这类光前端层面的逻辑就涉及产品、设计等多方人员合作讨论而完成的优化(即逻辑本身并非纯出自前端人员的脑子),前端高并发这种前端层面逻辑纯由前端人员全把控的优化,或许作为前端的我,能得出来的思考观点会更深刻和更通用一些。
一、普遍的优化思路
说到优化,大家在收到“优化指标”任务的时候。通常会做两件事情——分析“优化指标”对应的痛点、寻找解决痛点的技术方案并施行。那这样是否就足够了呢?我的答案是否定的。在我的认知里这只是第一层的优化,虽然在结果上往往我们使用更优的技术后确实可以达到更好的优化效果,但却又不那么完美,优化效果还可以做得更好。那究竟缺了什么呢?下面,我会逐步阐述我的优化思路。首先,普遍的优化思路是基础,我们先来看看在普遍的优化思路下,基本的前端高并发策略是怎么样的?
二、分析本质痛点
高并发场景,与普通场景的核心区别是并行的访问量激增。因此,前端高并发策略本质要解决的是由访问量激增带来的问题。那访问量激增带来的是什么问题呢?
我们先来看一张H5正常的访问流图:
正常情况下,从用户端到后台的数据流动是很均衡的,用户的访问量在后台可承受的范围内。
而在高并发场景下,若不进行任何的高并发策略应对,原访问流图会变成这样(前端到后台红色部分的请求会被后台拒掉甚至可能会击垮后台):
图中可以很明显地看出高并发的痛点:数据流动过程两端失衡了。解决这一痛点,需要把两端重新回到数据流动的平衡状态。可以从两方面着手,一方面是后台层面尽可能地提供更大的承载能力(如增加机器等);另一方面则是在前端层面尽可能地加强其作为用户与后台之间的“门”的精简过滤能力。
加强前端“门”的精简过滤能力后,我们期望看到的访问流图是这样的:
虽然用户并发量很大,但在前端高并发策略下,两端失衡这一痛点得到了解决。那这些高并发策略都有哪些呢?我们来一个个地寻找。
三、寻找可行技术方案
前端“门”的角色要加强的是两方面的能力,一个是精简,另一个是过滤。
精简
首先,我们先看精简的技术方案。如果把后台的承载能力比作成一个“圆”,那前端和后台之间的通道就相当于一个以此圆为出口宽度的水管,其中的水可以理解为H5中的请求。而这样的圆在H5中,实际有两个,一个是最大并发数、一个是最大流量。对应的则是我们并行请求中的请求数和请求大小,精简这两者,即可在“圆”的面积固定的情况下,提供更多的“水”进出。
所以,在精简的技术方案上,需要能针对并行请求中的请求数和请求大小进行精简。
1、请求数精简
当请求数从逻辑层面已无法再精简时(如去掉一些无用请求),这时我们往往会将焦点聚焦到纯技术方案上。
H5请求数精简的方案,目前大致方案如下,核心为:合并。
图中列出的是H5中常用的资源类型(还有别的如视频、音频,不一一列举)。可以看出,就图中列出的目前的技术,对于请求数的减少,可以说要多极致可有多极致。极端情况下,一个业务只有一个请求也是可以做到的。
2、请求大小精简
同样的,当请求大小从逻辑层面已无法再精简时(如去掉一些无用函数、代码),这时我们往往会将焦点聚焦到纯技术方案上。H5请求大小精简的方案,目前大致方案如下,核心为:压缩
可以看到,就目前的技术,对于请求大小的精简,每种资源都可以进一步的压缩精简。
过滤
上面说的是前端“门”精简能力的技术方案,那下面我们再来看下前端“门”过滤能力的技术方案。还是刚刚水管的比喻,前端的过滤,可以理解为在前端“门”上加了一层可反弹特定水的网,用于把无须进入的水反弹掉(不知道这个比喻于水而言是否恰当,总之要表达的就是类似这么个道理)。能把“水”反弹的方式有很多种,一种是被动式的,即只允许特定量的水通过,超了的部分就进不来了,这策略一般用于后台,叫“过载保护”;另一种则是主动式的,通过对数据时效性的牺牲把数据往更前的一端进行储存。在前端层面,一般叫“本地缓存”,当请求时发现前端有缓存的内容,就不用再去访问服务器了。
所以,在过滤的技术方案上,前端可以通过缓存来完成。
1、缓存过滤请求
H5请求过滤的方案,目前大致方案如下,核心为:缓存。
通过具体的前端缓存技术,可将原本需要通过到达后台的请求直接从前端缓存处获取而达到“过滤”的效果。
四、普遍优化思路下的基本策略
完成上述两步——分析本质痛点、寻找可行技术方案,接下来大家普遍的做法是选择其中的合适方案,然后用到我们的项目中。对于合并,我们会把同类型的文件统一做下合并;对于压缩,我们会把没压缩的代码都统一做下压缩;对于缓存,我们统一启用较长的http cache、使用localstorage缓存、使用离线包。整体策略下来,虽然在一定程度上会有效果,但是我认为这往往又是不够的。要做到更彻底的优化,就需要对优化方案和优化场景本身做更深入的思考和策略调整。而这,往往是需要靠相应的思维模式驱动的。下面我来分别说说我总结出来的一些适用于更深层优化的思维,其中会着重谈谈差异化思维。
差异化思维
差异化思维,讲求的是在深入理解技术与场景后,对技术与场景进行差异化分解,以达到每个差异场景的进一步技术最优。
从前两步中——分析本质痛点、寻找可行技术方案,我们了解到高并发应对在前端技术层面可以从合并、压缩、缓存三方面着手。一个很浅显的道理是,这些策略做得越彻底,前端层面能挡掉的并发量就越多。但事实上,往往我们却又并不能这么做,而只能选取其中一种比较折中的方案。
比如,考虑到对页面访问耗时的影响,我们并不会把整个H5项目资源合并成为一个请求。原因在于,从本质上来说,每项纯技术策略,有其优点的同时,就必然会带来或多或少的缺点,正所谓万物有利必有弊。而当这个弊端成为了影响项目核心能力(如体验方面能力中的页面访问耗时)的时候,即使是能更好地提高并发能力的方案,在利弊权衡之后往往最终也并不会采用。这就是我前面说的只选择折中的优化方案时优化不够彻底的原因。
在利弊权衡之下,往往我们会选择一个折中方案(如下图那样中选择的策略3):
而更彻底的优化应该是,了解每个方案所导致的弊端影响,由弊端影响对项目场景进行差异化分析,按各场景对弊端影响面的容忍程度,实行策略方案的差异化。对于能接受弊端影响的场景,使用最优方案;而对于不太能接受弊端影响的场景,使用较优方案;依次类推到使用折中方案。从而做到差异化的精细优化。优化后的总体策略方案会变成类似下图的形式,原有项目只是单纯的使用折中策略3,而差异化处理后,会抽离出部分项目模块使用更优的策略1和策略2。
下面,我会使用这种思维,对上面两步得出的前端高并发中的三种策略——合并、压缩、缓存进行进一步的差异化优化。
差异化的合并策略
代码合并,合并到一定程度其弊端就会逐步放大显露。
弊端有:单个请求过大,造成对页面首屏渲染耗时的影响;动静请求合并后(cgi+html),缓存时效性的要求会大大地提高(缓存时效性取决于各合并资源中要求最高者,木桶原理)。
根据每个弊端的影响,下面针对具体场景进行差异化分析。
1、“资源首屏体验相关度”差异化分解
对于合并后单文件过大,其影响的是页面首屏渲染耗时。那我们可以从影响点出发,对页面网络资源请求按首屏体验相关性(影响点)进行差异化拆分,从而最大程度地减少合并对体验的影响。最简单的我们可以把资源分成两部分——高相关性资源(首屏)和低相关性资源(非首屏)。每部分资源单独策略处理,尽可能地做到极致的合并,提高并发能力。当再次遇到合并后文件太大而影响渲染耗时时,则在本级中再进一步分级,以此类推。如对于css、首屏渲染相关的js和图片资源,可作为高相关性资源,把图片base64进css,然后再全部内联进html页面,与页面合并。对于非首屏相关的js和图片资源,作为低相关性资源单独合并。这样即可在不影响页面首屏渲染耗时体验的同时,又保证了最大程度的减少并发请求数。
2、"资源时效依赖度"差异化分解
对于动静请求合并(cgi+html),其影响的是缓存时效性,导致缓存时效性要求变高。那我们也可以从影响点出发,对页面请求按时效依赖度(影响点)进行差异化拆分。最简单的我们可以把页面分成两类——高时效性要求页面(入口不可控,本来就不做缓存)和低时效性要求页面(入口完全可控,可通过修改页面离线包等方式做更新,可缓存)。对于高时效性要求的页面,动静请求合并后不会对该类页面有影响,此类页面可将cgi和html进行合并。而对于低时效性要求的页面,这类页面是可以缓存的(如使用离线包),则不进行cgi和html合并。
针对具体场景,差异化地采用相应最优的合并策略,优化效果将会再进一步地提升。
差异化的压缩策略
同样的,代码压缩,也有其弊端。弊端有:压缩程度越高,代码可读性越差,不便于线上问题的定位;虽有更优的压缩算法,但算法本身又存在自身的局限性。
1、“资源可读依赖度”差异化分解
对于代码可读性的影响,市面上其实已有代码层面的解决方案,如项目支持debug模式切换(此解决思路就是一种差异化思维,按使用场景差异化分成代码可读性要求高场景和代码可读性要求低场景,如线上的代码属于代码可读性要求低的,采用极端压缩版的代码;开发debug模式下的代码属于代码可读性要求高的,采用非压缩版的代码,两种模式可参数化切换)、sourcemap等。
2、“资源平台支持程度”差异化分解
对于各压缩算法的局限性(或者说各压缩算法下的产物的局限性,如图片资源有多种格式,而每种格式又有其局限性),其影响的是其所不支持(局限外)的那部分平台,会导致那部分平台无法使用。那我们从影响点出发,可以对该网络资源按平台支持程度(影响点)进行差异化拆分。我们可以按压缩算法的压缩效果进行方案的排序,从高到低地对方案的平台支持程度进行差异化判断筛选,支持则使用当前算法类型(格式),不支持则判断使用下一种算法类型(格式)。
例如对于图片资源,图片的格式丰富多样,多样的格式实际来源于每种格式使用的压缩算法的不同,都有其擅长的领域。这时,我们不能只用一种最普遍适用的格式,而应该利用上面的差异化思路来加载图片。按照各图片格式的压缩程度,对于支持tpg(公司内叫sharpp)的平台请求tpg格式图片,不支持tpg的则再去判断是否支持webp;支持webp的平台则请求webp格式的图片,若不支持webp则再往下判断。而针对图片格式的擅长领域,对于色彩丰富的图片采用jpg格式、色彩较简单或需要透明通道的采用png格式,按最适合的进行图片格式差异化选取。甚至对于图片的尺寸(尺寸与压缩无关,但目的都是为了减少请求大小,故用于类比),我们也可以采用这个差异化思路,如根据当前客户端的分辨率,返回最适大小的图片给客户端,从而做到高分辨率客户端请求返回高分辨率图片,低分辨率客户端请求返回低分辨率图片。
针对具体场景,差异化地采用相应最优的压缩策略,优化效果也将会再进一步地提升。
差异化的缓存策略
与上面的类似,缓存策略同样也有相应的弊端。弊端有:缓存时间越长,数据的准确性就越差,会存在缓存数据虽有效但已与最新数据有较大差异的问题。
1、“资源时效依赖度”差异化分解
针对资源有效性受缓存时间长短的影响,我们可以对资源进行时效性分级。可大致分成更新可控资源和更新不可控资源。此处的可不可控指的是资源更新后页面能否实时感知到更新。对于前端开发人员部署的js、css、图片等资源,均可作为更新可控资源,设置极长缓存,因为这类资源更新的同时可以将版本信息实时同步给前端页面(如拉取的文件改名了、改时间戳了等)。而对于无法实时同步版本信息给前端页面的资源,可作为更新不可控资源。对于这部分资源,我们可以再根据业务对各资源的时效性要求程度进行差异化分级。
以手Q中的H5项目中用到的QQ头像资源为例,此场景下头像是一个对项目更新不可控的资源。用户通过使用手Q或PC QQ修改自身头像后,各H5项目对于这个修改是无感知的,H5并不会实时地收到更新通知(除非双方在接口层面上做同步通知)。此时,如果头像缓存时间设置较长,就会出现用户更新了头像,但在H5项目中看到的头像还是旧的的情况。但如果不缓存,在高并发场景下势必对头像服务器造成极大的并发压力。这时,就需要对这一更新不可控资源做进一步差异化分解。
对于手Q中社交性较强的H5项目(如手Q红包、手Q AA收款等),其中虽然有很多头像,但是各头像对时效性的要求还是有差异的。最简单的我们可以把头像分成两类,高时效性头像和低时效性头像。稍作分析后,其实可以发现,对于使用者而言,用户自身(主人态)的头像变更是最敏感的,如果用户在手Q或PC QQ上修改完自己的头像后,进入该H5后发现自己的头像没有变是不太能容忍的。此时,用户自身的头像可作为高时效性头像。而对于其他用户(客人态)的头像的时效性,变没变,使用者其实倒不会太过在意,所以对于非用户自身的头像可作为低时效性头像。最后在策略上,对于高时效性头像,缓存一个较短时间;对于低时效性头像,则缓存一个相对较长时间。(实现起来也很容易,可将差异化逻辑放在前端判断然后加时间戳决定缓存时间即可。)
针对具体场景,差异化地采用相应最优的缓存策略,优化效果也将会再进一步地提升。
五、更多“维”的优化
在差异化思维的指导下,高并发优化策略得到了更进一步的完善。该思维的核心思想是针对方案的优缺与实际场景进行差异权衡。从通用性角度来看,这项思维也适用于工作上的很多事情,是一项通用化的思维,而并不仅仅局限于使用在解决前端高并发这个问题点上。同时,也并非所有的方案都只采用差异化思维就能完美地解决问题。差异化思维只是众多思维中的一种,实际上,还有很多思维。一个优秀的优化方案往往是在多“维”的思考权衡下的最终产物。
边界放大思维
比如,边界放大思维,指的是我们在做一项事情的时候,视野不应只停留在自己所能完全把控的领域,而应该把边界放大,从更外围的视野来思考这个问题的解决方案。
如前面说到的缓存策略,其实有一个当前H5的缓存策略弊端需要通过使用边界放大思维来优化。这个弊端是:当前浏览器缓存技术有其自身的局限性,缓存的有效性依赖于用户的二次访问程度。弊端的核心问题在于:缓存时机与用户首次访问相耦合。这使得在一些超高并发的H5活动中(活动类与业务类H5不同,活动类H5大部分是第一次访问的用户),缓存带来的效果并没有想象中大。
这是在纯前端技术层面无法解决的。但当我们把思维边界放大,扩展考虑到承载H5的平台时,这个弊端也许就能获得解决。因为这个弊端核心要解决的是资源的缓存与页面访问解耦的问题,而承载平台方(尤其终端)是有这个能力完成的。如在手Q中,这个解决方案叫“离线包”。离线包支持被动缓存的同时,也支持主动缓存。可将页面内容无须用户主动访问而通过预下载或主动push的方式缓存到用户手Q客户端上。首次访问的用户也可直接命中缓存。这样即可大大提高缓存的有效性。春节期间,手Q各高并发H5无不使用这项技术来提高页面的高并发能力。
逻辑全面性思维
再如,逻辑全面性思维,指的是我们在做一项事情的时候,视野不能只停留于逻辑的局部,而应该看到逻辑的全状。如逻辑有正常状态,也有异常状态,我们不能只考虑正常状态。逻辑有双向也有单向,对于双向的逻辑,我们不能只考虑其中的正向。
其实,前面我说的所有前端高并发策略(包括前面画的图),都仅仅只考虑到了数据流的正向逻辑段,即数据从用户端流向服务端的过程。而数据流的反向逻辑段其实是没有考虑的(即数据从服务端回到用户端的这一段逻辑情况)。而在高并发场景下,数据的反向逻辑段往往也会作为逻辑中非常关键的一环。没考虑数据流反向逻辑段的高并发策略,优化数据再好也只能说完成了一半。下面是数据流动的全逻辑过程(红色部分是数据流动的反向逻辑段):
在数据流的反向逻辑段中,前端在这层逻辑中的角色变成了数据接受方,而接受的数据可能存在多种状态,前端需要对这些状态都做好相应的处理。在高并发下,若后台过载了,那就会有部分数据返回异常。此时,最简单的我们可以分成两种状态——成功、失败,成功状态页面正常展示,异常状态则需要尽可能做到体验降级而非完全不可用。如静态资源cdn请求失败了,前端可以进行这样的一层异常逻辑处理:将当前异常用户的静态资源域名临时切换到备份域名(如页面域名或备用域名),这样可以将本来应该白屏无法使用的体验降级到访问速度较慢的体验,同时也给了错误率超过阀值时的cdn机器扩容提供调整时间。当然,若再结合上面的差异化思想,我们还可以将当前服务器的总体状态进行差异化分级(如当前负载程度),通过配置返回等策略告知页面当前服务器并发情况的程度,前端针对这些状态做差异化处理,逐步降级。如当cgi并发超过一定限度时,前端可以考虑将一些非核心但访问量较高的cgi的页面入口逐步屏蔽掉,到最后仅保留核心的cgi入口,从而保障项目核心功能不受高并发影响。
五、结语
本文算是笔者从事4年前端工作以来的一些思考。基于前端高并发策略这一优化点,向各位逐步阐述笔者在其“术”方面和“思维”方面的一些思路。受限于自身的经历和视野,观点也许有其局限性。希望文中提到的策略和思维,能给能作为读者的你一些收获。文章篇幅较长,文字较多,感谢耐心阅读!