HTTP/2特性及其在实际应用中的表现

4,385 阅读19分钟

引言

HTTP/2是于2015年正式发布的新一代HTTP协议。其在保持HTTP1.x语义、不变更网络基础设施的前提下致力于降低用户可感知延时,提升网络传输效率。本文将从HTTP历史、HTTP/2新特性、升级方法、效果分析四个部分进行介绍。主要涉及了为什么我们需要HTTP/2,HTTP/2能带给我们什么,如何升级,当前HTTP/2在实际应用的表现怎样四个问题。

HTTP历史

自从1991年Tim Berners-Lee提出HTTP协议的设想到现在已经20多年过去了,在这20多年中Web的发展也是日新月异。为了满足不同时代Web的需求,HTTP协议的更新迭代经历了HTTP0.9、HTTP/1.0、HTTP/1.1、SPDY、HTTP/2几个版本。接下来就这几个版本的HTTP进行简要介绍。

HTTP 0.9 (1991)

HTTP/0.9产生于Web发展的萌芽阶段,是一个简单到只有一条请求行(request line)的HTTP协议。HTTP/0.9只支持GET POST HEAD请求,请求行中不不包含版本号(那时也没有别的版本)。在发起请求、服务器响应(仅支持纯文本,没有响应头)后连接即会关闭。现在流行的Web服务器依然支持HTTP/0.9,因为支持它实在花不了多大代价。

GET /index
(响应)
(连接关闭)

HTTP/1.0

1991-1995年,随着Web浏览器的兴起,人们对Web页面的需求越来越多,最典型的就是人们不再满足于仅包含文本和超链接的超文本文档,而是需要能展现文字、样式、图片等多种媒体类型的数据。因此HTTP/0.9这种简单的传输协议很快就难以为继。1996年,HTTP-WG发布了了RFC1945,阐述了HTTP/1.0的主要特性:

  • 请求和响应有多个头部,请求行包含了协议版本
  • 包含了响应行,主要包括协议版本和响应状态
  • 响应头中包含了Content-Type,可以支持多种类型的影响内容
  • 每个请求响应结束后,连接即关闭

其实,从现在开始HTTP协议包含的内容已经超出了其名字(超文本传输)所指代的范围,但是这个名字仍然沿用了下来。

/* 请求 */
GET /rfc/rfc1945.txt HTTP/1.0
User-Agent: CERN-LineMode/2.15 libwww/2.17b3 Accept: */* /* 响应 */
HTTP/1.0 200 OK
ontent-Type: text/plain
Content-Length: 137582
Expires: Thu, 01 Dec 1997 16:00:00 GMT Last-Modified: Wed, 1 May 1996 12:45:26 GMT Server: Apache 0.84

HTTP/1.1

1997年定义HTTP/1.1的RFC2068正式发布,随后在1999年发布的RFC2616中集合了对HTTP/1.1的改进和更新。总的来讲,HTTP/1.1主要明确了之前HTTP/1.0中存在歧义的点,并在此基础上增加了许多新特性:

  • 持久传输
  • 分块编码传输
  • 字节范围请求
  • 传输编码
  • 增强的缓存机制
  • 管道化请求
// 请求1
GET /index.html HTTP/1.1
Host: website.org
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_4)... (snip)

// 响应1
HTTP/1.1 200 OK
Server: nginx/1.0.11
Accept-Ranges: bytes // 如果不为none,代表server支持范围请求
Connection: keep-alive // 持久传输,本次请求响应结束后不关闭连接
Content-Type: text/html; charset=utf-8
Cache-Control: max-age=0, no-cache // 缓存控制
Transfer-Encoding: chunked // 传输编码

100 // 当前响应块大小(256Bytes)
<!doctype html> ... (snip) // 响应内容

100 // 另一块(256Bytes)
... (snip) // 响应内容

0 // 响应分块结束

// 请求2
GET /favicon.ico HTTP/1.1 // 上次请求未关闭连接,本次重用该连接
Range: bytes=0-1023 // 请求字节范围
Connection: close // 告知服务器本次请求响应后关闭连接

// 响应2
HTTP/1.1 200 OK // 第二次请求的响应行
Content-Range: bytes 0-1023/146515 // 响应字节范围//整体字节数
Connection: close // 告知浏览器响应完成或关闭该连接

SPDY & HTTP/2

