Nginx实现HTTP/2——原理、实践与数据分析

HTTP/2(Hypertext Transfer Protocol Version 2)无疑是下一代互联网加速技术的基石与方向,自诞生以来经历了00~17 共 18 版草案,目前,已经拥有了明确的RFC标准:RFC 7540RFC 7541 
目前支持 HTTP/2 的浏览器有 Chrome 41+、Firefox 36+、Safari 9+、Windows 10 上的 IE 11 和 Edge。服务器方面则有 F5、H2O、nghttp2 等数十种选择,各种语言实现的都有。Nginx自1.9.5版本开始支持HTTP/2的http_v2_module模块,在这个过程中修复了一些bug,目前稳定的版本是nginx-1.10.1。另外,移动端开发可以使用OkHttp来实现HTTP/2。 
HTTP/2浏览器支持性,其中最差的是IE,只有11能够支持
综上所述,无论是移动APP还是浏览器,无论是客户端还是服务端,都已经具备了支持HTTP/2的能力,HTTP/2已经是大势所趋,笔者将基于nginx-1.10.1来实现HTTP/2,并分析HTTP/2的性能优势,结合其特点来确定如何在线上环境中得到有效应用。



1.HTTP/2原理

如果要深入讲解HTTP/2的原理,一节的内容是远远不够的,后期笔者会专门来做一个HTTP/2的系列。那本节内容主要对HTTP/2的改动与优势做简单介绍,并且回答两个问题(这两个问题也曾深深困扰着我):1. 如果浏览器不支持HTTP/2,而服务端开启了HTTP/2,会否有兼容性影响?2. HTTP/2请求是否一定要是HTTPS请求呢?通过这些对HTTP/2有一个全面的认识。

1.1 HTTP/1的弊端

先来说说HTTP/1.1的问题,这里请原谅笔者的啰嗦,需要把事情说清楚。 
基本的HTTP/1.1是基于文本格式的,单个请求单个连接,客户进程建立一条同服务器进程的TCP连接,然后发出请求并读取服务器进程的响应。服务器进程关闭连接表示本次响应结束。因此,页面加载开销是非常大的。 
为此,HTTP/1.1自然不会坐以待毙,也做了一些优化,包括:

  • 浏览器并发控制Pipelining

浏览器Pipelining是将同一Host的多个HTTP请求整批提交的技术,而在传送过程中不需先等待服务端的回应。不同浏览器的并发数是不同的,比如IE是默认两个请求。 
为了使Pipelining的效益最大化,需要将请求的域名Host分散,如下图所示:

这里写图片描述
而这样,其弊端是很明显的,不同Host需要额外的寻址消耗,会导致DNS时间增长,尤其当某些地区DNS解析不稳定时,甚至会导致大量请求的阻塞,得不偿失。

  • HTTP Keep-Alive

即我们常见的:Connection:keep-alive。数据传输完成了保持TCP连接不断开,等待在同域名下继续用这个通道传输数据。这样就减少了TCP的建连次数。 
需要注意的TCP Keep-Alive和HTTP Keep-Alive是不同层次上的概念,TCP的keep alive是检查当前TCP连接是否活着;HTTP的Keep-alive是要让一个TCP连接在timeout周期内永久存活。 
一般而言,HTTP Keep-Alive和Pipelining技术是相结合使用的,这样同一个域名下的请求就能实现并发而非串行,然而,事情真的有我们想的那么好嘛?

线头阻塞问题:由于要求服务端返回响应数据的顺序必须跟客户端请求时的顺序一致,这容易导致Head-of-line blocking:第一个请求的响应发送影响到了后边的请求,因为这个原因导致HTTP流水线技术对性能的提升并不明显。 
另外,由于域名是分散的,同一个站点,浏览器往往要保持8-10条长连接,资源消耗更大了。 
综上所述,由于HTTP/1.1协议本身的缺陷(基于文本,过于简单),即使是已有的优化,也无法满足日益增长的性能需求。

1.2 HTTP/2的优点

HTTP/2的优点总结为一个字,就是“”:

  • HTTP/2是完全多路复用的,而非有序并阻塞的;

  • 请求优先级

  • 使用报头压缩,HTTP/2降低了开销

  • HTTP/2实现了服务端响应的主动推送

