前端遇上Go:静态资源增量更新的新实践

大家知道,前端能够服务用户的前提是JavaScript和CSS等静态资源能够正确加载。如果网络环境恶劣,那么我们的静态资源尺寸越大,用户下载失败的概率就越高。

根据我们的数据统计,我们的业务中有2%的用户流失与资源加载有关。因此每次更新的代价越小、加载成功率越高,用户流失率也就会越低,从而就能够变相提高订单的转化率。

作为一个发版频繁的业务,要降低发版的影响,可以做两方面优化:

针对第一点,我们有自己的模块加载器来做,这里先按下不表,我们来重点聊聊增量更新的问题。

看图说话:

我们的增量更新通过在浏览器端部署一个SDK来发起,这个SDK我们称之为Thunder.js。

Thunder.js在页面加载时,会从页面中读取最新静态资源的版本号。同时,Thunder.js也会从浏览器的缓存(通常是localStorage)中读取我们已经缓存的版本号。这两个版本号进行匹配,如果发现一致,那么我们可以直接使用缓存当中的版本;反之,我们会向增量更新服务发起一个增量补丁的请求。

增量服务收到请求后,会调取新旧两个版本的文件进行对比,将差异作为补丁返回。Thunder.js拿到请求后,即可将补丁打在老文件上,这样就得到了新文件。

总之一句话:老文件+补丁=新文件。

补丁本身是一个微型的DSL(DomainSpecificLanguage)。这个DSL一共有三种微指令,分别对应保留、插入、删除三种字符串操作,每种指令都有自己的操作数。

例如,我们要生成从字符串“abcdefg”到“acdz”的增量补丁,那么一个补丁的全文就类似如下:

=1\t-1\t=2\t-3\t+z这个补丁当中,制表符\t是指令的分隔符,=表示保留,-表示删除,+表示插入。整个补丁解析出来就是:

具体的JavaScript代码就不在这里粘贴了,流程比较简单,相信大家都可以自己写出来,只需要注意转义和字符串下标的维护即可。

那么我们是不是就已经做到万事无忧了呢?

我们最主要的问题是增量计算的速度不够快。

对于小流量业务来说,计算一次增量补丁然后缓存起来,即使第一次计算耗时一些也不会有太大影响。但用户侧的业务流量都较大,每月的增量计算次数超过10万次,并发计算峰值超过100QPS。

那么不够快的影响是什么呢?

我们之前的设计大致思想是用一个服务来承接流量,再用另一个服务来进行增量计算。这两个服务均由Node.js来实现。对于前者,Node.js的事件循环模型本就适合进行I/O密集型业务;然而对于后者,则实际为Node.js的软肋。Node.js的事件循环模型,要求Node.js的使用必须时刻保证Node.js的循环能够运转,如果出现非常耗时的函数,那么事件循环就会陷入进去,无法及时处理其他的任务。常见的手法是在机器上多开几个Node.js进程。然而一台普通的服务器也就8个逻辑CPU而已,对于增量计算来说,当我们遇到大计算量的任务时,8个并发可能就会让Node.js服务很难继续响应了。如果进一步增加进程数量,则会带来额外的进程切换成本,这并不是我们的最优选择。

“让JavaScript跑的更快”这个问题,很多前辈已经有所研究。在我们思考这个问题时,考虑过三种方案。

Node.jsAddon是Node.js官方的插件方案,这个方案允许开发者使用C/C++编写代码,而后再由Node.js来加载调用。由于原生代码的性能本身就比较不错,这是一种非常直接的优化方案。

后两种方案是浏览器侧的方案。

其中ASM.js由Mozilla提出,使用的是JavaScript的一个易于优化的子集。这个方案目前已经被废弃了。

