HTTP 的代理服務以及緩存代理


HTTP 的代理服務

前面介紹 HTTP 協議的時候,我們嚴格遵循了 HTTP 的請求 - 響應模型,協議中只有兩個互相通信的角色,分別是作為請求方的瀏覽器(客戶端)和作為響應方的服務器。今天,我們要在這個模型里引入一個新的角色,那就是 HTTP 代理。引入 HTTP 代理后,原來簡單的雙方通信就變復雜了一些,加入了一個或者多個中間人,但整體上來看,還是一個有順序關系的鏈條,而且鏈條里相鄰的兩個角色仍然是簡單的一對一通信,不會出現越級的情況。

瀏覽器 <---> 代理服務器 <---> 服務器(源)

鏈條的起點還是客戶端(也就是瀏覽器),中間的角色被稱為代理服務器(proxy server),鏈條的終點被稱為源服務器(origin server),意思是數據的源頭、起源。

代理服務

代理這個詞聽起來好像很神秘,有點高大上的感覺。但其實 HTTP 協議里對它並沒有什么特別的描述,它就是在客戶端和服務器原本的通信鏈路中插入的一個中間環節,也是一台服務器,但提供的是代理服務。

所謂的代理服務就是指:服務本身不生產內容,而是處於中間位置轉發上下游的請求和響應,具有雙重身份。面向下游的用戶時,表現為服務器,代表源服務器響應客戶端的請求;而面向上游的源服務器時,又表現為客戶端,代表客戶端發送請求。我們掛的 威批恩 就是典型的代理服務器,假設我們訪問谷歌,真正給谷歌發請求的其實是代理服務器,給我們提供響應的也是代理服務器。並且根據代理的性質,我們可分為匿名代理、透明代理、正向代理、反向代理。而下面我們將要介紹的是反向代理,它在傳輸鏈路中更靠近源服務器,為源服務器提供代理服務。

代理的作用

為什么要有代理呢?換句話說,代理能干什么、帶來什么好處呢?你也許聽過這樣一句至理名言:計算機科學領域里的任何問題,都可以通過引入一個中間層來解決(在這句話后面還可以再加上一句,如果一個中間層解決不了問題,那就再加一個中間層)。TCP/IP 協議棧是這樣,而代理也是這樣。

由於代理處在 HTTP 通信過程的中間位置,相應地就對上屏蔽了真實客戶端,對下屏蔽了真實服務器,簡單的說就是欺上瞞下。在這個中間層的小天地里就可以做很多的事情,為 HTTP 協議增加更多的靈活性,實現客戶端和服務器的雙贏。

代理最基本的一個功能是負載均衡。因為在面向客戶端時屏蔽了源服務器,客戶端看到的只是代理服務器,源服務器究竟有多少台、是哪些 IP 地址都不知道。於是代理服務器就可以掌握請求分發的大權,決定由后面的哪台服務器來響應請求。

代理中常用的負載均衡算法大家應該有所耳聞,比如輪詢、一致性哈希等等,這些算法的目標都是盡量把外部的流量合理地分散到多台源服務器,提高系統的整體資源利用率和性能。在負載均衡的同時,代理服務還可以執行更多的功能,比如:

  • 健康檢查:使用 心跳 等機制監控后端服務器,發現有故障就及時踢出集群,保證服務高可用
  • 安全防護:保護被代理的后端服務器,限制 IP 地址或流量,抵御網絡攻擊和過載
  • 加密卸載:對外網使用 SSL/TLS 加密通信認證,而在安全的內網不加密,消除加解密成本
  • 數據過濾:攔截上下行的數據,任意指定策略修改請求或者響應
  • 內容緩存:暫存、復用服務器響應,我們稍后再說

我們以便利店為例:因為便利店和超市之間是專車配送,所以有了便利店,以后你買東西就更省事了,打電話給便利店讓它去幫你取貨,不用關心超市是否停業休息、是否人滿為患,而且總能買到最新鮮的。便利店同時也方便了超市,不用額外加大店面就可以增加客源和銷量,貨物集中裝卸也節省了物流成本,由於便利店直接面對客戶,所以也可以把惡意騷擾電話擋在外面。

代理相關頭字段

代理的好處很多,但因為它欺上瞞下的特點,隱藏了真實客戶端和服務器,如果雙方想要獲得這些丟失的原始信息,該怎么辦呢?首先,代理服務器需要用字段 Via 標明代理的身份。

