You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

134 lines
13 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 16 | HTTP/2是怎样提升性能的
你好,我是陶辉。
上一讲我们从多个角度优化HTTP/1的性能但获得的收益都较为有限而直接将其升级到兼容HTTP/1的HTTP/2协议性能会获得非常大的提升。
HTTP/2协议既降低了传输时延也提升了并发性已经被主流站点广泛使用。多数HTTP头部都可以被压缩90%以上的体积这节约了带宽也提升了用户体验像Google的高性能协议gRPC也是基于HTTP/2协议实现的。
目前常用的Web中间件都已支持HTTP/2协议然而如果你不清楚它的原理对于Nginx、Tomcat等中间件新增的流、推送、消息优先级等HTTP/2配置项你就不知是否需要调整。
同时许多新协议都会参考HTTP/2优秀的设计如果你不清楚HTTP/2的性能究竟高在哪里也就很难对当下其他应用层协议触类旁通。而且HTTP/2协议也并不是毫无缺点到2020年3月时它的替代协议[HTTP/3](https://zh.wikipedia.org/wiki/HTTP/3) 已经经历了[27个草案](https://tools.ietf.org/html/draft-ietf-quic-http-27)推出在即。HTTP/3的目标是优化传输层协议它会保留HTTP/2协议在应用层上的优秀设计。如果你不懂HTTP/2也就很难学会未来的HTTP/3协议。
所以这一讲我们就将介绍HTTP/2对HTTP/1.1协议都做了哪些改进从消息的编码、传输等角度说清楚性能提升点这样你就能理解支持HTTP/2的中间件为什么会提供那些参数以及如何权衡HTTP/2带来的收益与付出的升级成本。
## 静态表编码能节约多少带宽?
HTTP/1.1协议最为人诟病的是ASCII头部编码效率太低浪费了大量带宽。HTTP/2使用了静态表、动态表两种编码技术合称为HPACK极大地降低了HTTP头部的体积搞清楚编码流程你自然就会清楚服务器提供的http2\_max\_requests等配置参数的意义。
我们以一个具体的例子来观察编码流程。每一个HTTP/1.1请求都会有Host头部它指示了站点的域名比如
```
Host: test.taohui.tech\r\n
```
算上冒号空格以及结尾的\\r\\n它占用了24字节。**使用静态表及Huffman编码可以将它压缩为13字节也就是节约了46%的带宽!**这是如何做到的呢?
我用Chrome访问站点test.taohui.tech并用Wireshark工具抓包关于如何用Wireshark抓HTTP/2协议的报文如果你还不太清楚可参见[《Web协议详解与抓包实战》第51课](https://time.geekbang.org/course/detail/175-104932)下图高亮的头部就是第1个请求的Host头部其中每8个蓝色的二进制位是1个字节报文中用了13个字节表示Host头部。
![](https://static001.geekbang.org/resource/image/09/1f/097e7f4549eb761c96b61368c416981f.png)
HTTP/2能够用13个字节编码原先的24个字节是依赖下面这3个技术。
首先基于二进制编码,就不需要冒号、空格和\\r\\n作为分隔符转而用表示长度的1个字节来分隔即可。比如上图中的01000001就表示Host而10001011及随后的11个字节表示域名。
其次使用静态表来描述Host头部。什么是静态表呢HTTP/2将61个高频出现的头部比如描述浏览器的User-Agent、GET或POST方法、返回的200 SUCCESS响应等分别对应1个数字再构造出1张表并写入HTTP/2客户端与服务器的代码中。由于它不会变化所以也称为静态表。
![](https://static001.geekbang.org/resource/image/5c/98/5c180e1119c1c0eb66df03a9c10c5398.png)
这样收到01000001时根据[RFC7541](https://tools.ietf.org/html/rfc7541) 规范前2位为01时表示这是不包含Value的静态表头部
![](https://static001.geekbang.org/resource/image/cd/37/cdf16023ab2c2f4f67f0039b8da47837.png)
再根据索引000001查到authority头部Host头部在HTTP/2协议中被改名为authority。紧跟的字节表示域名其中首个比特位表示域名是否经过Huffman编码而后7位表示了域名的长度。在本例中10001011表示域名共有11个字节8+2+1=11且使用了Huffman编码。
最后使用静态Huffman编码可以将16个字节的test.taohui.tech压缩为11个字节这是怎么做到的呢根据信息论高频出现的信息用较短的编码表示后可以压缩体积。因此在统计互联网上传输的大量HTTP头部后HTTP/2依据统计频率将ASCII码重新编码为一张表参见[这里](https://tools.ietf.org/html/rfc7541#page-27)。test.taohui.tech域名用到了10个字符我把这10个字符的编码列在下表中。
![](https://static001.geekbang.org/resource/image/81/de/81d2301553c825a466b1f709924ba6de.jpg)
这样接收端在收到下面这串比特位最后3位填1补位通过查表请注意每个字符的颜色与比特位是一一对应的就可以快速解码为
![](https://static001.geekbang.org/resource/image/57/50/5707f3690f91fe54045f4d8154fe4e50.jpg)
由于8位的ASCII码最小压缩为5位所以静态Huffman的最大压缩比只有5/8。关于Huffman编码是如何构造的你可以参见[每日一课《HTTP/2 能带来哪些性能提升?》](https://time.geekbang.org/dailylesson/detail/100028441)。
## 动态表编码能节约多少带宽?
虽然静态表已经将24字节的Host头部压缩到13字节**但动态表可以将它压缩到仅1字节这就能节省96%的带宽!**那动态表是怎么做到的呢?
你可能注意到当下许多页面含有上百个对象而REST架构的无状态特性要求下载每个对象时都得携带完整的HTTP头部。如果HTTP/2能在一个连接上传输所有对象那么只要客户端与服务器按照同样的规则对首次出现的HTTP头部用一个数字标识随后再传输它时只传递数字即可这就可以实现几十倍的压缩率。所有被缓存的头部及其标识数字会构成一张表它与已经传输过的请求有关是动态变化的因此被称为动态表。
静态表有61项所以动态表的索引会从62起步。比如下图中的报文中访问test.taohui.tech的第1个请求有13个头部需要加入动态表。其中Host: test.taohui.tech被分配到的动态表索引是74索引号是倒着分配的
![](https://static001.geekbang.org/resource/image/69/e0/692a5fad16d6acc9746e57b69b4f07e0.png)
这样后续请求使用到Host头部时只需传输1个字节11001010即可。其中首位1表示它在动态表中而后7位1001010值为64+8+2=74指向服务器缓存的动态表第74项
![](https://static001.geekbang.org/resource/image/9f/31/9fe864459705513bc361cee5eafd3431.png)
静态表、Huffman编码、动态表共同完成了HTTP/2头部的编码其中前两者可以将体积压缩近一半而后者可以将反复传输的头部压缩95%以上的体积!
![](https://static001.geekbang.org/resource/image/c0/0c/c08db9cb2c55cb05293c273b8812020c.png)
那么是否要让一条连接传输尽量多的请求呢并不是这样。动态表会占用很多内存影响进程的并发能力所以服务器都会提供类似http2\_max\_requests这样的配置限制一个连接上能够传输的请求数量通过关闭HTTP/2连接来释放内存。**因此http2\_max\_requests并不是越大越好通常我们应当根据用户浏览页面时访问的对象数量来设定这个值。**
## 如何并发传输请求?
HTTP/1.1中的KeepAlive长连接虽然可以传输很多请求但它的吞吐量很低因为在发出请求等待响应的那段时间里这个长连接不能做任何事而HTTP/2通过Stream这一设计允许请求并发传输。因此HTTP/1.1时代Chrome通过6个连接访问页面的速度远远比不上HTTP/2单连接的速度具体测试结果你可以参考这个[页面](https://http2.akamai.com/demo)。
为了理解HTTP/2的并发是怎样实现的你需要了解Stream、Message、Frame这3个概念。HTTP请求和响应都被称为Message消息它由HTTP头部和包体构成承载这二者的叫做Frame帧它是HTTP/2中的最小实体。Frame的长度是受限的比如Nginx中默认限制为8Khttp2\_chunk\_size配置因此我们可以得出2个结论HTTP消息可以由多个Frame构成以及1个Frame可以由多个TCP报文构成TCP MSS通常小于1.5K)。
再来看Stream流它与HTTP/1.1中的TCP连接非常相似当Stream作为短连接时传输完一个请求和响应后就会关闭当它作为长连接存在时多个请求之间必须串行传输。在HTTP/2连接上理论上可以同时运行无数个Stream这就是HTTP/2的多路复用能力它通过Stream实现了请求的并发传输。
[![](https://static001.geekbang.org/resource/image/b0/c8/b01f470d5d03082159e62a896b9376c8.png "图片来源https://developers.google.com/web/fundamentals/performance/http2")](https://developers.google.com/web/fundamentals/performance/http2)
虽然RFC规范并没有限制并发Stream的数量但服务器通常都会作出限制比如Nginx就默认限制并发Stream为128个http2\_max\_concurrent\_streams配置以防止并发Stream消耗过多的内存影响了服务器处理其他连接的能力。
HTTP/2的并发性能比HTTP/1.1通过TCP连接实现并发要高。这是因为**当HTTP/2实现100个并发Stream时只经历1次TCP握手、1次TCP慢启动以及1次TLS握手但100个TCP连接会把上述3个过程都放大100倍**
HTTP/2还可以为每个Stream配置1到256的权重权重越高服务器就会为Stream分配更多的内存、流量这样按照资源渲染的优先级为并发Stream设置权重后就可以让用户获得更好的体验。而且Stream间还可以有依赖关系比如若资源A、B依赖资源C那么设置传输A、B的Stream依赖传输C的Stream即可如下图所示
[![](https://static001.geekbang.org/resource/image/9c/97/9c068895a9d2dc66810066096172a397.png "图片来源https://developers.google.com/web/fundamentals/performance/http2")](https://developers.google.com/web/fundamentals/performance/http2)
## 服务器如何主动推送资源?
HTTP/1.1不支持服务器主动推送消息因此当客户端需要获取通知时只能通过定时器不断地拉取消息。HTTP/2的消息推送结束了无效率的定时拉取节约了大量带宽和服务器资源。
![](https://static001.geekbang.org/resource/image/f0/16/f0dc7a3bfc5709adc434ddafe3649316.png)
HTTP/2的推送是这么实现的。首先所有客户端发起的请求必须使用单号Stream承载其次所有服务器进行的推送必须使用双号Stream承载最后服务器推送消息时会通过PUSH\_PROMISE帧传输HTTP头部并通过Promised Stream ID告知客户端接下来会在哪个双号Stream中发送包体。
![](https://static001.geekbang.org/resource/image/a1/62/a1685cc8e24868831f5f2dd961ad3462.png)
在SDK中调用相应的API即可推送消息而在Web资源服务器中可以通过配置文件做简单的资源推送。比如在Nginx中如果你希望客户端访问/a.js时服务器直接推送/b.js那么可以这么配置
```
location /a.js {
http2_push /b.js;
}
```
服务器同样也会控制并发推送的Stream数量如http2\_max\_concurrent\_pushes配置以减少动态表对内存的占用。
## 小结
这一讲我们介绍了HTTP/2的高性能是如何实现的。
静态表和Huffman编码可以将HTTP头部压缩近一半的体积但这只是连接上第1个请求的压缩比。后续请求头部通过动态表可以压缩90%以上这大大提升了编码效率。当然动态表也会导致内存占用过大影响服务器的总体并发能力因此服务器会限制HTTP/2连接的使用时长。
HTTP/2的另一个优势是实现了Stream并发这节约了TCP和TLS协议的握手时间并减少了TCP的慢启动阶段对流量的影响。同时Stream之间可以用Weight权重调节优先级还可以直接设置Stream间的依赖关系这样接收端就可以获得更优秀的体验。
HTTP/2支持消息推送从HTTP/1.1的拉模式到推模式信息传输效率有了巨大的提升。HTTP/2推消息时会使用PUSH\_PROMISE帧传输头部并用双号的Stream来传递包体了解这一点对定位复杂的网络问题很有帮助。
HTTP/2的最大问题来自于它下层的TCP协议。由于TCP是字符流协议在前1字符未到达时后接收到的字符只能存放在内核的缓冲区里即使它们是并发的Stream应用层的HTTP/2协议也无法收到失序的报文这就叫做队头阻塞问题。解决方案是放弃TCP协议转而使用UDP协议作为传输层协议这就是HTTP/3协议的由来。
![](https://static001.geekbang.org/resource/image/38/d1/3862dad08cecc75ca6702c593a3c9ad1.png)
## 思考题
最后留给你一道思考题。为什么HTTP/2要用静态Huffman查表法对字符串编码基于连接上的历史数据统计信息做动态Huffman编码不是更有效率吗欢迎你在留言区与我一起探讨。
感谢阅读,如果你觉得这节课对你有一些启发,也欢迎把它分享给你的朋友。