美国的创智赢家(Shark Tank),英国的龙穴之创业投资(Dragons’ Den),以及德国的“Die H?hle der L?wen (DHDL)”等电视节目为年轻的初创公司提供了一个在海量观众面前向商业巨头展现自己产品的机会。然而这些初创公司的主要收益通常并不在于评委提供的战略性投资?,? 毕竟只有很少数交易最终能够完成 ,而在于通过电视节目获得的关注:电视上出现几分钟的画面通常就可以为网站吸引成百上千的新访客,借此可以有效提升每周、每月,甚至更长时间内的网站流量。然而前提是,网站必须要能承受一开始的负载尖峰不能掉链子……
网店们通常会发现自己处于一种非常尴尬的境地,因为他们不仅仅是一些消遣性的项目(例如博客),而是通常需要由创始人背负大量投资,并且必须想方设法盈利。对企业来说,最糟糕的情况莫过于服务器超载导致无法处理用户请求,甚至整个网站彻底崩溃。这种情况其实比你想象得更常见:当季DHDL节目中露脸的所有网店中,约有半数因为上电视后不堪重负而崩溃。而能继续保持在线也只是走完了万里长征一半的路程,因为 用户满意度直接决定了转化率 ,也就是说,用户满意度直接决定了最终能获得的销售额。
来源: http://infographicjournal.com/how-page-load-time-can-impact-conversions/
针对网页加载时间对客户满意度和转化率的影响已经有太多 研究 ,这足以证明这些因素的重要性。例如Aberdeen Group发现延迟每增加1秒会导致页面访问量减少11%,转化率降低7%。就算你去问 谷歌 或者 亚马逊 ,他们的结论也是相同的。
在给初创公司 Thinks 开发网店(该公司参加了9月6日的DHDL节目)的过程中,我们遇到了一个挑战:要求所开发的网站能在顺利迎接成千上万访客的同时实现不超过1秒的加载速度,这对我们而言是一个巨大的挑战。从这个项目的实施,以及在数据库和Web性能领域多年的研究成果中,我们获得了很多宝贵的经验。
影响Web应用程序页面加载速度最主要的因素有三个,如下图所示。
为了给网店提速,需要逐个消除这些性能瓶颈。
影响前端性能最重要的因素在于关键呈现路径( Critical Rendering Path,CRP )。这个概念描述了浏览器将网页呈现给用户的5个必要步骤,如下图所示。
关键呈现路径所涉及的步骤
上述每个步骤单独看都相当简单,但是不同步骤之间的依赖性使得事情变得复杂,并会对性能产生影响。DOM和CSSOM的构造通常会对性能产生最大影响。
下图展示了一个关键呈现路径,箭头所指内容为需要花时间等待的依赖项。
关键呈现路径中重要的依赖项
在加载CSS并构造完整CSSOM之前,客户端上什么都不会显示。因此CSS也被称之为 “呈现”的拦路虎 。
JavaScript(JS)的情况更糟,因为无论DOM或CSSOM都可被JavaScript所访问并更改。这意味着一旦在HTML中发现Script标记,DOM的构造过程将暂停,等待从服务器请求脚本。在脚本加载完之后,还需要等待取回所有CSS并完成CSSOM的构造,随后才能继续执行。例如在下面的例子中,当CSSOM构造完成并且JS执行完毕后,终于可以访问并修改DOM和CSSOM了。DOM的构造过程直到此时才能继续处理,并将网页显示在客户端上。因此JavaScript也被叫做“解析”的拦路虎。
Example of JavaScript accessing CSSOM and changing DOM: <script> ... var old = elem.style.width; elem.style.width = "50px"; document.write("alter DOM"); ... </script>
JS造成的麻烦远不止如此。例如 jQuery插件 可以访问计算完成后的HTML元素布局信息,随后开始反复修改CSSOM,直到就所需布局达成共识。因此浏览器必须反复执行JS,反复构造呈现树和布局,这一过程中用户只能看到一片惨白的屏幕。
为了对CRP进行优化,需要考虑三个 基本概念 :
此外 浏览器缓存 是一种始终值得考虑使用的非常有效的做法。这种方法适合用于上述三种类型内容的优化,因为在将资源缓存后,就无需像第一次访问那样从服务器加载。
CRP优化是一个非常复杂的话题,尤其是内联、合并,以及异步加载等措施会彻底毁掉代码的可读性。好在目前有很多实用的工具可以帮助我们进行优化,可以考虑将其集成到自己的构建和部署流程中。下面这几个工具绝对值得一试…
在这些工具的帮助下,只须少量工作即可打造出前端性能更出色的网站。访客首次访问Thinks网店的页面加载速度测试结果如下:
Google PageSpeed对thinks.com网站的评测结果
有趣的是,PageSpeed Insights中检测到唯一存在问题的地方竟在于Google Analytics脚本的缓存寿命过短。所以谷歌基本上就是在抱怨自家的问题咯。
从加拿大(GTmetrix)访问位于法兰克福的服务器时的首次页面加载速度
网络延迟是网页加载速度最大的影响因素,同时也是最难优化的。但在实际进行优化前先来看看浏览器首次请求分解后的步骤:
在浏览器中输入 https://www.thinks.com/ 并按下回车后,浏览器首先通过 DNS查询 解析该域名对应的IP地址。访问每个域名都需要进行这样的查询。
在获得IP地址后,浏览器发起一个到服务器的 TCP连接 。TCP握手需要2次往返(1次可使用 TCP Fast Open )。对于安全的 SSL连接 ,TLS握手需要额外的2次往返(1次可使用 TLS False Start 或 Session Resumption )。
初始连接完成后,浏览器发出实际请求并等待返回的数据。 获得首个字节所需的时间 主要取决于客户端和服务器之间的距离,其中还包含服务器呈现网页(包括会话查询、数据库查询、模板呈现等环节)所需的时间。
最后一步需要 下载资源 (本例中为HTML)这一过程需要多次往返。尤其是新连接通常需要更多往返,因为初始拥塞窗口通常很小。这意味着TCP并不能在一开始就全面使用所有可用带宽,而是会随着时间流逝逐渐增加带宽用量(可参阅 TCP拥塞控制 )。具体速度受制于启动速度缓慢的算法,这种情况下会让每个往返的拥塞窗口内片段数量翻倍,直到数据包真正开始丢失。在移动和Wifi网络中,因为这种情况导致的数据包丢失会对性能产生极大影响。
另外还要注意:在使用HTTP/1.1的情况下,最多只能创建 6个并发连接 (如果浏览器依然沿袭了最初的标准,则最多只能创建2个连接)。因此最多只能并行请求6个资源。
为了更直观地了解网络性能会对页面加载速度产生多大影响, httparchive 提供了大量统计数据。例如平均来说,每个网站需要用超过100个请求才能加载大约2.5MB的数据。
来源: http://httparchive.org/interesting.php#reqTotal
也就是说,网站需要用大量小请求加载数量众多的资源,但网络带宽不是总在增加吗?最终可以通过宽带提速等措施解决这一问题对吧?未必…
来自《 High Performance Browser Networking 》,作者Ilya Grigorik
实际上 带宽 超过5Mbps后将无法继续对网页加载速度产生任何改善。但是降低每个请求的 延迟 可以大幅加快网页加载速度。这意味着带宽翻倍后加载时间依然不会有变化,但延迟减半可以让网页加载速度提高一倍。
既然延迟是网络性能的决定性因素,我们能做些什么?
简而言之,网络延迟方面有一些必做,以及绝对不能做的事,但总的来说,主要局限因素依然在于往返的次数以及物理网络的延迟。破解这一局限唯一有效的方式是拉近数据和客户的距离。先进的Web缓存技术就是用来做这种事的,但这种技术只能用于静态资源。
对于Thinks,我们完全遵照上述原则使用了 Fastly CDN以及更激进的浏览器缓存机制,甚至动态数据也使用了一种新颖的 Bloom Filter算法 确保缓存数据的一致性。
重复对www.thinks.com进行加载测试可了解浏览器缓存的覆盖率
反复进行页面加载测试的过程中,唯一未能从浏览器缓存完成的请求(见上图)是两个对谷歌分析API进行的异步调用,初始HTML的请求则是从CDN获取的。因此在反复进行的测试中,页面加载速度有了显著提高。
在后端性能方面,我们需要同时考虑延迟和吞吐率。为了实现低延迟,我们需要将服务器的处理工作所需时间降至最低。为了实现更高吞吐率并应对负载尖峰,我们需要采取一种可以 横向缩放 的架构。虽然不准备深入介绍太多细节,但设计方面对性能产生的影响是极为巨大的,需要重点关注的组件和属性包括:
可缩放后端栈包含的组件:负载均衡器、无状态应用程序服务器、分布式数据库
首先需要 负载均衡 (例如Amazon ELB或DNS负载均衡),借此将传入的请求分配至多台应用程序服务器中的一台。此外还可以实施 自动缩放 以便在需要时添加额外的应用程序服务器,并通过 故障转移 机制替换故障服务器,将请求重新路由至正常运转的服务器。
为将协调的次数降至最低, 应用程序服务器 应当维持 最小化的共享状态 ,并可使用 无状态会话处理 机制实现更自由的负载均衡。此外服务器的代码和IO应尽可能 高效 ,借此即可将服务器处理时间降至最低。
数据库也需要能应对尖峰期的负载,并尽量将处理工作所需的时间降至最低。与此同时,数据库需要具备建模和数据查询需求所需的能力。市面上有大量可缩放的数据库(尤其是NoSQL数据库),每种产品都各有利弊。更多详情可参阅我们有关该话题的调查和决策指南: NoSQL数据库:调查和决策指南 。
Thinks的网店基于 Baqend 构建,使用了下列后端栈:
Baqend的后端栈:MongoDB充当的主数据库、无状态应用程序服务器、HTTP缓存体系,以及适用于Web前端的REST和JS SDK
Thinks使用 MongoDB 作为主数据库。为了实现会在短时间内过期的Bloom筛选器(用于浏览器缓存),我们出于更高写入吞吐率的考虑使用了 Redis 。无状态应用程序服务器( Orestes Servers )提供了用于访问后端功能(文件托管、数据存储、实时查询、推送通知、访问控制等)的接口,并由该服务器负责处理动态数据的缓存连贯性。这些服务器可通过充当负载均衡器的 CDN 发起请求。网站前端使用了一种基于 REST API 的 JS SDK 访问后端,后端可自动利用完整的 HTTP缓存体系 对请求进行加速,并确保缓存数据处于最新状态。
为了对Thinks网店面对高负载情况的表现进行测试,我们使用位于法兰克福的两个t2.medium AWS实例作为应用程序服务器进行了负载测试。MongoDB运行在两个t2.large实例上。我们的负载测试使用20台 IBM SoftLayer 计算机运行 JMeter ,借此模拟 200000用户 在 15分钟 内访问该网站的负载。其中20%的用户(40000个)被配置为额外执行支付操作。
该网店的负载测试环境
测试发现支付系统方面存在几个瓶颈,例如我们必须从原本对库存进行乐观更新(通过 findAndModify 实现)的做法改为使用MongoDB的部分更新操作( inc )。但在这之后服务器面对负载表现非常出色,实现了平均5ms的请求延迟。
JMeter负载测试结果:12分钟内680万个请求,平均延迟5ms
所有负载测试总共生成了大约 1千万个请求 并传输了 460GB数据 ,CDN的 缓存命中率高达99.8% 。
负载测试之后的仪表盘概述信息
简而言之,良好的用户体验需要从三方面来实现:前端、网络,以及后端性能。
前端性能在我们看来是最容易实现的,因为市面上已经有很多现成的工具以及各种最佳实践,照做很容易就能搞定。但依然有很多网站没有参照这些最佳实践,甚至完全没有对前端进行任何优化。
网络性能是页面加载速度的最大影响因素,同时也是最难优化的。缓存和CDN是最有效的优化方法,但需要注意到,这些机制只能对静态内容进行优化。
后端性能主要取决于单台服务器的性能以及分布式环境的规模。横向扩展非常难以实现,因此从一开始就要妥善考虑。很多项目将缩放能力和性能放在最后考虑,随着业务的增长最终将遇到非常棘手的问题。
围绕Web性能和可缩放系统的设计,目前有很多不错的图书。Ilya Grigorik撰写的《 High Performance Browser Networking 》一书几乎涉及了有关网络和浏览器性能的方方面面,此外本书还提供了一个持续更新,可免费在线阅读的版本!Martin Kleppmann撰写的《 Designing Data-Intensive Applications 》虽然目前尚处于早期发布阶段,但已经成为该领域最棒的图书。其中谈及了大量可缩放后端系统内部的技术基本知识,并包含很多细节介绍。Lara Callender Hogan撰写的《 Designing for Performance 》谈到了如何构建速度快,可提供出色用户体验的网站,并提供了大量最佳实践。
此外还有大量在线指南、教程和工具可供借鉴。从面向新手的Udacity课程 网站性能优化 到谷歌的 开发者性能指南 ,再到诸如 Google PageSpeed Insights 、 GTmetrix 以及 WebPageTest 等分析工具,都能为我们提供巨大的帮助。
谷歌正在通过 PageSpeed Insights 、 开发者指南 等有关Web性能的项目帮助大家提高对Web性能的重视,甚至将网页加载速度作为该公司 页面排名 的一个重要依据。
谷歌搜索在改善页面加载速度和用户体验方面的最新项目名为 移动页面加速( AMP ) 。该项目意在让新闻文章、产品页面,以及其他搜索内容能通过谷歌搜索结果立刻呈现。为此必须以AMP的方式构建这些页面。
AMP页面范例
AMP主要做了两件事:
第一件事最为关键,AMP会通过某种方式对HTML、JS和CSS内容进行限制,使得通过这种方式构建的页面可以获得最优化的关键呈现路径,并能被谷歌爬虫更轻松地爬网检索。AMP会强制实施 多种限制 ,例如所有CSS必须是内联的,所有JS必须是异步的,页面上包含的一切内容必须为固定大小(这是为了避免“重绘”)。虽然就算无需这些限制,按照上文提到的Web性能最佳实践也可以实现相同结果,但AMP也许是一种更好的做法,就算一些很简单的网站也可以使用。
第二件事意味着谷歌会在对你的网站进行爬网检索的同时将内容缓存在Google CDN中,借此实现更快速的交付。当爬网程序再次索引你的网站时,缓存的网站内容会被更新。该CDN还会遵守服务器设置的静态TTL,但与此同时至少会进行 微缓存(Micro-caching) :认为资源在至少一分钟之内是新鲜的,并在收到用户请求后在后台对资源进行更新。这样的做法使得AMP最适合网站以静态内容为主的用例,例如以人工方式进行内容编辑的新闻网站或其他用于出版发布用途的网站。
另一种方法(同样由谷歌提出)是 Progressive Web Apps ( PWA )。该方式意在浏览器中使用 服务工作进程(Service worker) 对网站中的静态部分创建缓存。通过这种方式,重新查看网页时就可以立即加载缓存的内容,并可脱机使用。但网站上的动态内容依然需要从服务器加载。
应用外壳(App shell)(单页应用程序的逻辑)可在后台重新验证。如果发现应用外壳有更新,会通过一则消息要求用户更新页面。例如 Gmail的Inbox 就是这样做的。
然而服务工作进程的代码会对每个网站的静态资源进行缓存并进行重新验证,这会产生不小的开销。此外目前仅Chrome和Firefox对服务工作进程提供了完善的支持。
所有缓存方法都会遇到一个问题:无法处理动态内容。这主要是由于HTTP缓存的工作原理导致的。缓存主要有两种类型: 基于失效(Invalidation-based) 的缓存(例如转发代理缓存和CDN),以及 基于过期(Expiration-based) 的缓存(例如ISP缓存、企业代理以及 浏览器缓存 )。基于失效的缓存可由服务器主动指定为失效,基于过期的缓存只能由客户端进行重新验证。
在使用基于失效的缓存时,最棘手的问题在于,首次将数据从服务器交付给客户端时,必须指定缓存的寿命(TTL)。随后对于已经缓存的数据就完全丧失了控制能力。在TTL过期前,浏览器将始终是用当时缓存的内容。对于静态内容来说,这个问题并不是很麻烦,因为通常这类内容只在部署新版本Web应用程序之后才会有改动。因此可以使用诸如 gulp-rev-all 和 grunt-filerev 等实用工具对这些资产创建哈希。
但对于会在应用程序运行过程中加载和改动的各类数据又该怎么办?更改用户资料,更新已发布的内容,或发布新的评论,这些操作涉及的内容似乎无法与浏览器缓存很好地结合在一起,因为我们无法预计此类更新会发生在未来的什么时候。此时只能禁用缓存,或使用非常短的TTL。
被其他客户端更新后,缓存的动态数据变为陈旧状态的范例
Baqend 研究并开发了一种在客户端实际获取之前检查URL陈旧度的方法。在启动每个用户会话后,我们首先会获取一个非常小的数据结构,该结构名为Bloom筛选器,是一种高度压缩的内容,其中描述了所有陈旧的资源。通过查询Bloom筛选器,客户端可以知道每个资源是否陈旧(包含在Bloom筛选器中)或确定其为新鲜状态。对于有可能陈旧的资源,我们会绕过浏览器缓存从CDN获取相应的内容。在其他任何情况下,我们会直接通过浏览器缓存提供内容。使用浏览器缓存可以减少网络通信并节约带宽,同时速度相当快。
此外通过在资源变得陈旧后立刻进行清理,确保了CDN(以及其他基于失效的缓存,例如Varnish)始终包含最新数据。
Baqend保障被缓存动态数据新鲜度的范例
Bloom筛选器 是一种基于概率的数据结构,并使用了可调整的假阳性率(False positive rate),这意味着该数据集可能会对从未加入过的对象进行限制,但绝对不会漏掉任何一个需要限制的对象。换句话说,我们可能偶尔会对新鲜资源进行重新验证,但 绝对不会交付陈旧的数据 。另外值得注意的是,假阳性率非常低,借此才可以让整个数据集的规模尽可能小。例如我们只需要11K字节的数据集就可以存储20000个不同的更新。
服务器端会进行大量流处理(查询匹配检测)、机器学习(优化TTL的估算),以及分布式协调(可缩放Bloom筛选器的维护)。如果你对相关细节感兴趣可参阅这篇 论文 或 这些幻灯片 以进一步详细了解。
诸多努力就是为了改善性能。
Baqend的缓存基础架构可以通过哪些方面改善网页加载速度?
为了展示Baqend技术对性能的改进,我们通过后端即服务(BaaS)领域每个主要竞争对手的平台构建了一个非常简单的新闻应用程序,并测试了从全球不同位置加载页面所需的时间。如下图所示,Baqend技术的页面加载速度始终不超过1秒,平均来说是竞争对手速度的6.8倍。就算所有客户端均与服务器位于同一个位置,由于浏览器缓存技术的使用,Baqend的速度也快了150%。
通过一个简单的新闻应用程序对平均加载时间进行的比较
为了对BaaS竞争对手的服务进行比较,我们还构建了一个 可供大家实际操作进行比较的Web应用 。
实际操作比较 结果的屏幕截图
当然,这只是一个测试场景,并非有真实用户的Web应用程序。我们重新回到Thinks网店这个例子,看看真实环境中的表现到底如何吧。
当DHDL(德国版的“创智赢家”电视节目)在9月6日首播并吸引了270万观众时,我们正坐在电视机前密切关注着Google Analytics的统计信息,同时还在为Thinks创始人所介绍的产品而感到激动。
从他们介绍自家产品开始,网店的并发用户数在短时间内增加了大约10000人,但真正的尖峰发生在节目插播广告的时候,突然之间超过45000个并发用户涌入网店打算购买Towell+:
从插播广告前一刻开始的Google Analytics统计结果
在Thinks上电视的30分钟内,我们收到了 340万 请求, 300000 个访客,高达 50000 个并发访客,以及每秒最高20000个请求?—?所有这一切都在CDN层面实现了 98.5%的缓存命中率 以及平均 3%的服务器CPU负载 。
总的来说,全时段内 低于1秒 的 页面加载时间 最终产生了 高达7.%的转化率 ,这个结果让人很满意。
如果再看看同一期DHDL节目中介绍的其他商家,我们会发现其中有四家 彻底宕机 ,其余几个商家仅使用了微不足道的性能优化技术。
9月6日播出的DHDL节目中推荐的商家的整体可用性和谷歌页面加载速度评分
在设计快速可缩放网站过程中,我们解决了很多性能瓶颈:我们全面掌握了 关键呈现路径 ,充分理解了网络方面的限制和 缓存 的重要性,并设计出一套可 横向缩放 的后端系统。
我们发现有很多实用工具很适合用来解决某些具体的问题,此外还可以通过移动页面加速( AMP )和Progressive Web Apps( PWA )实现更全面的优化。但 动态数据的缓存 这个问题依然存在。
Baqend采取的方法是尽量减少前端Web开发的工作量,通过JS SDK从全面托管的Baqend云服务获得所需后端功能,包括数据和文件的存储、(实时)查询、推送通知、用户管理、OAuth,以及访问控制。通过使用完整的HTTP缓存体系,该平台可以自动加速所有请求,同时可用性与可缩放性也更有保障。
感谢 Felix Gessert 、 Hannes Kuhlmann 以及 Wolfram Wingerath 。
Erik Witt ,本文的翻译和发布已获原作者授权。 阅读英文原文 : Building a Shop with Sub-Second Page Loads: Lessons Learned
感谢韩婷对本文的审校。
给InfoQ中文站投稿或者参与内容翻译工作,请邮件至editors@cn.infoq.com。也欢迎大家通过新浪微博(@InfoQ,@丁晓昀),微信(微信号: InfoQChina )关注我们。