掌握 HTTP 快取——從請求到響應過程的一切(下)

鬍子大哈發表於2017-03-06

作者:Ulrich Kautz

編譯:鬍子大哈

翻譯原文:huziketang.com/blog/posts/…

英文原文:Mastering HTTP Caching - from request to response and everything

轉載請註明出處,保留原文連結以及作者資訊


CDN類的網站曾經一度雄踞 Alexa 域名排行的前 100。以前一些小網站不需要使用 CDN 或者根本負擔不起其價格,不過這一現象近幾年發生了很大的變化,CDN 市場上出現了很多按次付費,非公司性的提供商,這使得 CDN 變成人人都能負擔的起的一種服務了。本文講述的就是如何使用這種簡單易用的快取服務。

上篇文章《掌握 HTTP 快取——從請求到響應過程的一切(上)》我們討論了關於利用 HTTP 頭來解決快取問題,這篇文章我們將介紹快取和 Cookie之間的關係。

Cookies

你已經知道了快取頭是如何起作用的,現在我們來看下在快取裡面 cookie 起了什麼作用。首先, Cookie 的設定也在 HTTP 響應頭中,名字是 Set-Cookie。設定一個 cookie 的目的是標識這個使用者,就是說你需要為每個使用者設定一個 cookie。

想象一下快取的場景,你是否會快取一個包含了 Set-Cookie的 HTTP 響應,在快取時間內,每個人都會得到相同的 cookie 和同樣的使用者 session?你肯定不想這樣。

另外,使用者 session 狀態的改變可能會影響到響應內容的變化。一個簡單的場景:電商購物車。你給使用者要麼提供一個空購物車,要麼是使用者自己選了很多物品的購物車。同樣的道理,你不希望這個也被快取,畢竟每個使用者都應該有自己的購物車。

一個解決方法是在執行時通過 JavaScript 設定 Cookie,比如 Google Analytics。GA 通過 JS 設定 cookie,但這個 cookie 既不影響渲染,也不設定 Set-Cookie 頭。GA 會在目標網站上新增類似於 "you are tracked via Google Analytics" 的圖示,但是隻要這些改變都是在執行時新增進去的,就都沒有問題

首先你需要知道你網站的 cookie 的工作原理。cookie 是不是隻在特定時間使用(如在使用者登入過程中使用)?原則上,cookie 是不是會被注入到所有響應?

正如上一節所說的,不論何時伺服器返回了一個帶有 Set-Cookie 的響應,你都希望能夠保證它不會被快取。那麼問題就轉化成為,當你返回一個帶有“使用者特性”內容的響應時(如購物車),CDN /代理伺服器,會作何操作?

  • 如果沒設定 Set-Cookie,是不是允許快取呢?
  • 如果設定了 Set-Cookie,是不是自動丟棄所有 Cache-Control 頭呢?

其實,如果從應用層面來講,你儘管可以去實現你所喜歡的 web 應用就可以了,至於 cookie 和 CDN 都是自動設定的。還是用 Apache 的 .htaccess 來作為例子來解釋:

# 1) 如果 cookie 沒設定,允許快取
Header set Cache-Control "public max-age=3600" "expr=-z resp('Set-Cookie')

# 2) 如果 cookie 被設定,不允許快取
Header always remove Cache-Control "expr=-n resp('Set-Cookie')

# 2a) 第二條的另一種形式,如果設定了 cookie,快取時間設定成0
Header set Cache-Control "no-cache max-age=0 must-revalidate" "expr=-n resp('Set-Cookie')複製程式碼
  • 規則1:如果沒設定 Set-Cookie,則給 Cache-Control 設定一個預設值;
  • 規則2:如果設定了 Set-Cookie,則忽略 Cache-Control
  • 規則2a:是規則2的另一種表示形式,設定最大快取時間是 0。

一些 CMS /框架還在使用一種暴力的方式種 cookie。而實際上,決定是否種 cookie 取決於不同的因素,比如會話時間因素。如果你有一個很高安全性的 web 應用,設定會話時間是 5 分鐘,那麼為每個響應設定一個新 cookie 都不過分。而假設你的應用連“使用者特性”都沒有,也就是說所有的東西對所有使用者都是公用的,那麼設定任何形式的 cookie 都是沒有道理的。

所以下面這個例子是否適合你自己,很大程度上依賴於你的應用到底是什麼型別的。我們來一起看一下,我先給一下這個例子的上下文關係:假設你有個新網站,你的所有文章都在 www.foobar.tld/news/item/ 這個路徑下面。現在你希望能夠保證,所有訪問 /news/item/<ID> 的路徑都不包含 Set-Cookie,因為你確定不需要 cookie。

# 通用 PHP 重定向做法,將"?path=$1"寫到重定向規則裡
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.*)$ index.php?path=$1 [NC,L,QSA]
RewriteRule ^$ index.php [NC,L,QSA]

# 利用 query 中的 path= 來判斷
<If "%{QUERY_STRING} =~ m#path=news/item/[^&]+#">
    Header always unset Set-Cookie
</If>複製程式碼

通過這樣的設定,你就可以保證所有訪問 /news/item/<ID> 的路徑都不包含 Set-Cookie。而到底是否應該設定 cookie,需要你根據你自己的應用特點來判斷。

設計出來的快取能力

有很多設計方案可以使你的 web 應用具有高快取性。鑑於本文僅僅是一篇文章而不是一本書,我不可能每個點都深入的來講,但是我可以著重提一下通用的方法。

