[译] HPACK:http2中沉默的杀手

4,029 阅读7分钟

如果你有过HTTP/2的相关经验,你可能会知道HTTP/2强大的性能依靠了以下特点,比如流复用、显式流依赖以及服务端推送

但是还有一个并不明显注意到但是却很重要的功能点,那就是HPACK头部压缩。

这篇文章给出了设计HPACK的一些理由,以及它背后带来的带宽和缩减延时上的收益。

一些背景

常规HTTPS连接事实上是多层模型的多个连接的叠加。你通常关心的最基本的连接就是TCP连接(传输层),在它的上面你会有一个TLS连接(传输层/应用层混合),然后最后是一个HTTP连接(应用层)

以前HTTP压缩是在TLS层使用gzip去处理的。header和body都会被压缩,因为TLS层不感知传输的数据类型。实际上两者都是使用DEFLATE算法进行压缩的。

后来提出了新的专门进行头部压缩的算法SPDY。算法包括使用了一个预处理字典,包括动态Huffman编码和字符串匹配,尽管是为了headers特殊设计,,但是它依旧使用的是DEFLATE算法。

实际上DEFLATE和SPDY都有被攻击的危险,因为攻击者可以从压缩的头部里提取cookie中的授权秘钥:因为DEFLATE使用后向字符串匹配和动态Huffman编码,攻击者可以控制部分请求头部,然后修改请求部分,通过观察压缩之后大小变化,来逐步恢复完整cookie。

由于这种风险,大多数的边缘网络都禁用了头部压缩。直到HTTP/2的出现改变了这种窘况

HPACK

HTTP/2支持一种新的专门进行头部压缩的算法,叫做HPACK。HPACK的开发设计考虑到了被攻击的危险,因此可以安全使用。

HPACK能够防御攻击者攻击,因为它没有像DEFLATE一样使用后向字符串匹配和动态Huffman,它使用了下面这三种方法进行压缩:

  • 静态字典表:由61个常规header域和一些预定义的values组成的预定义字典表。
  • 动态字典表:在连接中遇到的实际header域的列表。这个字典表有大小限制,新的key进来,旧的key可能会被移除。
  • Huffman编码:静态的Huffman编码可以对任何字符串进行编码:名称或者是值。这种编码特定地用在HTTP的request/response头中,ASCII码和小写字母的编码会更短。编码最短可能只有5bits长(一个字节8bits),因此最大的原长和压缩长比率为8:5(或者是37.5%的压缩率)

HPACK流

当HPACK需要把一个header编码为name:value的形式,它首先会看静态和动态字典表。如果全部的name:value都有的话,它会简单地去找字典表的对应条目。 这通常需要1byte空间大小,在大多数情况下2bytes也就足够了。整个header编码成一个byte,太赞了。

因为许多header头都是重复的,所以上面的策略有很高的成功率。 举个例子,像这种headers:authority:www.cloudflare.com 或者是一些大的cookie通常是这种情况。

当HPACK在字典表里不能匹配整个header的时候,它就会去尝试找有相同name的header。大多数常见的haeder name会在静态表里有,比如content-encoding, cookie, etag。剩下的一些可能会有重复的会在动态表里。比如Cloudflare会为每一个response分配cf-rayheader,而它的值是不同的,但是name是可以复用的。

如果找到了name,它就可以再次用1~2个bytes来表示,否则的话会使用其他的原生编码或者是Huffman编码(两者中取短的那个)。header中的value也是同样的原理。

我们发现单独使用Huffman编码会节省30%的header大小。

尽管HPACK不做字符串匹配,对于攻击者来说为了找到header的value,他必须猜测整个字典表条目的value,而不是像DEFLATE一样逐步匹配,但是HPACK还是有可能受到攻击。

Request Headers

HPACK为HTTP提供的收益里面,request会比response收益更大。request的headers能够更有效的进行压缩,因为在header里面有更多的重复项。比如下面是两个requests的header,用的是Chrome浏览器:

Request #1:

authority: blog. cloudflare. com
method:GET
path: /
scheme:https
accept:text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,/;q=0.8
accept-encoding:gzip, deflate, sdch, br
accept-language:en-US,en;q=0.8
cookie: 297 byte cookie
upgrade-insecure-requests:1
user-agent:Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2853.0 Safari/537.36

