HTTP 緩存協議實戰

作者:vivo 互聯網客戶端團隊 - Chen Long

一、什麼是緩存

緩存,又稱作 Cache,我們把臨時存儲數據的地方叫做緩存池,緩存池裏面放的數據就叫做緩存。當用戶需要使用這些數據,首先在緩存中尋找,如果找到了則直接使用。如果找不到,則再去其他數據源中查找。

二、爲什麼要使用緩存技術

緩存的本質就是用空間換時間,以臨時存儲的數據暫時代替數據源中讀取最新的數據,這種方式帶來的好處在不同的場景下是不一樣的。

舉個例子:

當我們需要喝水時,我們會拿出一個水杯,去水龍頭接一杯水來喝。大家可以思考一下,爲什麼用杯子來喝水,而不是直接用嘴巴在水龍頭接水喝。

用杯子喝水確實存在一些既有的問題,比如杯子裏面的水容易變涼,而水龍頭流出的水確是恆溫的。我們可以想象一下,公司裏的同事們排隊在水龍頭下面喝水的場面,確實有點滑稽,我們寧願接受杯子裏的水會變涼這個既有問題。

用杯子喝水有以下幾個優勢:

我們把杯子看成一個緩存池,杯中的水看成緩存,我們接受了杯中水會變涼的問題,相當於犧牲了數據的實時性。把這些優勢換一個方式來描述,於是使用緩存的優勢變成了下面幾個:

三、HTTP 緩存的作用

網絡的其中一個特點就是不穩定性,很多用戶受到網速慢的困擾。

服務器在大量用戶訪問的場景下實時計算數據也很容易產生瓶頸,導致服務變慢。從緩存技術具備的優勢來看,很適合解決網絡服務不穩定的問題。

四、HTTP 緩存協議

協議是溝通過程中雙方都遵守並且使用的一種規則。舉個栗子,客戶端和服務器兩位大兄弟在新款機型問題上進行了幾次溝通?

客戶端:大哥,新款 nex 發佈沒?

服務器:老弟,還沒發,你記住,別老來問我!

一週後......

客戶端:大哥,我又來了,最新情況如何?

服務器:跟上次一樣。

一個月後.....

客戶端:大哥,這都一個月了,怎麼樣了啊?!

服務器:已經開售啦!

在這個例子裏面,客戶端與服務端溝通過程中就遵循某種規則,我們來看一下。

服務端說的這些話,客戶端都能看懂並且明白這些話中所蘊含的意義,這就是客戶端與服務端之間達成的某種通訊協議。

4.1 HTTP 消息頭

在介紹 HTTP 緩存協議之前,我們先來了解一下 HTTP 消息頭的基礎知識。我們對 HTTP/HTTPS 的數據請求都比較熟悉,在 HTTP 的數據請求中有一種信息叫做 “頭部信息”。

頭部信息是在客戶端請求或者服務端響應是傳遞給對方的一種信息。我們來看一下 HTTP 協議的組成部分。

HTTP 請求的組成

狀態行、請求頭、消息主體三部分組成。

HTTP 響應的組成

狀態行、響應頭、響應正文。

其中,請求頭和響應頭就是我們這裏說的 “頭部信息” 或者又叫 “消息頭”。那麼頭部信息有什麼作用呢?

4.2 請求頭

bwXAAk

如圖所示:

圖片

4.3 響應頭

ek5sbu

如圖所示:

圖片

我們今天要講的緩存協議——Cache-Control, 也是放在消息頭中進行控制的。

4.4 緩存協議

在第一節中,我們介紹了使用緩存技術的三個優勢,在網絡數據交換的過程中,使用緩存技術同樣有這三個優勢。

1)降低系統壓力

使用 HTTP 緩存技術,可以有效的降低服務端的壓力,服務端不需要實時計算數據並返回數據。

2)節省資源消耗

使用 HTTP 緩存技術,可以有效的避免大量的重複數據傳輸,降低流量消耗。

3)優化用戶體驗

使用 HTTP 緩存技術,本地緩存可以以較快的速度加載,減少用戶等待時間。

在講 HTTP 協議如何實現緩存之前,我們先來講一下緩存類型。HTTP 緩存一般被分爲兩類,私有緩存和共享緩存。

