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 | GET /about/ |
符合原始的定义: HTTP(Hypertext Transfer Protocol)
HTTP 1.0 非正式规范
这个时候给用户体验的是富媒体网页
1 | GET /rfc/rfc1945.txt HTTP/1.0 |
1 | HTTP/1.0 200 OK |
- 请求带有协议版本, 请求头
- 响应也带有协议版本, 有状态码, 还有响应头, 响应对象不局限于超文本
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)
- 由于 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慢启动, 也会对服务端造成更多压力
打包合并
- 合并Javascript
- 合并CSS
- 雪碧图(图片精灵)
- 导致开发/部署流程更加复杂
- 有可能产生不必要的数据传输
- 有可能产生大体积的缓存失效
- 雪碧图会增大客户端内存压力
内联资源
直接把资源放到文档内, 如inline CSS, inline Javscript, Base64的图片
但这样就无法对这些资源启用缓存机制
注意, 以上只是针对HTTP/1.x不足而采取的权宜之计
针对HTTP/2的优化
杜绝域名分区
HTTP/2 通过将一个 TCP 连接的吞吐量最大化来提升性能。在 HTTP/2 之下再使用多个连接(比如域名分区)反倒成了一种反模式
减少打包合并
HTTP/2支持多路复用, 所以资源数多不再是问题, 减少文件的合并可以更好地利用缓存机制, 但也不是说合并文件的方式就该退出历史舞台了.
把所有资源打包成一个大文件, 不一定是最优解; 请求大量的小文件, 也不一定就是最佳策略. 这里需要具体情况具体分析, 例如资源的更新频繁程度, 资源使用的时机等等, 在实践中做一个权衡.
减少内联文件
把内联资源换成使用服务器推送的方式传给客户端, 给客户端拒绝接受资源的权力, 并且实现资源的跨页面缓存
实践
用Nginx搭建HTTP/2静态站点
前提准备
- 一台拥有外网IP的服务器(假设操作系统为:
CentOS 7.4
) - 一个域名(解析到上述服务器的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 | server { |
找到443关键字, 把注释打开, 即把#
去掉
假设静态文件的根路径为 /path/dir
, 把它写在Nginx配置文件里
1 | listen 443 ssl http2 default_server; |
生成证书及密钥
1 | sudo letsencrypt certonly -a webroot --webroot-path=/path/dir -d www.example.com |
成功后, 控制后会打印出生成文件的路径, 把它们写在Nginx配置文件里
1 | ssl_certificate "/etc/letsencrypt/live/www.example.com/fullchain.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 | 0 0 * * 1 /usr/bin/letsencrypt renew >> /var/log/letsencrypt-renew.log |
每星期1的0点0分执行更新操作,0点5分执行Nginx 重启
检测站点是否已经使用HTTP/2协议
使用Chrome, 新建标签页, 地址栏输入:
1 | chrome://net-internals/#http2 |
刷新你的站点, 如果表格中出现了你的站点域名, 说明正在使用HTTP/2协议