从诞生之日到今天,HTTP/1.1以其顽强的生命力伴随一代人见证了Web的繁荣发展。但面对网络中越来越多的请求、网页规模的膨胀,HTTP/1.1逐渐开始表现得力不从心,主要表现在以下几个问题上:

  • 头阻塞:HTTP/1.1中只有该请求响应后才能重用当前连接发送下一次请求,即使管道化技术使得可以在本次响应完成之前发送下次请求,但响应依然严格按照顺序返回,也就是如果前一个响应被阻塞,后边的响应将不会到来。
  • 重复的未压缩头数据传输:自HTTP/1.0以后,HTTP请求中通常带有大量以ASCII编码的头部,这些头部通常大部分都不会变化,每次请求都会携带,这给本就拥挤不堪的网络带来了更多的压力(尤其是像User Agent、Cookie这种值比较长的头部字段)。

2009年,Google提出了一项实验性的协议SPDY(读音同speedy),旨在开发者不修改当前网站实现的前提下,提高页面加载速度。SPDY提出后,Chrome、Firefox、Opera等主流浏览器先后给出了实现,很多大型网站(如Google、Twitter、Facebook等)分别提供了其对SPDY会话的实现。2012年,HTTP-WG提出了在SPDY基础上构建HTTP/2的草案,2013年给出了第一个对HTTP/2的实现,自此HTTP/2、SPDY并行发展,在客户端和服务器上进行了广泛可靠的测试。2015年,Google 宣布放弃对SPDY的继续支持,标志着HTTP/2正式登上历史舞台。

HTTP/2 新特性

二进制帧层与多路复用

与HTTP/1.x采用ASCII码明文传输的方式不同,HTTP/2在服务器和客户端之间采用传输效率更高的二进制的形式。值得一提的是,HTTP/2中报文二进制帧被分为头部帧(Header Frame)和数据帧(Data Frame),这主要是因为HTTP/2为头部字段单独提供了一种压缩方式,可以使网络中传输的头部数据降低到最小(见下一节)。 在HTTP/2中,所有数据的传输可以分为连接(Connection)、流(Stream)和帧(Frame)三个层次。与HTTP/1.1中采用的并行连接不同,HTTP/2在客户端和服务器的整个会话都会复用一条连接。在这个连接中一组完整地请求、响应被称为流。流中的每段数据(头部、数据段)被称为帧。帧是HTTP/2中通信的最小单位,其中包含了其属于那条流的“身份”信息。 在HTTP/1.x中,通常采用维持多个连接的方式使请求和响应并行发送,因为计算和存储资源的限制,无法通过无限开启新连接(通常上限为六个)的方式彻底解决头阻塞问题。在HTTP/2中,所有的数据通信均在一个连接中完成。每个数据帧在发送端准备完成后即可发送,无需等待前一个请求的响应。数据接收端则在接到这些乱序发送的数据帧后,再根据每个帧携带的“身份”,对这些数据进行重新组装,以获得完整地请求或响应数据。HTTP/2中采用的多路复用技术彻底解决了头阻塞问题;重复利用了每一组请求、响应之间的连接的网络资源,在降低资源开销的同时,降低了数据传输延时。

头部压缩(Hpack)

前面已经提到,HTTP/1.x会在请求和响应中中重复地携带不常改变的、冗长的头部数据,给网络带来不必要地负担。HTTP/2除了将ASCII明文转化为二进制帧以提高网络传输的有效性外,还提出了一种专门针对报文头部数据进行压缩的方案——Hpack。其主要思想是,通过重用当前连接中之前发送请求中的头部,仅发送新加或更改的头部信息,以降低报文中头部字段大小。头部压缩的过程如下图所示:

  • 连接会维持一个查找表,该查找表中给出了62个常用字段及其默认值(来自对多个常用网站中请求的统计)。
  • 在第一次请求时,会首先查看查找表中对应字段的值是否为本次请求携带的值,如果不是则更改这个查找表,同时增加查找表中没有的字段。在查找表中将所有头部添加完成后才根据查找表对头部进行压缩并发送请求。
  • 在接下来的每次请求中(当前连接),报文中的压缩数据仅包含查找表中被修改的字段。鉴于一个连接中很多请求中大部分头部字段携带的值都是不常变化的,此举可以大大降低头部传输所需的网络资源。

流的优先级

在HTTP/2中,请求和响应是可以乱序传输的,因此我们需要一个机制可以确保哪些被其他响应数据所依赖的或者关键资源被优先传输,以使网页的呈现和使用具有最好的体验。HTTP/2中在流的层面,采用了“优先级树”的形式确保响应数据能够按照依赖关系和优先级顺序来传输。

“优先级树”可以表示成如下图所示的样子。其中子节点所表示的流响应依赖于父节点流,因此父节点流应该被优先传输。在兄弟节点中,被分配权重较大的点应该被分配更多的网络资源,被优先传输。 如下图中(3)所示的“优先级树”中,A,B依赖于C,C依赖于D,因此D应该被优先传输,D传输完后才应该传输C,同理C传输完成后才应该传输A,B,A,B的数据在传输过程中所占用的网络资源应遵循3:1的关系。

Server Push