然而在考虑了这三种方案之后,我们并没有得到一个很好的结论。这三个方案的都可以提升JavaScript的运行性能,但是无论采取哪一种,都无法将单个补丁的计算耗时从数十秒降到毫秒级。况且,这三种方案如果不加以复杂的改造,依然会运行在JavaScript的主线程之中,这对Node.js来说,依然会发生严重的阻塞。

于是我们开始考虑Node.js之外的方案。换语言这一想法应运而生。

更换编程语言,是一个很慎重的事情,要考虑的点很多。在增量计算这件事上,我们主要考虑新语言以下方面:

当然,除了这些点之外,我们还考虑了调优、部署的难易程度,以及语言本身是否能够快速驾驭等因素。

最终,我们决定使用Go语言进行增量计算服务的新实践。

在动手之前,我们首先用实际的两组文件,对Go和Node.js的增量模块进行了性能评测,以确定我们的方向是对的。

结果显示,尽管针对不同的文件会出现不同的情况,Go的高性能依然在计算性能上碾压了Node.js。这里需要注意,文件长度并不是影响计算耗时的唯一因素,另一个很重要的因素是文件差异的大小。

Go语言是Google推出的一门系统编程语言。它语法简单,易于调试,性能优异,有良好的社区生态环境。和Node.js进行并发的方式不同,Go语言使用的是轻量级线程,或者叫协程,来进行并发的。

专注于浏览器端的前端同学,可能对这种并发模型不太了解。这里我根据我自己的理解来简要介绍一下它和Node.js事件驱动并发的区别。

如上文所说,Node.js的主线程如果陷入在某个大计算量的函数中,那么整个事件循环就会阻塞。协程则与此不同,每个协程中都有计算任务,这些计算任务随着协程的调度而调度。一般来说,调度系统不会把所有的CPU资源都给同一个协程,而是会协调各个协程的资源占用,尽可能平分CPU资源。

相比Node.js,这种方式更加适合计算密集与I/O密集兼有的服务。

当然这种方式也有它的缺点,那就是由于每个协程随时会被暂停,因此协程之间会和传统的线程一样,有发生竞态的风险。所幸我们的业务并没有多少需要共享数据的场景,竞态的情况非常少。

实际上Web服务类型的应用,通常以请求->返回为模型运行,每个请求很少会和其他请求发生联系,因此使用锁的场景很少。一些“计数器”类的需求,靠原子变量也可以很容易地完成。

Go语言的模块依赖管理并不像Node.js那么成熟。尽管吐槽node_modules的人很多,但却不得不承认,Node.js的CMD机制对于我们来说不仅易于学习,同时每个模块的职责和边界也是非常清晰的。

具体来说,一个Node.js模块,它只需关心它自己依赖的模块是什么、在哪里,而不关心自己是如何被别人依赖的。这一点,可以从require调用看出:

简单来说,Node.js的模块体系是一棵树,最终本地模块就是这样:

|-src|-module-a|-submodule-aa|-submodule-ab|-module-b|-module-c|-submodule-ca|-subsubmodule-caa|-bin|-docs但Go语言就不同了。在Go语言中,每个模块不仅有一个短的模块名,同时还有一个项目中的“唯一路径”。如果你需要引用一个模块,那么你需要使用这个“唯一路径”来进行引用。比如:

|-src|-module-a|-submodule-aa|-submodule-ab|-module-b|-module-c|-submodule-ca|-subsubmodule-caa|-bin|-docs现在你不太可能直接把某个模块按目录拆出去了,因为它们之间的关系完全无法靠目录来断定了。

较新版本的Go推荐将第三方模块放在vendor目录下,和src是平级关系。而之前,这些第三方依赖也是放在src下面,非常令人困惑。

目前我们项目的代码规模还不算很大,可以通过命名来进行区分,但当项目继续增长下去,就需要更好的方案了。

Go有一个命令行工具,专门负责下载第三方包,叫做“go-get”。和大家想的不一样,这个工具没有版本描述文件。在Go的世界里并没有package.json这种文件。这给我们带来的直接影响就是我们的依赖不仅在外网放着,同时还无法有效地约束版本。同一个go-get命令,这个月下载的版本,可能到下个月就已经悄悄地变了。

