学习HTTP/2

2015 HTTP/2已成为正式标准, 与HTTP/1.x相比, 它的API与HTTP/1.x完全兼容, 更关注Web性能: 浏览器与站点之间只建一条链接, 多路复用, 头部压缩, 资源有优先级, SSL加密, 服务端推送.

Nginx已经支持持HTTP/2, 可以用Nginx来搭建一个基于HTTP/2的静态站点

Web基础

带宽与延迟

所有网络通性, 有决定性影响的两个因素: 延迟与带宽

  • 延迟
    分组从信息源发送到目的地所需的时间。
  • 带宽
    逻辑或物理通信路径最大的吞吐量。

延迟的构成(屏蔽细节):

  • 传播延迟
    请求从发送端到接收端需要的时间,与信号传播距离和速度有关
  • 处理延迟
    服务器处理时间
  • 排队延迟
    请求排队时间

目标: 高带宽, 低延迟

实际: 大多数网站性能的瓶颈都是延迟,而不是带宽.

TCP基础

TCP 负责在不可靠的传输信道之上提供可靠的抽象层, 它专门为精确传送做了优化,但并未过多顾及时间

三次握手

也即, 每一个新的HTTP连接, 每次传输应用数据之前,都必须经历一次完整的往返, 也即一个RTT(Round-Trip Time: 往返时延)

慢启动

无论带宽多大,每个 TCP 连接都必须经过慢 启动阶段。换句话说,我们不可能一上来就完全利用连接的最大带宽!

对于很多短暂、突发的HTTP连接, 常常会出现还没有达到最大窗口请求就被终止的情况。

HTTP简史

HTTP 0.9 只有一行的协议

这个时候给用户体验的是超文本文档

HTTP 0.9 其实是民间称呼

1
2
3
4
5
GET /about/

返回 html

(连接关闭)

符合原始的定义: HTTP(Hypertext Transfer Protocol)

HTTP 1.0 非正式规范

这个时候给用户体验的是富媒体网页