简单来讲,Server Push就是允许Server对还没有发出的请求进行响应。如下图所示,在第一个请求中,客户端发出了一条对index.html的请求。通过对index.html的分析,服务器可以推测出其该html的加载还需要style.css和script.js,因此在第一条请求的响应中可以将这些内容一并发送过去,避免了接下来两条请求、响应带来的延迟(RTT, Round-trip Time)。虽然在HTTP/1.x中采用inline的方式,也可以达到类似Server Push的效果,但inline具有以下弊端:

  • CSS或js没有单独的缓存,如果html的缓存方式与被inline的内容不一致,在之后对该页面访问时,本来可以缓存的css或js内容被重复请求加载
  • 其他页面用到该样式会被重复请求加载

升级到HTTP/2

在升级之前首先要确认你当前使用的客户端(浏览器)和服务器均支持了HTTP/2。幸运的是,迄今为止绝大多数浏览器均已对HTTP/2会话提供了支持,目前为止主流的Web服务器最新版本已经可以支持HTTP/2(Nginx ^1.9.5, Apache ^2.4.24 mod_http2)。所以仅需要安装最新版本的服务器即可使用其提供的HTTP/2新特性。需要注意的是,在升级HTTP/2之前要确认服务器支持SSL(支持HTTPS)。HTTP/2自身并不要求必须底层支持SSL,但当服务器不支持SSL时,几乎所有的浏览器会无视HTTP/2连接。

去除某些针对HTTP/1.x的优化

在HTTP/1.x中存在一些用于Web性能提升的“奇技淫巧”,但这些技术可能在HTTP/2中起到相反的作用,因此我们第一步就是先去除这些可能会妨碍性能的优化。

域名分片(domain sharding)

HTTP/1.x中,通信两端最多只有六个连接,且是通过区分不同域名来维持管理的。为了突破这个限制,通常会把请求资源至于不同的域名下(如 shard1.example.org, shard2.example.org)。而在HTTP/2中,因为不需要新开连接来解决头阻塞问题,所以不需要通过这种方式来增加通信的连接数。相反的在HTTP/2中采用域名分片会造成以下两个问题:

  • 增加DNS域名解析时间
  • 增加传输中压缩头部的大小。前文已经提到,头部压缩中的数据复用是在一个连接上维持的,域名分片后新开的连接无法复用之前已经发送过的头部数据,造成一些不必要地数据在网络上的传输。

雪碧图

HTTP/1.x中,为了减少请求多个图片带来的头阻塞问题,通常采用把多个图片拼接成一个大图,然后一个请求将所有图片加载在浏览器中,然后使用CSS技术将所需要的部分按需展示出来。显然的,雪碧图会带来如下问题:

  • 增加代码量,需要写一些本不必要地CSS代码
  • 在浏览器渲染过程中,内存中需要加载更多的图片内容

拼接JavaScript CSS

同雪碧图的原理一样,拼接JavaScript、CSS的做法同样是为了减少请求数,以避免潜在的头阻塞问题。在HTTP/2中,因为不存在头阻塞问题,因此应该避免这种优化方式带来的一些不必要地资源的加载。

升级

根据不同的业务类型和复杂程度,可以选择不同的升级方式,这里给出三种升级HTTP/2的方式。

将静态资源存放到支持HTTP/2的CDN中

通常情况下,一个网站中绝大多数的请求是针对静态资源发起的,因此将静态资源部署到支持HTTP/2的CDN上是一种最方便快捷的升级方法。在这种情况下访问静态资源的请求通过HTTP/2完成,而API访问则通过HTTP/1.x完成。这种方法虽然比较便捷,但由于对服务端HTTP/2的支持失去控制,往往难以充分利用所有已经实现的新特性(如Server Push、优先级控制等)。

升级反向代理服务器

反向代理服务器位于客户端和真正的服务器之间,用于对高并发访问的服务器进行负载均衡。在这种情况下头阻塞主要发生在客户端和代理服务器之间的通信,因此在代理服务器中支持HTTP/2可以消除大部分头阻塞带来的性能问题。而代理服务器和真正的服务器之间仍然采用HTTP/1.x进行通信。

服务器完全升级

当然,如果要充分利用HTTP/2带来的性能提升,就需要对所有的服务器启用支持,以保证客户端和服务器之间的所有通信均采用HTTP/2执行。值得注意的是,现在HTTP/2中的很多特性仍处于实验阶段,不同的服务器、包可能对不同特性的支持略有差别,所以在升级之前需要充分阅读文档,确保你需要的特性已经被支持。

案例分析

case1