1.2.1 多路复用

完全的多路复用是HTTP/2的核心,HTTP/2采用二进制帧格式而非文本格式进行传输,单个连接内多个流(请求-响应)之间并行处理,减少网路资源占用,可避免了TCP频繁的打开、关闭。如下图所示,一言以蔽之,对一个站点,现在只需建连一次,就可以多路复用。 
这里写图片描述

通过Wireshark抓包,可以明显对比看到HTTP/1.1和HTTP/2的区别: 
HTTP/1.1的包是典型的一条请求发出,等待响应,响应后再发第二条请求,再等待响应。 
这里写图片描述
HTTP/2的包(如何用Wireshark抓HTTP/2包?请参考)是请求并行发出,同时得到响应,且不管是请求还是响应都是二进制帧格式,而不是文本格式,实现了多路复用高并发。 
这里写图片描述
这里写图片描述

1.2.2 请求优先级

流的优先级(priority)属性建议终端(客户端+服务器端)需要按照优先级值进行资源合理分配,优先级高的需要首先处理,优先级低的可以稍微排排队,这样的机制可保证重要数据优先处理。

优先级改变:

  • 终端可在新建的流所传递HEADERS帧中包含优先级priority属性
  • 可单独通过PRIORITY帧专门设置流的优先级属性

然而,具体如何应用到线上,到目前为止,还没有看到有成熟的例子,这一点还有待研究。

1.2.3 HPACK压缩

HTTP/2使用了报头压缩技术,压缩算法使用HPACK。可让报头更紧凑,更快速传输,有利于移动网络环境 
需要注意的是,HTTP/2关注的是报头(header)压缩,而我们常用的gzip等是报文内容(body)的压缩。二者不仅不冲突,且能够一起达到更好的压缩效果。 
以一个空文档(body大小可忽略)为例,HTTP/1.1的大小为252B 
这里写图片描述
HTTP/2的大小为140B,压缩了100B,接近50%。 
这里写图片描述

1.2.4 服务端主动推送

传统方式:客户端请求,服务器响应,客户端逐一解析需要后续请求的图片、样式等资源,再次一一发送资源请求 
HTTP/2服务器根据客户端请求,计算出响应内容所包含的资源,在客户端发起请求之前提前发送给客户端 
这样做,节省了客户端主动发起请求的时间的往返时间。 
需要注意的是,目前Nginx仍不支持Server Push功能

1.3 HTTP/2如何建立连接

HTTP/2请求其实并不一定要是HTTPS请求,可分为两种:

  • h2,基于TLS之上构建的HTTP/2,作为ALPN的标识符,两个字节表示,0x68, 0x32,即必须是https
  • h2c,直接在TCP之上构建的HTTP/2,缺乏安全保证,即http

然而,目前所有支持HTTP/2的浏览器都是基于TLS 1.2协议之上构建HTTP/2的,因此,实际应用中,PC端要使用HTTP/2必须先支持https;移动端(比如OkHttp)可以直接在http请求上实现HTTP/2

HTTP/2保留并兼容了HTTP/1.1的所有语义,但传输语法(或者说传输方式)改变。换言之,究竟是使用HTTP/2还是HTTP/1.1来传输数据是可以由客户端和服务端协商的,当浏览器使用HTTPS传输时,协商是强制,封装在TLS之上的ALPN扩展协议上。协商一致,则使用HTTP/2进行传输,若协商过程中发现服务端不支持HTTP/2,则使用HTTP/1.1进行传输。其过程如下所示:

这里写图片描述

每个站点只需要协商一次,且协商过程是在第一次SSL握手过程中就完成的: 
这里写图片描述
Client Hello中询问,你支持HTTP/2吗? 
这里写图片描述 
Server Hello中应答,我支持! 
这里写图片描述

2. Nginx实现HTTP/2

Nginx 自1.9.5版本新增了 http_v2_module 模块用于提供 HTTP/2 服务。建议测试环境可使用1.10.1后的版本,因为在此之间的版本并不是稳定的,且修复了一些HTTP/2的问题(当然,1.10.1也不是完美无缺的,比如也存在HTTP/2 POST Bug),真正生产环境建议使用Nginx 1.11.0 稳定版。