我把那些使用静态字典表的可以被压缩的headers标注了红色。有3个field: method:GET, path:/scheme:https,它们始终在静态字典表里,并且会被编码成1个byte.一些其他的field只有name会被编码为1byte:authority, accept, accept-encoding, accept-language, cookie , user-agent

所有其他的部分标记为绿色,会按照Huffman编码进行处理。

没有匹配上的headers,会插到动态字典表里为后面的request去使用

让我们看一下另外一种情况:

authority:blog.cloudflare.com
method:GET
path:/assets/images/cloudflare-sprite-small.png
scheme:https
accept:image/webp,image/,/*;q=0.8
accept-encoding:gzip, deflate, sdch, br
accept-language:en-US,en;q=0.8
cookie: 297 byte cookie
referer:blog.cloudflare.com/assets/css/…
user-agent:Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2853.0 Safari/537.36

这里我加上了蓝色的编码域,它们表明了那些header域匹配上了动态字典表。很明显那些域在不同的requests里面都有重复。这里面有两个域又一次出现在静态字典表里,也就是说每个域可以编码为1或者2个bytes串。一个是大概有300byte场的cookie头,另一个是大概130byte长的user-agent。把430bytes的长度压缩到仅仅4个bytes,压缩了99%。

总之多于所有的重复的request,只有两三个短字符串会编码为Huffman编码。

这个是Cloudflare在6个小时里的访问入口的流量headers的情况

我们可以看到请求来的头部压缩了76%。因为headers占访问流量大部分,因此所有的访问流量的空间节约十分可观

我们可以看到由于HPACK压缩,整个流量数据量大小减少了53%。

关于http不同版本的区别参考这篇文章https://mp.weixin.qq.com/s/GICbiyJpINrHZ41u_4zT-A

Response Headers

对于response头部,HPACK的收益相对少一些。

Response #1:

cache-control:public, max-age=30
cf-cache-status:HIT
cf-h2-pushed:</assets/css/screen.css?v=2237be22c2>,</assets/js/jquery.fitvids.js?v=2237be22c2>
cf-ray:2ded53145e0c1ffa-DFW
content-encoding:gzip
content-type:text/html; charset=utf-8
date:Wed, 07 Sep 2016 21:41:23 GMT
expires:Wed, 07 Sep 2016 21:41:53 GMT
link:<//cdn.bizible.com/scripts/bizible.js>; rel=preload; as=script,code.jquery.com/jquery-1.11…; rel=preload; as=script
server:cloudflare-nginx
status:200
vary:Accept-Encoding
x-ghost-cache-status:From Cache
x-powered-by:Express

第一个response的大部分header头会编码为霍夫曼编码,还有一些匹配上了静态字典表。

Response #2:

cache-control:public, max-age=31536000
cf-bgj:imgq:100
cf-cache-status:HIT
cf-ray:2ded53163e241ffa-DFW
content-type:image/png
date:Wed, 07 Sep 2016 21:41:23 GMT
expires:Thu, 07 Sep 2017 21:41:23 GMT
server:cloudflare-nginx
status:200
vary:Accept-Encoding
x-ghost-cache-status:From Cache
x-powered-by:Express

又一次看到,蓝色的部分匹配上了动态字典表,红色表明匹配上了静态字典表,绿色部分代表了Huffman编码的串。

第二个response可能匹配了全部12个headers中的7个。剩下的5个里面,有4个header的name可以匹配上,然后有6个字符串会去使用Huffman编码。

尽管有两个expires的header是几乎完全相同的,它们也仅使用Huffman编码,因为不能完全匹配上。

有越多的请求去处理,动态字典表就会越大,就会有越多的headers被匹配上,就会提高压缩率。

下面是返回的流量里的header情况。

平均的压缩率是69%。但是整个出口流量的影响并没有很大。

可能压缩情况不容易观察,但是在整个HTTP/2的出口流量里,我们还是有1.4%的节约量。虽然看起来不多,但是数据压缩量的在很多情况还是有增加的。这个数字也会因为网站处理大文件的时候受到影响。我们测量了一些网站的节约量在15%左右

原文链接:

blog.cloudflare.com/hpack-the-s…

关于http/1.0 http/1.x http/2的一些相关内容可以参考这篇文章https://mp.weixin.qq.com/s/GICbiyJpINrHZ41u_4zT-A