Akamai公司给出了一个测试页面用于对比在存在大量请求情况下HTTP/2与HTTP/1.x之间巨大的性能的差异。通过查看HTTP/2请求与HTTP/1.x请求的瀑布流可以发现,HTTP/1.x连接存在严重的头阻塞问题,每个时刻最多只可能有6条请求在6条连接上执行,而HTTP/2采用多条请求复用一个连接的机制,同一时刻可以接收到的请求数不受连接数的限制,能更加充分地利用网络带宽。在笔者的网络环境下,后者可以将加载同样资源的时间降低50倍。虽然这是一个比较极端的案例,但HTTP/2带来的性能提升可见一斑。

case2

Dropbox提供了其Web服务器从SPDY升级到HTTP/2(Niginx 1.9.15)的过程及分析。下图给出其在升级过程中,SPDY连接和HTTP/2连接的占比情况,可以看到起其升级过程是在多个服务器中逐步完成的,这给我们对比升级前后的性能提供了很好地素材。 下图给出了在升级过程中,请求(ingress)和响应(egress)所占据网络带宽相对于升级前平均值的比。可以看到在切换后,请求所占据网络带宽有了较大幅度的下降(约为原来的一半),这主要是由HTTP/2的头部压缩带来的(虽然SPDY也提供了头部压缩的特性,但由于存在安全问题,通常都不会启用)。而响应占据的网络资源没有太大变化,这主要是因为头部数据在响应中仅占很小一部分。 在升级中也发现了一些存在的问题。通过下图可以看出对于POST请求来说,平均延时上升了50%。这是由于Nginx 1.9.15对HTTP/2的支持缺陷造成的。具体来讲,Nginx会将SETTINGS.INITIAL_WINDOW_SIZE字段设为0,这意味着在POST请求在完成TCP握手后并不能立即发送数据,而是需要一个RTT等待服务器将该值提升了一个较大的值后才能开始发送数据(细节参见TCP的慢启动机制)。虽然Nginx 1.11.0修复了这个问题,这依然提示我们在升级HTTP/2之前,需要充分调研所要采用的服务器和对应的包目前存在的问题,以避免升级可能带来的性能下降。

case3

99design网站也对其向HTTP/2迁移做了详细的总结。其迁移工作主要是通过将图片资源部署到支持HTTP/2的CDN上来实现的(升级方法1),衡量性能的指标是页面视觉呈现时间。通过对不同页面的测试可以发现以下三个现象(关于延时受限和带宽受限的区别可以参考这里):

  • 在延时受限的场景中,升级后的页面视觉呈现速度提升了5%左右
  • 在带宽受限的场景中,升级后的页面视觉呈现速度反而会降低15%
  • 在带宽和延时均受限的场景中,升级后的页面视觉呈现速度会进一步下降,甚至会达到25%以上(下面第一个图为http/1.1,第二个图为HTTP/2)

通过分析得出性能的下降的原因是升级HTTP/2后前端代码失去了加载资源优先级的控制,从而造成一些影响视觉呈现的关键资源被后置加载导致整个视觉呈现时间的拉长。具体来讲,在HTTP/1.x中,先发起请求的资源通常会先加载。因此我们可以在前端控制资源的加载优先级,将对于视觉呈现较为关键的资源进行优先加载。但在HTTP/2中,响应的返回是乱序的,一些非关键的资源可能会被先返回。当带宽不是限制因素时,这自然不是问题。但当这些非关键资源将带宽耗尽时,关键资源就只好等待带宽释放后才能返回。一言以蔽之,在HTTP/2的使用使得对资源加载顺序的控制权从前端转移到后端了。很显然,其采用的CDN是不支持对流的优先级传输的。这也印证了前文中提到的,使用CDN升级HTTP/2的方式可能会让我们无法使用HTTP/2的一些特性,甚至会完全失去控制。

总结

最后,总结一下。

为什么要用HTTP/2?

HTTP/1.x中的头阻塞问题会造成连接总是处于等待响应的状态,而未充分利用带宽;HTTP/1.x大量、重复的请求头在网络上传输,使网络负载了很大一部分本需要传输的数据量。

HTTP/2能带给我们什么?

除了解决了上述HTTP/1.x的问题,新的协议还包含了Server Push、流的优先级传输等新特性,使性能得到进一步提升。

如何升级?

目前的主流浏览器均已支持,因此一般情况下只需要升级服务端即可。三种方案:找一个支持HTTP/2的CDN部署网站中的静态资源;升级代理服务器;升级所有服务器。

HTTP/2实际表现如何?

在存在大量请求、延时受限的环境下,HTTP/2的性能表现相较于HTTP/1.x有明显优势。但考虑到HTTP/2尚年幼,很多特性的实现还未完善,许多特性的表现可能并非理想。因此在线上升级之前要做充分的测试,确保所采用的实现能够支持你需的特性,其缺陷不会对性能造成重大影响。

【参考文献】