3. VCL - Varnish 配置語(yǔ)言詳解

本文編譯自:
users-guide

本節(jié)講述如何使用 VCL 編寫(xiě)處理 HTTP 流量的策略。

Varnish 的配置系統(tǒng)與其他軟件不同,一般的配置是使用配置指令,用于打開(kāi)或關(guān)閉某個(gè)配置項(xiàng)。而 Varnish 使用 VCL 語(yǔ)言進(jìn)行配置。

每一個(gè)入站請(qǐng)求流經(jīng) Varnish 時(shí),會(huì)被 VCL 定義的策略處理。你可以通過(guò)修改 VCL 代碼,改變對(duì)請(qǐng)求的處理。Varnish 不僅僅是一個(gè)緩存,它還是一個(gè)強(qiáng)大的 HTTP 處理器,你可以對(duì) HTTP 請(qǐng)求做如下的事情:

  • 將某些請(qǐng)求調(diào)度至某些后端服務(wù)器
  • 對(duì)請(qǐng)求和響應(yīng)進(jìn)行修改
  • 根據(jù)請(qǐng)求和響應(yīng)的任意屬性,做特定的操作

VCL 被編譯成二進(jìn)制代碼,并在請(qǐng)求到來(lái)時(shí)被執(zhí)行。VCL 對(duì)性能的影響可忽略不計(jì)。

VCL 文件組成 subroutines,不同的 subroutines 在不同的時(shí)候被執(zhí)行。比如有一個(gè) subroutine 是當(dāng)接收到請(qǐng)求時(shí)執(zhí)行,另一個(gè)是當(dāng)從后端服務(wù)器獲取到響應(yīng)時(shí)執(zhí)行。

如果在你的 subroutine 中不調(diào)用 action,該 subroutine 執(zhí)行完畢之后,Varnish 會(huì)執(zhí)行內(nèi)建的 VCL 代碼。這些代碼在 builtin.vcl 文件中,它們被注釋了。

目錄:

  • VCL 語(yǔ)法
  • 內(nèi)建的 subroutines
  • Request and response VCL objects
  • actions
  • 后端服務(wù)器
  • 多個(gè)后端服務(wù)器
  • Varnish 中的后端服務(wù)器和虛擬主機(jī)
  • 調(diào)度器
  • 健康檢查
  • Hashing
  • 不工作的后端服務(wù)器
  • 使用 inline C to 擴(kuò)展 Varnish
  • VCL Examples
  • 設(shè)備檢測(cè)

VCL 語(yǔ)法


VCL 的語(yǔ)法從 C 語(yǔ)言繼承了很多,所以它讀起來(lái)像簡(jiǎn)單的 C 或者 Perl。代碼段被花括號(hào)括起來(lái),語(yǔ)句以分號(hào)結(jié)尾,注釋的方式可以是 C,C++,Perl 中的注釋方式,可隨意選擇你習(xí)慣的注釋語(yǔ)法。但 VCL 不支持循環(huán)和跳轉(zhuǎn)語(yǔ)句。

本節(jié)介紹 VCL 語(yǔ)法中較為重要的部分,詳細(xì)的介紹參考 reference 文檔。

Strings


基本的字符串被雙引號(hào)引用,比如 " ... ",其中不可包含換行符。\ 沒(méi)有轉(zhuǎn)義的特殊含義,比如在 regsub() 中,你不需要數(shù) \ 的個(gè)數(shù):

regsub("barf", "(b)(a)(r)(f)", "\4\3\2p") -> "frap"

長(zhǎng)字符串被 {" ... "} 引用,其中可包含任何字符,包括 " 字符,換行符及其它控制字符,除了 NUL (0x00) 字符。如果你真的想要把 NUL 字符放入字符串中,有一個(gè) VMOD 模塊可以創(chuàng)建這樣的字符串。

Access control lists (ACLs)


訪(fǎng)問(wèn)控制列表 ACLs。

聲明一個(gè) ACL,可以創(chuàng)建及初始化一個(gè)訪(fǎng)問(wèn)控制列表,它有一個(gè)名字,它可以被用于匹配 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
}

如果一個(gè) ACL entry 指定了一個(gè)不能解析的主機(jī)名,那么這個(gè) ACL 列表與任何地址都可以匹配。如果這個(gè)不能被解析的主機(jī)名前面有一個(gè) ! 符號(hào),它會(huì)拒絕每一個(gè)與它進(jìn)行比較的地址,這可能不是你希望的效果。如果把一個(gè) ACL entry 放入圓括號(hào) () 中,它將被忽略。

將一個(gè) IP 地址與一個(gè) ACL 進(jìn)行匹配,使用 ~ 運(yùn)算符:

if (client.ip ~ local) {
  return (pipe);
}

Operators


在 VCL 中可以使用的操作符:

=   賦值
==  等于
~   匹配,用于 regular expressions 或 ACLs 
!   邏輯非.
&&  邏輯與
||  邏輯或