4.4.1 私有緩存

緩存被存儲在設備本地或者獨立的賬戶體系下,僅供當前用戶使用,他可以用來降低服務器壓力,提高用戶體驗,甚至實現離線瀏覽。

圖片

4.4.2 共享緩存

共享緩存是在代理服務器或者其他中間服務器中進行二次緩存的數據,一般這裏我們常見的是 CDN,這種緩存可以被多個用戶訪問,用來減少流量和延遲。

圖片

對於一次網絡數據交互,本地緩存和共享緩存可以同時存在,HTTP 協議中規定了如何進行控制這些緩存的使用和更新。在 HTTP 中,控制緩存有兩種字段:一個是 Pragma;另一個是 cache-control。

Pragma 是一個在 HTTP/1.0 中定義的字段,從 mozilla 官網文檔上查詢,Pragma 支持現有的幾乎所有瀏覽器。

但是作爲舊時代的產物,cache-control 正在逐步的替代它。cache-control 是從 HTTP/1.1 開始引入的協議。有些前端開發者會選擇在 cache-control 的基礎上增加 Pragma 來向下兼容,事實上 android 的 webview 即支持 Pragma 又支持 cache-control。

而當 Pragma 和 cache-control 同時出現時,Pragma 的優先級大於 cache-control 當然,這不是今天的重點,有興趣的同學可以自行查閱相關資料。

下面我們就具體的來講一下 cache-control 緩存協議的具體定義。HTTP 協議規定,服務端通過響應頭中的 cache-control 將緩存方式通知給客戶端,同時客戶端也可以通過請求頭中的 cache-control 來將自己的緩存需求通知給服務器。

4.4.3 響應頭中的 cache-control

響應頭中的 cache-control 一般有如下取值:

4.4.4 請求頭中的 cache-control

請求頭中的 cache-control 一般有如下取值:

mozilla 開發者網站將這些取值分爲如下幾個類別進行描述。

4.4.5 可緩存性控制

public

表明響應可以被任何對象(包括:發送請求的客戶端,代理服務器,等等)緩存,即使是通常不可緩存的內容。(例如:1. 該響應沒有 max-age 指令或 Expires 消息頭;2. 該響應對應的請求方法是 POST 。)

private

表明響應只能被單個用戶緩存,不能作爲共享緩存(即代理服務器不能緩存它)。私有緩存可以緩存響應內容,比如:對應用戶的本地瀏覽器。

no-cache

在發佈緩存副本之前,強制要求緩存把請求提交給原始服務器進行驗證 (協商緩存驗證)。

no-store

緩存不應存儲有關客戶端請求或服務器響應的任何內容,即不使用任何緩存。

4.4.6 緩存有效性控制

max-age=

設置緩存存儲的最大週期,超過這個時間緩存被認爲過期 (單位秒)。與 Expires 相反,時間是相對於請求的時間。

s-maxage=

覆蓋 max-age 或者 Expires 頭,但是僅適用於共享緩存 (比如各個代理),私有緩存會忽略它。

max-stale[=]

表明客戶端願意接收一個已經過期的資源。可以設置一個可選的秒數,表示響應不能已經過時超過該給定的時間。

min-fresh=

表示客戶端希望獲取一個能在指定的秒數內保持其最新狀態的響應。

stale-while-revalidate=

 表明客戶端願意接受陳舊的響應,同時在後臺異步檢查新的響應。秒值指示客戶願意接受陳舊響應的時間長度。

**stale-if-error= **

表示如果新的檢查失敗,則客戶願意接受陳舊的響應。秒數值表示客戶在初始到期後願意接受陳舊響應的時間。

4.4.7 重新驗證和重新加載

must-revalidate

一旦資源過期(比如已經超過 max-age),在成功向原始服務器驗證之前,緩存不能用該資源響應後續請求。

proxy-revalidate

與 must-revalidate 作用相同,但它僅適用於共享緩存(例如代理),並被私有緩存忽略。

4.4.8 其他控制

no-transform

