HPACK:潛伏在HTTP/2身後的殺手

hdgara1發表於2020-01-09

HPACK:潛伏在HTTP/2身後的殺手

如果你體驗過HTTP/2,你很可能會對HTTP/2帶來的效能提升有所瞭解,這種提升應歸因於流複用、顯式流依賴和伺服器推送等特性。

但有一個特性卻沒那麼顯而易見,它就是HPACK頭壓縮。當前版本的Apache和nginx伺服器以及使用他們的edge網路和CDN網路,都不支援完整的HPACK實現。但我們已在nginx上實現了完整的HPACK,並向上溯及執行Huffman編碼的部分。

現在,我們將討論開發HPACK的緣由,以及它帶來的隱藏頻寬和延時方面的好處。

背景知識

一個規則的HTTPS連線實際上是由多層模型中的幾個連線的疊加。通常,人們關心的最基礎的連線是TCP連線(傳輸層),在其上是TLS連線(傳輸層和應用層的混合),最後是HTTP連線(應用層)。

在HTTP/2問世之前,HTTP壓縮是用gzip在TLS層執行的。由於TLS層無法知道傳輸的資料型別,頭部和主體被無差別地壓縮。也就是說,這兩部分均由DEFLATE演算法進行壓縮。

有一個叫做SPDY的新演算法是專門用來進行頭部壓縮的。儘管它專門為頭部設計且使用了預先設定的字典,但換湯不換藥,SPDY仍使用DEFLATE演算法,包括動態Huffman編碼和字串匹配。

這兩種演算法都易受一種叫做CRIME的攻擊。這種攻擊能夠從被壓縮的頭部提取加密的認證cookie:由於DEFLATE使用向後字串匹配和動態Huffman編碼,攻擊者透過修改請求頭部的一部分並觀察這個請求在壓縮中的長度變化,便能逐漸還原出整個cookie。

正因如此,大部分edge網路都不再使用頭部壓縮,直到HTTP/2的來臨。

HPACK

HTTP/2支援一種新的頭部壓縮演算法,叫作HPACK。HPACK的開發是著眼於防禦CRIME一類的攻擊,因此被認為是安全的。HPACK對CRIME的適應力強,因為它不像DEFLATE一樣使用部分向後字串匹配和動態Huffman編碼。它使用以下三種壓縮方法:

  • 靜態字典:一個具有61個常用頭部欄位的預定義字典,有些已預先定義了具體值。
  • 動態字典:一個在連線過程中遇到的當前頭部列表。這個字典大小是有限的,當有新的詞條加入後,就會有舊的詞條被逐出。
  • Huffman編碼:一個靜態Huffman編碼可以被用於為任何字串編碼:名稱或值。這種編碼是專門為HTTP響應/請求頭部計算出來的-ASCII數字和小寫字母的編碼更短。最短的編碼可能只有5位,因此最高的壓縮率可以達到8:5(或者說37.5%以下)。

HPACK

當HPACK需要以“名稱:值”的格式對一個頭進行編碼時,它會首先檢視靜態和動態字典。如果找到完整的“名稱:值”在字典中,它就會直接引用字典中的詞條。這通常會佔用一個位元組,最多兩個位元組就夠了!啥?一個完整的頭部居然用一個位元組就完成編碼?這太瘋狂了吧!

由於很多頭部是重複的,這個策略有很高的成功率。例如,像:authority:這樣的頭,或者有時是巨大的cookie頭,這些通常都很可疑。

當HPACK不能在一個字典中匹配整個頭時,它會試圖尋找一個有著相同名稱的頭。大部分流行的頭名稱都會在靜態表中,例如: content-encoding , cookie , etag。其餘部分很可能是重複的,因此也會在動態表中。例如,Cloudflare為每個響應分配了一個唯一的cf-ray頭,當這個欄位的值不同的時候,名稱仍能重複使用。

如果這個名稱被發現,大部分情況下它就又可以用一到兩個位元組表示,否則這個名稱就會被進行原編碼或Huffman編碼:採用兩個中較短的那一個。對於頭的值來說同理。

經測試,僅Huffman編碼就節省了幾乎30%的頭的長度。

儘管HPACK也進行字串匹配,但攻擊者要想找到一個頭的值,他們必須直接猜對整個值,而不像對於易受CRIME攻擊的DEFLATE匹配那樣可以逐漸靠近答案。

請求頭

HPACK為HTTP請求頭帶來的效益比其為響應頭帶來的要多。其中重複的內容越多,請求頭壓縮得越好。

以下是兩個對我們部落格的請求,使用Chrome瀏覽器:

請求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 10116) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2853.0 Safari/537.36

我用 紅色標出了這個可以用靜態字典壓縮的頭。有三個欄位 ::method:GET、:path:/、:scheme:https在靜態字典中總是出現,其中每一個都會用一個位元組進行編碼。這樣有些欄位被壓縮後就只有一個位元組表示他們的名稱 : :authority、accept、accept-encoding、accept-language、cookie、user-agent都出現在靜態字典中。

其他用綠色標記的部分,將使用Huffman演算法編碼。

未匹配到的頭將被插入到動態字典中,等待下一個請求使用。

我們來看看下一個請求:

請求2:

: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:same 297 byte cookie

referer:https://blog.cloudflare.com/assets/css/screen.css?v=2237be22c2

user-agent:Mozilla/5.0 (Macintosh; Intel Mac OS X 10116) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2853.0 Safari/537.36

此處, 藍色的編碼欄位用來表示動態字典中匹配的欄位。顯然不同請求的大部分欄位都會重複。這次又有兩個欄位出現在靜態字典中,還有五個重複,因此出現在動態字典中,這意味著他們都可以用一到兩個位元組進行編碼。其中一個是約300位元組的 cookie頭,還有約130位元組的 user-agent。這430個位元組僅用4個位元組編碼,壓縮率達99%!

總之,對於重複的請求,只有三個短字串會被進行Huffman編碼。

這是Cloudflare的edge網路在六小時中的入口頭的流量圖:

a

從圖中我們看到,對入口的頭的壓縮率平均為76%。當頭佔了入口流量的大部分時,整個入口流量就會有很大的節省。

b

我們看到,整個入口流量因HPACK壓縮而減少了53%。而相較HTTP/1,HTTP/2的入口流量只有HTTP/1的一半。

響應頭

對響應頭(出口流量)來說,效益稍低,但仍很驚人。

響應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,< rel=preload; as=script
server: cloudflare-nginx
status:200
vary: Accept-Encoding
x-ghost-cache-status:From Cache
x-powered-by:Express

第一個響應的大部分會被進行Huffman編碼,其中一些欄位名在靜態字典找到匹配。

響應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編碼字串。

在第二個響應中,12個頭中的7個可能完全匹配。剩餘5箇中的4個的名稱可以完全匹配,6個字串將被進行有效的靜態Huffman編碼。

儘管2個expires頭幾乎是相同的,但他們只能進行Huffman壓縮,因為他們不能完整匹配。

處理的請求越多,動態表就會變得越大,能被匹配的頭也就越多,這使得壓縮率增大。

以下Cloudflare的edge網路顯示的出口處的頭流量:

c

出口處的頭平均壓縮了69%。但全部出口流量的節省並不那麼明顯:

d

流量的節省十分細微,但即便如此,我們還是節省了整個出口HTTP/2流量的1.4%。儘管看起來不多,但仍比很多情況下的壓縮率要高。這個資料在一些處理打檔案的網站上被大大提高:我們測出有些網站流量節省超過15%。

測試你的HPACK

如果你安裝了nghttp2,你就可以用一個叫作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

全球可信CA機構


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/31483669/viewspace-2672591/,如需轉載,請註明出處,否則將追究法律責任。

相關文章