2.1 ngx_http_v2_module模块安装

ngx_http_v2_module模块提供了对HTTP/2的支持,用了替代ngx_http_spdy_module模块。该模块不是默认构建的,所以集成的话必须重新构建Nginx并添加配置 –with-http_v2_module,并且必须支持OpenSSL version 1.0.2.以上(ALPN协议需要)。具体步骤如下: 
(1)配置

./configure --with-http_ssl_module \ --with-http_v2_module \ --with-debug \ --with-openssl=/path/to/openssl-1.0.2
		
  • 1
  • 2
  • 3
  • 4

(2)编译并安装

make&make install
		
  • 1

(3)修改nginx.conf配置

server {
     listen 443 default_server ssl http2;

     ssl_certificate      server.crt;
     ssl_certificate_key  server.key; ... }
		
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

三步完成后,Nginx的443端口已经支持HTTP/2协议了。

2.2 ngx_http_v2_module模块指令说明

ngx_http_v2_module模块可以通过指令修改一系列配置,来调整HTTP/2性能:

  • http2_chunk_size
Syntax: http2_chunk_size size; Default:    
http2_chunk_size 8k; Context:    http, server, location 
		
  • 1
  • 2
  • 3
  • 4
  • 5

设置响应报文内容(response body)分片的最大长度。如果这个值过小,将会带来更高的开销,如果值过大,则会导致线头阻塞的问题。默认大小8k。

  • http2_body_preread_size
Syntax: http2_body_preread_size size; Default:    
http2_body_preread_size 64k; Context:    http, server
		
  • 1
  • 2
  • 3
  • 4

用于解决HTTP/2 POST Bug,1.11.0版本以上有效。请求内容在被处理前存储缓冲区的大小。1.9.5~1.10.0这个值都是默认为0的,1.11.0默认是64k。

  • http2_idle_timeout
Syntax: http2_idle_timeout time; Default:    
http2_idle_timeout 3m; Context:    http, server
		
  • 1
  • 2
  • 3
  • 4

设置空闲连接关闭的超时时间。

  • http2_max_concurrent_streams
Syntax: http2_max_concurrent_streams number; Default:    
http2_max_concurrent_streams 128; Context:    http, server
		
  • 1
  • 2
  • 3
  • 4

设置一个连接中最大并发流的数量

  • http2_max_field_size
Syntax: http2_max_field_size size; Default:    
http2_max_field_size 4k; Context:    http, server
		
  • 1
  • 2
  • 3
  • 4

限制经过HPACK压缩后请求头中每个字段的最大尺寸。

  • http2_max_header_size
Syntax: http2_max_header_size size; Default:    
http2_max_header_size 16k; Context:    http, server
		
  • 1
  • 2
  • 3
  • 4

限制经过HPACK压缩后完整请求头的最大尺寸。

  • http2_recv_buffer_size
Syntax: http2_recv_buffer_size size; Default:    
http2_recv_buffer_size 256k; Context:    http
		
  • 1
  • 2
  • 3
  • 4

设置每一个worker的输入缓冲区大小

  • http2_recv_timeout
Syntax: http2_recv_timeout time; Default:    
http2_recv_timeout 30s; Context:    http, server
		
  • 1
  • 2
  • 3
  • 4

设置当连接关闭后,等到客户端是否发送更多的数据来的超时时间。

另外,还提供了参数$http2,来表示是否使用了HTTP/2。

3. 数据分析与效果对比

使用HTTP/2的目的最终是为了提高用户体验,所以其效果必须以数据说话。笔者认为重点应该关注两个方面: 
1. 服务器高负载情况,对比http、HTTPS、HTTP/2的Tps; 
2. 不同浏览器(包括支持和不支持HTTP/2),在使用http、HTTPS、HTTP/2下的加载性能。

3.1 服务端压测Tps对比

(1)压测工具选择 
http和HTTPS的压测工具我们使用Apache ab,HTTP/2的压测工具我们使用nghttp2(极力推荐这款工具,包括了nghttp客户端、nghttpd服务端、nghttpx代理、h2load压测四个模块)。 
(2)压测场景 
高并发情况,100用户,10万个请求。 
请求大小250B,其中内容只有16B。