Subroutines


subroutine 可被稱(chēng)為子例程。

一個(gè) subroutine 是一組代碼,可被重用,下面是一個(gè)定義 subroutine 的示例:

sub pipe_if_local {
  if (client.ip ~ local) {
    return (pipe);
  }
}

在 VCL 中,subroutine 不可接受參數(shù),也沒(méi)有返回值。

調(diào)用 subroutine,使用 call 關(guān)鍵字:

call pipe_if_local;

Varnish 有許多內(nèi)置的 subroutines,當(dāng) transaction 流經(jīng) Varnish 時(shí),它們將被調(diào)用。內(nèi)置的 subroutine 命名為 vcl_*,你自己定義的 subroutine 不可以命名為以 vcl_ 起始的名字。

內(nèi)建的 subroutines


Varnish 處理 client 的請(qǐng)求和后端服務(wù)器的響應(yīng)時(shí),會(huì)調(diào)用多個(gè)內(nèi)置的 subroutines 進(jìn)行處理。通過(guò) CLI 執(zhí)行 vcl.load 和 vcl.discard 時(shí),也會(huì)調(diào)用內(nèi)置的 subroutines。

下面對(duì)前端(client-side)和后端(backend-side)的處理分別進(jìn)行介紹:

1.client-side


1.1 vcl_recv


vcl_recv 被調(diào)用的時(shí)機(jī)是:

  • 當(dāng)一個(gè)請(qǐng)求接收完成,并且被解析之后
  • 重啟之后
  • as the result of an ESI include

vcl_recv 子例程用于決定:是否對(duì)一個(gè)請(qǐng)求提供服務(wù),它很可能會(huì)對(duì)請(qǐng)求進(jìn)行修改,然后再?zèng)Q定如何做進(jìn)一步處理。

A backend hint may be set as a default for the backend processing side.

vcl_recv 子例程可使用 return() 結(jié)合下面的其中一個(gè)關(guān)鍵字進(jìn)行終止:

hash
    請(qǐng)求的對(duì)象被認(rèn)為是一個(gè)可能被緩存的對(duì)象,將繼續(xù)對(duì)其進(jìn)行處理。將控制權(quán)轉(zhuǎn)交給 vcl_hash 子例程。

pass
    轉(zhuǎn)換至 pass 模式??刂茩?quán)最終交給 vcl_pass 子例程。

pipe
    轉(zhuǎn)換至 pipe 模式??刂茩?quán)最終交給 vcl_pipe 子例程。

synth(status code, reason)
    轉(zhuǎn)移到 vcl_synth 子例程,synth() 的參數(shù)值被預(yù)置為 resp.status 和 resp.reason

purge
    清除請(qǐng)求的對(duì)象,以及它的變量(variants)??刂茩?quán)先交給 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


進(jìn)入 pipe 模式時(shí),vcl_pipe 子例程將被調(diào)用。在這個(gè)模式中,請(qǐng)求將被傳遞給后端服務(wù)器,這時(shí) Varnish 會(huì)降級(jí)成為一個(gè) TCP 代理,只充當(dāng)一個(gè)數(shù)據(jù)流的通道,不會(huì)對(duì)數(shù)據(jù)進(jìn)行任何修改,當(dāng) client 或 server 端決定關(guān)閉連接時(shí),該模式結(jié)束。在調(diào)用 vcl_pipe 之后,對(duì)于一個(gè)處于 pipe 模式的連接,其他任何的 VCL 子例程都不會(huì)被調(diào)用。

vcl_pipe 子例程可使用 return() 結(jié)合下面的其中一個(gè)關(guān)鍵字進(jìn)行終止:

pipe
    繼續(xù)以 pipe mode 運(yùn)行

synth(status code, reason)
    轉(zhuǎn)移到 vcl_synth 子例程,synth() 的參數(shù)值被預(yù)置為 resp.status 和 resp.reason

1.3 vcl_pass


在進(jìn)入 pass 模式時(shí),vcl_pass 將被調(diào)用,請(qǐng)求被轉(zhuǎn)發(fā)給后端服務(wù)器,后端服務(wù)器的響應(yīng)被轉(zhuǎn)發(fā)給 client,但是響應(yīng)不會(huì)被緩存。來(lái)自該 client 連接的后續(xù)請(qǐng)求,將被正常處理。

vcl_pass 子例程可使用 return() 結(jié)合下面的其中一個(gè)關(guān)鍵字進(jìn)行終止:

fetch
    繼續(xù)以 pass mode 運(yùn)行 - 發(fā)起一個(gè)對(duì)后端服務(wù)器的請(qǐng)求

restart
    重啟該 transaction。增加 restart 計(jì)數(shù)器的計(jì)數(shù)。如果計(jì)數(shù)超過(guò)了 max_restarts,Varnish 發(fā)出一個(gè)錯(cuò)誤:guru meditation error.