目前Go社区有很多种不同的第三方工具来做,我们最终选择了glide。这是我们能找到的最接近npm的工具了。目前官方也在孕育一个新的方案来进行统一,我们拭目以待吧。

对于镜像,目前也没有太好的方案,我们参考了moby(就是docker)的做法,将第三方包直接存入我们自己项目的Git。这样虽然项目的源代码尺寸变得更大了,但无论是新人参与项目,还是上线发版,都不需要去外网拉取依赖了。

Go语言在美团内部的应用较少,直接结果就是,美团内部相当一部分基础设施,是缺少Go语言SDK支持的。例如公司自建的RedisCluster,由于根据公司业务需求进行了一些改动,导致开源的RedisClusterSDK,是无法直接使用的。再例如公司使用了淘宝开源出KV数据库——Tair,大概由于开源较早,也是没有Go的SDK的。

由于我们的架构设计中,需要依赖KV数据库进行存储,最终我们还是选择用Go语言实现了Tair的SDK。所谓“工欲善其事,必先利其器”,在SDK的编写过程中,我们逐渐熟悉了Go的一些编程范式,这对之后我们系统的实现,起到了非常有益的作用。所以有时候手头可用的设施少,并不一定是坏事,但也不能盲目去制造轮子,而是要思考自己造轮子的意义是什么,以结果来评判。

要经受生产环境的考验,只靠更换语言是不够的。对于我们来说,语言其实只是一个工具,它帮我们解决的是一个局部问题,而增量更新服务有很多语言之外的考量。

因为有前车之鉴,我们很清楚自己面对的流量是什么级别的。因此这一次从系统的架构设计上,就优先考虑了如何面对突发的海量流量。

首先我们来聊聊为什么我们会有突发流量。

对于前端来说,网页每次更新发版,其实就是发布了新的静态资源,和与之对应的HTML文件。而对于增量更新服务来说,新的静态资源也就意味着需要进行新的计算。

有经验的前端同学可能会说,虽然新版上线会创造新的计算,但只要前面放一层CDN,缓存住计算结果,就可以轻松缓解压力了不是吗?

这是有一定道理的,但并不是这么简单。面向普通消费者的C端产品,有一个特点,那就是用户的访问频度千差万别。具体到增量更新上来说,就是会出现大量不同的增量请求。因此我们做了更多的设计,来缓解这种情况。

这是我们对增量更新系统的设计。

放在首位的自然是CDN。面对海量请求,除了帮助我们削峰之外,也可以帮助不同地域的用户更快地获取资源。

在CDN之后,我们将增量更新系统划分成了两个独立的层,称作API层和计算层。为什么要划分开呢?在过往的实践当中,我们发现即使我们再小心再谨慎,仍然还是会有犯错误的时候,这就需要我们在部署和上线上足够灵活;另一方面,对于海量的计算任务,如果实在扛不住,我们需要保有最基本的响应能力。基于这样的考虑,我们把CDN的回源服务独立成一个服务。这层服务有三个作用:

那如果API层没能将流量拦截下来,进一步传递到了计算层呢?

为了防止过量的计算请求进入到计算环节,我们还针对性地进行了流量控制。通过压测,我们找到了单机计算量的瓶颈,然后将这个限制配置到了系统中。一旦计算量逼近这个数字,系统就会对超量的计算请求进行降级,不再进行增量计算,直接返回全量文件。

另一方面,我们也有相应的线下预热机制。我们为业务方提供了一个预热工具,业务方在上线前调用我们的预热工具,就可以在上线前预先得到增量补丁并将其缓存起来。我们的预热集群和线上计算集群是分离的,只共享分布式存储,因此双方在实际应用中互不影响。

