首页 > 代码库 > 全面解析HTTP/2:历史、特性、调试、性能

全面解析HTTP/2:历史、特性、调试、性能

原文地址

写在前面

超文本传输协议(英文:HyperText Transfer Protocol,缩写:HTTP)是互联网上应用最为广泛的一种网络协议。设计 HTTP 最初的目的是为了提供一种发布和接收 HTML 页面的方法。通过 HTTP 或者 HTTPS 协议请求的资源由统一资源标识符(URI)来标识。

虽然HTTP/1.1稳定运行了十多年了,但HTTP/2来势汹汹,作为技术工程师有必要学习一下HTTP/2。今天,阿里云CDN安防技术专家金九将从历史、特性、调试、性能四个层面,来全面解析HTTP/2,希望本文可以给你带来一些启发。


一、历史

1、 HTTP/0.9
最早的原型,1991年发布,该版本极其简单,只支持 GET 方法,不支持 MIME 类型和各种 HTTP 首部等等。

2、 HTTP/1.0
1996年发布。HTTP/1.0在HTTP/0.9的基础之上添加很多方法,各种 HTTP 首部,以及对多媒体对象的处理。

除了GET命令,还引入了POST命令和HEAD命令,丰富了浏览器与服务器的互动手段。

任何格式的内容都可以发送。这使得互联网不仅可以传输文字,还能传输图像、视频、二进制文件。这为互联网的大发展奠定了基础。

HTTP请求和回应的格式也变了。除了数据部分,每次通信都必须包括头信息(HTTP header),用来描述一些元数据。

可以说,HTTP/1.0是对HTTP/0.9做了革命性的改变,但HTTP/1.0依然有一些缺点,其主要缺点是每个TCP连接只能发送一个请求,发送数据完毕后连接就关闭,如果还要请求其他资源,就得再新建一个连接。虽然有些浏览器为了解决这个问题,用了一个非标准的Connection头部,但这个不是标准头部,各个浏览器和服务器实现有可能不一致,因此不是根本解决办法。

3 、HTTP/1.1
1999年正式发布。HTTP/1.1是当前主流的 HTTP 协议。完善了之前 HTTP 设计中的结构性缺陷,明确了语义,添加和删除了一些特性,支持更加复杂的的 Web 应用。

经过了十多年将近20年的发展,这个版本的HTTP协议已经很稳定了,跟HTTP/1.0相比,它新增了很多引人注目的新特性,比如Host协议头、Range分段请求、默认持久连接、压缩、分块传输编码(chunked)、缓存处理等等,至今都大量使用,而且很多软件依赖这些特性。

虽然HTTP/1.1并不像HTTP/1.0对于HTTP/0.9那样的革命性,但是也有很多增强,目前主流浏览器均默认采用HTTP/1.1。

# 4、SPDY
SPDY(发音:speedy)协议由Google开发,主要解决 HTTP/1.1 效率不高的问题,于2009年公开,到2016年初结束使命。因为HTTP/2已经被IETF标准化了,以后各种新版浏览器都会支持HTTP/2,Google认为SPDY已经没有存在的必要了,接下来的使命由HTTP/2去完成。

5、HTTP/2
HTTP/2是最新的HTTP协议,已于2015年5月份正式发布, Chrome、 IE11、Safari以及Firefox 等主流浏览器已经支持 HTTP/2协议。

注意是HTTP/2而不是HTTP/2.0,这是因为IETF(Internet Engineering Task Force,互联网工程任务组)认为HTTP/2已经很成熟了,没有必要再发布子版本了,以后要是有重大改动就直接发布HTTP/3。

其实,HTTP/2的前身是SPDY,甚至它俩的目标、原理和基本实现都差不多。IETF组委会中有很多Google工程师,将SPDY推动成为标准也就不足为奇了。

HTTP/2不仅优化了性能而且兼容了HTTP/1.1的语义,其几大特性与SPDY差不多,与HTTP/1.1有巨大区别,比如它不是文本协议而是二进制协议,而且HTTP头部采用HPACK进行压缩,支持多路复用、服务器推送等等。


二、特性

1、二进制协议

