1. <acronym id="pirhh"><pre id="pirhh"><dd id="pirhh"></dd></pre></acronym>
        1. <tt id="pirhh"><pre id="pirhh"><dd id="pirhh"></dd></pre></tt>
          <rt id="pirhh"></rt> <code id="pirhh"><object id="pirhh"></object></code>
            <listing id="pirhh"><object id="pirhh"><tr id="pirhh"></tr></object></listing>
            <code id="pirhh"></code>

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

            为什么要做增量更新

            美团金融的业务在过去的一段时间里发展非常快速。在业务增长的同时,我们也注意到,很多用户的支付环?#24120;?#20854;实是在弱网环境中的。

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

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

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

            1. 更高效地使用缓存,减少静态资源的重复下载。
            2. 使用增量更新,降低单次发版时下发的内容尺寸。

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

            增量更新是怎么一个过程

            看图说话。

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

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

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

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

            增量补丁的生成,主要?#35272;?#20110; Myers 的 diff 算法。生成增量补丁的过程,就是寻找两个字符串最短编辑路径的过程。算法本身比较复杂,大家可以在网上找一些比较详细的算法描述,比如这篇 《The Myers diff algorithm》 ,这里就不详细介绍了。

            补丁本身是一个微型的 DSL(Domain Specific Language)。这个 DSL 一共有三种微指令,?#30452;?#23545;应保留、插入、?#22659;?#19977;种字符串操作,每种指令?#21152;?#33258;己的操作数。

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

            =1\t-1\t=2\t-3\t+z

            这个补丁当中,制表符 \t 是指令的?#25351;?#31526;, = 表示保留, - 表?#26087;境?+ 表?#38745;?#20837;。整个补丁解析出来就是:

            z
            

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

            增量更新其实不是前端的新鲜技术,在客户端领域,增量更新早已经应用多年。看过我们 《美团金融扫码付静态资源加载优化实践》 的朋友,应该知道我们其实之前已有实践,在当时仅仅靠增量更新,?#31449;?#33410;省流量达30多GB。而现在这个数字已经随着业务量变得更高了。

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

            我们之前的增量更新实践遇到了什么问题

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

            之前的优化实践中,我们绝大部分的优化其实都是为了优化增量计算的速?#21462;?#25991;本增量计算的速度确实慢,慢到什么程度呢?以前端比较常见的JS资源尺寸——200KB——来进行增量计算,进行一次增量计算的时间依据文?#38745;?#21516;的数量,从数十毫秒到十?#35813;?#29978;至几十秒?#21152;?#21487;能。

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

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

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

            更高性能的可能方案

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

            Node.js Addon

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

            ASM.js / WebAssembly

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

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

            取而代之的 WebAssembly ,由 W3C 来领导,采用的是更加紧凑、接近汇编的字节码来提速。目前在市面上刚刚崭露?#26041;牽?#30456;关的工具链还在完善中。 Mozilla 自己已经有一些尝试案例了,例如将 Rust 代码编译到 WebAssembly 来提速 sourcemap 的解析。

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

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

            换语言

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

            • 运行速度
            • 并发处理
            • 类型系统
            • ?#35272;?#31649;理
            • 社区

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

            最终,我们决定使用 Go 语?#36234;?#34892;增量计算服务的新实践。

            选择Go带来了什么

            高性能

            增量补丁的生成算法,在 Node.js 的实现中,对应 diff 包;而在 Go 的实现中,对应 go-diff 包。

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

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

            不一样的并发模型

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

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

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

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

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

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

            不一样的模块机制

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

            具体?#27492;擔?#19968;个 Node.js 模块,它只需关心它自己?#35272;?#30340;模块是什么、在哪里,而不关心自己是如何被别人?#35272;?#30340;。这一点,可?#28304;?require 调用看出:

            const util = require('./util');
            const http = require('http');
            
            module.exports = {};

            这是一个非常简单的模块,它?#35272;?#20004;个其他模块,其中 util 来自我们本地的目录,而 http 则来自于 Node.js 内置。在这种情形下,只要你有?#24049;?#30340;模块?#35272;?#20851;系,一个自己写好的模块想要给别人复用,只需要把整个目录独立上传到 npm 上即可。

            简单?#27492;擔?Node.js 的模块体系是一棵树,最终本地模块就是这样:

            |- src
                |- module-a
                    |- submodule-aa
                    |- submodule-ab
                |- module-b
                |- module-c
                    |- submodule-ca
                        |- subsubmodule-caa
            |- bin
            |- docs

            但 Go 语言就不同了。在 Go 语言中,每个模块不仅有一个短的模块名,同时还有一个项目中的“唯一路?#19969;薄?#22914;果你需要引用一个模块,那么你需要使用这个“唯一路?#19969;?#26469;进行引用。比如:

            package main
            
            import (
                "fmt"
                "github.com/valyala/fasthttp"
                "path/to/another/local/module"
            )

            第一个?#35272;?#30340; fmt 是 Go ?#28304;?#30340;模块,简单明了。第二个模块是一个位于 Github 的开源第三方模块,看路径?#38382;?#23601;能够大致推断出?#27492;?#26159;第三方的。而第三个,则是我们项目中一个可复用模块,这就有点不太?#40092;?#20102;。其实如果 Go 支持?#30701;?#30340;模块关系的话,相当于每个?#35272;?#20174;根目录算起就可以了,能够避免出现 ../../../../root/something 这种尴尬的向上查找。但是, Go 是不支持本地?#35272;?#20043;间的文件夹?#30701;?#30340;。这样一来,所有的本地模块,都会平铺在同一个目录里,最终会变成这样:

            |- src
                |- module-a
                |- submodule-aa
                |- submodule-ab
                |- module-b
                |- module-c
                |- submodule-ca
                |- subsubmodule-caa
            |- bin
            |- docs

            现在你不太可能直?#24433;?#26576;个模块按目录拆出去了,因为它们之间的关?#20302;?#20840;无法靠目录来断定了。

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

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

            过于简单的去中心化第三方包管理

            和有 npm 的 Node.js 另一个不一样是: Go 语言没有自己的包管理平台。对于 Go 的工具链?#27492;擔?#23427;并不关?#21738;?#30340;第三方包到?#36164;?#35841;来托管的。 社区里 Go 的第三方包遍?#20960;?#20010; Git 托管平台,这不仅让我们在搜索包时花费更多时间,更麻烦的是,我们无法通过在企?#30340;誆看?#24314;一个类似 npm 镜像的平台,来降低大家每次下载第三方包的耗时,同时也难以在不?#35272;低?#32593;的情况下,进行包的自由安装。

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

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

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

            匮乏的内部基础设施支持

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

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

            语言之外

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

            如何面对海量突发流量

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

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

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

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

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

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

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

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

            1. 通过?#28304;?#20648;系统的访问,如果有已经计算好的增量补丁,那么可以直接返回,?#35805;?#26368;需要计算的任务传递给计算层。
            2. 如果计算层出现问题,API 层保有响应能力,能够进行服务降级,返回全量文件内容。
            3. 将对外的接口管理起来,避免接口变更对核心服务的影响。在这个基础上可?#36234;?#34892;一些简单的聚合服务,提供诸如请求合并之类的服务。

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

            为了防止过量的计算请求进入到计算环节,我们还针对性地进行了流量控制。通过?#20849;猓?#25105;们?#19994;?#20102;单机计算量的瓶?#20445;缓?#23558;这个限制配置到了系统中。一旦计算量逼近这个数字,系统就会对超量的计算请求进行降级,不再进行增量计算,直接返回全量文件。

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

            如何容灾

            有关容灾,我们总结了以往见到的一些常见?#25910;希?#20998;了四个门类?#21019;?#29702;。

            • 线路?#25910;稀?#25105;们在每一层服务中都内置了单机缓存,这个缓存的作用一方面是可以泄洪,另一方面,如果线路出现?#25910;希?#21333;机缓存也能在一定程度上降低对线路的?#35272;怠?/li>
            • 存储?#25910;稀?#23545;于存储,我们直接采用了两种公司内非常成熟的分?#38469;?#23384;储?#20302;常?#23427;们互为备份。
            • CDN ?#25910;稀?#20570;前端的同学或多或少都遇到过 CDN 出?#25910;?#30340;时候,我们也不例外。因此我们准备了两个不同的 CDN ,有效隔离了来自 CDN ?#25910;?#30340;风险。

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

            回顾与总结

            服务上线运转一段时间后,我们总结了新实践所带来的效果:

            ?#31449;?#22686;量计算成功率 ?#31449;?#22686;量更新占比 单日人均节省流量峰值 项目静态文件总量
            99.97% 64.91% 164.07 KB 1184 KB

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

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

            现在来回顾一下,在我们的新实践中,?#21152;?#21738;些大家可以真正借鉴的点:

            1. 不同的语言和工具有不同的用武之地,不要试图?#20040;?#23376;去锯?#23601;貳?#35813;换语言就换,不要想着一个语言或工具解决一?#23567;?/li>
            2. 更换语言是一个重要的决定,在决定之前首先需要思考是否应当这么做。
            3. 语?#36234;?#20915;更多的是局部问题,架构解决更多的是系统问题。换了语言也不代表就万事大吉了。
            4. 构建一个系统时,首?#20154;?#32771;它是如何垮的。想清楚你的系统潜在瓶颈会出现在?#27169;?#22914;何加强它,如何考虑它的备用方案。

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

            最后,如果大家对我们所做的事情也有兴趣,想要和我们一起共建大前端团队的话,?#38431;?#21457;?#22270;?#21382;至 [email protected]

            作者简介

            洋河,2013年加入携程UED实习,参与研发了人生中第一个星数超过100的 Github 开源项目。2014年加入小?#33258;?#24179;台,同时负责网页前端开发、客户端开发及路由器固件开发,积累了丰富的端开发经验。2017年加入美团,现负责金服平台基础组件的开发工作。

            发现文章有错误、对内容有疑问,都可以关注美团技术团队微信公众号(meituantech),在后台给我们留言。我们每周会挑选出一位热心小伙伴,送上一份精美的小礼品。快来扫码关注我?#21069;桑?/p>

            我来评几句
            登录后评论

            已发表评论数()

            相关站点

            ?#35753;?#25991;章
            天辰线上娱乐

            1. <acronym id="pirhh"><pre id="pirhh"><dd id="pirhh"></dd></pre></acronym>
                  1. <tt id="pirhh"><pre id="pirhh"><dd id="pirhh"></dd></pre></tt>
                    <rt id="pirhh"></rt> <code id="pirhh"><object id="pirhh"></object></code>
                      <listing id="pirhh"><object id="pirhh"><tr id="pirhh"></tr></object></listing>
                      <code id="pirhh"></code>

                      1. <acronym id="pirhh"><pre id="pirhh"><dd id="pirhh"></dd></pre></acronym>
                            1. <tt id="pirhh"><pre id="pirhh"><dd id="pirhh"></dd></pre></tt>
                              <rt id="pirhh"></rt> <code id="pirhh"><object id="pirhh"></object></code>
                                <listing id="pirhh"><object id="pirhh"><tr id="pirhh"></tr></object></listing>
                                <code id="pirhh"></code>
                                浙江6十1查询结果 金花百人牛牛下载软件 七乐彩走势图带连线 时时彩7码后一能赚钱吗 快乐十分云南开奖结果 吉林时时彩预测软件手机版 至尊单双王单双中特 129期码报资料图 重庆快乐十分开户 山东时时彩怎么中奖号码 急速赛车注册 踢足球图片简笔画人物 新疆风采福利彩票官网 qq刮刮乐无法刮奖 安徽快3今天