不得對資源進行轉換或轉變。Content-Encoding、Content-Range、Content-Type 等 HTTP 頭不能由代理修改。例如,非透明代理或者如 Google's Light Mode 可能對圖像格式進行轉換,以便節省緩存空間或者減少緩慢鏈路上的流量。no-transform 指令不允許這樣做。

only-if-cached

表明客戶端只接受已緩存的響應,並且不要向原始服務器檢查是否有更新的拷貝。

從這些描述以及分類中可以看出來,可緩存性控制 + 緩存有效性控制 + 其他控制 ,這幾個控制維度是不衝突的,可以共同實現緩存的實現方式限定。

事實上 cache-control 確實是可以同時接受多個取值的,多個不同的指令可以搭配使用來對緩存進行控制。如果使用了相矛盾的多個指令取值,那麼指令就會按照優先級進行緩存控制。

比如 no-store 和 max-age 這兩種在行爲上矛盾的指令取值放在一起下發,那麼終端就只會按照 no-store 來進行緩存。

4.4.9 協議工作實戰分析

專業的運維人員,一定很瞭解這些描述所表達的意思。然而作爲客戶端或者前端的我們,光是看這些專業術語,可能很難理解不同配置取值下實際的緩存效果。

因此爲了搞明白取值對實際緩存效果的影響。我使用兩臺電腦,分別搭建了一個靜態資源服務器(源服務器),一個代理服務器,通過模擬線上服務器的場景,來對常見的幾種緩存控制模式進行驗證。nginx 的安裝比較簡單,此處不在贅述。

靜態資源服務器(源服務器)

**windows+nginx,**配置如下:

圖片

代理服務器

windows+nginx,配置如下:

圖片

服務器搭建完成後,我們逐個改變 cache-control 的取值,來模擬幾種常見的緩存控制模式,來幫助大家理解這些取值,加深印象。在日常的使用過程中,cache-control 更多的是被放在響應頭中來控制瀏覽的緩存行爲,因此我們先來驗證一下 cache-control 放在響應頭中的情況。

場景:靜態資源服務器(源服務器)的響應頭中沒有添加任何 cache-control 標識。沒有添加標識,其實對應的就是 public 標識。

public 通常可以看成默認值,如果我們不在響應中添加任何有關 Cache-control 的 header,那麼這次響應默認的處理邏輯就類似 Cache-control: public。

(這裏使用 "通常","類似" 這種不確定的字眼,需要解釋一下,如果服務器返回了 302 或者 307 這種重定向響應時,添加 Cache-control: public 會讓瀏覽器把重定向響應也緩存起來,但是如果不添加 Cache-control,則不會緩存,也存在不同網絡框架或者瀏覽器做不同處理的可能性)。

public 的意思是瀏覽器或者代理服務器都可以對靜態資源服務器(源服務器)返回的資源進行緩存。使用瀏覽器直接訪問靜態資源服務器(不經過代理服務器)。

第一次訪問

圖片

第一次訪問,服務器返回了 200 狀態並將靜態 html 傳回給客戶端。同時,服務器還帶上了 ETag 和 Last-Modified 兩個字段,我們先繼續往下看。此時客戶端做了幾件事情:

點擊瀏覽器刷新按鈕

圖片

點擊瀏覽器的刷新按鈕後,客戶端瀏覽器帶上了第一次請求時返回的 ETag 和 Last-Modified 再次請求了服務器。服務端通過這兩個參數認爲客戶端已經緩存了資源,服務器不需要再次返回資源了。於是服務器返回了 304。

那如果有代理服務器摻和進來又是一個什麼樣的場景呢?還記得我們之前配置的那臺代理服務器嗎,我們將代理服務的代理緩存時間設定在了 10 秒。

第一次訪問

圖片

點擊瀏覽器刷新按鈕

圖片

點擊瀏覽器的刷新按鈕時,客戶端瀏覽器帶上了第一次請求時返回的 ETag 和 Last-Modified 再次請求了服務器。服務端通過這兩個參數認爲客戶端已經緩存了資源,服務器不需要再次返回資源了,於是服務器返回了 304。

注意這次刷新時,ngiux-cache-status 的狀態時 HIT 標識這次命中了代理服務器的緩存,這次的客戶端緩存有效性判斷是由代理服務器完成的。