Via 是一個通用字段,請求頭或響應頭里都可以出現。每當報文經過一個代理節點,代理服務器就會把自身的信息追加到字段的末尾,就像是經手人蓋了一個章。如果通信鏈路中有很多中間代理,就會在 Via 里形成一個鏈表,這樣就可以知道報文究竟走過了多少個環節才到達了目的地。

例如下圖中有兩個代理:proxy1 和 proxy2,客戶端發送請求會經過這兩個代理,依次添加就是 Via: proxy1, proxy2,等到服務器返回響應報文的時候就要反過來走,頭字段就是 Via: proxy2, proxy1

Via 字段只解決了客戶端和源服務器判斷是否存在代理的問題,還不能知道對方的真實信息。但服務器的 IP 地址應該是保密的,關系到企業的內網安全,所以一般不會讓客戶端知道。不過反過來,通常服務器需要知道客戶端的真實 IP 地址,方便做訪問控制、用戶畫像、統計分析。但可惜的是 HTTP 標准里並沒有為此定義頭字段,但已經出現了很多事實上的標准,最常用的兩個頭字段是 X-Forwarded-For 和 X-Real-IP。

X-Forwarded-For 的字面意思是"為誰而轉發",形式上和 Via 差不多,也是每經過一個代理節點就會在字段里追加一個信息。但 Via 追加的是代理主機名(或者域名),而 X-Forwarded-For 追加的是請求方的 IP 地址。所以,在字段里最左邊的 IP 地址就客戶端的地址。

X-Real-IP 是另一種獲取客戶端真實 IP 的手段,它的作用很簡單,就是記錄客戶端 IP 地址,沒有中間的代理信息,相當於是 X-Forwarded-For 的簡化版。如果客戶端和源服務器之間只有一個代理,那么這兩個字段的值就是相同的。

有時候除了 X-Forwarded-For、X-Real-IP 之外,你可能還會看到 X-Forwarded-Host、X-Forwarded-Proto,它們的作用與 X-Real-IP 類似,只記錄客戶端的信息,分別是客戶端請求的原始域名和原始協議名。

代理協議

有了 X-Forwarded-For 等頭字段,源服務器就可以拿到准確的客戶端信息了,但對於代理服務器來說它並不是一個最佳的解決方案。因為通過 X-Forwarded-For 操作代理信息必須要解析 HTTP 報文頭,這對於代理來說成本比較高,原本只需要簡單地轉發消息就好,而現在卻必須要費力解析數據再修改數據,會降低代理的轉發性能。另一個問題是 X-Forwarded-For 等頭必須要修改原始報文,而有些情況下是不允許甚至不可能的(比如使用 HTTPS 通信被加密)。

所以就出現了一個專門的代理協議(The PROXY protocol),它由知名的代理軟件 HAProxy 所定義,也是一個事實標准,被廣泛采用(注意並不是 RFC)。代理協議有 v1 和 v2 兩個版本,v1 和 HTTP 差不多,也是明文,而 v2 是二進制格式。這里先只介紹比較好理解的 v1,它在 HTTP 報文前增加了一行 ASCII 碼文本,相當於又多了一個頭。而這一行文本其實非常簡單,開頭必須是 PROXY 五個大寫字母,然后是 TCP4 或者 TCP6,表示客戶端的 IP 地址類型,再后面是請求方地址、應答方地址、請求方端口號、應答方端口號,最后用一個換行(\r\n)結束。

PROXY TCP4 1.1.1.1 2.2.2.2 55555 80\r\n
GET / HTTP/1.1\r\n
Host: www.xxx.com\r\n
\r\n

例如上面這個請求,在 GET 請求行前多出了 PROXY 信息行,客戶端的真實 IP 地址是 1.1.1.1,端口號是 55555。服務器看到這樣的報文,只要解析第一行就可以拿到客戶端地址,不需要再去理會后面的 HTTP 數據,省了很多事情。不過代理協議並不支持 X-Forwarded-For 的鏈式地址形式,所以拿到客戶端地址后再如何處理就需要代理服務器與后端自行約定。

補充

指明的代理軟件有 HAProxy、Squid、Varnish 等等,而 Nginx 雖然是 Web 服務器,但也可以作為代理服務器,而且功能毫不遜色。

Via 是 HTTP 協議里規定的標准頭字段,但有的服務器返回的響應報文里會使用 X-Via,含義是相同的。

因為 HTTP 是明文傳輸,請求頭很容易被篡改,因此 X-Forwarded-For 也不是完全可信的。

RFC7239 定義了字段 Forwarded,它可以代理 X-Forwarded-For 和 X-Forwarded-Host 等字段,但是應用的不多。