synth(status code, reason)
    轉(zhuǎn)移到 vcl_synth 子例程,synth() 的參數(shù)值被預(yù)置為 resp.status 和 resp.reason

1.4 vcl_hit


當(dāng)緩存查找成功,vcl_hit 將被調(diào)用。緩存對(duì)象可能會(huì)過(guò)期,其 ttl 可能為 0 或者負(fù)數(shù),with only grace or keep time left.

vcl_hit 子例程可使用 return() 結(jié)合下面的其中一個(gè)關(guān)鍵字進(jìn)行終止:

deliver
    發(fā)送該對(duì)象。如果該對(duì)象過(guò)期,將觸發(fā)一個(gè) fetch 調(diào)用,更新該對(duì)象。

fetch
    盡管緩存命中,但是會(huì)同步地從后端服務(wù)器更新緩存對(duì)象??刂茩?quán)最終轉(zhuǎn)交給 vcl_miss。

pass
    轉(zhuǎn)換至 pass 模式??刂茩?quán)最終交給 vcl_pass 子例程。

restart
    重啟該 transaction。增加 restart 計(jì)數(shù)器的計(jì)數(shù)。如果計(jì)數(shù)超過(guò)了 max_restarts,Varnish 發(fā)出一個(gè)錯(cuò)誤:guru meditation error.

synth(status code, reason)
    轉(zhuǎn)移到 vcl_synth 子例程,synth() 的參數(shù)值被預(yù)置為 resp.status 和 resp.reason

1.5 vcl_miss


當(dāng)緩存查找失敗,或者當(dāng) vcl_hit 返回一個(gè) fetch 時(shí),調(diào)用 vcl_miss。

vcl_miss 用于決定是否嘗試從后端服務(wù)器獲取文件。

A backend hint may be set as a default for the backend processing side.

vcl_miss 子例程可使用 return() 結(jié)合下面的其中一個(gè)關(guān)鍵字進(jìn)行終止:

fetch
    從后端服務(wù)器獲取請(qǐng)求的對(duì)象??刂茩?quán)最終轉(zhuǎn)交給 vcl_backend_fetch。

pass
    轉(zhuǎn)換至 pass 模式??刂茩?quán)最終交給 vcl_pass 子例程。

restart
    重啟該 transaction。增加 restart 計(jì)數(shù)器的計(jì)數(shù)。如果計(jì)數(shù)超過(guò)了 max_restarts,Varnish 發(fā)出一個(gè)錯(cuò)誤:guru meditation error.

synth(status code, reason)
    轉(zhuǎn)移到 vcl_synth 子例程,synth() 的參數(shù)值被預(yù)置為 resp.status 和 resp.reason

1.6 vcl_hash


當(dāng) vcl_recv 為請(qǐng)求創(chuàng)建了一個(gè) hash 值時(shí)被調(diào)用。使用該值作為 key 進(jìn)行緩存查找。

vcl_hash 子例程只能以 return(lookup) 終止:

lookup
    在緩存中查找請(qǐng)求的對(duì)象。如果從 vcl_recv 返回 return(purge),控制權(quán)轉(zhuǎn)交給 vcl_purge。
    否則,如果緩存查找的結(jié)果是 hit,控制權(quán)轉(zhuǎn)交給 vcl_hit;如果緩存查找的結(jié)果是 miss,控制權(quán)轉(zhuǎn)交給 vcl_miss;
    如果緩存查找的結(jié)果是 hit on a hit-for-pass 對(duì)象 (object with obj.uncacheable == true),控制權(quán)轉(zhuǎn)交給 vcl_pass。

1.7 vcl_purge


執(zhí)行 purge 之后,vcl_purge 被調(diào)用,緩存對(duì)象被清除(失效),其所有變量(variants)將被回避。

vcl_purge 子例程可使用 return() 結(jié)合下面的其中一個(gè)關(guān)鍵字進(jìn)行終止:

restart
    重啟該 transaction。增加 restart 計(jì)數(shù)器的計(jì)數(shù)。如果計(jì)數(shù)超過(guò)了 max_restarts,Varnish 發(fā)出一個(gè)錯(cuò)誤:guru meditation error.

synth(status code, reason)
    轉(zhuǎn)移到 vcl_synth 子例程,synth() 的參數(shù)值被預(yù)置為 resp.status 和 resp.reason

1.8 vcl_deliver


發(fā)送對(duì)象給客戶(hù)端前調(diào)用,除了將一個(gè) vcl_synth 結(jié)果發(fā)送給客戶(hù)端時(shí)不會(huì)調(diào)用。

vcl_deliver 子例程可使用 return() 結(jié)合下面的其中一個(gè)關(guān)鍵字進(jìn)行終止:

deliver
    發(fā)送對(duì)象給 client

restart
    重啟該 transaction。增加 restart 計(jì)數(shù)器的計(jì)數(shù)。如果計(jì)數(shù)超過(guò)了 max_restarts,Varnish 發(fā)出一個(gè)錯(cuò)誤:guru meditation error.

