;

HPACK: HTTP/2 里的沉默杀手(新特性)

如果你已经使用过 HTTP/2,你很可能意识到使用它将会获得显而易见的性能提升,这是由于 HTTP/2 的新特性,例如流复用、明确的流依赖以及服务器推送

然而,HTTP/2 里有一个重要的特性是被严重忽略的,那就是 HPACK 头部压缩。目前,nginx、边际网络以及 CDN 的实现都在使用这个技术,但它们都没有完整的实现 HPACK。我们已经在 nginx 里完整实现了 Huffman 编码的 HPACK,并且它放置于 nginx 的 upstreamed 中使用。(译者注:nginx 主要可以分为三个部分:handler、filter 和 upstream,其中 upstream 主要用于网络数据的接收、处理和转发)

CC BY 2.0 图片来自于 Conor Lawless

这篇文章会对 HPACK 的发展原因,以及它所带来的隐藏带宽和潜在利益进行概述。

背景

正如你可能知道的,一个常规的 HTTPS 连接实际上是一个在多层模型里的对数个连接的包装。通常你所关注的最基础连接是 TCP 连接(传输层),在这之上的 TLS 连接(传输层和应用层的混合),最后则是 HTTP 连接(应用层)。

在过去,HTTP 压缩主要是使用在 TLS 层里的 gzip。因为 TLS 层处于较低层次,无法识别传输数据的类型,所以 headers 和 body 会被粗暴地混合在一起压缩。

接着,一种新的头部压缩专用算法出现了——SPDY。尽管 SPDY 里的预置字典是为头部特殊设计的,但它使用的仍然是 DEFLATE 算法,其中包括动态 Huffman 编码以及字符串匹配。

不幸的是,因为 DEFLATE 算法使用的是落后的字符串匹配以及动态 Huffman 编码,攻击者可以从压缩头部里提取密钥认证 cookies,从而很容易进行犯罪行为。攻击者还可以控制部分请求头部,并且通过修改部分请求来找回整个 cookie 以及观察到在压缩情况下请求改变的总大小。

大多数边际网络,包括 Cloudflare(译者注:美国一家云计算公司),由于犯罪原因都禁止了头部压缩,直到 HTTP/2 的来临。

HPACK

HTTP/2 支持一种叫 HPACK 的新的头部压缩专用算法。HPACK 被开发成具有类似心理犯罪的攻击性,因此被认为可以被安全的使用。

HPACK 对犯罪是有弹性的,因为它不使用像 DEFLATE 一样使用了部分落后的字符串匹配以及动态 Huffman 编码的方法。相反,它使用以下这三种压缩方法:

HPACK 流

如果 HPACK 需要为键:值格式的头部编码,首先它会去查看静态和动态的字典。如果键:值是完整存在的,就简单把从字典里引用该项。这通常会消耗一个字节,最多两个字节就足够了!整个头部就被编码为一个字节!这是多么疯狂的事情?

因为许多头部是重复的,所以这个策略有着非常高的成功率。举个例子,在这个案例中,头部里的:authority:www.cloudflare.com 或者某些时候的大体积的 cookie 通常都是“惯犯”(译者注:意为经常出现)。

当 HPACK 在字典里无法匹配到一个完整的头部,它会尝试去寻找一个有相同的头部。大多数常用的头部都会列在静态表里,例如:content-encoding, cookie, etag。剩下的可能是重复的头部,会存在于动态表里。例如,Coludflare 给每个响应都加了一个独特的 cf-ray 头部,同时这个字段的值永远不会相同,但是键可以被复用!

如果这个键被找到,在大多数情况下可以被压缩到一个或者两个字节,否则这个键会被原编码或者使用 Huffman 算法进行编码:最少需要两个字节。头部的值同样适用这种策略。

我们发现仅仅是使用 Huffman 编码就可以节省 30% 的头部大小。

在 DEFLATE 算法下是有可能通过渐进的方式获得某个头部的值,这很容易进行犯罪。尽管 HPACK 也需要做字符串匹配,但是对于攻击者来说,如果想找出某个头部的值,他们就必须要猜出所有头部的值。

请求头部

相对 HTTP 的响应头部,HPACK 带来的优势对于请求头部更具有意义。这是因为请求头部有更多重复,所以可以获得更好的压缩。举个例子,以下是在 Chrome 下我们的博客发出的两个请求:

请求 #1:

Request-1

我用红色标记的头部将会使用静态字典进行压缩。这三项(:method:GET, :path:/ and :scheme:https)总是出现在静态字典里,所以都会被编码为一个字节。接着另外一些头部的键出现在静态字典里(:authority, accept, accept-encoding, accept-language, cookieuser-agent ),它们同样被压缩为一个字节。

另外,绿色标记部分会使用 Huffman 编码。

没有匹配到的头部会被插入到动态字典给接下来的请求使用。

我们来看看接下来这个请求:

请求 #2:

Request-2

这里我用蓝色标识的编码项,表示这些项是从动态字典里匹配到的。显而易见,在头部里的大多数项都是重复。在这个案例里,有两项完全(键和值)出现在静态字典里,以及有五项因重复出现而记录在动态字典,同时这意味他们可以每个都可以被编码至一个或者两个字节。cookie 有 330 个字节,user-agent 有 130 个字节。一共430 个字节被编码为 4 个字节,99% 的压缩率!

对于所有重复请求,只有三个短字符串是会被 Huffman 编码的。

这是 Cloudflare 边际网络六小时周期下的入口头部流量呈现:

alt

入口头部流量在整个入口流量里可以提供大量的节省空间,我们可以看见对于入口头部平均会有 76% 的压缩:

alt

我们可以清楚的看见整个入口流量在 HPACK 的作用下节省了 53%!

今时今日,我们处理 HTTP/1 和 HTTP/2 的数量仍然是超过 HTTPS 的,但是 HTTP/2 的 入口流浪仅仅只是 HTTP/1 的一半。

响应头部

对于响应头部(出口流量),HPACK 带来的收益相对较少,但是仍然是非常可观的:

响应 #1:

Response-1

第一个响应大部分头部是被 Huffman 编码的,一些字段的键是从静态字典匹配到的。

响应 #2:

Response-2

同样的,蓝色部分为从动态字典匹配,红色为静态字典匹配,绿色则是由 Huffman 编码的字符串。

在第二个响应里,很可能会完全匹配到十二个头部里的七个。剩下的五个中的四个头部的键可以被全部匹配到,以及六个字符串(译者注:五个头部键值共有十个字符串,六个字符串包括一个键和五个值)将会使用 Huffman 编码进行有效编码。

尽管两个 expires 头部的键是完全相同的,但他们因为不能完全匹配上(译者注:值未匹配上),所以它们只可以用 Huffman 编码进行压缩。

随着请求越多,动态表会不断增长,那么更多头部会被匹配到,压缩比率就会更高。

以下是 Cloudflare 边际网络里出口头部流量的呈现:

alt

平均情况下,出口头部压缩率为 69%,然而在整个出口流量的节省就不是如此的明显了:

alt

我们很难用肉眼去确定节省了多少,但是整个出口 HTTP/2 流量确实节省了 1.4% 左右。尽管看上去不是很多,但在大多数案例里它节省的流量仍然比使用增加数据压缩比的方法更多。

测试 HPACK

如果你已经安装了 nghttps,你可以用一个叫 h2load 的工具测试你的网站里 HPACK 的压缩效率。

例如:

h2load https://blog.cloudflare.com | tail -6 |head -1  
traffic: 18.27KB (18708) total, 538B (538) headers (space savings 27.98%), 17.65KB (18076) data

我们可以看见头部节省了 27.98% 的空间。但这只是单独一个请求,大多数收益是由于 Huffman 编码而生成的。如果网站需要利用 HPACK 所有压缩能力去测试,我们需要使用两个请求,例如:

h2load https://blog.cloudflare.com -n 2 | tail -6 |head -1  
traffic: 36.01KB (36873) total, 582B (582) headers (space savings 61.15%), 35.30KB (36152) data

如果两个相同的请求可以节省 50% 甚至更多,那么这就很可能使用了 HPACK 的全部能力。

请留意接下来这个请求:

h2load https://blog.cloudflare.com -n 4 | tail -6 |head -1  
traffic: 71.46KB (73170) total, 637B (637) headers (space savings 78.68%), 70.61KB (72304) data

结论

通过为 HTTP 响应实现 HPACK 压缩,我们可以看见出口带宽的显著下降。Cloudflare 的 HTTP/2 客户已经在使用 HPACK,他们已经在享受着 HPACK 带来的更快、更小的响应。