HTTP 的緩存代理

我們了解 HTTP 的緩存控制以及 HTTP 的代理服務,那么把這兩者結合起來就是下面所要說的緩存代理,也就是支持緩存控制的代理服務。之前談到緩存時,主要說了客戶端(瀏覽器)上的緩存控制,它能夠減少響應時間、節約帶寬,提升客戶端的用戶體驗。但 HTTP 傳輸鏈路上,不只是客戶端有緩存,服務器上的緩存也是非常有價值的,可以讓請求不必走完整個后續處理流程,就近獲得響應結果。

特別是對於那些讀多寫少的數據,例如突發熱點新聞、爆款商品的詳情頁,一秒鍾內可能有成千上萬次的請求。即使僅僅緩存數秒鍾,也能夠把巨大的訪問流量擋在外面,讓 RPS(request per second)降低好幾個數量級,減輕應用服務器的並發壓力,對性能的改善是非常顯著的。HTTP 的服務器緩存功能主要由代理服務器來實現(即緩存代理),而源服務器系統內部雖然也經常有各種緩存(如 Memcache、Redis、Varnish 等),但與 HTTP 沒有太多關系,所以這里暫且不說。

緩存代理服務

我們 生鮮速遞 + 便利店 做比喻,看看緩存代理是怎么回事。

便利店作為超市的代理,生意非常紅火,顧客和超市雙方都對現狀非常滿意。但時間一長,超市發現還有進一步提升的空間,因為每次便利店接到顧客的請求后都要專車跑一趟超市,還是挺麻煩的。干脆這樣吧,給便利店配發一個大冰櫃,水果海鮮什么的都可以放在冰櫃里,只要產品在保鮮期內,就允許顧客直接從冰櫃提貨。這樣便利店就可以一次進貨多次出貨,省去了超市之間的運輸成本。

通過這個比喻,你可以看到:在沒有緩存的時候,代理服務器每次都是直接轉發客戶端和服務器的報文,中間不會存儲任何數據,只有最簡單的中轉功能。但加入了緩存后就不一樣了,代理服務收到源服務器發來的響應數據后需要做兩件事。第一個當然是把報文轉發給客戶端,而第二個就是把報文存入自己的 Cache 里。下一次再有相同的請求,代理服務器就可以直接發送 304 或者緩存數據,不必再從源服務器那里獲取。這樣就降低了客戶端的等待時間,同時節約了源服務器的網絡帶寬。

在 HTTP 的緩存體系中,緩存代理的身份十分特殊,它既是客戶端,又是服務器;同時也既不是客戶端,又不是服務器。

說它即是客戶端又是服務器,是因為它面向源服務器時是客戶端,在面向客戶端時又是服務器,所以它即可以用客戶端的緩存控制策略也可以用服務器端的緩存控制策略,也就是說它可以同時使用之前的各種 Cache-Control 屬性。

但緩存代理也即不是客戶端又不是服務器,因為它只是一個數據的中轉站,並不是真正的數據消費者和生產者,所以還需要有一些新的 Cache-Control 屬性來對它做特別的約束。

源服務器的緩存控制

之前說了 4 種服務器端的 Cache-Control 屬性:max-age、no_store、no_cache 和 must-revalidate,應該還有印象吧?這 4 種緩存屬性可以約束客戶端,也可以約束代理。

但客戶端和代理是不一樣的,客戶端的緩存只是用戶自己使用,而代理的緩存可能會為非常多的客戶端提供服務。所以,需要對它的緩存再多一些限制條件。

首先,我們要區分客戶端上的緩存和代理上的緩存,可以使用兩個新屬性 private 和 public。private 表示緩存只能在客戶端保存,是用戶私有的,不能放在代理上與別人共享。而 public 的意思就是緩存完全開放,誰都可以存,誰都可以用。

比如你登錄論壇,返回的響應報文里用 Set-Cookie 添加了論壇 ID,這就屬於私人數據,不能存在代理上。不然,別人訪問代理獲取了被緩存的響應就麻煩了。其次,緩存失效后的重新驗證也要區分開(即使用條件請求 Last-modified 和 ETag),must-revalidate 只要過期就必須回源服務器驗證,而新的 proxy-revalidate 只要求代理的緩存過期后必須驗證,客戶端不必回源,只驗證到代理這個環節就行了。

再次,緩存的生存時間可以使用新的 s-maxage(s 是 share 的意思,注意 maxage 中間沒有 -),只限定在代理上能夠存多久,而客戶端仍然使用 max-age。

