掌握 HTTP 緩存——從請求到響應(yīng)過程的一切(上)
CDN類的網(wǎng)站曾經(jīng)一度雄踞 Alexa 域名排行的前 100。以前一些小網(wǎng)站不需要使用 CDN 或者根本負擔不起其價格,不過這一現(xiàn)象近幾年發(fā)生了很大的變化,CDN 市場上出現(xiàn)了很多按次付費,非公司性的提供商,這使得 CDN 變成人人都能負擔的起的一種服務(wù)了。本文講述的就是如何使用這種簡單易用的緩存服務(wù)。
使用內(nèi)容分發(fā)網(wǎng)絡(luò)( CDN )你需要先正確地認識 HTTP 響應(yīng)頭:和 HTTP 響應(yīng)頭中的哪些標簽相關(guān)?它們是怎么起作用的?如何使用它們?文章中我會回答這些問題。
本文講的并不會像教科書那么精確,實際上在某些情況下,為了敘述的清晰、簡潔,我會按自己的理解簡化某些問題,文章中會通過一些實際的例子來介紹緩存理論。在這篇文章的基礎(chǔ)上,還會寫一些文章來介紹對于某些指定的 CMS 或框架如何使用 CDN 來作為緩存層。
為什么使用 CDN?
CDN 是一個全球分布式網(wǎng)絡(luò),它把網(wǎng)站內(nèi)容更快地傳遞給全球范圍內(nèi)的一個具體位置,而往往這個具體的位置離實際的內(nèi)容服務(wù)器距離很遠。舉個例子,你的網(wǎng)站主機在愛爾蘭,而你的用戶則在澳大利亞訪問。這時當你的用戶訪問你的網(wǎng)站的時候,延遲會很大,把你的(靜態(tài))數(shù)據(jù)用 CDN 放到澳大利亞則會很大程度上提高用戶訪問網(wǎng)站的體驗。
然而 CDN 的使用并不局限于此。其實 CDN 可以理解成一個普通緩存,如代理緩存(邊緣緩存)。即便你并不關(guān)心用戶的具體地理位置,你也應(yīng)該考慮使用 CDN 的代理緩存來提高你的用戶體驗。
為什么使用代理緩存?
簡而言之,代理緩存會緩存你網(wǎng)站一些頁面,通過緩存來傳輸“靜態(tài)”內(nèi)容非???。一個簡單的例子,假設(shè)你有一個帶有開始頁面的博客,這里面列出了所有近期的博客列表。完成這一過程,PHP 腳本要從數(shù)據(jù)庫中獲取到最近的文章實體,并且將它們轉(zhuǎn)換成 HTML 結(jié)果頁并返回給用戶。因此,對于一次請求(訪問)包含了:一次 PHP 執(zhí)行 + 一組數(shù)據(jù)庫查詢。對于 1000 次請求(訪問)包含了:1000 次 PHP 執(zhí)行 + 1000 組數(shù)據(jù)庫查詢。每一次 PHP 執(zhí)行都要進行 CPU、內(nèi)存和 I/O 操作,對于數(shù)據(jù)庫操作也是同樣。
請求的需求量和訪問用戶的多少呈線性正比關(guān)系。聽起來怎么樣?不怎么樣,因為這個線性關(guān)系是有***限度的:磁盤***只能提供一定程度的 I/O,CPU 和內(nèi)存也都不是***的。這樣下去到了某個點,也就是說某個資源到了瓶頸的時候,就出現(xiàn)問題了:你的網(wǎng)站會訪問的非常慢,甚至會出現(xiàn)所有人都不能訪問的情況。其實這時其他資源并沒有被全部打滿。誠然,這時你可以擴展你的硬件規(guī)模來突破這一瓶頸,但是這將使工程變得很復雜,成本也更高。實際上還有更簡單、更便宜的解決方法。
在中間加一層代理緩存,會減少資源對你的限制。拿前面的例子來講,使用代理緩存只有***次請求需要執(zhí)行 PHP 腳本、查詢數(shù)據(jù)庫和生成 HTML 結(jié)果頁。所有后面過來的請求都會從這個緩存中取內(nèi)容,讀取緩存幾乎和直接讀取內(nèi)存一樣快。這意味著,上面的線性規(guī)模瓶頸的問題解決了!100 個用戶或者1000 個用戶都沒關(guān)系,依然只有 1 次 PHP 執(zhí)行、1 次數(shù)據(jù)庫查詢和 1 次的結(jié)果頁生成。
CDN != CDN
CDN 的類型也各有不同。網(wǎng)站管理者可能會好奇數(shù)據(jù)是怎么存儲的?存放在哪?以及數(shù)據(jù)是如何分布在 CDN 上的?是如何分發(fā)的呢?本文不是寫給網(wǎng)站管理者的而是寫給開發(fā)者的,所以在這我只能告訴你有“經(jīng)典 CDN”和“對等 CDN”,后者是現(xiàn)在主流采用的方法。
對于開發(fā)者,相比于把數(shù)據(jù)拿到 CDN 以后做什么來說,會對如何把數(shù)據(jù)放到 CDN 中更感興趣。說起來,有 push CDN 和 pull CDN 兩種。顧名思義,“push CDN” 表示你要給 CDN 提供內(nèi)容;“pull CDN” 表示如何從 CDN 取內(nèi)容。
本文將主要介紹 pull CDN,因為在很多情況下 pull CDN 更加簡單易用,不需要費多大事就能集成到現(xiàn)有的網(wǎng)站中。
pull CDN 是如何起作用的?
我們來做個例子,假設(shè)你有一個可訪問的網(wǎng)站,URL 是 https://www.foobar.tld。在這樣的場景下,域名http://www.foobar.tld 會被放到 pull CDN 服務(wù)器中,而不是你的網(wǎng)站服務(wù)器中。CDN 作為你網(wǎng)站服務(wù)器的一個代理。
還有一個不被公開的域名指向?qū)嶋H的網(wǎng)站服務(wù)器。在這個例子中假設(shè)它是 direct.foobar.tld,實際網(wǎng)站服務(wù)器叫做源。
這個 CDN 將會接受所有的請求。如果它的緩存中有結(jié)果的話將會直接返回給用戶,否則會將這個請求托管給你實際的網(wǎng)站服務(wù)器,然后把返回的結(jié)果緩存起來為以后的請求做儲備,同時將結(jié)果返回給用戶。
最簡單的 pull CDN 運行的過程如下:
- 獲取一個頁面的請求,這個頁面:http://www.foobar.tld/some/page
- 把 some/page 當做緩存 key 檢查緩存中是否存在
- 在緩存中則直接從緩存中返回結(jié)果給用戶
- 不在緩存則請求 http://direct.foobar.tld/some/page,把返回的結(jié)果以 some/page 作為 key 寫入緩存,并返回結(jié)果給用戶
靜態(tài)內(nèi)容 VS 動態(tài)內(nèi)容
上面的這一過程對于完全靜態(tài)的內(nèi)容完全適用。靜態(tài)內(nèi)容指的是如果用戶訪問同一個 URL 地址,返回的所有數(shù)據(jù)都是一樣的。比如 CSS 文件就有這樣的特點,http://www.foobar.tld/public/css/main.css這個文件是一個普通文件,對于所有訪問網(wǎng)站的用戶都是一樣的,那么它就特別適合用緩存存起來。
和靜態(tài)文件相對的是動態(tài)文件。內(nèi)容在運行時才能確定,這種情況也是非常常見的。比如多語言問題,需要根據(jù)瀏覽器語言來返回內(nèi)容。還有一些和 “user session” 相關(guān)的內(nèi)容,比如當用戶登陸了以后,就要把“登陸”按鈕換成“退出”按鈕,你肯定不希望這個被緩存。這些高度活躍的內(nèi)容(如每小時或者更短時間更新的頁面)不能被緩存,或者說不能在緩存中停留時間過長。
這就是緩存有意思的地方,理解和實現(xiàn)它并不難。
緩存頭
絕大多數(shù)的 pull CDN 采用以“每頁”緩存形式解決動態(tài)內(nèi)容的問題。為了達到這樣的效果,一個簡單的方法是 HTPP 響應(yīng)緩存頭。
首先對于緩存頭你需要知道有“舊版本”和“新版本”兩種,就是說它并不是一開始就設(shè)計成當前所使用的這個版本的,也有一個逐漸演變的過程。新版本指的是 HTTP/1.1,而舊版本指的是 HTTP/1.0。它有特別多的可選選項,每個人對這個問題都很頭疼。我認為這是大家不愿意使用緩存頭的最重要的原因。
言歸正傳,我們只關(guān)注 ETag 和 Cache-Control 這兩個標簽就足以了。大多數(shù) CDN 還支持舊版本(Expires,Pragma 和 Age),不過這些只作為向后兼容來使用。
ETag 頭
我們從最簡單的開始 ETag:它是文檔版本的標識符。通常是內(nèi)容的 MD5 值,不過它也可以包含其他內(nèi)容,代表的是文檔的版本/日期,如: 1.0 或者 2017-02-27。這里注意一點是,它必須用雙引號括起來,如:ETag: "d3b07384d113edec49eaa6238ad5ff00"。
二次驗證
現(xiàn)在來考慮 ETag 的實際應(yīng)用:二次驗證。我們暫時不考慮前面代理+源的架構(gòu)模式,只考慮簡單的客戶端-服務(wù)器模式。如下圖:
假設(shè)客戶端請求了 http://www.foobar.tld/hello.txt,接著服務(wù)端返回了如下的響應(yīng)內(nèi)容:
- # REQUEST
- GET /hello.txt HTTP/1.1
- Host: www.foobar.tld
- # RESPONSE
- HTTP/1.1 200 OK
- Date: Sun, 05 Feb 2017 12:34:56 UTC
- Server: Apache
- Last-Modified: Sun, 05 Feb 2017 10:34:56 UTC
- ETag: "8a75d48aaf3e72648a4e3747b713d730"
- Content-Length: 8
- Content-Type: text/plain; charset=UTF-8
- the body
在響應(yīng)里面,有兩個有意思的頭標識:一個是 ETag,內(nèi)容的 MD5值,一個是 Last-Modified,這是 hello.txt 文件***一次被修改的時間。
這里就是二次驗證起作用的地方:當客戶端在很短的時間內(nèi)再次訪問上面的 URL,客戶端瀏覽器會使用 If-* 請求頭。如 If-None-Match 檢查 ETag 的內(nèi)容是否有改變。也就是說,如果 ETag 發(fā)生變化,客戶端接收到的一個完整的新響應(yīng);如果 ETag 沒變化,客戶端接收到的是一個表明內(nèi)容沒變化的標識。
- GET /hello.txt HTTP/1.1
- If-None-Match: "8a75d48aaf3e72648a4e3747b713d730"
- Host: www.foobar.tld
如果 ETag 沒有改變,那么服務(wù)端將會返回:
- HTTP/1.1 304 Not Modified
- Date: Sun, 05 Feb 2017 12:34:57 UTC
- Server: Apache
- Last-Modified: Sun, 05 Feb 2017 10:34:56 UTC
- ETag: "8a75d48aaf3e72648a4e3747b713d730"
- Content-Length: 8
- Content-Type: text/plain; charset=UTF-8
正如上面所展示的,這次服務(wù)器的響應(yīng)里面不是 200 ok,而是304 Not Modified,這就是說它略過包體部分,讓客戶端直接去自己的緩存里拿數(shù)據(jù)。在這個例子中,包體內(nèi)容是 the body,比較小,效果不明顯。可是想象一下如果是很大的內(nèi)容呢,或者是很復雜的動態(tài)生成內(nèi)容呢,價值就很大了。
作為一個開發(fā)者,你可能會想:“并沒有那么好用嘛,我還不得不掌握 IF- 類的頭標識,比以前更費事了”。
別急,這只是介紹了共享緩存,也就是代理緩存的由來,我們看原始的架構(gòu):<客戶端-代理-源端>,代理根據(jù)自己的緩存返回給客戶端 304 Not Modified,接下來的章節(jié)詳解介紹,介紹之前我要先講一下 Last-Modfied 頭。
在處理上面那個 hello.txt 靜態(tài)文件的例子時,客戶端還可以使用 If-Not-Modified-Since: Sun, 05 Feb 2017 10:34:56 UTC 來達到同樣的效果(返回 304 響應(yīng))。這對于靜態(tài)文件來說也很好用,因為響應(yīng)頭中的 Last-Modified 標識是根據(jù)服務(wù)器磁盤上的“更改時間戳”自動生成的。然而,“更改時間戳”對于動態(tài)文件通常沒什么用,因為動態(tài)生成文件頻繁更新,時間戳很難確定。我們都知道,你最想緩存起來的是內(nèi)容,生成內(nèi)容的代價是***的,所以 ETag 頭是更好的選擇。
Cache-Control頭
Cache-Control 頭相對來講難一些。兩個原因:***,Cache-Control 既可以用于請求頭,也可以用于響應(yīng)頭。本文中著重討論響應(yīng)頭,因為這是開發(fā)者所必須要掌握的。第二,它控制著兩個緩存:本地緩存(又稱私有緩存)和共享緩存。
本地緩存,是指在客戶端本地機器中的緩存。站在開發(fā)者的角度,它并不完全受你的控制,通常瀏覽器會自己決定是否把某些內(nèi)容放到緩存中,這意味著:不要依賴于本地緩存。用戶也可能在關(guān)閉瀏覽器的時候清理所有緩存,而你并不知道有這樣的操作。除非你監(jiān)測到了某個用戶的流量不斷上漲,導致緩存內(nèi)容迅速失效,這時候你才會意識到。
共享緩存,也就是本文所介紹的:處于客戶端和服務(wù)器之間的緩存。即 CDN。你對共享緩存擁有絕對的控制,應(yīng)該好好地利用它。
現(xiàn)在我們來用一些代碼作為示例深入學習一下。
- Cache-Control: public max-age=3600
- Cache-Control: private immutable
- Cache-Control: no-cache
- Cache-Control: public max-age=3600 s-maxage=7200
- Cache-Control: public max-age=3600 proxy-revalidate
乍一看這些代碼很令人困惑,但是不要擔心,它并沒有那么難,我來一點點介紹。首先你要知道 Cache-Control 有三種屬性:緩沖能力、過期時間和二次驗證。
首先是緩沖能力,它關(guān)注的是緩存到什么地方,和是否應(yīng)該被緩存。他的幾個重要的屬性是:
- private:表示它只應(yīng)該存在本地緩存;
- public:表示它既可以存在共享緩存,也可以被存在本地緩存;
- no-cache:表示不論是本地緩存還是共享緩存,在使用它以前必須用緩存里的值來重新驗證;
- no-store:表示不允許被緩存。
第二個是過期時間,很顯然它關(guān)注的是內(nèi)容可以被緩存多久。它的幾個重要的屬性是:
- max-age=
:設(shè)置緩存時間,設(shè)置單位為秒。本地緩存和共享緩存都可以; - s-maxage=
:覆蓋 max-age 屬性。只在共享緩存中起作用。
***一個是二次驗證,表示精細控制。它的幾個重要屬性是:
- immutable:表示文檔是不能更改的。
- must-revalidate:表示客戶端(瀏覽器)必須檢查代理服務(wù)器上是否存在,即使它已經(jīng)本地緩存了也要檢查。
- proxy-revalidata:表示共享緩存(CDN)必須要檢查源是否存在,即使已經(jīng)有緩存。
通過上面的具體解釋,現(xiàn)在再來描述上面 Cache-Control 的那段代碼所表達的意思就好理解多了:
- 本地緩存和 CDN 緩存均緩存 1 小時;
- 不能緩存在 CDN,只能緩存在本地。并且一旦被緩存了,則不能被更新;
- 不能緩存。如果一定要緩存的話,確保對其進行了二次驗證;
- 本地緩存 1 小時,CDN 上緩存 2 小時;
- 本地和 CDN 均緩存 1 小時。但是如果 CDN 收到請求,則盡管已經(jīng)緩存了 1 小時,還是要檢查源中文檔是否已經(jīng)被改變。
實例
理論會很單調(diào)乏味,現(xiàn)在用短的實例來演示如何自動注入 ETag 和 Cache-Control 頭。例子是一個 Apache 的 .htaccess 文件,但是我希望你能夠領(lǐng)會要領(lǐng),并且根據(jù)你自己的實際情況,應(yīng)用到你自己的 Web 應(yīng)用中。
- # 為所有圖片設(shè)置 ETag,以及緩存時間為 1 天
- <FilesMatch "\.(gif|flv|jpg|jpeg|png|gif|swf)$">
- FileETag -INode MTime Size
- Header set Cache-Control "max-age=86400 public"
- </FilesMatch>
- # 為所有的 CSS 文件、JS 文件設(shè)置 ETag,以及緩存時間為 2 小時,同時保證進行了二次驗證
- <FilesMatch "\.(js|css)$">
- FileETag -INode MTime Size
- Header set Cache-Control "max-age=7200 public must-revalidate"
- Header unset Last-Modified
- </FilesMatch>
上面例子,是一個對 URL:http://www.foobar.tld/baz.jpg 的響應(yīng)。包含了一個 ETag 頭,由更改時間和文件大小所構(gòu)成,還有 Cache-Control 頭來設(shè)定緩存 1 天的時間。見下面的請求和響應(yīng):
- # REQUEST
- GET /baz.jpg HTTP/1.1
- Host: www.foobar.tld
- # RESPONSE
- HTTP/1.1 200 OK
- Date: Tue, 07 Feb 2017 15:01:20 GMT
- Last-Modified: Tue, 07 Feb 2017 15:01:15 GMT
- ETag: "4-547f20501b9e9"
- Content-Length: 123
- Cache-Control: max-age=86400 public
- Content-Type: image/jpeg
對于 URL: http://www.foobar.tld/dist/css/styles.css 的響應(yīng)同樣也包含了 ETag 頭。由更改時間、文件大小和限定了 2 小時的 Cache-Control 構(gòu)成。Last-Modfied 頭也刪除掉以確保只有 ETag 用來做二次驗證。見下面的請求和響應(yīng):
- # REQUEST
- GET /styles.css HTTP/1.1
- Host: www.foobar.tld
- # RESPONSE
- HTTP/1.1 200 OK
- Date: Tue, 07 Feb 2017 15:00:00 GMT
- Server: Apache
- ETag: "20-547f1fbe02409"
- Content-Length: 32
- Cache-Control: max-age=7200 public must-revalidate
- Content-Type: text/css
小結(jié)
本文中我們介紹了:
- 為什么要使用 CDN,以及 CDN 是如何起作用的。
- 靜態(tài)內(nèi)容和動態(tài)內(nèi)容分別是什么情況。
- HTTP 頭是如何解決緩存問題的。
那么想象一下這樣的場景,假設(shè)你有個網(wǎng)站,需要保存用戶的登錄狀態(tài),而針對不同狀態(tài)的用戶需要不同的顯示。通常情況下,我們是用 Cookie 來解決用戶特性的問題。這時候問題就來了,如果 Cookie 也在 CDN 中緩存,那么將會導致所有用戶的 Cookie 都一樣,這不是我們想要看到的。那么怎么解決呢?我們會在《掌握 HTTP 緩存——從請求到響應(yīng)過程的一切(下)》中詳細介紹。
店家《掌握 HTTP 緩存——從請求到響應(yīng)過程的一切(上)》閱讀原文。
【本文是51CTO專欄作者“胡子大哈”的原創(chuàng)文章,轉(zhuǎn)載請聯(lián)系作者本人獲取授權(quán)】