10 秒後的第三次刷新

圖片

前面說了 代理服務器的緩存有效期,我們配置成了 10 秒。第三次刷新時服務器依然返回了 304,資源不需要更新。

但是這次刷新時,ngiux-cache-status 的狀態是 EXPIRED,這標識代理服務器的緩存已經失效了,不能用來做有效性判斷,  這個時候,代理服務器就會將這次的請求透傳給靜態資源服務器(源服務器),通過靜態資源服務器(源服務器)完成的緩存的有效性判斷。

在這個過程中,代理服務器又會對自己的緩存進行更新,於是有了下面第四次。

第四次刷新

圖片

邏輯圖如下;

圖片

通過這四次請求,我們能夠清晰的瞭解了整個的邏輯,代理服務器在某些情況下直接代替了靜態資源服務器(源服務器)。因爲 public 指令告訴代理服務器,可以緩存數據,於是代理服務器按照配置將數據緩存了 10 秒,超過 10 秒後就會重新將請求轉發給靜態資源服務器(源服務器),同時重新進行緩存。

這時候有的同學會問了,代理服務器有緩存的時間限制,在沒有達到時間限制之前是不會重新請求靜態資源服務器(源服務器)的,這時候就降低了靜態資源服務器(源服務器)的壓力。那爲什麼在上面的例子裏面,瀏覽器一直在請求代理服務器呢?

這裏要跟大家說明一下,在上述的案例中,我們其實一直在點擊瀏覽器的刷新按鈕,刷新按鈕的意思就是讓客戶端瀏覽器重新請求服務器來驗證緩存內容的有效性。

大家仔細看下所有截圖中的 Request-Header 是不是都有一個 **max-age = 0 ,**這個指令就是瀏覽器在刷新請求時,告訴服務器——我本地的緩存可能到期了,你要幫我驗證一下。如果你嘗試將網址複製到瀏覽器的新窗口然後點擊回車打開 url,而不是點擊刷新按鈕,這個時候就會像下圖這樣。

圖片

瀏覽器不會訪問網絡,注意看 Status Code 那裏括號裏面的備註,Status Code:  200 OK (from disk cache)   表示這次的響應數據,其實是從磁盤緩存裏面拿的。

在 android 系統的 WebView 中,正常情況下是沒有提供刷新按鈕的(除非開發者自己寫一個)那麼這種場景下 webview 就不會請求網絡,每次都從磁盤緩存中拿數據,對應在抓包時,就看不到網絡請求。

瞭解了整個邏輯之後,我們再來看 mozilla 提供的描述,再結合上述的邏輯,是不是就已經有了初步的概念了。

4.4.10 在響應頭中的可緩存性控制

public

表明響應可以被任何對象(包括:發送請求的客戶端,代理服務器,等等)緩存,即使是通常不可緩存的內容。(例如:1. 該響應沒有 max-age 指令或 Expires 消息頭;2. 該響應對應的請求方法是 POST 。)這個其實就是我們剛剛驗證的場景。

private

表明響應只能被單個用戶緩存,不能作爲共享緩存(即代理服務器不能緩存它)。私有緩存可以緩存響應內容,比如:對應用戶的本地瀏覽器。

如果使用 private,代表着這個資源,可以被私有用戶緩存,緩存不會被共享,實際測試,當標註爲 private 時,瀏覽器可以進行緩存,但是代理服務器不會緩存這個資源。有些材料裏面提到,private 是可以指定緩存的 user_id 的,這種屬於比較複雜的配置了,有興趣的同學可以研究下。

no-cache

強制要求緩存把請求提交給原始服務器進行驗證 (協商緩存驗證)。

這是一個服務端經常使用的指令,也是一個比較容易與 no-store 混淆的指令,許多前端和客戶端的同學都認爲當服務端的響應中標註了 no-cache,那麼客戶端就不會進行緩存,每次都會請求服務器獲取新的內容。其實只說對了一半。

在這種場景下,瀏覽器確實會每次都請求服務器,但是並不意味着瀏覽器不緩存資源,mozilla 的官方解釋是 “把請求提交給原始服務器進行驗證” 如果緩存沒有問題,那麼服務器就會返回 304,讓瀏覽器繼續使用自己本地的緩存”。

