本文编译自:
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);
}
}