還有一個代理專用的屬性 no-transform,代理有時候會對緩存下來的數據做一些優化,比如把圖片生成 png、webp 等幾種格式,方便今后的請求處理,而 no-transform 就會禁止這樣做,不允許偷偷摸摸搞小動作。

這些新的緩存控制屬性比較復雜,還是用 便利店冷櫃 來舉例好理解一些。

水果上貼着標簽 private, max-age=5,這就是說水果不能放進冷櫃,必須直接給顧客,保鮮期 5 天,過期了還得去超市重新進貨。

凍魚上貼着標簽 public, max-age=5, s-maxage=10,這個的意思就是可以在冰櫃里存 10 天,但顧客那里只能存 5 天,過期了可以來便利店取,只要在 10 天之內就不必再找超市。

排骨上貼着標簽 max-age=30, proxy-revalidate, no-transform,因為緩存默認是 public 的,那么它在便利店和顧客的冰箱里就都可以存 30 天,過期后便利店必須去超市進新貨,而且不能擅自對數據進行改動。

但是注意,源服務器在設置完 Cache-Control 后必須要為報文加上 Last-modified 或 ETag 字段。否則,客戶端和代理后面就無法使用條件請求來驗證緩存是否有效,也就不會有 304 緩存重定向。

客戶端的緩存控制

說完了服務器端的緩存控制策略,稍微歇一口氣,我們再來看看客戶端。

客戶端在 HTTP 緩存體系里要面對的是代理和源服務器,也必須區別對待。

max-age、no_store、no_cache 這三個屬性已經介紹過了,它們也是同樣作用於代理和源服務器。

關於緩存的生存時間,多了兩個新屬性 max-stale 和 min-fresh。

max-stale 的意思是如果代理上的緩存過期了也可以接受,但不能過期太多,超過 x 秒也會不要;min-fresh 的意思是緩存必須有效,而且必須在 x 秒后依然有效。比如,草莓上貼着標簽 max-age=5,現在已經在冰櫃里存了 7 天。如果有請求 max-stale=2,意思是過期兩天也能接受,所以剛好能賣出去。但要是 min-fresh=1,這是絕對不允許過期的,就不會買走。這時如果有另外一個菠蘿是 max-age=10,那么 7+1<10,在一天之后還是新鮮的,所以就能賣出去。

有的時候客戶端還會發出一個特別的 only-if-cached 屬性,表示只接受代理緩存的數據,不接受源服務器的響應。如果代理上沒有緩存或者緩存過期,就應該給客戶端返回一個 504(Gateway Timeout)。

其他問題

緩存代理的知識就快講完了,下面再簡單說兩個相關的問題。

第一個是 Vary 字段,曾經說過它是內容協商的結果,相當於報文的一個版本標記。同一個請求,經過內容協商后可能會有不同的字符集、編碼、瀏覽器等版本。比如,Vary: Accept-Encoding、Vary: User-Agent,緩存代理必須要存儲這些不同的版本。當再收到相同的請求時,代理就讀取緩存里的 Vary,對比請求頭里相應的 Accept-Encoding、User-Agent 等字段,如果和上一個請求的完全匹配,比如都是 gzip、Chrome,就表示版本一致,可以返回緩存的數據。

另一個問題是 Purge,也就是緩存清理,它對於代理也是非常重要的功能,例如:

  • 過期的數據應該及時淘汰,避免占用空間
  • 源站的資源有更新,需要刪除舊版本,主動換成最新版(即刷新)
  • 有時候會緩存了一些本不該存儲的信息,例如網絡謠言或者危險鏈接,必須盡快把它們刪除

清理緩存的方法有很多,比較常用的一種做法是使用自定義請求方法“PURGE”,發給代理服務器,要求刪除 URI 對應的緩存數據。

補充

常見的緩存代理軟件有 Squid、Varnish、ATS(Apache Traffic Server)等,而 Nginx 不僅是 Web 服務器、代理服務器,也是一個出色的緩存代理服務器,堪稱全能。

有的緩存代理在 Cache Hit 的時候會在響應報文里加一個 Age 頭字段,表示報文的生存時間,即已經緩存了多久,通常它會小於 Cache-Control 里的 max-age 值,如果大於就意味着數據是陳舊的(stale)。

判斷緩存是否命中(Hit)類似於查詢 hash 表,使用的 key 通常是 URI,在 Nginx 中可以用指令 proxy_cache_key 自定義。

Nginx 對 Vary 的處理實際上是做了 MD5,把 Vary 頭摘要寫入緩存,請求時不僅比較 URI,也會比較摘要。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM