30 分鐘 HTTP 查漏補缺之 Vary

寫在前面

最近抽空參加了幾場大廠的面試,突然發現一個現象,就是不論面試偏服務端的職位還是偏客戶端的職位,不論面試的 5 年以上的高級職位,還是 3 年左右的中級職位,面試官開頭所問問題必然是關於 HTTP 的。

我記得之前找工作的時候,似乎都是先考察一些職位所需技能領域的基礎知識,之後再考察關於 HTTP 的東西,現在大家都將 HTTP 的問題放到面試的開頭來問,我覺的應該是越來越多的招聘者意識到,作爲一個 Web 開發者,HTTP 真的是太重要了,必須要先考察。

回想起來,這幾年我自己對於 HTTP 的學習大多是碎片化的,很多東西無法系統地在腦海中組織起來。雖然感覺 HTTP 整體的學習難度是比較低的,但是各個知識點交雜在一起又變得很複雜很難,相信大家都會有同感。同時有些知識點,如果在實際工作中沒有采坑或者刻意深挖的話,很自然地就被忽略了。

由於在之前一次面試中,被狠狠地問了若干關於 Vary 的問題,所以想抽一些時間整理一下那些比較容易讓人忽略的知識點,算是查漏補缺吧。

內容協商

首先需要了解的是內容協商這個術語。當我們通過某個 URI 來訪問其指向的資源時,HTTP 協議可以通過內容協商機制提供資源的不同的展示形式。

如果缺少服務端開發經驗話,對於這個概念可能會感到陌生,但其實我們在工作中幾乎都會遇到它,比如在調用接口時,經常會用到 Accept: application/json 這個頭部,有時可能會用到 Accept: application/xml,這就是內容協商,前者期望接口返回 json 格式的數據,而後者期望返回 xml 格式的數據。

一般客戶端涉及的常見頭部有以下幾個:

  • Accept: 聲明客戶端可以處理的資源格式
  • Accept-Charset: 聲明客戶端可以處理的字符集類型
  • Accept-Language: 聲明客戶端可以理解的自然語言
  • Accept-Encoding: 聲明客戶端支持的編碼格式

而服務端涉及的常見頭部包括:

  • Content-Type: 指示資源的 MIME 類型
  • Content-Language: 指示該資源所期望的自然語言
  • Content-Encoding: 指示資源使用該編碼格式進行內容轉換

仔細觀察的話,會發現它們其實存在着一定程度的對應關係。原因也很簡單,既然是協商,那必然就會和兩個人在進行說話一樣,如果兩者之間的對話內容沒有關聯,他們還怎麼溝通呢?客戶端和服務端進行溝通同理。

如果想詳細瞭解該機制,可以參考MDN的文檔,很詳細,這裏就不多說了。

這裏順帶說明一下,對於內容協商機制中涉及的頭部,從 web 發展歷史上來看已經沒有什麼實質的用途了,原因如下(有興趣的話可以閱讀這篇wiki):

  • Accept-Charset: 由於 utf-8 成爲主流的字符集類型,所以使用其他字符集類型的服務可以將其轉換爲 utf-8 類型
  • Accept-Language: 大體包含以下幾點

    • 提供多種語言服務的網站往往是基於某種特定語言構建,再提供其他語言支持的,這樣每種語言類型的內容在質量上層次不齊,而訪問者可能會更傾向於內容質量更高的那一種語言,而內容協商機制無法替代用戶的主觀判斷
    • 實踐中,對於切換網站語言的功能,切換方式往往更傾向於主動切換(比如提供一個切換的按鈕)而非自動切換
    • 瀏覽器在用戶不提供語言相關配置的情況下,很難猜測用戶的自然語言傾向(一般可能會根據地理定位、ip等因素猜測),打個比方,比如我會經常出差去日本,但這不代表我會說日語,同時雖然我掛了加拿大的 vps,但是提供中文內容的網站,我還是傾向於看中文
  • Accept: 與 Accept-Language 類似,同樣因爲內容的格式會因用戶的主觀意識而不同,還有諸多其他因素制約內容協商機制,所以最終失敗了。

唯一有些用途的是 Accept-Encoding,但鑑於如今大部分現代瀏覽器都已支持多種壓縮方式(常見的如 gzip、br),因此一定程度上已經不需要額外聲明這個頭部了,雖然大部分瀏覽器都會自動發送這個頭部,但其實這會造成額外 23 字節的浪費。

Vary 頭部

在理解(或者鞏固)了內容協商的概念後,就可以介紹 Vary 這個頭部了。直接引用 MDN 對於它的描述:

The Vary HTTP response header determines how to match future request headers to decide whether a cached response can be used rather than requesting a fresh one from the origin server.

Vary 是一個HTTP響應頭部信息,它決定了對於未來的一個請求頭,應該使用一個緩存作爲響應還是向源服務器請求一個新的響應。

單純靠文檔對於 Vary 的描述來理解它其實是有些困難的,最起碼我會有這種感覺。

這個頭部的語法和其他的 HTTP 頭部類似,如下:

Vary: <header-name>, <header-name>, ...

不同的頭部之間使用逗號進行分割,同時可以指定 * 爲它的值,這樣等價於將資源視爲唯一,並不進行緩存,但這並不是最佳實踐,因此不建議這麼做。

Vary 的工作原理

一句話概括它的工作原理就是,就是它表示某個響應因某個響應頭部而不同。舉個例子,比如 Vary: Accept 的意思即爲,響應因請求資源格式頭部而不同,那麼通過相同 URI 訪問的資源就可以根據這個頭上知道其內容格式不同。

但我們已經知道,對於大部分內容協商機制中涉及的頭部,已經被看作是失敗的,那麼 Vary 和這些頭部搭配使用還有什麼意義呢?話雖如此,但 Vary 還可以與 HTTP 中其他的頭部來搭配使用,從而滿足很多應用場景下的特殊需求,比如動態服務、防止緩存錯亂等。

Vary 的應用場景

以下簡單羅列一些常用的應用場景以及採坑指南。

Vary 與 動態服務

關於動態服務,最常見的莫過於 Vary: User-Agent。衆所周知,UA 是一段特徵字符串,通常包含區分客戶端類型、操作系統、版本號等信息,隨着移動 web 應用變得越流行,一個應用網站同時提供桌面和移動兩種版本的應用是很常見的事情。通過設置 Vary: User-Agent 頭部,對於搜索引擎,對於關鍵字的搜索結果可以提供更加準確的應用版本,對於客戶端,可以使其從緩存服務器獲取到相應應用類型的緩存版本,而不是錯誤地將桌面版緩存傳遞給移動版應用。

web 應用的性能在加載速度這一指標上,很大程度上取決於加載資源的大小,而圖片資源是所佔比例最大的一塊。爲了減少圖片的大小,除了對常見的圖片格式進行壓縮以外,chrome 推出的 WebP 格式也是不錯的選擇。但是這裏的問題是,不是所有的瀏覽器都支持 WebP 圖片格式的,所以這裏使用 Vary: Accept 來針對瀏覽器的支持情況返回相應的緩存副本,支持則返回 WebP 格式,不支持則返回縮略圖或者原圖。

還有其他關於動態服務的場景,比如要針對不同分辨率的屏幕加載不同質量的圖片(Client Hints 相關的頭部)、針對不同用戶身份提供不同的資源(Cookie頭部)等等。

Vary 與 緩存錯亂

有時候我們會發現響應中存在 Vary: Accept-Encoding 頭部信息,我原先按照內容協商機制中所描述的內容來理解,但到後來才發現,其實很大程度上是爲了防止緩存錯亂的問題。

設想一下,如果沒有這個頭部,當兩個分別支持 gzip 和 不支持 gzip 的客戶端對同一份資源進行獲取時,結果會變得十分微妙。如果不支持 gzip 的客戶端先訪問,緩存代理會緩存未壓縮的版本,那麼當支持 gzip 的客戶端再訪問時,由於命中緩存,雖然它支持 gzip 但也只能加載未壓縮的資源。反過來同樣如此,支持 gzip 客戶端先訪問,則緩存代理會緩存壓縮版本,當不支持 gzip 的客戶端再訪問時,緩存同樣命中,但是由於它無法對壓縮資源解碼,所以會呈現亂碼。

通過 Vary: Accept-Encoding 我們可以防止這種情況的發生,因爲 Vary 在這裏其實是扮演着校驗器的角色,它會進一步對命中緩存的資源進行再校驗,如果發現頭部信息不同,則會將緩存資源視爲無效,從而將請求繼續轉發至源服務器。這對於緩存代理服務器也有一定的益處,因爲可以有有依據地針對不同的 Accept-Encoding 緩存不同的資源副本。

Vary 與 緩存命中率

Vary 雖然可以防止緩存錯亂,但並不代表可以濫用,盲目的使用會適得其反,比如之前提及的 Vary: *,這樣等價於將每個請求視爲唯一,並且不緩存其響應資源,除非有意爲之,不然沒有人會犧牲緩存帶來的性能提升。

同時對於一些 Header 的值是開放性的,比如之前提及的 User-Agent,如果單純從字面量來匹配的話,衆多桌面瀏覽器的值會因各種因素而不同的,如果僅是簡單地將 UA 作爲區分桌面端和移動端的依據,那麼緩存命中率會達到一個很低的水平。如何解決這個問題呢?可以將這些 UA 頭部的值進行標準化,比如可以通過正則匹配所有桌面瀏覽器的 UA 並重新更改爲 Desktop,之後再轉發至緩存代理和源服務器,這樣有利於提高緩存命中率,關於這部分的內容,可以參考這篇文章,其中有很細緻的講解。

所以我們要時刻留意,在使用 Vary 時,一定要根據緩存命中率作出調整,在不發生緩存錯亂的情況之下,儘可能的提高資源的緩存命中率。

Vary 與 CORS

對於跨域的有情況,Vary 也包含一些內容。HTTP 協議規定,當服務端響應包含 Access-Control-Allow-Origin 頭部,且它的值是一個具體的域名而不是通配符 *,那麼這時必須要包含 Vary: Origin 這個頭部。

爲什麼要包含這個頭部,因爲請求頭中的 Origin 頭部代表了該請求來源的具體域名信息,那麼對於不同域名網站所發起的請求,會使用僅屬於它本身的緩存。一般而言,我們很少會遇到這種問題,因爲一般都將 Access-Control-Allow-Origin 設置爲了 *,至少我自己是這樣的。如果想進一步瞭解 Vary 和 CORS 的內容,可以參考這篇文章

最後

差不多就這麼多內容了,如有錯誤,還望指正。

參考鏈接

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章