1
2
3
GET /rfc/rfc1945.txt HTTP/1.0 
User-Agent: CERN-LineMode/2.15 libwww/2.17b3
Accept: */*
1
2
3
4
5
6
7
8
9
10
HTTP/1.0 200 OK 
Content-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 互联网标准

从这个时期起, 给用户体验的是交互式Web应用

持久连接

TCP连接可以复用

Connection: keep-alive

通过新 TCP 连接在往返时间为 56 ms 的客户端与服务器间传输一个 20 KB 的文件, 服务器生成响应的处理时间 40 ms

第一个文件的获取总共花了 264 ms

同一个连接, 请求同样大小的另一个文件,但没有三次握手和慢启动,只花了 96 ms

HTTP管道

持久HTTP连接多次请求必须严格满足先进先出(FIFO)的队列顺序:发送请求,等待响应完成,再发送客户端队列中的下一个请求。

HTTP管道可以让我们把 FIFO 队列从客户端(请求队列)迁移到服务器(响应队列)。

但HTTP 1.x 只能严格串行地返回响应。特别是,HTTP 1.x 不允许一个连接上的多个响应数据交错到达(多路复用),因而一个响应必须完全返回后,下一个响应才会开始传输, 所以上图的实际结果是

  • HTML 和 CSS 请求同时到达,但先处理的是 HTML 请求;
  • 服务器并行处理两个请求
  • CSS 请求先处理完成,但被缓冲起来以等候发送 HTML 响应, 这种情况通常被称作队首阻塞(HOL blocking)
  • 发送完 HTML 响应后,再发送服务器缓冲中的 CSS 响应。

因为这种局限性, 如果浏览器是 Web 应用的主要交付工具,那还是很难指望通过 HTTP 管道来提升性能, 所以对于浏览器而言, 该措施流产了

HTTP 2.0:改进传输性能

HTTP/2的前身是SPDY, Google开发的实验性协议, 它不是一个标准, 但促成了HTTP/2的形成. 当HTTP/2成为正式标准后, 也就是2015年, Google宣布淘汰对SPDY的支持,拥抱HTTP/2

HTTP/2高层语义并不改变, API与HTTP/1.x兼容, 主要目标是改进传输性能,实现低延迟和高吞吐量

HTTP/2特性

一个站点一个连接(One Connection Per Origin)

http2-1

  • 由于 TCP 连接减少而使网络拥塞状况得以改观;
  • 慢启动时间减少,拥塞和丢包恢复速度更快。

二进制分帧层(Binary Framing Layer)

HTTP/2 性能增强的核心,全在于新增的二进制分帧层

该层将所有传输的信息分割为更小的消息和帧,并对它们采用二进制格式的编码, 它对应用而言是不可见的

流、消息和帧(Streams, Messages, and Frames)

二进制分帧机制改变了客户端与服务端数据交换的方式


  • 已建立的连接上的双向字节流. 一个连接可以有多个流, 一个流内含一个或多个消息。
  • 消息
    一个请求或响应. 一个消息包含了一系列完整的数据帧.

  • HTTP/2 通信的最小单位,每个帧包含帧首部,标识所属的消息及流。 所以不同消息的帧可以交错传输

多路复用(Request and Response Multiplexing)

把 HTTP 消息分解为独立的帧,交错发送,然后在另一端重新组装

  • 可以并行交错地发送请求,请求之间互不影响;
  • 可以并行交错地发送响应,响应之间互不干扰;
  • 消除不必要的延迟,从而减少页面加载的时间;
  • 不必再为绕过 HTTP 1.x 限制而多做很多工作;

请求优先级(Stream Prioritization)

请求带有权重, 数字为1~256, 数字越大, 权重越大, 也即优先级越高

一般而言, CSS, Javscript优先级会比图片高

这些权重组成一棵优先级树, 告诉服务端, 客户端希望优先获得的文件, 服务端按此来为处理请求分配CPU, 内存等资源

首部压缩(Header Compression)

首部不再是明文传输, 它会被压缩.

同时HTTP/2连接会维护一个头部表, 不再需要每个请求发送一个完整的头部

服务端推送(Server Push)

客户端一个请求, 服务端可以多个响应

  • 客户端可以缓存推送过来的资源;
  • 推送资源可以由不同的页面共享;
  • 服务器可以按照优先级推送资源;
  • 客户端可以拒绝推送过来的资源。

服务器必须遵循请求 - 响应的循环,只能借着对请求的响应推送资源。也就是说,服务器不能随意发起推送流。

其次,所有服务器推送流都由 PUSH_PROMISE 发端, PUSH_PROMISE 帧必须在返回响应之前发送,以免客户端出现竞态条件

TLS加密(TLS Encryption )

协议上, TLS(Transport Layer Security, SSL的继承者)加密不是必需的, 但在现阶段, HTTP/2 基于HTTPS才能实现

优化手段分析

经典最佳实践

前面说过, 所有网络通性, 有决定性影响的是两个因素: 延迟与带宽

所以所有应用优化手段都是基于两条简单的准则: 消除或减少网络延迟,将需要传输的数据压缩至最少

  • 减少DNS查找

每一次主机名解析都需要一次网络往返,从而增加请求的延迟时间,同时还会阻
塞后续请求。

  • 减少HTTP重定向

HTTP 重定向极费时间,这里面既有额外的 DNS 查询、TCP 握手,还有其他延迟。最佳的重定向次数为零。

  • 重用TCP连接

尽可能使用持久连接,以消除 TCP 握手和慢启动延迟。

  • 使用CDN(内容分发网络)

把数据放到离用户地理位置更近的地方,可以显著减少每次 TCP 连接的网络延
迟,增大吞吐量。

  • 去掉不必要的请求

任何请求都不如没有请求。

另外, HTTP提供了额外的优化机制

  • 缓存
  • 压缩传输内容
  • 减少HTTP头部数据(比如Cookie)
  • 并行处理请求与响应

针对HTTP/1.x的优化

使用管道

如果应用对客户端及客户端都有完全的控制权的话, 如果客户端是浏览器, 那就看下一条.

域名分片

由于 HTTP 1.x 不支持多路复用, 退而求其次的方法是打开多个TCP连接.

而在 HTTP/1.1 协议中 浏览器客户端在同一时间,针对同一域名下的请求有一定数量限制。超过限制数目的请求会被阻塞

实际上, 浏览器对同一个域名可以同时打开6~8个TCP连接.

把资源分散到不同子域名下, 这些域名都指向同一个IP地址, 从而让客户端打开更多的TCP连接

但这样会导致更多的DNS查询以及TCP慢启动, 也会对服务端造成更多压力

打包合并

  1. 合并Javascript
  2. 合并CSS
  3. 雪碧图(图片精灵)
  • 导致开发/部署流程更加复杂
  • 有可能产生不必要的数据传输
  • 有可能产生大体积的缓存失效
  • 雪碧图会增大客户端内存压力

内联资源

直接把资源放到文档内, 如inline CSS, inline Javscript, Base64的图片

但这样就无法对这些资源启用缓存机制

注意, 以上只是针对HTTP/1.x不足而采取的权宜之计

针对HTTP/2的优化

杜绝域名分区

HTTP/2 通过将一个 TCP 连接的吞吐量最大化来提升性能。在 HTTP/2 之下再使用多个连接(比如域名分区)反倒成了一种反模式

减少打包合并

HTTP/2支持多路复用, 所以资源数多不再是问题, 减少文件的合并可以更好地利用缓存机制, 但也不是说合并文件的方式就该退出历史舞台了.

把所有资源打包成一个大文件, 不一定是最优解; 请求大量的小文件, 也不一定就是最佳策略. 这里需要具体情况具体分析, 例如资源的更新频繁程度, 资源使用的时机等等, 在实践中做一个权衡.

减少内联文件

把内联资源换成使用服务器推送的方式传给客户端, 给客户端拒绝接受资源的权力, 并且实现资源的跨页面缓存

实践

用Nginx搭建HTTP/2静态站点

前提准备

  1. 一台拥有外网IP的服务器(假设操作系统为: CentOS 7.4)
  2. 一个域名(解析到上述服务器的IP)(假设域名为: www.example.com)

注: Ubuntu版本参考这里

搭建HTTPS环境

1.首先更新yum仓库(需要等待一段时间)

1
yum update

2.安装Nginx

1
yum install nginx

可以先跑一下Nginx

1
nginx

然后在浏览器访问www.example.com, 如果看到Nginx的欢迎页, 说明安装成功

3.安装letsencrypt以获取免费证书

1
yum install letsencrypt

修改Nginx配置:

1
vi /etc/nginx/nginx.conf

强制对80端口的请求重定向到HTTPS协议

1
2
3
4
5
6
server {
listen 80;
location / {
return 302 https://$host$request_uri;
}
}

找到443关键字, 把注释打开, 即把#去掉

假设静态文件的根路径为 /path/dir, 把它写在Nginx配置文件里

1
2
listen       443 ssl http2 default_server;
root /path/dir;

生成证书及密钥

1
sudo letsencrypt certonly -a webroot --webroot-path=/path/dir -d www.example.com

成功后, 控制后会打印出生成文件的路径, 把它们写在Nginx配置文件里

1
2
ssl_certificate "/etc/letsencrypt/live/www.example.com/fullchain.pem";
ssl_certificate_key "/etc/letsencrypt/live/www.example.com/privkey.pem";

4.查检openssl版本

1
openssl version

如果版本大于等于1.02, 那就是ok的, 否则, 需要升级openssl.

如何升级? 如果使用最新内核的Linux操作系统, 是不会遇到这种问题的, 真的遇到了, 自己google吧😄

5.重启Nginx

1
nginx -s reload

重新访问你的站点, 发现地址栏多了一把绿色的锁, 恭喜, 这正是使用HTTPS协议的标志!

6.定期更新证书

因为Let’s Encrypt 的证书90天就过期了, 所以可以写个定时任务

1
crontab -e
1
2
0 0 * * 1 /usr/bin/letsencrypt renew >> /var/log/letsencrypt-renew.log
5 0 * * 1 /bin/systemctl reload nginx

每星期1的0点0分执行更新操作,0点5分执行Nginx 重启

检测站点是否已经使用HTTP/2协议

使用Chrome, 新建标签页, 地址栏输入:

1
chrome://net-internals/#http2

刷新你的站点, 如果表格中出现了你的站点域名, 说明正在使用HTTP/2协议

参考资料

High Performance Browser Networking

NGINX HTTP2 White Paper

文档地址

Fork me on GitHub