HTTP快取協議實戰

vivo網際網路技術發表於2022-02-16

一、什麼是快取

快取,又稱作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

相關文章