HTTP/2 采用二进制格式传输数据,而非HTTP/1.x的文本格式。消息头和消息体均采用二进制格式,并称之为”帧“(Frame)。Frame二进制基本格式如下(摘自rfc7540#section-4.1):

+-----------------------------------------------+
| Length (24) |
+---------------+---------------+---------------+
| Type (8) | Flags (8) |
+-+-------------+---------------+-------------------------------+
|R| Stream Identifier (31) |
+=+=============================================================+
| Frame Payload (0...) ...
+---------------------------------------------------------------+

之所以说是基本格式,是因为所有HTTP/2 Frame都是由该基本格式来封装,类似于TCP头,目前有10个Frame,由Type字段来区分,各个Frame都有自己的二进制格式,都封装Frame Payload中。

其中有两个重要的Frame:Headers Frame(Type=0x1)和Data Frame(Type=0x0),分别对应HTTP/1.1中的消息头(Header)和消息体(Body),由此可见语义并没有太大变化,而是文本格式变成二进制的Frame。二者的转换和关系如下图(摘自 《High Performance Browser Networking》):

技术分享

此外,HTTP/2中还有流(Stream)和消息(Message)的概念,通过Stream Identifier(即流ID)字段来标识,流ID一样的是同一个流,流中包含消息,这个消息对应HTTP/1.x的请求消息(Request Message)或者响应消息(Response Message),消息是通过帧(Frame)来传输的,响应消息比较大,可能由多个Data Frame来传输。HTTP/2中流、消息和帧的对应关系如下图(摘自 《High Performance Browser Networking》):

技术分享

2、头部压缩

HTTP/1.x 每次请求和响应,都会携带大量冗余消息头信息,比如Cookie和User Agent,基本一样的内容,每次请求浏览器都会默认携带,这会浪费很多带宽资源,也影响了速度。这是因为HTTP是无状态协议,每次请求都必须附上所有信息,从而导致了每次请求都带上大量重复的消息头。

为此,HTTP/2做了优化,对消息头采用HPACK格式进行压缩传输,并对消息头建立索引表,相同的消息头只发送索引号,从而提高效率和速度。但付出的代价是客户端和服务器均维护一个索引表,在如今内存不值钱的时代,这点空间换取时间还是非常值得的。

关于HPACK请参考RFC7541。

3、多路复用

多路复用是指在一个TCP连接里,客户端和服务器都可以同时发送多个请求或者响应,对HTTP/1.x来说各个请求和响应都是有严格的次序要求,而在HTTP/2中,不用按照次序一一对应,而且并发的多个请求或者响应中任何一个请求阻塞了不会影响其他的请求或者响应,这样就避免了“队头堵塞”。如下图(摘自 《High Performance Browser Networking》):
技术分享

4、服务器推送

服务器推送(Server Push)是指在HTTP/2中服务器未经请求可以主动给客户端推送资源。例如服务端可以主动把 图片、JS 和 CSS 文件推送给浏览器,而不需要浏览器解析HTML后再发送这些请求。当浏览器解析HTML后这些需要的资源都已经在浏览器里了,大大提高了网页加载的速度。如下图(摘自 《High Performance Browser Networking》):

技术分享

浏览器发起请求page.html这个页面,这个页面中引用了script.js和style.css,服务器在响应page.html后顺便推送了script.js和style.css这两个文件,这样浏览器解析完page.html后发现引用的script.js和style.css已经在本地了,不需要再发送请求了,这样就节省了两次请求和这两次请求所花的网络时间,大大提高了网络性能和用户体验。

5、安全

HTTP的安全是由SSL/TLS来保障,也就是HTTPS,其实HTTP/2并不强制要求依赖SSL/TLS,但是,当前主流浏览器均只支持基于SSL/TLS的HTTP/2,况且在网络劫持日益猖獗的互联网环境下,HTTPS将是未来的趋势,HTTP/2基于HTTPS也是未来的趋势,而各大主流浏览器在实现HTTP/2之初均只支持SSL/TLS的HTTP/2,可见安全也是HTTP/2的重要特性之一。


三、调试

从原理和目标上看HTTP/2和SPDY差不多,从Nginx官方代码上看HTTP/2和SPDY的实现也差不多。Nginx官方代码中已经删除了spdy模块的代码,取而代之的是http2模块(ngx_http_v2_module)。

1、启用

1.1、在编译参数中加入http2模块(默认已经有ssl模块了):

# git clone https://github.com/alibaba/tengine.git
# cd tengine
# ./configure --prefix=/opt/tengine --with-http_v2_module
# make
# make install

1.2、生成测试证书和私钥

# cd /etc/pki/CA/
# touch index.txt serial
# echo 01 > serial
# openssl genrsa -out private/cakey.pem 2048
# openssl req -new -x509 -key private/cakey.pem -out cacert.pem
# cd /opt/tengine/conf
# openssl genrsa -out tengine.key 2048
# openssl req -new -key tengine.key -out tengine.csr
...
Country Name (2 letter code) [AU]:CN
State or Province Name (full name) [Some-State]:ZJ
Locality Name (eg, city) []:HZ
Organization Name (eg, company) [Internet Widgits Pty Ltd]:Aliyun
Organizational Unit Name (eg, section) []:CDN
Common Name (e.g. server FQDN or YOUR name) []:www.tengine.com
Email Address []:

Please enter the following ‘extra‘ attributes
to be sent with your certificate request
A challenge password []:
An optional company name []:
...
# openssl x509 -req -in tengine.csr -CA /etc/pki/CA/cacert.pem -CAkey /etc/pki/CA/private/cakey.pem -CAcreateserial -out tengine.crt

1.3、配置http2

server {
        listen       443 ssl http2; 
        server_name  www.tengine.com;
        default_type  text/plain;
        ssl_protocols       TLSv1 TLSv1.1 TLSv1.2;
        ssl_certificate     tengine.crt;
        ssl_certificate_key tengine.key;
        ssl_ciphers         EECDH+CHACHA20:EECDH+CHACHA20-draft:EECDH+AES128:EECDH+AES256:EECDH+3DES:RSA+3DESi:RC4-SHA:ALL:!MD5:!aNULL:!EXP:!LOW:!SSLV2:!NULL:!ECDHE-RSA-AES128-GCM-SHA256;
        ssl_prefer_server_ciphers  on;

        location / {
            return 200 "http2 is ok";
        }
    }

1.4、启动tengine即可:

# /opt/tengine/sbin/nginx -c /opt/tengine/conf/nginx.conf
1.5、测试
先绑定/etc/hosts:
127.0.0.1 www.tengine.com
用nghttp工具测试:
jinjiu@j9mac ~/work/pcap$ nghttp ‘https://www.tengine.com/‘ -v
[  0.019] Connected
[  0.043][NPN] server offers:
          * h2
          * http/1.1
The negotiated protocol: h2
[  0.064] recv SETTINGS frame <length=18, flags=0x00, stream_id=0>
          (niv=3)
          [SETTINGS_MAX_CONCURRENT_STREAMS(0x03):128]
          [SETTINGS_INITIAL_WINDOW_SIZE(0x04):2147483647]
          [SETTINGS_MAX_FRAME_SIZE(0x05):16777215]
[  0.064] recv WINDOW_UPDATE frame <length=4, flags=0x00, stream_id=0>
          (window_size_increment=2147418112)
[  0.064] send SETTINGS frame <length=12, flags=0x00, stream_id=0>
          (niv=2)
          [SETTINGS_MAX_CONCURRENT_STREAMS(0x03):100]
          [SETTINGS_INITIAL_WINDOW_SIZE(0x04):65535]
[  0.064] send SETTINGS frame <length=0, flags=0x01, stream_id=0>
          ; ACK
          (niv=0)
[  0.064] send PRIORITY frame <length=5, flags=0x00, stream_id=3>
          (dep_stream_id=0, weight=201, exclusive=0)
[  0.064] send PRIORITY frame <length=5, flags=0x00, stream_id=5>
          (dep_stream_id=0, weight=101, exclusive=0)
[  0.077] send PRIORITY frame <length=5, flags=0x00, stream_id=7>
          (dep_stream_id=0, weight=1, exclusive=0)
[  0.077] send PRIORITY frame <length=5, flags=0x00, stream_id=9>
          (dep_stream_id=7, weight=1, exclusive=0)
[  0.077] send PRIORITY frame <length=5, flags=0x00, stream_id=11>
          (dep_stream_id=3, weight=1, exclusive=0)
[  0.077] send HEADERS frame <length=39, flags=0x25, stream_id=13>
          ; END_STREAM | END_HEADERS | PRIORITY
          (padlen=0, dep_stream_id=11, weight=16, exclusive=0)
          ; Open new stream
          :method: GET
          :path: /
          :scheme: https
          :authority: www.tengine.com
          accept: */*
          accept-encoding: gzip, deflate
          user-agent: nghttp2/1.9.2
[  0.087] recv SETTINGS frame <length=0, flags=0x01, stream_id=0>
          ; ACK
          (niv=0)
[  0.087] recv (stream_id=13) :status: 200
[  0.087] recv (stream_id=13) server: Tengine/2.2.0
[  0.087] recv (stream_id=13) date: Mon, 26 Sep 2016 03:00:01 GMT
[  0.087] recv (stream_id=13) content-type: text/plain
[  0.087] recv (stream_id=13) content-length: 11
[  0.087] recv HEADERS frame <length=63, flags=0x04, stream_id=13>
          ; END_HEADERS
          (padlen=0)
          ; First response header
http2 is ok[  0.087] recv DATA frame <length=11, flags=0x01, stream_id=13>
          ; END_STREAM
[  0.087] send GOAWAY frame <length=8, flags=0x00, stream_id=0>
          (last_stream_id=0, error_code=NO_ERROR(0x00), opaque_data(0)=[])

用chrome浏览器测试:
技术分享

2、抓包分析

从抓包来学习HTTP/2格式是最好的办法,但HTTP/2又是基于https的,也就是加密的,直接抓包看到的是密文,没有意义,还好Wireshark提供解密https流量的办法可以比较方便地调试HTTP/2。

2.1、先导出系统变量$SSLKEYLOGFILE,以OSX系统为例

#bash
echo "\nexport SSLKEYLOGFILE=~/ssl_debug/ssl_pms.log" >> ~/.bash_profile && . ~/.bash_profile

2.2、打开Chrome或者Firefox

#chrome
open /Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome 
#firefox
open /Applications/Firefox.app/Contents/MacOS/firefox

用打开的Chrome或者Firefox浏览器打开https网站,比如:https://www.taobao.com
然后看看文件~/ssl_debug/ssl_pms.log有没有内容,有内容就可以用Wireshark解密https数据了。

2.3、Wireshark设置

Wireshark->Perferences...->Protocols->SSL

启动抓包:
技术分享

技术分享

可见已经能得到HTTP/2的明文数据了。篇幅所限,在此不展开具体细节了,更多HTTP/2二进制协议细节请参考RFC7540。


四、性能

测试机器配置:cache1.cn1, 115.238.23.13, 16核Intel(R) Xeon(R) CPU L5630 @ 2.13GHz,48G内存,万兆网卡
测试工具:h2load
测试结果:

技术分享

技术分享


结论

1、无论是否keepalive,HTTP/2与SPDY/3.1性能相当,HTTP/2略优。
2、在size为1k、2k、4k测试结果中保持较低RT情况下QPS也较高,CPU没有达到瓶颈,加大压测客户端数量后QPS有所提高,但RT变大,5xx也变多(这部分数据没有给出,是测试时记录的现象)。
在size为16k、32k、64k、128k、256k的测试结果中CPU达到瓶颈,随着size变大,QPS降低,RT变高,CPU性能消耗较多的函数是gcm_ghash_clmul。
在size为512k时网卡达到瓶颈,CPU没有达到瓶颈。
3、在开启keepalive的情况下,HTTP/1.1的性能与HTTP/2的性能差距不是很大。但关闭keepalive时HTTP/2的性能比HTTP/1.1更好。

原文地址


技术分享

全面解析HTTP/2:历史、特性、调试、性能