有关容灾,我们总结了以往见到的一些常见故障,分了四个门类来处理。

最后,在这套服务之外,我们浏览器端的SDK也有自己的容灾机制。我们在增量更新系统之外,单独部署了一套CDN,这套CDN只存储全量文件。一旦增量更新系统无法工作,SDK就会去这套CDN上拉取全量文件,保障前端的可用性。

考虑到每个业务实际的静态文件总量不同,在这份数据里我们刻意包含了总量和人均节省流量两个不同的值。在实际业务当中,业务方自己也会将静态文件根据页面进行拆分(例如通过webpack中的chunk来分),每次更新实际不会需要全部更新。

由于一些边界情况,增量计算的成功率受到了影响,但随着问题的一一修正,未来增量计算的成功率会越来越高。

现在来回顾一下,在我们的新实践中,都有哪些大家可以真正借鉴的点:

对于Go语言,我们也是摸着石头过河,希望我们这点经验能够对大家有所帮助。

最后,如果大家对我们所做的事情也有兴趣,想要和我们一起共建大前端团队的话,欢迎发送简历至liuyanghe02@meituan.com。

THE END
1.Bi的ETL中怎么做增量处理如何在etl中,做增量处理Bi的ETL中怎么做增量处理 增量抽取 增量抽取只抽取自上次抽取以来数据库中要抽取的表中新增或修改的数据。在ETL使用过程中。增量抽取较全量抽取应用更广。如何捕获变化的数据是增量抽取的关键。对捕获方法一般有两点要求:准确性,能够将业务系统中的变化数据按一定的频率准确地捕获到;性能,不能对业务系统造成太大的https://blog.csdn.net/hzp666/article/details/70139867
2.数据仓库中如何做增量处理数据量大,只需要增量最新被更改的数据。 如何做增量 (1)insert into 比如行为数据,发生一条记录就插入一条,数据不会被update。 严格T+1,初始化时候限定created_at的时间。否则凌晨之后的数据会被重复插入。 (2)insert overwrite 初始化的时候不限定时间。 https://www.jianshu.com/p/ed3f698c819d
3.在“确定性”上做增量——“走在前开新局”评论员观察①在“确定性”上做增量,说到底是要提振信心、稳定预期。面对未知风险,企业敢不敢干,敢不敢闯,敢不敢投,既有对市场发展前景的考量,也有对稳定发展环境的评估。近年来,宁德时代、北汽整车、比亚迪等一批支撑性强、带动力大的大项目、好项目,相继在山东投资布局,看中的正是山东稳定、透明、可预期的市场环境和产业生https://m.jnnews.tv/guanzhu/p/2022-08/15/913861.html
4.必须统筹好做优增量和盘活存量的关系增量盘活存量存量“必须统筹好做优增量和盘活存量的关系”,是日前中央经济工作会议提出的五个“必须统筹”之一,是做好经济工作的重要规律性认识,是对未来发展的启示引领,必须深刻把握,学习好、领会好、运用好。 存量,是指某一时点上的总量或积累量。增量,是指某一时期内新增的数量或增长的速度。两者互为条件、相互转化、相互促进http://k.sina.com.cn/article_3167104922_bcc62f9a02001kcrc.html
5.MySQL定时备份(全量备份+增量备份)51CTO博客所以要对线上的数据库定时做全量备份和增量备份。 增量备份的优点是没有重复数据,备份量不大,时间短。但缺点也很明显,需要建立在上次完全备份及完全备份之后所有的增量才能恢复。 MySQL没有提供直接的增量备份方法,但是可以通过mysql二进制日志间接实现增量备份。二进制日志对备份的意义如下: http://bjiokn.blog.51cto.com/1021758/2491656
6.赋能小微商家,打造金融科技综合解决方案少做存量工作,多做增量工作 回顾行业过往,Rick提到从劳资科到人力资源,这个企业部门经历了从提供最基本资源,到驱动业务、驱动组织的职能转变,HR已经站在了业务最前沿,而不再只是支撑型的角色。 Rick坦言,“我感觉HR的工作非常适合我,到现在我仍然很享受这份职业。”他认为,HR对企业来说是一个非常重要的角色,对创业https://www.ersoft.cn/news.html?id=160
7.双赢思维。c.去做增量 博弈除了双赢,就是“零和”,我们都在有限的资源中瓜分,如果能延伸到增量,也会产生共赢的状态。 最简单的现象就是“加工资”,如果有员工给你提出加工资,但是以他的情况,现在打破这个规矩对其他成员不太公平,但是如果不加,这个员工可能离职,怎么办? https://www.niaogebiji.com/article-59062-1.html
8.陕北矿业:做优存量,做大增量,做强变量3月9日,陕煤集团宣布了关于陕北矿业及其所属大型主力煤矿管理体制调整的决定。面对陕煤集团陕北片区体制调整,陕北矿业如何用三至五年时间,做优存量、做大增量、做强变量,再造一个新陕北矿业?这是各级组织和干部职工首要思考、落实、完成的工作任务。 做优存量,现有实体加快发展 https://kyjapi.zgkyb.com/m/article/2580
9.宁夏落实应届大中专毕业生就业政策助学子圆梦——中国青年网政府做增量 “与大省份相比,我们的就业规模较小,但安置就业的难度却不小。”宁夏回族自治区人社厅就业促进与失业保险处处长李海说。 抓就业,首先是政策先行。李海告诉记者,前几年自治区出台了《关于做好当前和今后一段时期就业创业工作的实施意见》《关于吸引支持大学生在宁创新创业就业办法》,其中包括“稳就业13条https://t.m.youth.cn/transfer/index/url/qnsx.youth.cn/jjdt/202211/t20221121_14144518.htm
10.数据库VS数据仓库所以在做增量操作的时候,一定和开发说好这两个字段的定义和使用场景。 is_delete & is_valid 有些场景下,我们需要删除某些数据,一般不会物理删除,会通过一个字段来做逻辑删除,请和开发同学沟通好,使用固定的一个字段,并确认该字段双方的理解是一致的,不然后面又很多坑。 http://www.360doc.com/content/21/0416/05/5315_972549768.shtml
11.乡村振兴“浙盘棋”:“消薄+”加出了什么做增量与去存量 2018年,杭州全市309个集体经济总收入市定薄弱村和258个经营性集体经济薄弱村全部摘帽。如此多的薄弱村是如何全部摘帽的呢? 在2016年3月启动的新一轮“百千万”蹲点调研活动中,杭州100多家市直单位和企业,根据薄弱村的实际情况,为各村量身制定了“一村一策”的具体方案。在此后的实践中,杭州探索http://www.juece.net.cn/tmp/4g/read.html?id=3460
12.DRGDIPAPG究竟是增量改革,还是存量改革?DRG变量DRG改革要想能够胜利,主要的核心问题不是医保本身,根源在于经济,要能提供做增量改革的经济支持,要做增量改革,释放活力;反之如果经济衰落,做的是存量改革,这场改革的漏洞将会被放大无数倍,本就不太宽敞的房子里要来一次外科手术,那么结果很可能是产生一个处处漏风的陋室。 https://www.shangyexinzhi.com/article/12374213.html
13.先全量迁移后增量迁移,数据是否会不一致数据会不一致,迁移任务单独做增量数据迁移时,增量迁移开始同步的增量数据为启动任务的时间。所以在增量迁移任务启动之前,源数据库产生的增量数据都不会被同步到目标实例。如果需要进行不停机迁移,建议配置任务时,迁移类型选择结构迁移、全量数据迁移及增量数据迁移。 上一篇:DTS是否能支持两个不同阿里云账号下的RDS实例之https://help.aliyun.com/document_detail/43531.html