本文編譯自:
users-guide
本節講述如何使用 VCL 編寫處理 HTTP 流量的策略。
Varnish 的配置系統與其他軟件不同,一般的配置是使用配置指令,用于打開或關閉某個配置項。而 Varnish 使用 VCL 語言進行配置。
每一個入站請求流經 Varnish 時,會被 VCL 定義的策略處理。你可以通過修改 VCL 代碼,改變對請求的處理。Varnish 不僅僅是一個緩存,它還是一個強大的 HTTP 處理器,你可以對 HTTP 請求做如下的事情:
- 將某些請求調度至某些后端服務器
- 對請求和響應進行修改
- 根據請求和響應的任意屬性,做特定的操作
VCL 被編譯成二進制代碼,并在請求到來時被執行。VCL 對性能的影響可忽略不計。
VCL 文件組成 subroutines,不同的 subroutines 在不同的時候被執行。比如有一個 subroutine 是當接收到請求時執行,另一個是當從后端服務器獲取到響應時執行。
如果在你的 subroutine 中不調用 action,該 subroutine 執行完畢之后,Varnish 會執行內建的 VCL 代碼。這些代碼在 builtin.vcl 文件中,它們被注釋了。
目錄:
- VCL 語法
- 內建的 subroutines
- Request and response VCL objects
- actions
- 后端服務器
- 多個后端服務器
- Varnish 中的后端服務器和虛擬主機
- 調度器
- 健康檢查
- Hashing
- 不工作的后端服務器
- 使用 inline C to 擴展 Varnish
- VCL Examples
- 設備檢測
VCL 語法
VCL 的語法從 C 語言繼承了很多,所以它讀起來像簡單的 C 或者 Perl。代碼段被花括號括起來,語句以分號結尾,注釋的方式可以是 C,C++,Perl 中的注釋方式,可隨意選擇你習慣的注釋語法。但 VCL 不支持循環和跳轉語句。
本節介紹 VCL 語法中較為重要的部分,詳細的介紹參考 reference 文檔。
Strings
基本的字符串被雙引號引用,比如 " ... ",其中不可包含換行符。\ 沒有轉義的特殊含義,比如在 regsub() 中,你不需要數 \ 的個數:
regsub("barf", "(b)(a)(r)(f)", "\4\3\2p") -> "frap"
長字符串被 {" ... "} 引用,其中可包含任何字符,包括 " 字符,換行符及其它控制字符,除了 NUL (0x00) 字符。如果你真的想要把 NUL 字符放入字符串中,有一個 VMOD 模塊可以創建這樣的字符串。
Access control lists (ACLs)
訪問控制列表 ACLs。
聲明一個 ACL,可以創建及初始化一個訪問控制列表,它有一個名字,它可以被用于匹配 client 地址:
acl local {
"localhost"; // myself
"192.0.2.0"/24; // and everyone on the local network
! "192.0.2.23"; // except for the dialin router
}
如果一個 ACL entry 指定了一個不能解析的主機名,那么這個 ACL 列表與任何地址都可以匹配。如果這個不能被解析的主機名前面有一個 ! 符號,它會拒絕每一個與它進行比較的地址,這可能不是你希望的效果。如果把一個 ACL entry 放入圓括號 () 中,它將被忽略。
將一個 IP 地址與一個 ACL 進行匹配,使用 ~ 運算符:
if (client.ip ~ local) {
return (pipe);
}
Operators
在 VCL 中可以使用的操作符:
= 賦值
== 等于
~ 匹配,用于 regular expressions 或 ACLs
! 邏輯非.
&& 邏輯與
|| 邏輯或
Subroutines
subroutine 可被稱為子例程。
一個 subroutine 是一組代碼,可被重用,下面是一個定義 subroutine 的示例:
sub pipe_if_local {
if (client.ip ~ local) {
return (pipe);
}
}
在 VCL 中,subroutine 不可接受參數,也沒有返回值。
調用 subroutine,使用 call 關鍵字:
call pipe_if_local;
Varnish 有許多內置的 subroutines,當 transaction 流經 Varnish 時,它們將被調用。內置的 subroutine 命名為 vcl_*,你自己定義的 subroutine 不可以命名為以 vcl_ 起始的名字。
內建的 subroutines
Varnish 處理 client 的請求和后端服務器的響應時,會調用多個內置的 subroutines 進行處理。通過 CLI 執行 vcl.load 和 vcl.discard 時,也會調用內置的 subroutines。
下面對前端(client-side)和后端(backend-side)的處理分別進行介紹:
1.client-side
1.1 vcl_recv
vcl_recv 被調用的時機是:
- 當一個請求接收完成,并且被解析之后
- 重啟之后
- as the result of an ESI include
vcl_recv 子例程用于決定:是否對一個請求提供服務,它很可能會對請求進行修改,然后再決定如何做進一步處理。
A backend hint may be set as a default for the backend processing side.
vcl_recv 子例程可使用 return() 結合下面的其中一個關鍵字進行終止:
hash
請求的對象被認為是一個可能被緩存的對象,將繼續對其進行處理。將控制權轉交給 vcl_hash 子例程。
pass
轉換至 pass 模式。控制權最終交給 vcl_pass 子例程。
pipe
轉換至 pipe 模式。控制權最終交給 vcl_pipe 子例程。
synth(status code, reason)
轉移到 vcl_synth 子例程,synth() 的參數值被預置為 resp.status 和 resp.reason
purge
清除請求的對象,以及它的變量(variants)。控制權先交給 vcl_hash,最終交給 vcl_purge
[vcl_synth][2]
[2]:https://www.varnish-cache.org/docs/4.0/users-guide/vcl-built-in-subs.html#vcl-synth
1.2 vcl_pipe
進入 pipe 模式時,vcl_pipe 子例程將被調用。在這個模式中,請求將被傳遞給后端服務器,這時 Varnish 會降級成為一個 TCP 代理,只充當一個數據流的通道,不會對數據進行任何修改,當 client 或 server 端決定關閉連接時,該模式結束。在調用 vcl_pipe 之后,對于一個處于 pipe 模式的連接,其他任何的 VCL 子例程都不會被調用。
vcl_pipe 子例程可使用 return() 結合下面的其中一個關鍵字進行終止:
pipe
繼續以 pipe mode 運行
synth(status code, reason)
轉移到 vcl_synth 子例程,synth() 的參數值被預置為 resp.status 和 resp.reason
1.3 vcl_pass
在進入 pass 模式時,vcl_pass 將被調用,請求被轉發給后端服務器,后端服務器的響應被轉發給 client,但是響應不會被緩存。來自該 client 連接的后續請求,將被正常處理。
vcl_pass 子例程可使用 return() 結合下面的其中一個關鍵字進行終止:
fetch
繼續以 pass mode 運行 - 發起一個對后端服務器的請求
restart
重啟該 transaction。增加 restart 計數器的計數。如果計數超過了 max_restarts,Varnish 發出一個錯誤:guru meditation error.
synth(status code, reason)
轉移到 vcl_synth 子例程,synth() 的參數值被預置為 resp.status 和 resp.reason
1.4 vcl_hit
當緩存查找成功,vcl_hit 將被調用。緩存對象可能會過期,其 ttl 可能為 0 或者負數,with only grace or keep time left.
vcl_hit 子例程可使用 return() 結合下面的其中一個關鍵字進行終止:
deliver
發送該對象。如果該對象過期,將觸發一個 fetch 調用,更新該對象。
fetch
盡管緩存命中,但是會同步地從后端服務器更新緩存對象。控制權最終轉交給 vcl_miss。
pass
轉換至 pass 模式。控制權最終交給 vcl_pass 子例程。
restart
重啟該 transaction。增加 restart 計數器的計數。如果計數超過了 max_restarts,Varnish 發出一個錯誤:guru meditation error.
synth(status code, reason)
轉移到 vcl_synth 子例程,synth() 的參數值被預置為 resp.status 和 resp.reason
1.5 vcl_miss
當緩存查找失敗,或者當 vcl_hit 返回一個 fetch 時,調用 vcl_miss。
vcl_miss 用于決定是否嘗試從后端服務器獲取文件。
A backend hint may be set as a default for the backend processing side.
vcl_miss 子例程可使用 return() 結合下面的其中一個關鍵字進行終止:
fetch
從后端服務器獲取請求的對象。控制權最終轉交給 vcl_backend_fetch。
pass
轉換至 pass 模式。控制權最終交給 vcl_pass 子例程。
restart
重啟該 transaction。增加 restart 計數器的計數。如果計數超過了 max_restarts,Varnish 發出一個錯誤:guru meditation error.
synth(status code, reason)
轉移到 vcl_synth 子例程,synth() 的參數值被預置為 resp.status 和 resp.reason
1.6 vcl_hash
當 vcl_recv 為請求創建了一個 hash 值時被調用。使用該值作為 key 進行緩存查找。
vcl_hash 子例程只能以 return(lookup) 終止:
lookup
在緩存中查找請求的對象。如果從 vcl_recv 返回 return(purge),控制權轉交給 vcl_purge。
否則,如果緩存查找的結果是 hit,控制權轉交給 vcl_hit;如果緩存查找的結果是 miss,控制權轉交給 vcl_miss;
如果緩存查找的結果是 hit on a hit-for-pass 對象 (object with obj.uncacheable == true),控制權轉交給 vcl_pass。
1.7 vcl_purge
執行 purge 之后,vcl_purge 被調用,緩存對象被清除(失效),其所有變量(variants)將被回避。
vcl_purge 子例程可使用 return() 結合下面的其中一個關鍵字進行終止:
restart
重啟該 transaction。增加 restart 計數器的計數。如果計數超過了 max_restarts,Varnish 發出一個錯誤:guru meditation error.
synth(status code, reason)
轉移到 vcl_synth 子例程,synth() 的參數值被預置為 resp.status 和 resp.reason
1.8 vcl_deliver
發送對象給客戶端前調用,除了將一個 vcl_synth 結果發送給客戶端時不會調用。
vcl_deliver 子例程可使用 return() 結合下面的其中一個關鍵字進行終止:
deliver
發送對象給 client
restart
重啟該 transaction。增加 restart 計數器的計數。如果計數超過了 max_restarts,Varnish 發出一個錯誤:guru meditation error.
synth(status code, reason)
轉移到 vcl_synth 子例程,synth() 的參數值被預置為 resp.status 和 resp.reason
1.9 vcl_synth
調用 vcl_synth 可以發送一個 synthetic 對象給客戶端。synthetic 對象由 VCL 生成,不是從后端獲取的。可使用 synthetic() 函數構造 synthetic 對象。
vcl_synth 定義了一個對象,該對象不會被緩存,與其相反,vcl_backend_error 所定義的對象可能最終被緩存。
vcl_synth 子例程可使用 return() 結合下面的其中一個關鍵字進行終止:
deliver
直接將 vcl_synth 定義的對象發送給客戶端,不調用 vcl_deliver
restart
重啟該 transaction。增加 restart 計數器的計數。如果計數超過了 max_restarts,
Varnish 發出一個錯誤:guru meditation error.
2. backend-side
2.1 vcl_backend_fetch
對后端服務器發送請求時調用 vcl_backend_fetch。在這個子例程中,我們一般會修改請求,然后才發送給后端服務器。
vcl_backend_fetch 子例程可使用 return() 結合下面的其中一個關鍵字進行終止:
fetch
從后端服務器獲取對象
abandon
放棄對后端發起請求。除非后端請求是一個 background fetch,否則控制權將被轉交給 client-side 的 vcl_synth,
其 resp.status 被設置為 503。
2.2 vcl_backend_response
當成功從后端服務器獲取到 response headers 時,調用 vcl_backend_response。
vcl_backend_response 子例程可使用 return() 結合下面的其中一個關鍵字進行終止:
deliver
對于一個 304 響應,創建一個更新的緩存對象。否則,從后端獲取對象的 body,然后發起 delevery 返回給客戶端。
很可能是并行的(streaming)
abandon
放棄對后端發起請求。除非后端請求是一個 background fetch,否則控制權將被轉交給 client-side 的 vcl_synth,
其 resp.status 被設置為 503。
retry
重試發起 backend transaction。增加重試計數,如果重試次數超過 max_retries,控制權轉交給 vcl_backend_error
2.3 vcl_backend_error
當嘗試從后端獲取對象失敗,或則重試次數超過 max_retries 時,vcl_backend_error 將被調用。
VCL 生成一個 synthetic 對象,可使用 synthetic() 函數構造 synthetic 對象的 body。
vcl_backend_error 子例程可使用 return() 結合下面的其中一個關鍵字進行終止:
deliver
發送 vcl_backend_error 定義的對象,可能的話,緩存該對象。就如同該對象是從后端獲取的一般。這也被稱為 "backend synth"。
retry
重試發起 backend transaction。增加重試計數,如果重試次數超過 max_retries,調用 client-side 的 vcl_synth,
其 resp.status 被設置為 503。
3. vcl.load / vcl.discard
3.1 vcl_init
當加載 VCL 之后,vcl_init 被調用。一般用于初始化 VMODs。
vcl_init 子例程可使用 return() 結合下面的其中一個關鍵字進行終止:
ok
正常返回,VCL 繼續加載
fail
停止加載這個 VCL
3.2 vcl_fini
當一個 VCL 被廢棄,當該 VCL 處理完所有請求,調用 vcl_fini。一般用于清除 VMODs。
vcl_fini 子例程可使用 return() 結合下面的其中一個關鍵字進行終止:
ok
正常返回,VCL 將被廢棄。
Request and response VCL objects
在 VCL 中有一些重要的對象需要注意,這些對象可以使用 VCL 語言訪問和操控。
req
req 是請求對象。當 Varnish 收到請求之后,req 對象被創建,請求被填入該對象中。在 vcl_recv 中,你大部分的工作是對該對象進行的。
bereq
bereq 是后端請求對象。Varnish 構造該對象,然后發送給后端服務器。這個對象是基于 req 對象創建的。
beresp
beresp 是后端響應對象。該對象包含后端響應的 headers。如果要修改一個來自后端的響應,實際的操作是在 vcl_backend_response 子例程中對 beresp 對象進行修改。
resp
resp 對象,其內容發送給客戶端的 HTTP response,必要時,一般在 vcl_deliver 中對其進行修改。
obj
obj 是用于緩存的對象,它是只讀的。
actions
actions 是在終止一個內置子例程時,配合 return() 使用的,如 return(pass),最常用的 actions 是這些:
pass
當你返回 pass,請求將被傳遞給后端服務器,隨后的響應將從后端服務器傳遞回來。響應不會被緩存。pass 可從 vcl_recv 中返回。
hash
如果從 vcl_recv 中返回 hash,Varnish 將嘗試從緩存中查找并返回請求的對象。
pipe .. XXX:What is pipe? benc
如果從 vcl_recv 返回 pipe,將會進入 pipe 模式,Varnish 將前端與客戶端的連接,以及與后端服務器的連接合并成一個數據流的通道,Varnish 不對數據做任何修改,只是將數據在兩端發送,所以你的日志是不完整的。
deliver
將對象發送給客戶端。通常是從 vcl_backend_response 中返回 deliver。
restart
重啟對請求的處理。你可以重啟整個 transaction,對 req 對象的修改將被保留。
retry
重試對后端發起請求。這個 action 可以從 vcl_backend_response 或 vcl_backend_error 中返回。當你不喜歡后端返回的響應時,可以這樣使用。
后端服務器
后端服務器是 Varnish 中的一個概念,指真正提供內容的 web 服務器。它們也被稱為 “origin” server 或 “upstream” server。后端 web 服務器通過 Varnish 的緩存功能對訪問進行加速。
我們來編輯配置文件,如果是編譯安裝,路徑為 /usr/local/etc/varnish/default.vcl
,如果是軟件包安裝,路徑為 /etc/varnish/default.vcl
。
其中多數被注釋了,其中可能有如下內容:
vcl 4.0;
backend default {
.host = "127.0.0.1";
.port = "8080";
}
這里定義了一個后端服務器,名為 default。當 Varnish 需要從后端獲取內容,它會連接到 127.0.0.1:8080。
Varnish 可定義多個后端服務器,并且可以聯合多個后端服務器為一個后端集群,進行負載均衡。
多個后端服務器
在上面的基礎上,我們引入一個 java 服務器:
backend java {
.host = "127.0.0.1";
.port = "8000";
}
在 vcl_recv 中,我們根據請求的 URL,給請求打上不同的 hint,使其發往指定的后端服務器。
sub vcl_recv {
if (req.url ~ "^/java/") {
set req.backend_hint = java;
} else {
set req.backend_hint = default;
}
}
如果要將移動設備發出的請求,發給專門的后端服務器,可以做這樣的判斷:
if (req.http.User-agent ~ /mobile/) ..
Varnish 中的后端服務器和虛擬主機
Varnish 完全支持虛擬主機。
我們在 vcl_recv 中為 HTTP 請求設定路由,如果希望基于“虛擬主機”做路由,那么就對 req.http.host 變量做檢查,比如:
sub vcl_recv {
if (req.http.host ~ "foo.com") {
set req.backend_hint = foo;
} elsif (req.http.host ~ "bar.com") {
set req.backend_hint = bar;
}
}
第一個正則表達式,能匹配 "foo.com", "www.foo.com", "zoop.foo.com",以及其他以 "foo.com" 結尾的主機名。
也可以使用 == 進行準確的匹配:
sub vcl_recv {
if (req.http.host == "foo.com" || req.http.host == "www.foo.com") {
set req.backend_hint = foo;
}
}
調度器
我們可以將多個后端服務器組成一組,Varnish 中的組被稱為 directors。將多個后端服務器組成一組可以提高性能、容錯性和伸縮性。
我們定義多個后端服務器,然后組成一個組。這要求你加載一個 VMOD:directors 模塊。然后在 vcl_init 中調用一些 actions:
import directors; # load the directors
backend server1 {
.host = "192.168.0.10";
}
backend server2 {
.host = "192.168.0.11";
}
sub vcl_init {
new bar = directors.round_robin();
bar.add_backend(server1);
bar.add_backend(server2);
}
sub vcl_recv {
# send all traffic to the bar director:
set req.backend_hint = bar.backend();
}
這個 director 是一個輪詢調度器。另外還有一個隨機調度器。如果一個后端服務器失效,Varnish 可以檢查到,然后不再調度請求給該后端服務器。
健康檢查
我們來設置一個包含兩個后端服務器的 director,并且設置健康檢查,首先我們定義后端服務器:
backend server1 {
.host = "server1.example.com";
.probe = {
.url = "/";
.timeout = 1s;
.interval = 5s;
.window = 5;
.threshold = 3;
}
}
backend server2 {
.host = "server2.example.com";
.probe = {
.url = "/";
.timeout = 1s;
.interval = 5s;
.window = 5;
.threshold = 3;
}
}
probe 引入了健康檢查。此例中,Varnish 每 5 秒進行一次檢查,檢查超時為 1 秒。如果最近 5 次檢查中,有 3+ 次成功,該后端被標記為 “healthy”,否則被標記為 “sick”。發起檢查,其實是對后端服務器地址 http://host/ 發起一個 GET 請求.
更多信息可參考 Probes 。
我們現在來定義 director:
import directors;
sub vcl_init {
new vdir = directors.round_robin();
vdir.add_backend(server1);
vdir.add_backend(server2);
}
現在可以使用 vdir 作為 backend_hint,參考上一小節的描述,比如:
sub vcl_recv {
# send all traffic to the vdir director:
set req.backend_hint = vdir.backend();
}
Varnish 不會將流量調度給被認為不健康的后端主機。
如果所有后端服務器失效了,Varnish 可以返回 stale content(陳舊的內容)。參考 Misbehaving servers
請注意,Varnish 對所有加載的 VCL 進行檢查,而且會將相同的檢查進行合并。所以如果你加載了很多的 VCL,注意不要修改 probe 配置。卸載 VCL 之后,檢查不會繼續,更多信息請求參考:ref:reference-vcl-director
Hashing
Varnish 對內容做緩存時,也要存下對應的 hash key。在默認的情況下,hash key 是對 Host 首部字段的值或者 IP 地址做 hash 計算,并且對 URL 做 hash 計算,得到的值,為 hash key。
the default vcl:
sub vcl_hash {
hash_data(req.url);
if (req.http.host) {
hash_data(req.http.host);
} else {
hash_data(server.ip);
}
return (lookup);
}
在默認 vcl_hash 中,首先對 URL 計算 hash 值,然后繼續對 Host 字段或者 IP地址計算 hash 值。
瀏覽器一般會把 hostnames 轉為小寫,Varnish 不做這種轉換,所以在建立 hash key 時,對 hostnames 和 URL 的大小寫是敏感的。所以理論上來說,"Varnish.org/" and "varnish.org/" 計算出的 hash key 是不同的。
這里對 hash 值的計算是逐步累加的,所以你可以在 hash 計算中加入特定測試區域的值,然后基于該測試區域的值,可以提供不同的緩存內容。
比如,通過客戶端的 IP 地址獲知其所在的國家,基于這個提供不同的語言的網站頁面(需要加載一些 VMOD 獲知國家的代碼),比如:
In vcl_recv:
set req.http.X-Country-Code = geoip.lookup(client.ip);
And then add a vcl_hash:
sub vcl_hash {
hash_data(req.http.X-Country-Code);
}
vcl_* 系列子例程中的默認代碼會在執行完我們自己編寫的代碼后自動執行,所以在 vcl_hash 中,默認情況下,VCL 會自動對 host 和 URL 計算 hash 值,這部分不需要我們去做。
但要注意,不要輕易調用 return(lookup),因為它會終止默認 VCL 的執行,可能會導致 hash key 計算時,沒有加入 host 和 URL。
不工作的后端服務器
Varnish 可以屏蔽不正常的 web/應用 服務器。
優雅模式
當有多個客戶端請求同一個頁面時,Varnish 只對 backend 發起一個請求。有的產品稱之為“請求合并”。
如果你的站點每秒有幾千的點擊量,請求的等待隊列可能變得相當的長。這可能導致兩個問題,一是“驚群效益” - 突然開啟一千個線程,使系統負載一下子到達上限;二是 - 沒人愿意等待,人的耐心是有限的。面對這個問題,Varnish 可做的是,即使緩存對象超時(TTL)也不更新,向用戶提供過期的服務頁面。
所以,要提供過期內容,我們可以這樣設置,當緩存對象達到超時時間(TTL),讓其在緩存中多待 2 分鐘,如下:
sub vcl_backend_response {
set beresp.grace = 2m;
}
這樣設置以后,Varnish 允許在緩存對象超時的 2 分鐘之內,繼續返回已經過期的緩存對象。而且雖然 Varnish 返回過期對象,它會第一時間將更新納入計劃中,等到 2 分鐘后,更新會異步進行。
你也可以對默認的處理邏輯進行一定的修改,在 vcl_hit 中定義:
sub vcl_hit {
if (obj.ttl >= 0s) {
// A pure unaltered hit, deliver it 緩存對象沒有過期
return (deliver);
}
if (obj.ttl + obj.grace > 0s) {
// Object is in grace, deliver it 緩存對象過期了,但在 grace mode 的時間范圍內
// Automatically triggers a background fetch 自動觸發一個對上游服務器的 fetch 動作
return (deliver);
}
// fetch & deliver once we get the result 超出了 grace mode 的時間,對上游服務器發起 fetch 動作,并且更新緩存對象
return (fetch);
}
這里,優雅模式的邏輯是很清晰的。
如果你激活了健康檢查,你可以知道一個 backend 是否是正常的,你可以在確認了 backend 是正常的之后,再使用 優雅模式 。我們把第二個 if 語句替換為:
if (!std.healthy(req.backend_hint) && (obj.ttl + obj.grace > 0s)) {
return (deliver);
} else {
return (fetch);
}
所以,總的來說,優雅模式解決了兩個問題:
- 返回過期的緩存對象,以避免等待隊列過長
- 對于是否返回過期對象,可由你進行控制
使用 inline C to 擴展 Varnish
(Here there be dragons. Big and mean ones.) 一般不建議使用 inline C,所以這里就不翻譯了。
You can use inline C to extend Varnish. Please note that you can seriously mess up Varnish this way. The C code runs within the Varnish Cache process so if your code generates a segfault the cache will crash.
One of the first uses of inline C was logging to syslog.:
# The include statements must be outside the subroutines.
C{
#include <syslog.h>
}C
sub vcl_something {
C{
syslog(LOG_INFO, "Something happened at VCL line XX.");
}C
}
To use inline C you need to enable it with the vcc_allow_inline_c parameter.
VCL Examples
修改請求首部:
例如,對于 /images 目錄的請求,我們希望移除請求中的 cookie:
sub vcl_recv {
if (req.url ~ "^/images") {
unset req.http.cookie;
}
}
這樣做以后,如果請求的 URI 是以 /images 起始的,那么請求的 "Cookie:" 首部將被移除。
修改后端響應
對于后端的圖片類型的響應,我們在 vcl_backend_response 中做進一步處理:
- 刪除 “Set-Cookie” 首部,這是為了避免創建 hit-for-pass 對象,它不會被緩存
- 設置 TTL 時間為 1h
sub vcl_backend_response {
if (bereq.url ~ "\.(png|gif|jpg)$") {
unset beresp.http.set-cookie;
set beresp.ttl = 1h;
}
}
訪問控制列表 ACLs
訪問控制列表的創建和使用:
# Who is allowed to purge.... 允許誰進行 PURGE 操作
acl local {
"localhost";
"192.168.1.0"/24; /* and everyone on the local network */
! "192.168.1.23"; /* except for the dialin router */
}
sub vcl_recv {
if (req.method == "PURGE") {
if (client.ip ~ local) {
return(purge);
} else {
return(synth(403, "Access denied."));
}
}
}
Implementing websocket support
Websockets 技術,是在 HTTP 之上創建一個雙向的流式通道。
要通過 Varnish 運行 websockets,需要使用 pipe,并且復制 Upgrade 首部:
sub vcl_pipe {
if (req.http.upgrade) {
set bereq.http.upgrade = req.http.upgrade;
}
}
sub vcl_recv {
if (req.http.Upgrade ~ "(?i)websocket") {
return (pipe);
}
}
設備檢測
所謂設備檢測,是通過請求中的 User-Agent 首部判斷客戶端使用的設備類型。
使用場景舉例:給小屏幕的移動端客戶(一般也是高延遲的網絡)發送 size reduced files,或者提供客戶端能識別的 streaming video codec。
對這種使用場景,有幾種策略可以使用:
- 重定向至另一個 URL
- 對于特定的客戶端使用不同的 backend
- 修改對 backend 發起的請求,使 backend 發送裁剪過的內容
為了講清楚如何使用這些策略,我們假設存在一個首部名為 X-UA-Device(req.http.X-UA-Device),它可以區分不同的客戶端類型:
設置該首部是很簡單的:
sub vcl_recv {
if (req.http.User-Agent ~ "(?i)iphone" {
set req.http.X-UA-Device = "mobile-iphone";
}
}
對于識別客戶端,Varnish 提供了商業的和免費的方案。社區的免費方案是根據正則來做,請參考: https://github.com/varnish/varnish-devicedetect/.
1. 基于同一個 URL 提供不同的服務內容
基于同一個 URL 提供不同的服務內容,其中涉及的要點如下:
- 對客戶端進行檢測(非常簡單,只需要 include devicedetect.vcl 并調用它)
- 如何告訴 backend 客戶端的類型?舉例來說,可以設置一個 header,或者改變一個 header,甚至改變 backend 請求的 URL
- 對 backend 返回的響應進行修改,添加 "Vary" header,這樣 Varnish 會啟動內部的處理
- 在最后階段修改發送給客戶端的響應,so any caches outside our control don't serve the wrong content
這些都需要去做,而且要保證,對于每一個 URL,每一個設備類型,只對應一個緩存對象。
Example 1: 發送 HTTP header 給 backend
The basic case is that Varnish adds the 'X-UA-Device' HTTP header on the backend requests, and the backend mentions in the response 'Vary' header that the content is dependant on this header.
Everything works out of the box from Varnish' perspective.
VCL:
sub vcl_recv {
# call some detection engine that set req.http.X-UA-Device
}
# req.http.X-UA-Device is copied by Varnish into bereq.http.X-UA-Device
# so, this is a bit counterintuitive. The backend creates content based on
# the normalized User-Agent, but we use Vary on X-UA-Device so Varnish will
# use the same cached object for all U-As that map to the same X-UA-Device.
#
# If the backend does not mention in Vary that it has crafted special
# content based on the User-Agent (==X-UA-Device), add it.
# If your backend does set Vary: User-Agent, you may have to remove that here.
sub vcl_backend_response {
if (bereq.http.X-UA-Device) {
if (!beresp.http.Vary) { # no Vary at all
set beresp.http.Vary = "X-UA-Device";
} elseif (beresp.http.Vary !~ "X-UA-Device") { # add to existing Vary
set beresp.http.Vary = beresp.http.Vary + ", X-UA-Device";
}
}
# comment this out if you don't want the client to know your
# classification
set beresp.http.X-UA-Device = bereq.http.X-UA-Device;
}
# to keep any caches in the wild from serving wrong content to client #2
# behind them, we need to transform the Vary on the way out.
sub vcl_deliver {
if ((req.http.X-UA-Device) && (resp.http.Vary)) {
set resp.http.Vary = regsub(resp.http.Vary, "X-UA-Device", "User-Agent");
}
}
Example 2: Normalize the User-Agent string
Another way of signalling the device type is to override or normalize the 'User-Agent' header sent to the backend.
For example:
User-Agent: Mozilla/5.0 (Linux; U; Android 2.2; nb-no; HTC Desire Build/FRF91) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1
becomes:
User-Agent: mobile-android
when seen by the backend.
This works if you don't need the original header for anything on the backend. A possible use for this is for CGI scripts where only a small set of predefined headers are (by default) available for the script.
VCL:
sub vcl_recv {
# call some detection engine that set req.http.X-UA-Device
}
# override the header before it is sent to the backend
sub vcl_miss { if (req.http.X-UA-Device) { set bereq.http.User-Agent = req.http.X-UA-Device; } }
sub vcl_pass { if (req.http.X-UA-Device) { set bereq.http.User-Agent = req.http.X-UA-Device; } }
# standard Vary handling code from previous examples.
sub vcl_backend_response {
if (bereq.http.X-UA-Device) {
if (!beresp.http.Vary) { # no Vary at all
set beresp.http.Vary = "X-UA-Device";
} elseif (beresp.http.Vary !~ "X-UA-Device") { # add to existing Vary
set beresp.http.Vary = beresp.http.Vary + ", X-UA-Device";
}
}
set beresp.http.X-UA-Device = bereq.http.X-UA-Device;
}
sub vcl_deliver {
if ((req.http.X-UA-Device) && (resp.http.Vary)) {
set resp.http.Vary = regsub(resp.http.Vary, "X-UA-Device", "User-Agent");
}
}
Example 3: Add the device class as a GET query parameter
If everything else fails, you can add the device type as a GET argument.
http://example.com/article/1234.html --> http://example.com/article/1234.html?devicetype=mobile-iphone
The client itself does not see this classification, only the backend request is changed.
VCL:
sub vcl_recv {
# call some detection engine that set req.http.X-UA-Device
}
sub append_ua {
if ((req.http.X-UA-Device) && (req.method == "GET")) {
# if there are existing GET arguments;
if (req.url ~ "\?") {
set req.http.X-get-devicetype = "&devicetype=" + req.http.X-UA-Device;
} else {
set req.http.X-get-devicetype = "?devicetype=" + req.http.X-UA-Device;
}
set req.url = req.url + req.http.X-get-devicetype;
unset req.http.X-get-devicetype;
}
}
# do this after vcl_hash, so all Vary-ants can be purged in one go. (avoid ban()ing)
sub vcl_miss { call append_ua; }
sub vcl_pass { call append_ua; }
# Handle redirects, otherwise standard Vary handling code from previous
# examples.
sub vcl_backend_response {
if (bereq.http.X-UA-Device) {
if (!beresp.http.Vary) { # no Vary at all
set beresp.http.Vary = "X-UA-Device";
} elseif (beresp.http.Vary !~ "X-UA-Device") { # add to existing Vary
set beresp.http.Vary = beresp.http.Vary + ", X-UA-Device";
}
# if the backend returns a redirect (think missing trailing slash),
# we will potentially show the extra address to the client. we
# don't want that. if the backend reorders the get parameters, you
# may need to be smarter here. (? and & ordering)
if (beresp.status == 301 || beresp.status == 302 || beresp.status == 303) {
set beresp.http.location = regsub(beresp.http.location, "[?&]devicetype=.*$", "");
}
}
set beresp.http.X-UA-Device = bereq.http.X-UA-Device;
}
sub vcl_deliver {
if ((req.http.X-UA-Device) && (resp.http.Vary)) {
set resp.http.Vary = regsub(resp.http.Vary, "X-UA-Device", "User-Agent");
}
}
Different backend for mobile clients
If you have a different backend that serves pages for mobile clients, or any special needs in VCL, you can use the 'X-UA-Device' header like this:
backend mobile {
.host = "10.0.0.1";
.port = "80";
}
sub vcl_recv {
# call some detection engine
if (req.http.X-UA-Device ~ "^mobile" || req.http.X-UA-device ~ "^tablet") {
set req.backend_hint = mobile;
}
}
sub vcl_hash {
if (req.http.X-UA-Device) {
hash_data(req.http.X-UA-Device);
}
}
Redirecting mobile clients
If you want to redirect mobile clients you can use the following snippet.
VCL:
sub vcl_recv {
# call some detection engine
if (req.http.X-UA-Device ~ "^mobile" || req.http.X-UA-device ~ "^tablet") {
return(synth(750, "Moved Temporarily"));
}
}
sub vcl_synth {
if (obj.status == 750) {
set obj.http.Location = "http://m.example.com" + req.url;
set obj.status = 302;
return(deliver);
}
}