本节介绍如何使用 Varnish 配置语言 (VCL) 告诉 Varnish 如何处理 HTTP 流量。
Varnish 有一个很棒的配置系统。大多数其他系统都使用配置指令,基本上就是打开或关闭许多开关。而我们选择使用一种名为 VCL 的域专用语言来实现这一点。
每个入站请求都会流经 Varnish,您可以通过修改 VCL 代码来影响请求的处理方式。您可以将某些请求引导到特定的后端,可以更改请求和响应,或者让 Varnish 根据请求或响应的任意属性采取各种行动。这使得 Varnish 成为一个功能极其强大的 HTTP 处理器,而不仅仅是用于缓存。
Varnish 将 VCL 翻译成二进制代码,然后在请求到达时执行。VCL 对性能的影响可以忽略不计。
VCL 文件分为多个子程序。不同的子程序在不同的时间执行。其中一个在收到请求时执行,另一个在从后端服务器获取文件时执行。
如果您没有在子程序中调用某个操作,而子程序又到达终点,Varnish 就会执行一些内置的 VCL 代码。您会在 Varnish Cache 随附的 buildin.vcl 文件中看到注释掉的 VCL 代码。
3.1 VCL 语法
VCL 从 C 继承了很多东西,它读起来很像简单的 C 或 Perl。
块由大括号分隔,语句以分号结尾,注释可以根据您自己的喜好用 C、C++ 或 Perl 编写。
请注意,VCL 不包含任何循环或跳转语句。
本节概述了语法中更重要的部分。有关 VCL 语法的完整文档,请参阅 参考资料中的VCL 。
3.1.1 字符串
基本字符串用“…”括起来,并且不能包含换行符。
反斜杠并不特殊,因此例如在regsub()中,您不需要执行“计算反斜杠”波尔卡:
regsub("barf", "(b)(a)(r)(f)", "\4\3\2p") -> "frap"
长字符串包含在 {” … “} 或 “”” … “”” 中。它们可以包含任何字符,包括“、换行符和除 NUL (0x00) 字符之外的其他控制字符。如果您确实想要在字符串中包含 NUL 字符,可以使用 VMOD 来创建此类字符串。
3.1.2 访问控制列表 (ACL)
ACL 声明创建并初始化一个命名访问控制列表,该列表稍后可用于匹配客户端地址:
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 条目指定了 Varnish 无法解析的主机名,它将匹配与之比较的任何地址。因此,如果它前面有一个否定标记,它将拒绝与之比较的任何地址,这可能不是您想要的。但是,如果该条目包含在括号中,则它将被忽略。
要将 IP 地址与 ACL 进行匹配,只需使用匹配运算符:
if (client.ip ~ local) {
return (pipe);
}
在 7.0 之前的 Varnish 版本中,ACL 始终会在 VSL 日志中发出VCL_acl记录,从 7.0 及以后版本,必须通过指定+log标志显式启用此功能:
acl local +log {
"localhost"; // myself
"192.0.2.0"/24; // and everyone on the local network
! "192.0.2.23"; // except for the dialin router
}
3.1.3 运算符
VCL 中提供了以下运算符。请参阅下面的示例,了解示例。
=
赋值运算符。
==
比较。
~
匹配。可以与正则表达式或 ACL 一起使用。
!
否定。
&&
逻辑和
||
逻辑或
3.1.4 内置子程序
Varnish 有许多内置子程序,每个事务流经 Varnish 时都会调用这些子程序。这些内置子程序都被命名为 vcl_*,并在 VCL 步骤中进行了说明。
内置子程序的处理以返回 () 结束(请参阅 VCL 操作)。
内置 VCL 还包含由内置子程序调用的自定义辅助子程序,这些子程序也以 vcl_ 作为前缀。
3.1.5 自定义子例程
您可以编写自己的子程序,但其名称不能以 vcl_ 开头。
子程序通常用于将代码分组,以便于阅读或重复使用:
sub pipe_if_local {
if (client.ip ~ local) {
return (pipe);
}
}
要调用子程序,请使用调用关键字,后面跟上子程序的名称:
call pipe_if_local;
VCL 中的自定义子程序不接受参数,也不返回值。
如上例所示,return ()(请参阅 VCL 操作)从顶层内置子程序(请参阅 VCL 步骤)开始返回值,该子程序可能经过多个步骤,最终调用了自定义子程序。
无操作的 return 在调用子程序的调用语句后恢复执行。
3.2 内置 VCL
每当加载一个 VCL 程序时,内置的 VCL 都会附加到程序中。vcl 内置子程序(VCL 步骤)有一个特殊属性,它们可以多次出现,其结果是所有内置子程序的串联。
例如,我们来看看下面的代码段:
sub vcl_recv {
# loaded code for vcl_recv
}
提供给编译器的有效 VCL 如下所示:
sub vcl_recv {
# loaded code for vcl_recv
# built-in code for vcl_recv
}
这就是如何保证所Varnish处理状态至少有一个.return (<action>)
一般建议不要从已加载的代码中一成不变地返回,而是让 Varnish 执行内置代码,因为内置代码基本上为 HTTP 缓存提供了合理的默认行为。
3.2.1 内置子程序分割
然而,内置的 VCL 规则并不总能在状态结束时生效,因此一些子程序(如 vcl_recv)会被拆分成多个调用其他子程序的程序。
按照惯例,这些辅助子程序以其操作的变量命名,如 req 或 beresp,这样就可以规避默认行为。
例如,内置 VCL 中的 vcl_recv 可以防止在客户端有 cookie 时进行缓存。如果您可以相信您的后端会始终指定响应是否可缓存,而不管请求是否包含 cookie,您就可以这样做:
sub vcl_req_cookie {
return;
}
这样,内置 vcl_recv 的所有其他默认行为都会执行,只有 cookie 处理会受到影响。
另一个例子是内置 vcl_backend_response 如何将负 TTL 视为不缓存信号。这是一种将响应标记为不可缓存的历史机制,但前提是内置vcl_backend_response 不能通过return () 来规避。
不过,在后端可能是另一台 Varnish 服务器的多层架构中,你可能希望缓存过期的响应,以允许交付受保护对象,并在下一次获取时重新验证。这可以通过以下代码段来实现:
sub vcl_beresp_stale {
if (beresp.ttl + beresp.grace > 0s) {
return;
}
}
这种粒度以及内置子程序拆分的总体目标是,在不放弃整个逻辑的情况下,规避默认规则的某个特定方面。
3.2.2 内置 VCL 参考
Varnish 安装时可能会提供 builtin.vcl 文件的副本,但 __varnishd __是确定附加到任何加载的 VCL 的代码的参考。
VCL 编译分两次进行:
第一次只编译内置的 VCL、
第二步是编译加载的 VCL 和内置 VCL 的连接。
内置 VCL 中的任何 VCL 子程序都可以扩展,在这种情况下,加载的 VCL 代码将在内置代码之前执行。
3.3 请求和响应 VCL 对象
在 VCL 中,有几个重要的对象需要注意。可以使用 VCL 访问和操作这些对象。
req
请求对象。当 Varnish 收到请求后,就会创建并填充 req 对象。vcl_recv 中的大部分工作都是在 req 对象上或通过 req 对象完成的。
bereq
后端请求对象。Varnish 在将其发送至后端之前会构建该对象。它基于 req 对象。
beresp
后台响应对象。它包含来自后端的对象的标头。如果要修改来自服务器的响应,请在 vcl_backend_response 中修改此对象。
resp
HTTP 响应在交付给客户端之前的状态。它通常在 vcl_deliver 中修改。
obj
缓存中存储的对象。只读。
3.4 后端服务器
Varnish 有一个 "后端 "或 "源 "服务器的概念。后端服务器是提供 Varnish 将加速的内容的服务器。
我们的第一项任务是告诉 Varnish 在哪里可以找到它的后端服务器。启动您最喜欢的文本编辑器,打开相关的 VCL 文件。
在顶部的某个地方,会有一个看起来有点像这样的部分:
# backend default {
# .host = "127.0.0.1";
# .port = "8080";
# }
我们去掉这个文本节中的注释标记,使它看起来像…:
backend default {
.host = "127.0.0.1";
.port = "8080";
}
现在,这段配置在 Varnish 中定义了一个名为 default 的后端。当 Varnish 需要从这个后端获取内容时,它会连接到 localhost (127.0.0.1) 上的 8080 端口。
Varnish 可以定义多个后端,你甚至可以将多个后端连接成后端集群,以达到负载平衡的目的。
3.4.1 “无”后端
也可以使用以下语法将后端声明为 “无”::
backend default none;
none
后端很特殊:
- 所有声明的后端
none
比较相等:
backend a none;
backend b none;
sub vcl_recv {
set req.backend_hint = a;
if (req.backend_hint == b) {
return (synth(200, "this is true"));
}
}
- 当在布尔上下文中使用时,后端
none
的计算结果为:false
backend nil none;
sub vcl_recv {
set req.backend_hint = nil;
if (! req.backend_hint) {
return (synth(200, "We get here"));
}
}
- 当监控发现没有健康的后端时,他们通常会返回后
none
端
3.4.2 多个后端
在某些情况下,您可能需要 Varnish 来缓存来自多个服务器的内容。您可能希望 Varnish 将所有 URL 映射到一台主机,也可能不这样做。有很多选择。
比方说,我们需要在 PHP 网站中引入 Java 应用程序。假设我们的 Java 应用程序要处理以 /java/ 开头的 URL。
我们设法让它在端口 8000 上运行。现在,让我们来看看 default.vcl:
backend default {
.host = "127.0.0.1";
.port = "8080";
}
我们添加一个新的后端:
backend java {
.host = "127.0.0.1";
.port = "8000";
}
现在我们需要告诉 Varnish 将差异 URL 发送到哪里。让我们看看vcl_recv:
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 将继续使用默认后端。如果没有名为默认的后端,vcl 中找到的第一个后端将被用作默认后端。
3.4.3 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;
}
}
3.4.4 通过代理连接
从此版本开始,Varnish 可以使用 PROXY2 协议通过代理连接到实际目的地。可能还会添加其他协议。
目前,该功能的典型用例是通过 TLS onloader 进行 TLS 加密连接。加载器需要支持动态连接,其目标地址信息取自 __PROXY2 __前言。例如,在使用 2.2 版或更高版本的 haproxy 时,可将此片段作为配置开加载器的基础:
# to review and adjust:
# - maxconn
# - bind ... mode ...
# - ca-file ...
#
listen sslon
mode tcp
maxconn 1000
bind /path/to/sslon accept-proxy mode 777
stick-table type ip size 100
stick on dst
server s00 0.0.0.0:0 ssl ca-file /etc/ssl/certs/ca-bundle.crt alpn http/1.1 sni fc_pp_authority
server s01 0.0.0.0:0 ssl ca-file /etc/ssl/certs/ca-bundle.crt alpn http/1.1 sni fc_pp_authority
server s02 0.0.0.0:0 ssl ca-file /etc/ssl/certs/ca-bundle.crt alpn http/1.1 sni fc_pp_authority
# ...
# A higher number of servers improves TLS session caching
然后,在同一服务器/命名空间上运行的 Varnish 可以使用 带有该功能的加载器.via
(请参阅Attribute .via):
backend sslon {
.path = "/path/to/sslon";
}
backend destination {
.host = "my.https.service";
.port = "443";
.via = sslon;
}
如果该属性不同于 ,.authority
则可用于指定连接的SNI.host
。
3.4.5 Directors
您还可以将多个后端分组为一组后端。这些群组被称为 directors。这样可以提高性能和弹性。
您可以定义多个后端,并将它们组合到一个 Director 中。这需要加载一个 VMOD、一个 Varnish 模块,然后在 vcl_init 中调用某些操作:
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 是轮循 Director。这意味着该 Director 将以轮循方式分配收到的请求。还有一种随机 Director,它以随机方式分配请求,你猜对了,就是随机方式。如果这些还不够,你还可以编写自己的 Director(请参阅编写 Director)。
但如果其中一台服务器宕机了怎么办?Varnish 能把所有请求导向健康的服务器吗?当然可以。这就是健康检查发挥作用的地方。
3.4.6 健康检查
让我们建立一个包含两个后端和健康检查的 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;
}
}
这里的新功能是探针。在本示例中,Varnish 将每 5 秒钟检查一次每个后端是否健康,1 秒钟后计时结束。如果过去 5 次中有 3 次成功,则认为后端是健康的,否则将标记为 “生病”。
更多信息请参阅 __VCL __文档中的 "后端健康探测器 "部分。
现在我们定义 “director”:
import directors;
sub vcl_init {
new vdir = directors.round_robin();
vdir.add_backend(server1);
vdir.add_backend(server2);
}
您可以将此 vdir director 用作请求的后台提示(backend_hint),就像使用简单的后台一样。Varnish 不会向被标记为不健康的主机发送流量。
如果所有后端都宕机了,Varnish 也可以提供过时的内容。有关如何启用此功能的更多信息,请参阅 Grace mode 和 keep。
请注意,Varnish 会为所有加载的 VCL 保持运行健康探测器。Varnish 会合并看似相同的探针,因此如果要加载大量 VCL,请注意不要更改探针配置。卸载 VCL 会丢弃探针。有关如何操作的详细信息,请参阅 ref:reference-vcl-director。
3.4.7 分层
默认情况下,大多数控制器的.backend()
方法都会返回对控制器本身的引用。这允许分层,如下例所示:
import directors;
sub vcl_init {
new dc1 = directors.round_robin();
dc1.add_backend(server1A);
dc1.add_backend(server1B);
new dc2 = directors.round_robin();
dc2.add_backend(server2A);
dc2.add_backend(server2B);
new dcprio = directors.fallback();
dcprio.add_backend(dc1);
dcprio.add_backend(dc2);
}
初始化后,如果服务器 1A 或服务器 1B 都健康,dcprio.backend() 将解析到服务器 1A 或服务器 1B;如果只有一个健康,则解析到其中一个。只有当两个服务器都有问题时,才会从 dc2 返回一个健康的服务器(如果有的话)。
3.4.8 Director 决议
实际解析发生在 vcl_backend_fetch {} 或 vcl_pipe {} 返回后准备后端连接时。
在某些情况下(如服务器分片),VCL 中已经需要解析结果。在这种情况下,可以使用 .resolve() 方法,如本例所示:
set req.backend_hint = dcprio.backend().resolve();
将此语句与前面的示例代码一起使用时,req.backend_hint 将被设置为服务器*后端之一,如果它们都有问题,则设置为无后端。
.resolve() 适用于任何 BACKEND 类型的对象。
3.4.9 连接池
打开后端连接总是要付出代价的:根据连接类型和后端基础设施的不同,打开一个新连接的开销从本地 Unix 域套接字(参见后端定义 .path 属性)的相当低到通过可能的多跳和长网络路径建立可能的多个 TCP 和/或 TLS 连接的巨大开销不等。无论开销有多大,它总是存在的。
因此,由于重复使用现有连接通常可减少开销和延迟,Varnish 默认会将后端连接集中起来:每当后端任务完成时,使用过的连接不会被关闭,而是会被添加到连接池中,以供以后重用。为了避免连接被重复使用,可以在__ vcl_backend_fetch__ 中添加 Connection: close http 标头。
虽然后端是按 VCL 定义的,但连接池可跨 VCL 甚至跨后端使用:默认情况下,池连接的标识符由后端定义的 .host/.port 或 .path 属性构建(VMOD 可使用自定义标识符)。因此,只要两个后端共享相同的地址信息,无论它们在哪个 VCL 中定义,它们的连接都来自一个共同的池。
如果后端没有主动关闭,Varnish 会保持池中连接的开放状态,直到 backend_idle_timeout 到期。
3.5 散列
在内部,当 Varnish 在缓存中存储内容时,会使用一个哈希键来索引,以便再次找到该对象。在默认设置中,这个键是根据 URL、Host: 头信息计算的,如果没有,则根据服务器的 IP 地址计算:
sub vcl_hash {
hash_data(req.url);
if (req.http.host) {
hash_data(req.http.host);
} else {
hash_data(server.ip);
}
return (lookup);
}
如您所见,它首先对 req.url 进行散列,然后对 req.http.host 进行散列(如果存在的话)。值得注意的是,Varnish 在散列之前不会小写主机名或 URL,因此理论上 "Varnish.org/"和 "varnish.org/"会导致不同的缓存条目。不过,浏览器倾向于小写主机名。
您可以更改哈希值的内容。这样,您就可以让 Varnish 根据任意标准向不同的客户端提供不同的内容。
比方说,您想根据用户的 IP 地址为其提供不同语言的网页。您需要一些 Vmod 来获取国家代码,然后将其放入哈希值中。它可能是这样的
在 vcl_recv:
set req.http.X-Country-Code = geoip.lookup(client.ip);
然后添加 vcl_hash:
sub vcl_hash {
hash_data(req.http.X-Country-Code);
}
由于没有 return(lookup),内置 VCL 会像往常一样将 URL、Host: 或服务器 IP# 添加到哈希值中。
如果vcl_hash
返回了,即:
sub vcl_hash {
hash_data(req.http.X-Country-Code);
return(lookup);
}
那么只有国家代码是重要的,Varnish 会返回看似随机的对象,忽略 URL(但它们总是有正确的 X-国家代码)。
3.6 宽容模式和保持
有时,您希望 Varnish 提供有些陈旧的内容,而不是等待来自后台的新鲜对象。例如,如果您运行的是一个新闻网站,提供几秒钟前的主页面并不是问题,因为这样可以加快网站的加载速度。
在 Varnish 中,这可以通过使用宽容模式来实现。一个关联的想法是保持,这里也有解释。
3.6.1 宽容模式
当多个客户端请求访问同一个页面时,Varnish 会向后台发送一个请求,并在从后台获取一个副本时搁置其他请求。在某些产品中,这被称为 “请求聚合”,Varnish 会自动这样做。
如果您每秒提供数以千计的命中率,等待请求的队列可能会变得很大。存在两个潜在的问题 - 一个是“惊群问题”(thundering herd problem)- 突然释放上千个线程来提供内容可能会使负载激增。其次 - 没有人喜欢等待。
将对象的 Grace 设置为正值告诉 Varnish,它应该在 TTL 过期后为客户端提供该对象一段时间,同时 Varnish 会获取该对象的新版本。默认值由运行时参数控制default_grace
。
3.6.2 保持
设置对象的 keep 会告诉 Varnish,它应该在缓存中保留某个对象一段时间。设置 keep 的原因是使用该对象来构建有条件的 GET 后端请求(使用 If-Modified-Since: 和/或 Ìf-None-Match: 头信息),允许后端回复 304 Not Modified(未修改),这对后端来说可能更有效率,还能节省重新传输未修改正文的时间。
这些值是相加的,因此如果 grace 为 10 秒,keep 为 1 分钟,那么对象将在 TTL 过期后在缓存中存活 70 秒。
3.6.3 设置宽容和保持
我们可以使用 VCL 使 Varnish 将所有对象保留超过其 TTL 10 分钟,并有 2 分钟的宽限期:
sub vcl_backend_response {
set beresp.grace = 2m;
set beresp.keep = 8m;
}
3.6.4 宽容和保持的效果
对于大多数用户来说,设置默认的宽限期和/或为每个对象设置一个合适的宽限期就足够了。默认的 VCL 会做正确的事,并按上述方式行事。不过,如果您想自定义 Varnish 的行为方式,就应该了解一些工作细节。
当 sub vcl_recv
以 return (lookup)
结束时(这是默认行为),Varnish 将在缓存中查找匹配对象。然后,如果只找到一个 TTL 已耗尽的对象,Varnish 会考虑以下情况:
-
该对象是否已经在后台请求?
-
该对象是否在宽限期内?
然后,Varnish 使用以下规则做出反应:
-
如果宽限期已过并且没有正在进行的后端请求,则立即调用
sub vcl_miss
,并且该对象将用作 304 候选者。 -
如果宽限期已过并且有正在进行的后端请求,则该请求将等待,直到后端请求完成。
-
如果没有对该对象的后端请求,则会安排一个。
-
假设对象将被传递,则立即调用
sub vcl_hit
请注意,后端获取是异步发生的,当新对象进入其中时,它将替换我们已经获得的对象。
如果您没有定义自己的,则使用默认的。它看起来像这样:sub vcl_hit
sub vcl_hit {
return (deliver);
}
请注意,(在 sub vcl_hit
中)obj.ttl + obj.grace > 0s 条件将始终为真。在早期版本(6.0.0 及更早版本)中,情况并非如此,有必要在内置 VCL 中进行测试,以确保 “keep objects”(缓存中 TTL 和 grace 均已耗尽的对象)不会传送到客户端。
在当前版本中,当只有 "保留对象 "可用时,将调用sub vcl_miss
,并开始获取新对象。
3.6.5 行为不当的服务器
Varnish 的一个主要功能是保护您免受网络和应用服务器故障的影响。
如果启用了 "健康检查 "功能,就可以检查后端是否生病,并在出现问题时修改行为。具体方法如下:
sub vcl_backend_response {
set beresp.grace = 24h;
// no keep - the grace should be enough for 304 candidates
}
sub vcl_recv {
if (std.healthy(req.backend_hint)) {
// change the behavior for healthy backends: Cap grace to 10s
set req.grace = 10s;
}
}
在上面的示例中,设置了特殊变量 req.grace。其效果是,当后端处于健康状态时,grace大于10秒的对象将具有10秒的有效宽限期。当后端出现问题时,将触发默认的VCL,并使用较长的宽限期。
此外,您可能希望在后端获取返回 5xx 错误时停止插入缓存:
sub vcl_backend_response {
if (beresp.status >= 500 && bereq.is_bgfetch) {
return (abandon);
}
}
3.6.6 总结
宽容模式允许 Varnish 向客户端提供稍微陈旧的内容,同时从后端获取新版本。结果是以更低的成本实现更快的加载时间。
可以通过设置 req.grace
来限制查找过程中的宽限期,然后在宽限期到来时改变行为。这样做通常是为了根据后台的健康状况改变有效的宽限期。
3.7 单独的 VCL 文件
在同一个 Varnish 中拥有多个不同的虚拟主机是一个非常典型的用例,从 Varnish 5.0 开始,可以为单独的虚拟主机或任何其他不同的请求子集提供单独的 VCL 文件。
假设我们要处理varnish.org
一个 VCL 文件和varnish-cache.org
另一个 VCL 文件。
首先加载两个 VCL 文件:
vcl.load vo_1 /somewhere/vo.vcl
vcl.load vc_1 /somewhere/vc.vcl
这些是 100% 正常的 VCL 文件,就像您在 Varnish 实例上仅运行该单个域时的样子一样。
接下来我们需要将 VCL 标签指向它们:
vcl.label l_vo vo_1
vcl.label l_vc vc_1
接下来,我们编写顶级 VCL 程序,该程序根据请求中的 Host: 标头分支到其他两个程序:
import std;
# We have to have a backend, even if we do not use it
backend default { .host = "127.0.0.1"; }
sub vcl_recv {
# Normalize host header
set req.http.host = std.tolower(req.http.host);
if (req.http.host ~ "\.?varnish\.org$") {
return (vcl(l_vo));
}
if (req.http.host ~ "\.?varnish-cache\.org$") {
return (vcl(l_vc));
}
return (synth(302, "http://varnish-cache.org"));
}
sub vcl_synth {
if (resp.status == 301 || resp.status == 302) {
set resp.http.location = resp.reason;
set resp.reason = "Moved";
return (deliver);
}
}
最后,我们加载顶层 VCL 并将其设为活动 VCL:
vcl.load top_1 /somewhere/top.vcl
vcl.use top_1
如果要更新分离的 VCL 之一,请加载新的 VCL 并更改标签以指向它:
vcl.load vo_2 /somewhere/vo.vcl
vcl.label l_vo vo_2
如果您想更改顶级 VCL,请像往常一样进行操作:
vcl.load top_2 /somewhere/top.vcl
vcl.use top_2
细节,细节,细节:
-
所有请求始终从活动 VCL 开始 - 来自以下位置的请求
vcl.use
-
只有 VCL 标签可以在
return(vcl(name))
. 如果没有这个限制,每次单独的 VCL 之一发生更改时,都必须重新加载顶级 VCL。 -
您只能从活动 VCL 切换 VCL。如果您从单独的 VCL 之一尝试,您将得到 503
-
vcl.discard
如果任何 VCL 包含,则无法删除 VCL 标签(带有)return(vcl(name_of_that_label))
-
您无法删除带有标签的 VCL。
-
此代码在测试用例 c00077 中进行了测试
-
这是一个非常新的功能,可能会改变
-
我们非常希望得到您的反馈
3.8 使用内联 C 来扩展 Varnish
您可以使用内联 C来扩展 Varnish。请注意,这样可能会严重搞乱 Varnish。C 代码在 Varnish Cache 进程中运行,因此如果您的代码生成段错误,则缓存将崩溃。
内联 C 语言最早的用途之一是向系统日志记录日志:
# 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
}
要使用内联 C,您需要使用参数启用它vcc_allow_inline_c
。
3.9 VCL 示例
这些是展示 VCL 语言的一些功能的简短示例集合。
3.9.1 在 VCL 中操作请求标头
比方说,我们要删除网络服务器 /images 目录中所有对象的 cookie:
sub vcl_recv {
if (req.url ~ "^/images") {
unset req.http.cookie;
}
}
现在,当请求被处理到后端服务器时,将没有 cookie 头信息。最有趣的一行是 if 语句。它匹配请求对象中的 URL,并将其与正则表达式进行匹配。请注意匹配操作符。如果匹配成功,请求的 Cookie: 标头将被取消设置(删除)。
3.9.2 改变后端响应
在这里,如果来自后台的对象符合特定条件,我们将覆盖该对象的 TTL:
sub vcl_backend_response {
if (bereq.url ~ "\.(png|gif|jpg)$") {
unset beresp.http.set-cookie;
set beresp.ttl = 1h;
}
}
我们还删除了所有 Set-Cookie 标头,以避免创建 “命中未命中”对象。请参阅 VCL 操作。
3.9.3 ACLs
您可以使用 acl 关键字创建已命名的访问控制列表。您可以使用匹配操作员将客户端的 IP 地址与 ACL 匹配:
# Who is allowed to 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."));
}
}
}
3.9.4 添加 WebSocket 支持
WebSockets 是一种通过 HTTP 创建基于流的双向通道的技术。
要通过 Varnish 运行 WebSocket,您需要通过管道传输请求并复制 Upgrade 和 Connection 标头,如下所示:
sub vcl_recv {
if (req.http.upgrade ~ "(?i)websocket") {
return (pipe);
}
}
sub vcl_pipe {
if (req.http.upgrade) {
set bereq.http.upgrade = req.http.upgrade;
set bereq.http.connection = req.http.connection;
}
}
3.10 设备检测
设备检测是根据请求中提供的 User-Agent 字符串,确定向客户端提供何种内容。
例如,在高延迟网络上向屏幕较小的移动客户端发送尺寸较小的文件,或提供客户端能理解的流媒体视频编解码器。
有几种典型的策略可用于这种情况:1)重定向到另一个 URL。2) 为特殊客户端使用不同的后端。3) 更改后端请求,以便后端发送定制内容。
为了使这些策略更容易理解,我们在此假设 req.http.X-UA-Device 标头是存在的,而且每个客户机类别都是唯一的。
设置该标头可以很简单,只需
sub vcl_recv {
if (req.http.User-Agent ~ "(?i)iphone" {
set req.http.X-UA-Device = "mobile-iphone";
}
}
有不同的商业和免费产品可对客户进行更详细的分组和识别。有关基于社区的基本正则表达式集,请参阅 https://github.com/varnishcache/varnish-devicedetect/。
3.10.1 在同一 URL 上提供不同的内容
其中涉及的技巧有
1.检测客户端(非常简单,只需包含 devicedetect.vcl 并调用即可)。
2.想办法向后台发出客户端类的信号。这包括设置标头、更改标头,甚至更改后端请求 URL。
-
修改来自后端的任何响应,添加缺失的 "Vary "头信息,以便 Varnish 在内部处理此问题。
-
修改发送到客户端的输出,以免我们无法控制的缓存提供错误的内容。
所有这些都需要在确保每个设备类的每个 URL 只能获得一个缓存对象的前提下完成。
示例 1:向后端发送 HTTP 标头
基本情况是,Varnish 在后端请求中添加 "X-UA-Device "HTTP 头,而后端在响应的 "Vary "头中提到内容依赖于此头。
从 Varnish 的角度来看,一切正常。
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");
}
}
示例 2:规范化用户代理字符串
另一种显示设备类型的方法是覆盖或规范发送到后端的 "User-Agent "头。
例如:
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
变成:
User-Agent: mobile-android
当后台看到时。
如果后端不需要原始头信息,就可以使用这种方法。在 CGI 脚本中,默认情况下只有一小部分预定义的头信息可供脚本使用。
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 req.http.User-Agent = req.http.X-UA-Device; } }
sub vcl_pass { if (req.http.X-UA-Device) { set req.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");
}
}
示例 3:添加设备类作为 GET 查询参数
如果其他方法都无效,可以添加设备类型作为 GET 参数。
http://example.com/article/1234.html -> http://example.com/article/1234.html?devicetype=mobile-iphone
客户端本身不会看到这种分类,只有后台请求会发生变化。
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");
}
}
3.10.2 移动客户端的不同后端
如果您有一个为移动客户端提供页面的不同后台,或在 VCL 中有任何特殊需求,您可以像这样使用 "X-UA-Device "标头:
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);
}
}
3.10.3 重定向移动客户端
如果您想重定向手机客户端,可以使用以下代码段。
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);
}
}