synth(status code, reason)
    轉(zhuǎn)移到 vcl_synth 子例程,synth() 的參數(shù)值被預(yù)置為 resp.status 和 resp.reason

1.9 vcl_synth


調(diào)用 vcl_synth 可以發(fā)送一個(gè) synthetic 對(duì)象給客戶(hù)端。synthetic 對(duì)象由 VCL 生成,不是從后端獲取的。可使用 synthetic() 函數(shù)構(gòu)造 synthetic 對(duì)象。

vcl_synth 定義了一個(gè)對(duì)象,該對(duì)象不會(huì)被緩存,與其相反,vcl_backend_error 所定義的對(duì)象可能最終被緩存。

vcl_synth 子例程可使用 return() 結(jié)合下面的其中一個(gè)關(guān)鍵字進(jìn)行終止:

deliver
    直接將 vcl_synth 定義的對(duì)象發(fā)送給客戶(hù)端,不調(diào)用 vcl_deliver

restart
    重啟該 transaction。增加 restart 計(jì)數(shù)器的計(jì)數(shù)。如果計(jì)數(shù)超過(guò)了 max_restarts,
    Varnish 發(fā)出一個(gè)錯(cuò)誤:guru meditation error.

2. backend-side


2.1 vcl_backend_fetch


對(duì)后端服務(wù)器發(fā)送請(qǐng)求時(shí)調(diào)用 vcl_backend_fetch。在這個(gè)子例程中,我們一般會(huì)修改請(qǐng)求,然后才發(fā)送給后端服務(wù)器。

vcl_backend_fetch 子例程可使用 return() 結(jié)合下面的其中一個(gè)關(guān)鍵字進(jìn)行終止:

fetch
    從后端服務(wù)器獲取對(duì)象

abandon
    放棄對(duì)后端發(fā)起請(qǐng)求。除非后端請(qǐng)求是一個(gè) background fetch,否則控制權(quán)將被轉(zhuǎn)交給 client-side 的 vcl_synth,
    其 resp.status 被設(shè)置為 503。

2.2 vcl_backend_response


當(dāng)成功從后端服務(wù)器獲取到 response headers 時(shí),調(diào)用 vcl_backend_response。

vcl_backend_response 子例程可使用 return() 結(jié)合下面的其中一個(gè)關(guān)鍵字進(jìn)行終止:

deliver
    對(duì)于一個(gè) 304 響應(yīng),創(chuàng)建一個(gè)更新的緩存對(duì)象。否則,從后端獲取對(duì)象的 body,然后發(fā)起  delevery 返回給客戶(hù)端。
    很可能是并行的(streaming)

abandon
    放棄對(duì)后端發(fā)起請(qǐng)求。除非后端請(qǐng)求是一個(gè) background fetch,否則控制權(quán)將被轉(zhuǎn)交給 client-side 的 vcl_synth,
    其 resp.status 被設(shè)置為 503。
        
retry
    重試發(fā)起 backend transaction。增加重試計(jì)數(shù),如果重試次數(shù)超過(guò) max_retries,控制權(quán)轉(zhuǎn)交給 vcl_backend_error

2.3 vcl_backend_error


當(dāng)嘗試從后端獲取對(duì)象失敗,或則重試次數(shù)超過(guò) max_retries 時(shí),vcl_backend_error 將被調(diào)用。

VCL 生成一個(gè) synthetic 對(duì)象,可使用 synthetic() 函數(shù)構(gòu)造 synthetic 對(duì)象的 body。

vcl_backend_error 子例程可使用 return() 結(jié)合下面的其中一個(gè)關(guān)鍵字進(jìn)行終止:

deliver
    發(fā)送 vcl_backend_error 定義的對(duì)象,可能的話(huà),緩存該對(duì)象。就如同該對(duì)象是從后端獲取的一般。這也被稱(chēng)為 "backend synth"。

retry
    重試發(fā)起 backend transaction。增加重試計(jì)數(shù),如果重試次數(shù)超過(guò) max_retries,調(diào)用 client-side 的 vcl_synth,
    其 resp.status 被設(shè)置為 503。

3. vcl.load / vcl.discard


3.1 vcl_init


當(dāng)加載 VCL 之后,vcl_init 被調(diào)用。一般用于初始化 VMODs。

vcl_init 子例程可使用 return() 結(jié)合下面的其中一個(gè)關(guān)鍵字進(jìn)行終止:

ok
    正常返回,VCL 繼續(xù)加載

fail
    停止加載這個(gè) VCL

3.2 vcl_fini


當(dāng)一個(gè) VCL 被廢棄,當(dāng)該 VCL 處理完所有請(qǐng)求,調(diào)用 vcl_fini。一般用于清除 VMODs。

vcl_fini 子例程可使用 return() 結(jié)合下面的其中一個(gè)關(guān)鍵字進(jìn)行終止:

ok
    正常返回,VCL 將被廢棄。

Request and response VCL objects


在 VCL 中有一些重要的對(duì)象需要注意,這些對(duì)象可以使用 VCL 語(yǔ)言訪(fǎng)問(wèn)和操控。

req

req 是請(qǐng)求對(duì)象。當(dāng) Varnish 收到請(qǐng)求之后,req 對(duì)象被創(chuàng)建,請(qǐng)求被填入該對(duì)象中。在 vcl_recv 中,你大部分的工作是對(duì)該對(duì)象進(jìn)行的。

bereq

bereq 是后端請(qǐng)求對(duì)象。Varnish 構(gòu)造該對(duì)象,然后發(fā)送給后端服務(wù)器。這個(gè)對(duì)象是基于 req 對(duì)象創(chuàng)建的。

beresp

beresp 是后端響應(yīng)對(duì)象。該對(duì)象包含后端響應(yīng)的 headers。如果要修改一個(gè)來(lái)自后端的響應(yīng),實(shí)際的操作是在 vcl_backend_response 子例程中對(duì) beresp 對(duì)象進(jìn)行修改。

resp

resp 對(duì)象,其內(nèi)容發(fā)送給客戶(hù)端的 HTTP response,必要時(shí),一般在 vcl_deliver 中對(duì)其進(jìn)行修改。

obj

obj 是用于緩存的對(duì)象,它是只讀的。

actions


actions 是在終止一個(gè)內(nèi)置子例程時(shí),配合 return() 使用的,如 return(pass),最常用的 actions 是這些:

pass

當(dāng)你返回 pass,請(qǐng)求將被傳遞給后端服務(wù)器,隨后的響應(yīng)將從后端服務(wù)器傳遞回來(lái)。響應(yīng)不會(huì)被緩存。pass 可從 vcl_recv 中返回。

hash

如果從 vcl_recv 中返回 hash,Varnish 將嘗試從緩存中查找并返回請(qǐng)求的對(duì)象。

pipe .. XXX:What is pipe? benc

如果從 vcl_recv 返回 pipe,將會(huì)進(jìn)入 pipe 模式,Varnish 將前端與客戶(hù)端的連接,以及與后端服務(wù)器的連接合并成一個(gè)數(shù)據(jù)流的通道,Varnish 不對(duì)數(shù)據(jù)做任何修改,只是將數(shù)據(jù)在兩端發(fā)送,所以你的日志是不完整的。

deliver

將對(duì)象發(fā)送給客戶(hù)端。通常是從 vcl_backend_response 中返回 deliver。

restart

重啟對(duì)請(qǐng)求的處理。你可以重啟整個(gè) transaction,對(duì) req 對(duì)象的修改將被保留。

retry

重試對(duì)后端發(fā)起請(qǐng)求。這個(gè) action 可以從 vcl_backend_response 或 vcl_backend_error 中返回。當(dāng)你不喜歡后端返回的響應(yīng)時(shí),可以這樣使用。

后端服務(wù)器


后端服務(wù)器是 Varnish 中的一個(gè)概念,指真正提供內(nèi)容的 web 服務(wù)器。它們也被稱(chēng)為 “origin” server 或 “upstream” server。后端 web 服務(wù)器通過(guò) Varnish 的緩存功能對(duì)訪(fǎng)問(wèn)進(jìn)行加速。

我們來(lái)編輯配置文件,如果是編譯安裝,路徑為 /usr/local/etc/varnish/default.vcl,如果是軟件包安裝,路徑為 /etc/varnish/default.vcl。

其中多數(shù)被注釋了,其中可能有如下內(nèi)容:

vcl 4.0;

backend default {
    .host = "127.0.0.1";
    .port = "8080";
}

這里定義了一個(gè)后端服務(wù)器,名為 default。當(dāng) Varnish 需要從后端獲取內(nèi)容,它會(huì)連接到 127.0.0.1:8080。

Varnish 可定義多個(gè)后端服務(wù)器,并且可以聯(lián)合多個(gè)后端服務(wù)器為一個(gè)后端集群,進(jìn)行負(fù)載均衡。

多個(gè)后端服務(wù)器


在上面的基礎(chǔ)上,我們引入一個(gè) java 服務(wù)器:

backend java {
    .host = "127.0.0.1";
    .port = "8000";
}

在 vcl_recv 中,我們根據(jù)請(qǐng)求的 URL,給請(qǐng)求打上不同的 hint,使其發(fā)往指定的后端服務(wù)器。

sub vcl_recv {
    if (req.url ~ "^/java/") {
        set req.backend_hint = java;
    } else {
        set req.backend_hint = default;
    }
}

如果要將移動(dòng)設(shè)備發(fā)出的請(qǐng)求,發(fā)給專(zhuān)門(mén)的后端服務(wù)器,可以做這樣的判斷:

if (req.http.User-agent ~ /mobile/) .. 

Varnish 中的后端服務(wù)器和虛擬主機(jī)


Varnish 完全支持虛擬主機(jī)。

我們?cè)?vcl_recv 中為 HTTP 請(qǐng)求設(shè)定路由,如果希望基于“虛擬主機(jī)”做路由,那么就對(duì) 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;
    }
}

第一個(gè)正則表達(dá)式,能匹配 "foo.com", "www.foo.com", "zoop.foo.com",以及其他以 "foo.com" 結(jié)尾的主機(jī)名。

也可以使用 == 進(jìn)行準(zhǔn)確的匹配:

sub vcl_recv {
    if (req.http.host == "foo.com" || req.http.host == "www.foo.com") {
        set req.backend_hint = foo;
    }
}

調(diào)度器


我們可以將多個(gè)后端服務(wù)器組成一組,Varnish 中的組被稱(chēng)為 directors。將多個(gè)后端服務(wù)器組成一組可以提高性能、容錯(cuò)性和伸縮性。

我們定義多個(gè)后端服務(wù)器,然后組成一個(gè)組。這要求你加載一個(gè) VMOD:directors 模塊。然后在 vcl_init 中調(diào)用一些 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();
}

這個(gè) director 是一個(gè)輪詢(xún)調(diào)度器。另外還有一個(gè)隨機(jī)調(diào)度器。如果一個(gè)后端服務(wù)器失效,Varnish 可以檢查到,然后不再調(diào)度請(qǐng)求給該后端服務(wù)器。

健康檢查


我們來(lái)設(shè)置一個(gè)包含兩個(gè)后端服務(wù)器的 director,并且設(shè)置健康檢查,首先我們定義后端服務(wù)器:

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 秒進(jìn)行一次檢查,檢查超時(shí)為 1 秒。如果最近 5 次檢查中,有 3+ 次成功,該后端被標(biāo)記為 “healthy”,否則被標(biāo)記為 “sick”。發(fā)起檢查,其實(shí)是對(duì)后端服務(wù)器地址 http://host/ 發(fā)起一個(gè) GET 請(qǐng)求.

更多信息可參考 Probes

我們現(xiàn)在來(lái)定義 director:

import directors;

sub vcl_init {
    new vdir = directors.round_robin();
    vdir.add_backend(server1);
    vdir.add_backend(server2);
}

現(xiàn)在可以使用 vdir 作為 backend_hint,參考上一小節(jié)的描述,比如:

sub vcl_recv {
    # send all traffic to the vdir director:
    set req.backend_hint = vdir.backend();
}

Varnish 不會(huì)將流量調(diào)度給被認(rèn)為不健康的后端主機(jī)。

如果所有后端服務(wù)器失效了,Varnish 可以返回 stale content(陳舊的內(nèi)容)。參考 Misbehaving servers

請(qǐng)注意,Varnish 對(duì)所有加載的 VCL 進(jìn)行檢查,而且會(huì)將相同的檢查進(jìn)行合并。所以如果你加載了很多的 VCL,注意不要修改 probe 配置。卸載 VCL 之后,檢查不會(huì)繼續(xù),更多信息請(qǐng)求參考:ref:reference-vcl-director

Hashing


Varnish 對(duì)內(nèi)容做緩存時(shí),也要存下對(duì)應(yīng)的 hash key。在默認(rèn)的情況下,hash key 是對(duì) Host 首部字段的值或者 IP 地址做 hash 計(jì)算,并且對(duì) URL 做 hash 計(jì)算,得到的值,為 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);
}

在默認(rèn) vcl_hash 中,首先對(duì) URL 計(jì)算 hash 值,然后繼續(xù)對(duì) Host 字段或者 IP地址計(jì)算 hash 值。

瀏覽器一般會(huì)把 hostnames 轉(zhuǎn)為小寫(xiě),Varnish 不做這種轉(zhuǎn)換,所以在建立 hash key 時(shí),對(duì) hostnames 和 URL 的大小寫(xiě)是敏感的。所以理論上來(lái)說(shuō),"Varnish.org/" and "varnish.org/" 計(jì)算出的 hash key 是不同的。

這里對(duì) hash 值的計(jì)算是逐步累加的,所以你可以在 hash 計(jì)算中加入特定測(cè)試區(qū)域的值,然后基于該測(cè)試區(qū)域的值,可以提供不同的緩存內(nèi)容。

比如,通過(guò)客戶(hù)端的 IP 地址獲知其所在的國(guó)家,基于這個(gè)提供不同的語(yǔ)言的網(wǎng)站頁(yè)面(需要加載一些 VMOD 獲知國(guó)家的代碼),比如:

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_* 系列子例程中的默認(rèn)代碼會(huì)在執(zhí)行完我們自己編寫(xiě)的代碼后自動(dòng)執(zhí)行,所以在 vcl_hash 中,默認(rèn)情況下,VCL 會(huì)自動(dòng)對(duì) host 和 URL 計(jì)算 hash 值,這部分不需要我們?nèi)プ觥?/p>

但要注意,不要輕易調(diào)用 return(lookup),因?yàn)樗鼤?huì)終止默認(rèn) VCL 的執(zhí)行,可能會(huì)導(dǎo)致 hash key 計(jì)算時(shí),沒(méi)有加入 host 和 URL。

不工作的后端服務(wù)器


Varnish 可以屏蔽不正常的 web/應(yīng)用 服務(wù)器。

優(yōu)雅模式


當(dāng)有多個(gè)客戶(hù)端請(qǐng)求同一個(gè)頁(yè)面時(shí),Varnish 只對(duì) backend 發(fā)起一個(gè)請(qǐng)求。有的產(chǎn)品稱(chēng)之為“請(qǐng)求合并”。

如果你的站點(diǎn)每秒有幾千的點(diǎn)擊量,請(qǐng)求的等待隊(duì)列可能變得相當(dāng)?shù)拈L(zhǎng)。這可能導(dǎo)致兩個(gè)問(wèn)題,一是“驚群效益” - 突然開(kāi)啟一千個(gè)線(xiàn)程,使系統(tǒng)負(fù)載一下子到達(dá)上限;二是 - 沒(méi)人愿意等待,人的耐心是有限的。面對(duì)這個(gè)問(wèn)題,Varnish 可做的是,即使緩存對(duì)象超時(shí)(TTL)也不更新,向用戶(hù)提供過(guò)期的服務(wù)頁(yè)面。

所以,要提供過(guò)期內(nèi)容,我們可以這樣設(shè)置,當(dāng)緩存對(duì)象達(dá)到超時(shí)時(shí)間(TTL),讓其在緩存中多待 2 分鐘,如下:

sub vcl_backend_response {
  set beresp.grace = 2m;
}

這樣設(shè)置以后,Varnish 允許在緩存對(duì)象超時(shí)的 2 分鐘之內(nèi),繼續(xù)返回已經(jīng)過(guò)期的緩存對(duì)象。而且雖然 Varnish 返回過(guò)期對(duì)象,它會(huì)第一時(shí)間將更新納入計(jì)劃中,等到 2 分鐘后,更新會(huì)異步進(jìn)行。

你也可以對(duì)默認(rèn)的處理邏輯進(jìn)行一定的修改,在 vcl_hit 中定義:

sub vcl_hit {
   if (obj.ttl >= 0s) {
       // A pure unaltered hit, deliver it 緩存對(duì)象沒(méi)有過(guò)期
       return (deliver);
   }
   if (obj.ttl + obj.grace > 0s) {
       // Object is in grace, deliver it 緩存對(duì)象過(guò)期了,但在 grace mode 的時(shí)間范圍內(nèi)
       // Automatically triggers a background fetch 自動(dòng)觸發(fā)一個(gè)對(duì)上游服務(wù)器的 fetch 動(dòng)作
       return (deliver);
   }
   // fetch & deliver once we get the result 超出了 grace mode 的時(shí)間,對(duì)上游服務(wù)器發(fā)起 fetch 動(dòng)作,并且更新緩存對(duì)象
   return (fetch);
}

這里,優(yōu)雅模式的邏輯是很清晰的。

如果你激活了健康檢查,你可以知道一個(gè) backend 是否是正常的,你可以在確認(rèn)了 backend 是正常的之后,再使用 優(yōu)雅模式 。我們把第二個(gè) if 語(yǔ)句替換為:

if (!std.healthy(req.backend_hint) && (obj.ttl + obj.grace > 0s)) {
      return (deliver);
} else {
      return (fetch);
}

所以,總的來(lái)說(shuō),優(yōu)雅模式解決了兩個(gè)問(wèn)題:

  • 返回過(guò)期的緩存對(duì)象,以避免等待隊(duì)列過(guò)長(zhǎng)
  • 對(duì)于是否返回過(guò)期對(duì)象,可由你進(jìn)行控制

使用 inline C to 擴(kuò)展 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


修改請(qǐng)求首部:


例如,對(duì)于 /images 目錄的請(qǐng)求,我們希望移除請(qǐng)求中的 cookie:

sub vcl_recv {
  if (req.url ~ "^/images") {
    unset req.http.cookie;
  }
}

這樣做以后,如果請(qǐng)求的 URI 是以 /images 起始的,那么請(qǐng)求的 "Cookie:" 首部將被移除。

修改后端響應(yīng)


對(duì)于后端的圖片類(lèi)型的響應(yīng),我們?cè)?vcl_backend_response 中做進(jìn)一步處理:

  1. 刪除 “Set-Cookie” 首部,這是為了避免創(chuàng)建 hit-for-pass 對(duì)象,它不會(huì)被緩存
  2. 設(shè)置 TTL 時(shí)間為 1h
    sub vcl_backend_response {
       if (bereq.url ~ "\.(png|gif|jpg)$") {
         unset beresp.http.set-cookie;
         set beresp.ttl = 1h;
      }
    }

訪(fǎng)問(wèn)控制列表 ACLs


訪(fǎng)問(wèn)控制列表的創(chuàng)建和使用:

# Who is allowed to purge.... 允許誰(shuí)進(jìn)行 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 技術(shù),是在 HTTP 之上創(chuàng)建一個(gè)雙向的流式通道。

要通過(guò) Varnish 運(yùn)行 websockets,需要使用 pipe,并且復(fù)制 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);
     }
}

設(shè)備檢測(cè)


所謂設(shè)備檢測(cè),是通過(guò)請(qǐng)求中的 User-Agent 首部判斷客戶(hù)端使用的設(shè)備類(lèi)型。

使用場(chǎng)景舉例:給小屏幕的移動(dòng)端客戶(hù)(一般也是高延遲的網(wǎng)絡(luò))發(fā)送 size reduced files,或者提供客戶(hù)端能識(shí)別的 streaming video codec。

對(duì)這種使用場(chǎng)景,有幾種策略可以使用:

  1. 重定向至另一個(gè) URL
  2. 對(duì)于特定的客戶(hù)端使用不同的 backend
  3. 修改對(duì) backend 發(fā)起的請(qǐng)求,使 backend 發(fā)送裁剪過(guò)的內(nèi)容

為了講清楚如何使用這些策略,我們假設(shè)存在一個(gè)首部名為 X-UA-Device(req.http.X-UA-Device),它可以區(qū)分不同的客戶(hù)端類(lèi)型:

設(shè)置該首部是很簡(jiǎn)單的:

sub vcl_recv {
    if (req.http.User-Agent ~ "(?i)iphone" {
        set req.http.X-UA-Device = "mobile-iphone";
    }
}

對(duì)于識(shí)別客戶(hù)端,Varnish 提供了商業(yè)的和免費(fèi)的方案。社區(qū)的免費(fèi)方案是根據(jù)正則來(lái)做,請(qǐng)參考: https://github.com/varnish/varnish-devicedetect/.

1. 基于同一個(gè) URL 提供不同的服務(wù)內(nèi)容


基于同一個(gè) URL 提供不同的服務(wù)內(nèi)容,其中涉及的要點(diǎn)如下:

  1. 對(duì)客戶(hù)端進(jìn)行檢測(cè)(非常簡(jiǎn)單,只需要 include devicedetect.vcl 并調(diào)用它)
  2. 如何告訴 backend 客戶(hù)端的類(lèi)型?舉例來(lái)說(shuō),可以設(shè)置一個(gè) header,或者改變一個(gè) header,甚至改變 backend 請(qǐng)求的 URL
  3. 對(duì) backend 返回的響應(yīng)進(jìn)行修改,添加 "Vary" header,這樣 Varnish 會(huì)啟動(dòng)內(nèi)部的處理
  4. 在最后階段修改發(fā)送給客戶(hù)端的響應(yīng),so any caches outside our control don't serve the wrong content

這些都需要去做,而且要保證,對(duì)于每一個(gè) URL,每一個(gè)設(shè)備類(lèi)型,只對(duì)應(yīng)一個(gè)緩存對(duì)象。

Example 1: 發(fā)送 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);
    }
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 1.介紹 運(yùn)維日常: 2.Web Page Cache: varnish2.0,3.0處理過(guò)程 varnish4....
    尛尛大尹閱讀 3,474評(píng)論 0 0
  • 緩存的基礎(chǔ)知識(shí) 1、程序本身具有局部性 時(shí)間局部性過(guò)去訪(fǎng)問(wèn)到的數(shù)據(jù),也有可能被兩次訪(fǎng)問(wèn) 空間局部性一個(gè)數(shù)據(jù)被訪(fǎng)問(wèn)到...
    魏鎮(zhèn)坪閱讀 2,234評(píng)論 1 3
  • 頭皮瘙癢常令人情緒非常煩躁,坐立不安。那么頭皮為什么會(huì)癢呢? 小編百度了一下,得到了這樣的答案:頭皮瘙癢是頭皮菌群...
    Extreme膩閱讀 1,472評(píng)論 0 0
  • 一、我是莫語(yǔ) 我是莫語(yǔ),一個(gè)身材還不錯(cuò)的女人。我愛(ài)跑步,愛(ài)寫(xiě)字,愛(ài)看電影,愛(ài)看書(shū)。經(jīng)常晚上十一點(diǎn)前就睡覺(jué)了。 半年...
    周美妍閱讀 1,256評(píng)論 6 59
  • Gentoo Linux改為采用滾動(dòng)更新。Gentoo Linux的更新頻密度可達(dá)到每周皆提供更新版。 升級(jí)系統(tǒng)的...
    孤逐王閱讀 7,566評(píng)論 0 2

友情鏈接更多精彩內(nèi)容