我還用電商作為例子。假設電商網站首頁的 top 位置上展示了正在出售的物品,生成這些物品需要進行若干次的資料庫操作,代價比較大,因此希望把它們快取起來。但是,問題在於購物車,它是為那些登陸使用者準備的,所以希望得到的結果是: top 物品是一樣的,而針對登陸使用者展示購物車。

掌握 HTTP 快取——從請求到響應過程的一切(下)

那麼優化策略首先要為每個使用者提供一個和登陸狀態無關的“通用”頁。然後通過 JavaScript 為已經生成的網頁提供購物車。站在使用者的視角,最終展示形式是一樣的。那麼現在你有了兩個請求(整個網頁請求 + 購物車請求),而不是一個請求(整個網頁請求,包含購物車)。ok,現在你可以把代價很大的部分,即 top 物品分離出來,把它們快取起來了。

掌握 HTTP 快取——從請求到響應過程的一切(下)

這種方法或者其延伸方法,不適合已經開發好的專案。因為它可能會改變很多介面和檢視層(MVC 架構)的內容。最好你在一開始就設計好。

快取失效:busting 和 purging

使用 max-ages-maxage 你已經可以很好地控制一個指定的響應被快取多長時間。但是這不足以適用於所有的情況。這些設定都是在返回響應時預設的,而現實情況往往是並不知道一個響應應該設定多久期滿。回想一下剛才電商首頁的例子:假設它包含了展示在 top 位置的 10 個實體。你設定了 max-age=900給這個首頁以保證每15分鐘重新整理一次。現在,其中 1 個實體由於釋出了太久了要被撤銷,那麼你就需要把之前的快取響應刪掉,這時候其實還沒到 15 分鐘,那麼該怎麼辦?

不要擔心,這是一個常見的問題,有很多方法解決。首先我們先來解釋一下術語:

  • 快取 busting,是用來解決瀏覽器長期快取問題,它通過版本標識來告訴瀏覽器該檔案有一個新的版本。這時瀏覽器將不會從本地快取取內容,而從源伺服器請求新版本的檔案。關於快取 busting的詳細介紹在這裡:What is Cache Busting?。
  • 快取 purging,表示直接從快取中刪除內容(即響應),以使得快取可以立馬得到更新。

用於版本管理的快取 busting

這種方法經常使用在 CSS 檔案、JS 檔案上。通常一個確切的版本號、一串雜湊或者時間戳都可以用作標識,如下面的例子:

  • 數字版本號:style-v1.cssstyle.css?v=1
  • 雜湊串版本:style.css?d3b07384d113edec49eaa6238ad5ff00
  • 時間戳版本:styles.css?t=1486398121

這時候在釋出程式的時候,你只要注意檔案的版本就可以了。舉個例子,一個 HTML 網頁通過 <link rel="stylesheet" href=".."> 這種形式包含了一個 CSS 檔案。CSS 檔案將會被快取起來,這時如果你想讓你的新 CSS 檔案起作用,那麼用最新的版本號命名它就可以。如果不做任何變化的話,即便你更新了檔案,這個 HTML 還會使用快取中的舊 CSS 檔案。

快取 purging

不同 CDN 供應商清除快取的方式不一樣。很多供應商都是基於開源軟體 Varnish 來構建自己的 CDN 服務,所以一個通用的做法是在 HTPP 請求中使用 PURGE 結構,如:

PURGE /news/item/i-am-obsolete HTTP/1.1
Host: www.foobar.tld複製程式碼

使用這個請求通常需要許可權認證,或者是源確認(即 IP 白名單),不過不同供應商的要求也不一樣。

清除一個或幾個快取項比較容易,但是在某些場景下,卻不是這麼簡單。舉個例子,一個部落格的場景,部落格裡面都有關於作者的部分,現在你要改變關於作者的一些內容,那麼你需要手動清理所有包含了作者資訊的頁面。你確實可以一個一個手動清理,但是假設你有成千上萬個網頁被影響了,那問題就變得麻煩了。

下面介紹一個解決方案。

代理標籤

“代理標籤” 這個名字來源於 CDN 供應商 Fastly,不同供應商給它起的名字不一樣,比如還有叫它“快取標籤”的,Varnish 叫它 Hashtwo/Xkey,這裡我就不詳細介紹其他供應商的情況了。

不論它叫什麼,它們的目的都是一樣的:給響應打標籤。這樣你就可以輕鬆地從快取中刪除相關的標籤就可以,甚至都不用知道快取的到底是什麼東西。

還是拿<客戶端-代理-源端>來舉例子,源端返回一個含有代理標籤的響應:

HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 123
Surrogate-Key: top-10 company-acme category-foodstuff複製程式碼

這個例子中的標籤為:top-10company-acme,和 category-foodstuff。這裡給一個電商的實際場景來理解其含義:這個響應包含了電商首頁的前 10 個物品,這些物品由 ACME 公司提供,並且其目錄類別都設定為食品類。

設定了標籤以後,當物品發生了變化以後,你只需要刪除包含有 company-acmetop-10 的標籤就可以了。是不是很簡單?

同樣,具體如何清除快取的操作方法,不同 CDN 供應商是不一樣的。

寫在最後

上面討論的更多的是理論上的做法,還有很多文章專門介紹不同的 CDN 的使用。如果你想深入瞭解的話,下面的資料每篇可能都是你需要的。

歡迎大家關注我的前端大哈 - 知乎專欄,定期釋出高質量前端文章。


我最近正在寫一本《React.js 小書》,對 React.js 感興趣的童鞋,歡迎指點

相關文章