英文版权归Evert Pot所有,中译文作者yangyang(aka davidkoree)。双语版可用于非商业传播,但须注明英文版作者、版权信息,以及中译文作者。翻译水平有限,请广大读者指正。

在web服务开发领域,通过降低HTTP请求数量来提高服务性能,是一门常见的手艺。

这样做有很多好处,包括维持更少的字节传输量。但最主要的原因,还是因为众多主流浏览器在同一域名上仅能建立6个并发的HTTP请求,倒退到2008年,请求数量被限制在最多2个。

当到达最多请求数限制时,浏览器必须等前面的请求完成,才能发起新的请求。这样一来,直观的后果就是,等待所花费的延迟越高,全部请求完成需要的时间就越长。

来看一个模拟这种情况的例子。下面的动画演示了获取一个页面主体的请求过程,这个页面可能是一个网站主页,或是一个JSON数据。

当页面获取成功后,模拟器得到了其中链接的99个对象,它们可能是图片、脚本、或其他API形式的文档。

基于上述6个并发连接的限制,工程师们想出了各种优化手段,比如合并压缩后传输脚本,图片被合并成「精灵图」来显示(译注:sprite maps,把网页中用到的多个图像整合到一张图片文件里,再利用CSS精确定位技术分别予以展示)。

类似的情况也发生在web service领域,考虑到单HTTP连接的一些限制和性能代价,REST API及其他基于HTTP的服务设计者们,会采取把一大堆逻辑实体打包到单一的HTTP请求/响应过程中,以代替那种小而美的API封装方式。

举个例子,当某个客户端需要通过API获取一个文章列表时,设计者们往往将其封装为一个单次请求,并返回列表。而不是逐一地吐出每一篇文章的URI地址。

这种节省有非常显著的效果。下面的动画模拟与刚才相似,区别在于我们这回把所有资源合并到一个请求里面,予以响应输出。

如果客户端通过API向服务器请求体量较大的数据集,为了减少HTTP的请求数,开发人员可能会被迫暴露更多的API,每个API都针对用户的特殊使用场景做适配,或者搭建一套可以响应各种查询条件的系统。

在具体实现和形式上,最简单的可能是支持各种查询参数的API,更复杂的则是GraphQL,它用一种管道机制封装了自定义的请求/响应模型,允许范围更广的查询组合。

复合文档(compounding documents)的缺点

复合文档有很多缺点。对混和在一起的实体有依赖的系统,其前后端往往也额外复杂。

如果把单一实体看做拥有URI(译注:统一资源标识符)的对象,则可以通过GET方式获取它或它的缓存。取而代之的话,通过在客户端与服务器之间添加一个抽象层,那么它就会负责梳理和调配对象的工作。

进一步看,这种抽象其实是重新实现了一遍HTTP的逻辑,它带来一个不好的副作用,那就是与HTTP相关的其他特性也被重新实现了一遍。最常见的例子就是缓存机制(caching)。

在REST生态圈里,复合文档的例子比比皆是。JSON:API, HAL和Atom都有相应的观念。

如果你观察一下大多数符合JSON:API特性的客户端实现,你便会发现这些客户端往往包含了某种”实体大杂烩“,事无巨细地记录着实体信息,实际上维护着与HTTP缓存相同的东西。

还有一些系统存在另一个问题,那就是客户端通常更难以获取它们实际想要的数据。因为在复合文档中,要么没有包含目标数据,要么混杂着所有数据。又或者包含目标数据的文档结构非常复杂(见GraphQL)。

一个更严重的缺陷是,API设计者们越来越倾向让系统变得更不透明,由于体系互联的缺失,其在信息组织上不再网状化。

HTTP/2 和 HTTP/3

HTTP/2如今已十分流行,它在HTTP请求上的开销非常低。每个HTTP/1.1请求需要开启一个TCP连接。然而HTTP/2只需要为每个域名开启1个TCP连接,多个请求可以在其上并行且无序的流动。

实际上如今我们可以通过协议本身来实现复合文档中体现并行的场景。

利用多个HTTP/2请求来替代复合的HTTP/1.1请求,有如下优势:

  • 浏览器/应用程序不再需要从单一响应中梳理多个信息实体,所有东西都可以简化为通过GET来获取。集合不需要再嵌入元素,它只需要指向它们。
  • 如果浏览器中有集合数据的(部分)缓存,它可以聪明地跳过请求动作或者很快得到一个304 Not Modified响应。
  • 那些被更早识别的元素可以比其他元素更快地抵达浏览器终端,可以被更及时地渲染,而不是等到所有元素都加载完毕再渲染。