(3)压测结果 
http压测结果,Tps 6749 req/s,平均响应性能44.451ms 
同时,服务端的性能消耗主要在CPU,CPU消耗在15%。 
这里写图片描述 
这里写图片描述 
HTTPS压测结果,Tps仅仅只能达到871.98 req/s,平均响应性能114.682ms 
服务端CPU的消耗非常的大,在60%(因为SSL握手需要消耗大量的CPU运算,如何缓解这个问题?SSL优化我们将在其他篇幅中详述) 
这里写图片描述 
这里写图片描述 
HTTP/2压测结果出乎意料的好,Tps居然能达到10126.12 req/s,同时,HPACK压缩能够节省40%的头部空间。 
另外一方面,服务端CPU的消耗为25%,在可接受的范围。 
这里写图片描述 
这里写图片描述

(4)结论

HTTP/2通过多路复用,减少了连接数,明显提高了服务端的处理能力(接近1万Tps),优于HTTP/1.1。与此同时,其对服务端CPU的资源消耗又明显小于HTTPS。

3.2 浏览器加载时间对比

首先,我们在测试环境构建了一个模拟的商品促销页主会场,页面大小11.2MB,请求数302个。HTTP/1.1的页面中采用了多种HTTP/1.1的优化技术,包括Pipelining,Keep-Alive,脚本合并等。 
浏览器我们分别选择了支持HTTP/2的Chrome,Firefox和不支持HTTP/2的IE浏览器,并使用主动拨测工具Webpagetest(自动化的页面性能测试工具)。采集的页面加载数据如下所示: 
这里写图片描述 
我们可以很明显地看到,火狐和Chrome,HTTP/2的加速效果,其中Chrome的加速效果最佳,HTTP/2(1.196s左右)较HTTPS(2.233s左右)速度提高了80%,较http(1.689s)速度提高了40%。与此同时,就IE 8来看,HTTP/2的加载时间没有明显的变慢(因为其实已经降级为HTTP/1.1了) 
这里写图片描述

结论:数据是HTTP/2加速能力最有力的说明。

4. 如何更好地使用HTTP/2

在使用HTTP/2的过程中,并不是一次性的就能看到其显著的加速的效果,要使HTTP/2发挥作用,除了参数调优外,我们总结还需要注意两个方面:

  • 页面元素的相应调整

HTTP/1.1页面提倡脚本合并,大量图片域名发散(image1-5)。 
而HTTP/2是多路复用的,因此恰恰相反,最好做到: 
(1)脚本和样式打散为小资源,使用同一个域名,比如http2res.XXXX.com 
(2)图片资源使用同一个域名,比如http2img.XXXX.com

  • 结合Nginx的其他性能优化配置

Nginx对SSL优化。比如约定加密套件(ssl_ciphers)、SSL Session复用(ssl_session_cache | ssl_session_tickets )、keepalive_timeout的设置等。SSL优化和HTTP/2两者并不冲突,前者关注减少CPU消耗,后者重点是链路的复用。 
另外,还有Nginx TCP 优化、Gzip压缩等技术。当然,所有这些必须经过严格的压测和线上验证,这一部分的工作我们仍然在进行中。

  • 建议优先在移动端使用

为什么建议优先在移动端应用HTTP/2呢?因为经过笔者的测试,国内的所有浏览器,包括360、猎豹、百度、QQ、UC都不支持HTTP\2(虽然他们都是Chrome内核,这一点真的无力吐槽,没有抄袭到人家的精华..),所以并不能起到加速的效果。 
如果真要在PC端实现,可以通过UA来判断,对支持HTTP/2的浏览器指向HTTP/2加速的主机,对不支持的指向HTTP/1.1加速的主机。

以上,笔者已经简单介绍了HTTP/2的原理,Nginx如何实现HTTP/2以及相关参数调优。并且将我们所做的测试数据分享出来以证明HTTP/2的加速效果实至名归。还给出了一些使用HTTP/2的建议。最后想说,这只是个开始,后面还有很多挑战,希望大家一起愈挫愈勇,群策群力!

参考文献

请使用浏览器的分享功能分享到微信等