HTTP緩存協議實戰
一、什麼是緩存
緩存,又稱作Cache,我們把臨時存儲數據的地方叫做緩存池,緩存池裏面放的數據就叫做緩存。當用戶需要使用這些數據,首先在緩存中尋找,如果找到了則直接使用。如果找不到,則再去其他數據源中查找。
二、為什麼要使用緩存技術
緩存的本質就是用空間換時間,以臨時存儲的數據暫時代替數據源中讀取最新的數據,這種方式帶來的好處在不同的場景下是不一樣的。
舉個例子:
當我們需要喝水時,我們會拿出一個水杯,去水龍頭接一杯水來喝。大家可以思考一下,為什麼用杯子來喝水,而不是直接用嘴巴在水龍頭接水喝。
用杯子喝水確實存在一些既有的問題,比如杯子裏面的水容易變涼,而水龍頭流出的水確是恆溫的。我們可以想像一下,公司里的同事們排隊在水龍頭下面喝水的場面,確實有點滑稽,我們寧願接受杯子里的水會變涼這個既有問題。
用杯子喝水有以下幾個優勢:
-
用杯子喝水解決了總是要去找水龍頭的問題,因為杯子可以一次接更多的水。
-
用杯子喝水更不容易灑出來,不容易浪費水。
-
用杯子喝水比趴在水龍頭下喝水更優雅。
我們把杯子看成一個緩存池,杯中的水看成緩存,我們接受了杯中水會變涼的問題,相當於犧牲了數據的實時性。把這些優勢換一個方式來描述,於是使用緩存的優勢變成了下面幾個:
-
降低了系統壓力;
-
節省了資源消耗;
-
優化用戶體驗。
三、HTTP緩存的作用
網絡的其中一個特點就是不穩定性,很多用戶受到網速慢的困擾。
服務器在大量用戶訪問的場景下實時計算數據也很容易產生瓶頸,導致服務變慢。從緩存技術具備的優勢來看,很適合解決網絡服務不穩定的問題。
四、HTTP緩存協議
協議是溝通過程中雙方都遵守並且使用的一種規則。舉個栗子,客戶端和服務器兩位大兄弟在新款機型問題上進行了幾次溝通?
客戶端:大哥,新款nex發佈沒?
服務器:老弟,還沒發,你記住,別老來問我!
一周後……
客戶端:大哥,我又來了,最新情況如何?
服務器:跟上次一樣。
一個月後…..
客戶端:大哥,這都一個月了,怎麼樣了啊?!
服務器:已經開售啦!
在這個例子裏面,客戶端與服務端溝通過程中就遵循某種規則,我們來看一下。
-
數據部分:機型的內容;
-
協議部分:1)別老來問我,2)最新情況如何,3)跟上次一樣。
服務端說的這些話,客戶端都能看懂並且明白這些話中所蘊含的意義,這就是客戶端與服務端之間達成的某種通訊協議。
4.1 HTTP消息頭
在介紹HTTP緩存協議之前,我們先來了解一下HTTP消息頭的基礎知識。我們對HTTP/HTTPS的數據請求都比較熟悉,在HTTP的數據請求中有一種信息叫做「頭部信息」。
頭部信息是在客戶端請求或者服務端響應是傳遞給對方的一種信息。我們來看一下HTTP協議的組成部分。
HTTP 請求的組成
狀態行、請求頭、消息主體三部分組成。
HTTP 響應的組成
狀態行、響應頭、響應正文。
其中,請求頭和響應頭就是我們這裡說的「頭部信息」或者又叫「消息頭」。那麼頭部信息有什麼作用呢?
4.2 請求頭
如圖所示:
4.3 響應頭
如圖所示:
我們今天要講的緩存協議——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一般有如下取值:
-
Cache-control: public
-
Cache-control: private
-
Cache-control: no-cache
-
Cache-control: no-store
-
Cache-control: no-transform
-
Cache-control: must-revalidate
-
Cache-control: proxy-revalidate
-
Cache-Control: max-age=
-
Cache-control: s-maxage=
4.4.4 請求頭中的cache-control
請求頭中的cache-control一般有如下取值:
-
Cache-Control: max-age=
-
Cache-Control: max-stale[=]
-
Cache-Control: min-fresh=
-
Cache-control: no-cache
-
Cache-control: no-store
-
Cache-control: no-transform
-
Cache-control: only-if-cached
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。
點擊瀏覽器刷新按鈕
點擊瀏覽器的刷新按鈕後,客戶端瀏覽器帶上了第一次請求時返回的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和數據。大家仔細觀察請求頭和響應頭;
-
請求頭中的if-None-Match 其實就是保持的上次服務器返回的ETag;
-
請求頭中的if-Modified-Match 其實就是保持的上次服務器返回的Last-Modified;
現在這兩個值跟服務端的都對應不上了,所以服務器返回了最新的數據和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的邏輯後,也可以根據業務的形態跟運維討論自己緩存需求,有效的降低服務器的壓力和用戶的流量,提高網頁打開速度。
作者:vivo互聯網客戶端團隊-Chen Long