HTTP/2 Push

与上述多个请求的方式相比,复合请求仍有一些优点。

举个例子,我们创建了一个可以返回文章列表的API,当我们请求数据时,只是返回文章链接的列表,而非每篇文章。

GET /articles HTTP/1.1Host: api.example.orgHTTP/1.1 200 OKContent-Type: application/json{"_links": {"item": [{ "href": "/articles/1" },{ "href": "/articles/2" },{ "href": "/articles/3" },{ "href": "/articles/4" },{ "href": "/articles/5" },{ "href": "/articles/6" },{ "href": "/articles/7" }]},"total": 7}

作为客户端,它请求文章列表首先要拉取集合、等待响应、然后再并行拉取集合中的每个元素,这样会让延迟翻倍。

还有一个问题,就是服务端需要处理8个请求,1个是处理集合,另外还有7个是针对每个元素。获取整个列表往往更划算。这个有时也被称为N+1查询问题(译注:请自行搜索「Hibernate N+1」)。

这个问题可以通过HTTP/2的「服务器推送」(Server Push)技术来解决。服务器推送是HTTP/2的一项新特性,它可以让服务器在客户端实际请求资源之前主动发送它们。

不过这种方法也有一个缺点,就是服务器并不知道客户端缓存了哪些资源。它只会假设自己要发送所有资源,或会猜测哪些资源需要被发送。

曾经有一个未完善的提案,试图解决这个问题,它的方法是通过布隆过滤(bloom filter)来让客户端告诉服务器,自己有哪些缓存数据。不幸的是,我相信人们已经放弃了该提案。

所以你只能二选一。要么完全消除初始延迟,要么借助缓存来减小通信量。

理想的解决方案,可能是上述要素的复合形式。我一直在努力制定这样一个规范:它允许客户端在HTTP请求头中声明自己希望获得哪些链接关系——我叫他Prefer Push,看起来像这样:

GET /articles HTTP/2Prefer-Push: itemHost: api.example.org

如果服务端能解析这个头信息,它便能理解客户端想要所有相关的链接资源,并开始尽早地推送它们。

在服务端假想的框架控制器里,处理请求的代码如下:

function articlesIndex(request, response, connection) {const articles = articleServer.getIndex();response.body = articles.toLinks();if (request.prefersPush('item')) {for(const article of articles) {connection.push(article.url,article.toJson();};}}

CORS问题

CORS(译注:跨域资源共享)技术也有一些缺点,这里值得讲讲。CORS主要方便了一件事,就是让web应用能在某(些)域名下向其他域名下的API发起HTTP请求。

这个过程里包含了一些不同的手段,其中非常耗性能的一个是「预检请求」(preflight request)。

当执行不安全的跨域请求时,浏览器首先会发起OPTION请求,以便服务器明确地予以识别和选择。

实际上,大多数API请求都是「不安全」的,这对于每个HTTP请求来说可能会有双倍延迟。

有趣的是当年Macromedia Flash也遇到过类似问题,他们的解决方案是创建一个跨域多源请求策略,你所需要做的是把策略存入一个名为crossdomain.xml的文件,将之放到你的域名根目录下,Flash读取到该文件后就能记住它。

每隔几个月我就会上网搜索看是否有人会在Javascript上实现一套现代的CORS方案,如今我发现一个W3C草案(注1),也希望浏览器厂商们能跟进一下。

一个不太优雅的解决方案是在API所属域名下托管一个代理脚本,它嵌在<iframe>标签里,可以无限制地访问自己的源。在其之上的web应用程序可以通过window.postMessage()与它通讯。

完美的世界

在一个完美的世界里,HTTP/3已经广泛可用,它更好地优化了性能,浏览器针对缓存摘要(cache digests)也拥有了标准机制,客户端可以把它们需要的链接关系通知给服务器,API服务也可以尽快地把任何客户端可能需要的资源推送过去。各种全域的源策略合而为一。

最后一张模拟图展示了它可能的样貌。在这个例子中,浏览器有预热的缓存,缓存数据中的每一项都有对应的ETag。

当客户端发起请求,检查是否有新数据或需要更新的项时,客户端会携带缓存摘要,服务器以推送的方式予以响应,仅返回有变动的资源。

真实世界的性能测试

我们离完美世界还有段距离,但我们仍可以根据目前所掌握的东西来做事。我们可以利用HTTP/2的服务器推送功能,发起请求易如反掌。

对于我来说,HTTP/2使得「细小且众多的HTTP端点」成为一种简洁的设计,但是性能上是否站得住脚?找到一些证据可能会有帮助。

我做性能测试的目标是用下述不同方式来获取资源:

1. h1 - 独立的HTTP/1.1请求

2. h1-compound - 复合的HTTP/1.1集合

3. h2 - 独立的HTTP/2请求

4. h2-compound - 复合的HTTP/2集合

5. h2-cache - 一个HTTP/2集合,其中每项都是独立获取,有预热的缓存

6. h2-cache-stale - 一个HTTP/2集合,其中每项都是独立获取,有预热的缓存,但是需要重新检查更新

7. h2-push - 基于HTTP/2,无缓存,但每一项都被拉取过

我的预测

理论上,在完整的周期里,「复合请求」与「HTTP/2推送响应」都会传输同样数量的信息。

我认为,基于HTTP/2的HTTP请求仍会有一些开销。不过,在复合请求的场景下,它仍然具有一定的性能优势。

真正的好处将在缓存开始发挥作用时体现出来。对于一个典型API中的给定集合,我认为可以假设许多项可以被缓存。

因此「跳过了90%工作」的测试,在性能表现上应该是最快的。这种假设似乎是合乎逻辑的。

所以根据我的预测,由快到慢依次为:

1. h2-cache - 一个HTTP/2集合,其中每项都是独立获取,有预热的缓存

2. h2-cache-stale - 一个HTTP/2集合,其中每项都是独立获取,有预热的缓存,但是需要重新检查更新

3. h2-compound - 复合的HTTP/2集合

4. h1-compound - 复合的HTTP/1.1集合

5. h2-push - 基于HTTP/2,无缓存,但每一项都被拉取过

6. h2 - 独立的HTTP/2请求

7. h1 - 独立的HTTP/1.1请求

第一轮测试和初步观察

我首次测试是在一台本地装有Node.js的机器上,Node版本为12。所有HTTP/1.1测试在SSL上完成,HTTP/2测试在另一个端口完成。

为了模拟延迟,我给每个HTTP请求添加了40~80毫秒的延迟时间。

这是我第一版的测试工具

随即我就遇到了一些问题。Chrome浏览器禁用了自签名证书的缓存。实际上我没能真正弄清楚如何让Chrome接受我本地环境的自签名证书,于是我先放弃了,改用Firefox做测试。

在Firefox上,「服务器推送」看起来不太可靠。它经常在我重复发起推送测试时才管用。

但是,Firefox最让我惊讶的是,通过本地缓存获取数据的速度仅仅比我人工模拟延迟获取全新响应数据的速度快一点点。在跑了几轮测试之后,有些测试结果显示实际上前者比后者更慢了。

通过对比这些数据,我不得不改进我的测试方案。

更好的测试

再次测试,基础方案如下:

1. 我重复执行50次测试。

2. 我在AWS上跑测试,实例是在us-west-2启用,级别为t2.medium。

3. 我的测试是在真实互联网环境下执行,去掉了延迟模拟。

4. 我使用的是LetsEncrypt SSL安全证书。

5. 我在每种浏览器上跑2种测试:

  • 一种包含25个项的集合
  • 一种包含500个项的集合

测试1:25个请求

图中有一些有趣的地方。

首先,我们预计HTTP/1.1独立请求会是最慢的,如图中结果所示,没有意外。实际上它提供了一个基准线。

第二个最慢的是HTTP/2独立请求。

HTTP/2推送、HTTP/2缓存,它们只有略微提升。

Chrome和Firefox大都有同样的结果。让我们看看Chrome的具体测试结果:

复合请求显然是速度最快的。这表明我当初的猜测是错的。即便有缓存的加持,它仍然不能击败在一个单独的复合响应中重发全部集合。

与无缓存相比,有缓存时,性能会有小幅提升。

测试2:500个请求

让我再来测个大的。在这个测试中,由于请求变多,持续时间会更久,我们预计在某些方面差异会变大,同时也会有差异变小的情况出现。尤其是,最初请求的影响会弱化。

这2张图表明:

  • 对于请求最多的测试来说,Chrome是最慢的浏览器。
  • 对于使用服务器推送的测试,Firefox是最慢的。并且当浏览器缓存最多时,Firefox也最慢。

这与我自己的观察基本相符。Firefox的推送能力似乎不太可靠,使用缓存时看起来也慢。

我们可以看到的是,在500个请求中,复合请求在Firefox上要快1.8倍,在Chrome上快3.26倍。

最让人意想不到的是浏览器缓存的速度。我们的常规测试会发起501个HTTP请求,而预热缓存的测试只发起51个HTTP请求。

在Chrome上,上述测试结果表明发起501个请求的速度是发起51个请求的2.3倍,在Firefox上,这个值只有1.2倍

换句话说,Firefox从缓存中请求数据的速度仅仅比从链路另一侧获取资源的速度略微快一些。这非常让人意外。

这让我很好奇,是否Firefox的缓存普遍就是那么慢?或者在高并发情况下会尤其变差?我没有仔细研究,但感觉Firefox的缓存可能有一些不良的全局锁行为。

另一个突出现象,就是当并行发起500个请求时,Chrome表现得特别差,比Firefox慢2倍还多。这个巨大的差异让我对测试结果产生怀疑,随后我重新跑测试,但每次都得到相似结果。

我们还看到,使用推送的好处变得不那么明显,因为我们只有通过减少第一个请求的延迟才能真正节省时间。

结论

我的测试并不完美,HTTP/2大多数测试是用Node的实现来做的。为了得到真实证明,我认为在更多环境下做测试很重要。

我搭建的服务端也可能不是最佳方案。我的服务从文件系统提供文件,但真实环境中的系统可能有不同表现。

所以把这些测试结果当做参考结果,而不是论证结果。

这些测试结果让我明白,如果速度是最优先考虑的因素,你应该继续使用复合响应。

我确实相信,虽然这些测试结果足够接近真实情况,但为了获得一个潜在更简洁的系统设计,深入执行全方面的性能测试可能是值得的。

同时,缓存的介入实际上没有带来明显的差别。由于浏览器的优化手段较弱,经常性地执行新的HTTP请求,可能跟从缓存中获取数据所花的时间是一样的。Firefox尤其如此。

我也相信,推送所带来的效果可以明示但不够多。推送最大的优势在于新数据集的第一次加载,并且对于避免N+1查询问题也更为重要。在下列情况下,较早推送响应会很有用:

  • 当一次性搜集所有响应对服务器来说有好处的时候。
  • 当API中有多个原型(hops)需要获取全部数据时,智能化的推送机制会非常有效,可以降低复合延迟。

小结:

  • 如果对速度的要求极为苛刻,应坚持使用复合文档。
  • 如果更看重简洁性,维护小规模的、多点分散的API肯定更为切实可行。
  • 缓存只带来一点点小变化。
  • 较之客户端,优化策略更有利于服务器端。

然而,我对这些测试结果仍抱有怀疑。如果时间充裕,我会用Go语言实现的服务端做测试,并如实地模拟服务器端的各种情况。

我2020年的愿望清单

在本文结尾之际,我列一份2020年的愿望清单,并翘首以盼:

  • 在浏览器层面对全域跨域(domain-wide cross-domain)策略的支持
  • HTTP/3在众多服务器端系统中可用
  • 浏览器Etag支持缓存摘要布隆过滤
  • 预检推送技术被REST API工程师们采用
  • Chrome在并行请求上有更好的性能表现
  • Firefox在HTTP/2推送技术上更稳定,以及更好的缓存性能

这个心愿单充满野心。一旦我们达成心愿,我相信我们的REST客户可以更简单地获益,我们在服务器端的实现上可以更少的权衡性能和简单性,并且我们可以将浏览器和服务器当做索引化URI资源状态的同步引擎,它既健壮又快速。种种这些,都会有长足进步。

资源

原始的测试结果

Google Sheets格式的结果

测试脚本

译注:建议读者点击原文,通过点击start按钮来观看模拟动态图,它们更好的演示了作者在文中提到的HTTP请求响应过程。

[译文]性能测试对比——面向REST APIs的HTTP/1.1、HTTP/2、HTTP/2服务器推送相关推荐

  1. FreeSql与SqlSugar性能测试对比

    这篇文章主要是对SqlSugar 做一次简单的性能测试对比.主要针对插入.批量插入.批量更新.读取性能的测试: 测试环境 1..net core 2.2 2.FreeSql 0.3.17 3.sqlS ...

  2. Firefox 和 Chrome 性能测试对比

    Google 于上周推出了 Chrome 75 的首个稳定版,更新说明里面虽然一如既往地写到"包含性能改进",但对包括笔者在内的大部分用户而言,这些所谓的改进其实很难感知. 所以才 ...

  3. 对比面向过程方法和面向对象方法的优劣(全)

    对比面向过程方法和面向对象方法的优劣(全) 前言: 面对这个问题,我们首先能想到两个代表性的语言C/C++,亦或是Java.区别大,优劣也区别明显.我们刚开始学习编程时都会面对"HelloW ...

  4. 消息推送服务厂家对比 个推 - 极光 - 信鸽

    消息推送服务厂家对比 个推 - 极光 - 信鸽 厂家: 个推: https://www.getui.com/push 极光: https://www.jiguang.cn 信鸽: https://ww ...

  5. Win10一周更新系统开始面向企业分支推送

    微软面向消费者用户推送Win10一周年更新正式版系统5个月后,微软今天开始面向商业和企业分支(CCB)用户推送Win10周年版更新.根据此前反馈,微软新修复了上千个小问题,该分支推送版本是Window ...

  6. chromium浏览器_微软将全面向Windows 10用户推送Chromium版Edge浏览器

    大概在推出了半年时间之后,微软在上个月初面向Windows 10消费者用户推出了一枚系统更新补丁,更新内容就是把系统内置的采用EdgeHTML渲染引擎的Edge浏览器升级到使用Chromium内核版本 ...

  7. tt协议号服务器,TTIot: TTIoT云端物联网Iot组件;面向JAVA;netty;mqtt;异步推送;以事件为驱动;为设备提供安全可靠的连接通信能力;...

    TTIoT云端物联网组件;面向JAVA;以事件为驱动;为设备提供安全可靠的连接通信能力 TTIoT简介 TTIOT的Broker采用MQTT协议与设备进行交互,可以应用在数据采集.能源监控.智能生活. ...

  8. 苹果se2_太快了!苹果已面向iPhone SE 2用户推送iOS 13.4.1

    ☀苹果资讯频道是微信里最热的.粉丝最多的苹果类公众号!这里苹果迷的聚集地!查苹果保修.查苹果序列号.查iPhone报价.苹果iPhone估价.买卖二手iPhone.iPhone回收.鉴别苹果山寨机.找 ...

  9. Discuz! 6.1 - 自动禁止非公开版面向Home推送事件

    Discuz! 6.1 - 自动禁止非公开版面向Home推送事件 Discuz! 6.1中,支持通过UCenter向UCenter Home推送事件,但是没有按版面选择是否推送的功能.这个功能直到Di ...

  10. android 2.2.3,升还是不升 Android2.2与2.3性能测试对比

    Android是目前最火的手机系统,自从去年年底最新的Android2.3版系统和首款搭载该系统的谷歌手机Nexus S发布,其它Android智能手机用户就在等待自己的手机能尽快更新到最新版本. H ...

最新文章

  1. 梁佳玉 - 昨天的爱
  2. 【任务脚本】0601更新autojs客户端,回顾之前战绩,注意事项淘宝618活动领喵币autojs脚本,向大神致敬...
  3. 从传统操作系统角度理解Hadoop YARN
  4. java基础(七) 深入解析java四种访问权限
  5. 激励员工的首席执行官以及他们的秘诀
  6. tf 矩阵行和列交换_TF-搞不懂的TF矩阵加法
  7. 对象的浅拷贝和深拷贝
  8. 多线程-使线程具有有序性
  9. linux中最常用命令
  10. 推荐一款轻量级好用的开源PDF阅读器,确实好用~
  11. java导论pdf下载,人工智能导论 PDF 下载
  12. [常用工具] Python视频处理库VidGear使用指北
  13. “鲁班”画海报、“小蜜”当客服,“菜鸟”管物流……,双十一阿里黑科技知多少...
  14. /etc/mtab 文件
  15. KEIL平台下新建华大HC32F460单片机工程笔记
  16. nmap命令man详解与脚本目录
  17. 刚工作2年时15k运维工程师-简历
  18. 传输预编码matlab,基于MATLAB的MIMO系统预编码性能仿真.doc
  19. java spring+mybatis整合实现爬虫之《今日头条》搞笑动态图片爬取
  20. 前台请求后台接口数据后日期少一天Bug解决

热门文章

  1. Vue element tabs 点击锚点定位 , 鼠标滚动定位
  2. 【20考研】现在需要了解哪些事情?
  3. python开发基于SMTP协议的邮件代发服务
  4. PS电商产品banner设计
  5. 计算机用户无法删除文件,Win7电脑有些文件删不掉怎么办?
  6. RedHat8 服务安装(编译、rpm、dnf)
  7. python输出含省略号
  8. 在C++中,用“new”自定义动态数组
  9. 解决idea爆红 cant resolve symbol ‘String‘的情况
  10. Banner轮播图!