no-store

不應存儲有關客戶端請求或服務器響應的任何內容,即不使用任何緩存。

這個指令就是完全不使用本地緩存,在這種模式下,客戶端不會記錄任何緩存,包括 Etag 等,每次都會重新發起請求,並且得到 200 響應和對應的數據。如果前端希望自己的網頁完全不被緩存,那麼可以試下這個指令。

以上指令解決了客戶端以及代理服務器能不能緩存的問題,有的同學就會有疑問了,如果讓客戶端進行本地緩存,那麼正常情況下如果不去手動刷新,客戶端是不會請求服務器的,前端發新版後,客戶端如何選擇合適的時機請求服務器呢?

這個時候就要用到緩存有效性控制。瀏覽器和服務器之間的緩存校驗是相互的 ,也就是說服務器可以告知瀏覽器 這個緩存你能用多久,能保留多久。

先來看下服務器是如何通知客戶端緩存可以用多久的。緩存有效性控制指令一般會與可緩存性指令共同下發給客戶端。

圖片

我們在 server 的 header 中增加 max-age 屬性,同時,爲了避免代理服務器提前將代理緩存置爲無效,我們將代理服務器的緩存有效時間設置到 100 秒,超過靜態資源服務器(源服務器)設置的 max-age = 20。

第一次請求

圖片

我們使用刷新功能刷新瀏覽器,在 20 秒內我們持續得到 HIT 的狀態,說明命中了代理服務器的緩存。20 秒之後 代理服務器返回 EXPIRED 說明代理服務器響應了靜態資源服務器(源服務器)的指示,讓本地代理失效了,而代理服務器設置的 100 秒本地緩存時間,這個時候被忽略了。

這次我們依然使用了瀏覽器的刷新功能來強制瀏覽器去服務器校驗緩存的有效性,也就是說其實在上面的測試中,瀏覽器每次都是自己忽略 max-age,去訪問服務器的。

結論:新增的 max-age,控制了代理服務器保留的緩存時長,本地代理會忽略配置中的緩存時長直接使用靜態資源服務器(源服務器)下發的 max-age 作爲緩存時長。

下面爲了測試瀏覽器如何使用本地緩存, 我們用 android 上的 webview 來進行實驗,因爲 webview 是沒有刷新按鈕的(除非開發者自己造一個)。

第一次打開;

圖片

打開後在後面我們每隔兩秒再打開一次;

圖片

可以看到 20 秒內,webview 都沒有重複請求服務器下載站點的 index.html,在上面的截圖中,每顯示一個 favicon.ico 就是我打開一次站點鏈接,因爲我沒有在源服務器中配置 favicon.ico,所以每次打開,webview 都在找服務器下載這個資源。

超過 20 秒後,webview 發起了請求,此次服務器返回了 304,要求客戶端繼續使用緩存進行展示,這次 max-age 指令體現出來了。而 webview 在這次校驗之後,會將本地的緩存再延長 20 秒的有效期,在下一個 20 秒後,webview 纔會再次發起新的緩存驗證請求。

總結:客戶端 webview 會在 public 指令下緩存 index.html,然後在 max-age 要求限制的時間內,都不會發起任何網絡請求來校驗資源。

在官網商城的一個案例中,網站上線後,運維沒有配置任何 cache-control 協議,在默認 public 的模式下,客戶端 webview 一直使用本地緩存,開發人員發現前端發版後,客戶端無法及時更新頁面。於是在每一個打開的網址後面手動拼接了一個時間戳,來強制改變網址,讓瀏覽器的緩存失效,其實只要使用 nocache 或者 max-age 作爲 cache-control 協議就可以解決該問題。

除了 max-age,cache-control 在可緩存性控制指令的基礎上還可以增加如下幾個控制;

no-transform

源服務端告訴客戶端,客戶端在緩存數據的時候不可以對文件進行改變,比如壓縮,格式修改等...

must-revalidate

源服務端告知客戶端,一旦資源過期,在向靜態資源服務器(源服務器)發起驗證之前,該資源不得使用。

proxy-revalidate

與 must-revalidate 作用相同,僅僅適用於共享緩存(例如代理)。

max-age=

靜態資源服務器(源服務器)告知客戶端,X 秒內,客戶端都不需要對緩存進行校驗,可以直接使用。

s-maxage=

靜態資源服務器(源服務器)告知代理服務器,代理服務器可以在 X 秒內使用該緩存,並且不需要進行校驗,直接可以使用,但是客戶端會忽略這個指令。

問題又來了,在驗證的過程中,服務器是怎麼判斷瀏覽器的緩存是否有效的呢?

客戶端瀏覽器在有機會訪問服務器的時候就會告訴服務器,我的本地緩存是什麼時候的數據(Last-Modified),數據內容是什麼 (ETag),這樣服務端就能根據這兩個值來判斷客戶端的緩存是否是有效的。

我們來模擬一次前端的發版操作,將 index.html 的內容進行修改;然後使用 android webview 進行請求。

圖片

這一次服務器毫不吝嗇的返回了 200 和數據。大家仔細觀察請求頭和響應頭;

現在這兩個值跟服務端的都對應不上了,所以服務器返回了最新的數據和 200 狀態碼,並且帶上了最新的 Etag,Last-Modified。而客戶端下一次請求時,就會帶上最新的 Etag 和 Last-Modified。

在某些情況下,服務器返回的校驗字段會不完整,比如缺失了 Etag 和 Last-Modified 中某一個,那麼這種情況下的緩存校驗就會存在風險。

在 PC 官網的一個案例中,源站點服務器返回了靜態資源的 Etag 和 Last-Modified,但是代理服務器,也就是 CDN 廠商在返回時將 Etag 給清除了,導致缺少了 Etag 校驗。在正常情況下,服務器只使用文件的最後一次修改時間來做緩存校驗也沒啥問題。但是有這麼一個用戶,他的瀏覽器內緩存的靜態資源損壞了,瀏覽器每次讀取出來的資源無法使用,也就無法正常渲染頁面,但是在每次與服務器校驗資源的時候,服務器依然會告知客戶端 304(緩存可用)。這種場景下,只要源站點服務器不進行資源更新,也就是不變動這個 Last-Modified,那麼用戶將永遠打不開這個文件。

講完了這些,差不多整個緩存協議的下行及交互部分大家已經略知一二了。剩下的就是緩存協議的上行部分了,所謂上行部分就是將 cache-control 寫在瀏覽器訪問的請求頭上面。

前面我們也提過,瀏覽器的刷新請求,其實就是在請求頭裏面加了一個 **cache-control :****max-age = 0 。**這其實是告知服務器,客戶端希望接收一個存在時間不大於 0 秒的緩存,一般的源服務器,特別是靜態資源服務器,這個時候就會根據客戶端的緩存情況返回 200 或者 304。

4.4.11 在請求頭中的可緩存性控制

no-cache

告知代理服務器,不直接使用緩存,要求向源服務器發起請求。

no-store

所有的文件都不緩存到本地或者臨時文件夾中。

max-age

告知服務器客戶端希望接收一個存在時間不大於 X 秒的資源。

max-statle

告知服務器客戶端願意接受一個超過緩存時間的資源,時間爲 X 秒。

min-fresh

告知服務器客戶端希望接收一個在小於 X 秒內被更新過得資源。

no-transform

告知代理服務器,不允許代理服務器對資源進行壓縮,轉化,比如有些代理服務器會對圖片進行壓縮,格式轉換。

only-if-cached

告知代理服務器如果代理服務器有緩存內容,就直接給,不用再找源服務器要。

請求頭中的緩存控制因爲用的比較少,我就不過多的去解讀了,有興趣的同學可以去研究下。

五、總結

HTTP 的 cache-control 協議規定了客戶端,代理服務器,源服務器三者之間的緩存交互邏輯。做爲客戶端開發,經常出現一些與 cache 相關的問題在排查時無從下手,通過學習瞭解這部分內容,可以幫助快速的分析定位這部分問題。

前端同學熟悉 cache-control 的邏輯後,也可以根據業務的形態跟運維討論自己緩存需求,有效的降低服務器的壓力和用戶的流量,提高網頁打開速度。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/23WJXJFGJ-iMP6x-lFQ8og