Varnish的状态引擎和VCL

接下来我们就来更加深入的来看一下Varnish的相关.

我们之前已经通过使用Varnish所提供的命令行工具进行了一些基本的配置. 并且还知道可以通过对varnish.param文件的配置来操作他启动的一些行为参数.

Varnish的缓存功能配置主要都是通过编译和执行vcl来进行操作的, 那么为了能够操作Varnish进行负载均衡, 是否进行对Backend服务器群的健康状态检测等等, 就需要通过VCL来进行定义了.

状态引擎

VCL是一种域配置文件, 因此我们对VCL的配置操作对象就是很多类似我们代码块一样的东西, 只不过这里被叫做. 这里我们就可以引入varnish的状态引擎了.

那么什么是状态引擎呢, 这个可以类比我们之前学习过的netfilter的钩子函数. 由于到达我们varnish的请求可能是请求静态资源的, 可能是请求动态资源的, 也有可能是非法访问, 还有可能是请求的方法不支持或者不允许. 这么多情况, 他们都需要不同的应对措施, 这个就是varnish的状态引擎.

首先我们来看一下V3版本的状态引擎吧:

varnish_statusV3

这个图是一个简化版本, 类似流程图一样的东西. 这里的每一个矩形都代表一个状态引擎.

从图中, 我们可以很清楚的看到, 当用户的请求达到时, 我们的vcl_recv引擎先来进行处理, 如果请求的对象是可以被缓存的,那么就会去到下一个状态引擎, 如果不能被缓存. 那就会变得很简单了, 请求将会直接到达vcl_fetch状态, 由这个引擎来负责去后端服务器来取得数据.最后交给vcl_deliver来返回给用户.

如果之前的状态是vcl_hash. Varnish就会在自己的缓存空间中进行寻找, 如果可以找到, 就会进入命中状态, 如果没有就是miss状态, 接着交给fetch状态引擎去后台服务器取得数据, 然后一边缓存到自己的空间中, 一边将结果返回给用户.

这里我们也可以看到, 各个引擎之间是存在相关性的, 前一个engine如果可以有多个下游engine, 则上游需要使用return来指明要转移的下游engine.

接下来我们来看一下完全版的一个WORKFLOW:

varnish_statusV4

我们刚刚说过, 上一个engine使用return来返回下一个engine, 这里我们就能看到, 根据vcl_recv所返回的值不同, 到达的下一个engine也不一样. 例如说, 如果说用户发送过来的请求是不可缓存的, 那就没与必要去自己的缓存空间里寻找了, 于是就会返回pass, 直接到达vcl_pass.

VCL的语法:

这里我们稍微穿插一点关于VCL语法的小东西: 对于每一个状态引擎, 我们通过使用sub关键字来声明, 例如说, 如果对vcl_recv进行操作, 就需要有类似下面的声明语句:

1
2
3
sub vcl_recv {
// 这是注释
}

可以看到, 这里使用了双斜线来注释, 除此之外, 还可以通过#, /* foo */来实现单行和多行注释.

对于每一个Varnish的状态引擎, 都要很多内部定义好的内置变量供你调用, 这些变量他们的使用都是限制在状态里的(state-limited), 另外, vcl没有循环语句, 但是存在条件判断和分支语句.

通过在域中使用return函数来进行状态引擎的跳转. 除此之外, 还有很多内置函数, 具体就需要查阅官方的文档了.

操作符包括: =, ==, ~, !, &&, ||.

现在我们来看一个典型的vcl配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
sub vcl_recv {
if(req.restarts == 0) {
if (req.http.x-forwarded-for) {
set req.http-X-Forwarded-For =
req.http.X-Forwarded-For + "," + client.ip;
} else {
set req.http.X-Forwarded-For = client.ip;
}
}
if (req.request != 'GET' && req.request != "HEAD" &&
req.request != "PUT" && req.request != "POST" &&
req.request != "POST" && rq.request != "TRACE" &&
req.request != "OPTIONS" && req.request != "DELETE") {
return (pipe);
}
if (req.request != "GET" && req.request != "HEAD") {
return (pass);
}
return (lookup);
}

应该还是挺好看懂的吧. 设置变量的关键字就是set.

假如说现在我们的请求到达了上面vcl的最后一句return (lookup), 也就是到达了下一个状态引擎vcl_hash, 这里会发生什么呢? 同样我们还是看一个示例:

1
2
3
4
5
6
7
8
9
sub vcl_hash {
hash_data(req.url);
if (req.http.post) {
hash_data(req.http.host);
} else {
hash_data(server.ip);
}
return (hash);
}

计算得到hash, 也即是缓存的键之后就可以去尝试命中缓存了.

接下来的过程就省略掉了, 我们直接来小结一下, 各个状态引擎的工作流:

  • vcl_recv -> vcl_hash -> vcl_hit -> vcl_deliver 理想的状态, 命中缓存
  • vcl_recv -> vcl_hash -> vcl_miss -> vcl_fetch -> vcl_deliver 请求的资源可以缓存但是没有命中缓存
  • vcl_recv -> vcl_pass -> vcl_fetch -> vcl_deliver 请求的资源无法被缓存, 直接到后台服务器去取数据
  • vcl_recv -> vcl_pipe 请求不能理解, 直接发送到后端服务器

顺便一提, 我们这里列出来的状态引擎仍然不全, 这里列出来的只是前端处理的engine, 之前我们在说到varnish的子进程的时候, 提到过, 有一个专门负责和后台服务器进行交互的进程, 这个进程的状态引擎幼又包括: vcl_backend_fetch, vcl_backend_response, vcl_backend_error. 另外还有用来进行缓存删除的状态引擎: vcl_synthvcl_purge.

最后, 我们来看一下官方给出的图:

Varnish_workflow

接下来我们把官方给出的vcl_recv的示例拷贝到我们的配置文件中, 也即是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
sub vcl_recv {
if (req.method == "PRI") {
/* We do not support SPDY or HTTP/2.0 */
return (synth(405));
}
if (req.method != "GET" &&
req.method != "HEAD" &&
req.method != "PUT" &&
req.method != "POST" &&
req.method != "TRACE" &&
req.method != "OPTIONS" &&
req.method != "DELETE") {
/* Non-RFC2616 or CONNECT which is weird. */
return (pipe);
}

if (req.method != "GET" && req.method != "HEAD") {
/* We only deal with GET and HEAD by default */
return (pass);
}
if (req.http.Authorization || req.http.Cookie) {
/* Not cacheable by default */
return (pass);
}
return (hash);
}

我们复制一份配置文件, 然后使用varnish的命令行工具来进行载入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
vcl.list
200
active 0 boot

vcl.load test1 new.vcl
200
VCL compiled.
vcl.list
200
active 0 boot
available 0 test1

varnish> vcl.use test1
200
VCL 'test1' now active
vcl.list
200
available 0 boot
active 0 test1

首先我们使用load来编译这个VCL, 接着再声明我们使用它就行了. 还可以通过show命令来查看当前的VCL的内容.

但是这个默认行为我们更换之后还是没法看出效果, 现在我们来对vcl_deliver这个地方来进行一些设定:

1
2
3
4
5
6
7
sub vcl_deliver {
if (obj.hits > 0) {
set resp.http.X-Cache = "HIT";
} else {
set resp.http.X-Cache = "MISS";
}
}

添加进去之后我们再通过load和use命令来使得新修改的vcl文件生效.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
vcl.list
200
available 0 boot
active 0 test1

vcl.load test2 new.vcl
200
VCL compiled.
vcl.use test2
200
VCL 'test2' now active
vcl.list
200
available 0 boot
available 0 test1
active 0 test2

接下来我们来使用curl进行模拟请求, 现在后台Web服务器上新建一些客户端没有访问过的新页面:

1
2
3
[root@VM-node0 html]# for n in {1..10}; do echo "<h1>Web Page $n on WebServer1</h1>" > test$n.html; done
[root@VM-node0 html]# ls
forum index.html index.php test10.html test1.html test2.html test3.html test4.html test5.html test6.html test7.html test8.html test9.html wordpress

接下来就来使用curl进行请求,请求的效果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
[root@VM-node1 ~]# curl -I 192.168.16.100:6081/index.html 
HTTP/1.1 200 OK
Date: Wed, 12 Sep 2018 01:25:26 GMT
Server: Apache/2.4.6 (CentOS) OpenSSL/1.0.2k-fips PHP/5.4.16
Last-Modified: Fri, 20 Oct 2017 16:27:33 GMT
ETag: "19-55bfcf32d89d7"
Content-Length: 25
Content-Type: text/html; charset=UTF-8
X-Varnish: 32770 3
Age: 6
Via: 1.1 varnish-v4
X-Cache: HIT
Connection: keep-alive

[root@VM-node1 ~]# curl -I 192.168.16.100:6081/test1.html
HTTP/1.1 200 OK
Date: Wed, 12 Sep 2018 01:25:39 GMT
Server: Apache/2.4.6 (CentOS) OpenSSL/1.0.2k-fips PHP/5.4.16
Last-Modified: Wed, 12 Sep 2018 01:23:29 GMT
ETag: "22-575a2701b26a4"
Content-Length: 34
Content-Type: text/html; charset=UTF-8
X-Varnish: 7
Age: 0
Via: 1.1 varnish-v4
X-Cache: MISS
Connection: keep-alive

[root@VM-node1 ~]# curl -I 192.168.16.100:6081/test1.html
HTTP/1.1 200 OK
Date: Wed, 12 Sep 2018 01:25:39 GMT
Server: Apache/2.4.6 (CentOS) OpenSSL/1.0.2k-fips PHP/5.4.16
Last-Modified: Wed, 12 Sep 2018 01:23:29 GMT
ETag: "22-575a2701b26a4"
Content-Length: 34
Content-Type: text/html; charset=UTF-8
X-Varnish: 32772 8
Age: 1
Via: 1.1 varnish-v4
X-Cache: HIT
Connection: keep-alive

可以很清晰的看到我们自定义的头部被添加到了响应头中, 并且第一次访问test1.html的时候, 我们的状态是没有命中的, 但是在第二次访问的时候我们就是命中缓存了.

VCL参考

变量

来说明一下上面添加的obj.hit是什么意思呢, 很简单. 这个参数记录着请求的对象命中缓存的次数. 更多的对象和参数解释可以在下面的网站得到参考:

Varnish参考-变量

我们接下来整理一下这些变量的类型有哪些, 以及这些变量都是如何抽象的:

  • req: 就是用户发送过来的请求
  • bereq: 对后端服务器发送过去的请求
  • resp: 向用户发送的响应对象
  • bresp: 后端服务器发送过来的响应对象
  • storage: 对于我们从缓存存储空间的抽象
  • obj: 请求的对象
  • client: 用户和客户端
  • server: 就是varnish自己

更多的信息还是直接在官方站点上进行参考. 这里我们就列举一些比较常用的属性在这里:

bereq

bereq.http.HEADERS: 由Varnish发往backend server的请求报文的指定首部.

bereq.request: 请求方法

bereq.url: 所请求的目标URL

bereq.proto: 向后端服务器请求的协议版本

bereq.backend: 请求的后端主机

beresp

beresp.proto: 后端服务器返回的协议版本

beresp.status: 响应状态吗

beresp.reason: 原因短语, 就是跟在我们的状态码之后的那个字符串

beresp.backend.ip:

beresp.backend.name:

beresp.http.HEADER: 从后端服务器所响应的报文的首部

beresp.ttl: 后端服务器响应的内容的剩余时间

obj

obj.hits: 对象的命中次数

obj.ttl: 对象的ttl值.

Server

server.ip:

server.hostname:

函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
ban(expression)

Invalidates all objects in cache that match the expression with the ban mechanism.

hash_data(input)

Adds an input to the hash input. In the built-in VCL hash_data() is called on the host and URL of the request. Available in vcl_hash.

rollback()

Restore req HTTP headers to their original state. This function is deprecated. Use std.rollback() instead.

synthetic(STRING)

Prepare a synthetic response body containing the STRING. Available in vcl_synth and vcl_backend_error.

regsub(str, regex, sub)

Returns a copy of str with the first occurrence of the regular expression regex replaced with sub. Within sub, \0 (which can also be spelled \&) is replaced with the entire matched string, and \n is replaced with the contents of subgroup n in the matched string.

regsuball(str, regex, sub)

As regsub() but this replaces all occurrences.

模式匹配

我们可以使用正则表达式进行字符模式的匹配, 例如这样的一个例子, 当用户访问某个特别的页面时, 即使我们存在缓存, 但是我还是不想让他调用缓存, 而是还是从后端服务器取得结果.

1
2
3
if (req.url ~ "^/test7.html$") {
return (pass);
}

例如这里, 我们使用一个~来声明下面的字符串是模式匹配, 这样造成的效果就是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
[root@VM-node1 conf]# curl -I 192.168.16.100:6081/test3.html
HTTP/1.1 200 OK
Date: Wed, 12 Sep 2018 03:08:45 GMT
Server: Apache/2.4.6 (CentOS) OpenSSL/1.0.2k-fips PHP/5.4.16
Last-Modified: Wed, 12 Sep 2018 01:23:29 GMT
ETag: "22-575a2701b26a4"
Content-Length: 34
Content-Type: text/html; charset=UTF-8
X-Varnish: 32776
Age: 0
Via: 1.1 varnish-v4
X-Cache: MISS of 192.168.16.100
Connection: keep-alive

[root@VM-node1 conf]# curl -I 192.168.16.100:6081/test3.html
HTTP/1.1 200 OK
Date: Wed, 12 Sep 2018 03:08:45 GMT
Server: Apache/2.4.6 (CentOS) OpenSSL/1.0.2k-fips PHP/5.4.16
Last-Modified: Wed, 12 Sep 2018 01:23:29 GMT
ETag: "22-575a2701b26a4"
Content-Length: 34
Content-Type: text/html; charset=UTF-8
X-Varnish: 13 32777
Age: 1
Via: 1.1 varnish-v4
X-Cache: HIT from 192.168.16.100
Connection: keep-alive

[root@VM-node1 conf]# curl -I 192.168.16.100:6081/test7.html
HTTP/1.1 200 OK
Date: Wed, 12 Sep 2018 03:10:14 GMT
Server: Apache/2.4.6 (CentOS) OpenSSL/1.0.2k-fips PHP/5.4.16
Last-Modified: Wed, 12 Sep 2018 01:23:29 GMT
ETag: "22-575a2701b26a4"
Accept-Ranges: bytes
Content-Length: 34
Content-Type: text/html; charset=UTF-8
X-Varnish: 32784
Age: 0
Via: 1.1 varnish-v4
X-Cache: MISS of 192.168.16.100
Connection: keep-alive

[root@VM-node1 conf]# curl -I 192.168.16.100:6081/test7.html
HTTP/1.1 200 OK
Date: Wed, 12 Sep 2018 03:10:15 GMT
Server: Apache/2.4.6 (CentOS) OpenSSL/1.0.2k-fips PHP/5.4.16
Last-Modified: Wed, 12 Sep 2018 01:23:29 GMT
ETag: "22-575a2701b26a4"
Accept-Ranges: bytes
Content-Length: 34
Content-Type: text/html; charset=UTF-8
X-Varnish: 20
Age: 0
Via: 1.1 varnish-v4
X-Cache: MISS of 192.168.16.100
Connection: keep-alive

看到效果了, 访问test3.html的时候一切正常, 但当我们访问到模式匹配的test7的时候, 就会一直pass了. 当然这只是对单文件的, 我们还可以对目录进行模式匹配, 从而达到对一个目录下的全部文件都跳过缓存.

另外, 我们还可以类似的方式取消一些特定资源的私有cookie标识, 例如某些公共的图片资源, 由于后台的应用服务器配置或者是后台程序的设定和代码存在问题导致这些图片的响应头部中被加入了Set-Cookie属性, 我么可以通过模式匹配找到这些文件然后清除掉这些头部, 来看一个示例吧.

1
2
3
4
5
6
7
8
9
10
if (beresp.http.cache-control !~ "s-maxage") { //如果这个头部中的cache-control不包含s-max-age
if (bereq.url ~ "(?i)\.jpg$") {
set beresp.ttl = 3600s;
unset beresp.http.Set-Cookie;
}
if (bereq.url ~ "(?i)\.css$") {
set beresp.ttl = 600s;
unset beresp.http.Set-Cookie;
}
} // 当然这里如果通过设置flag的方式写会更加优雅一些不过算了

负载均衡

我们之前就在说Varnish的时候说过, 这给东西同样支持负载均衡的设定, 这个设定需要一个我们之前没有说过的状态引擎 – vcl_init

设定起来的样子就像这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
vcl 4.0;

backend b1 {
.host = "";
.port = "";
}

backend b2 {
.host = "...";
.port = "...";
}

sub vcl_init {
new cluster = directors.round_robin();
cluster.addbackend(b1, 1.0);
cluster.addbackend(b2, 1.0);
}

sub vcl_recv {
set req.backend_hint = cluster.backend();
}

这里我们使用轮询的算法来进行调度.

这里我们稍微的扩展一下对后端服务器的设定, 除了我们说过的hostport之外, 还有其他的一些, 官网的介绍在这里可以看到Backend-Definition.

其中比较重要的是probomax_connections 分别是对后端主机进行健康状态检测和并发连接的最大数量. 但是, 如果需要对后端主机进行健康状态检测, 我们是需要再来指定健康状态检测方式的, 通过使用probe块来指定:

1
2
3
probe name {
.attribute = "value";
}

和指定后端服务器的方法十分类似, 这里也需要提供一个名字和它对应的属性有哪些. 至于这些属性, 在上面的介绍链接里就有, 大致包括一些, 阈值, 期待的状态码, 间隔时间, 超时时长, 请求的URL, 发出的请求是什么样子的等等.

现在我们就来小小的试一下, 现在我的后端服务器有两个, 分别是192.168.16.101:8080192.168.16.101:8888. 这里就通过Apache HTTP服务器开启两个虚拟主机好了, 然后为了模拟第二台服务器挂掉的情况我打算通过修改防火墙规则来实现. 不知道可行不可行, 现在我就先去部署了哈哈.

OK, 现在的状态是, 我在一台主机上部署了web服务, 通过8080和8888两个端口来提供不同的Web服务. 现在我们通过配置Varnish来实现对后台两个服务的轮询访问和健康状态检查.

稍微提示一下, Varnish4版本对于这些设定和之前的版本差别很大, 这里踩了很多坑才实现这么一个功能.为了能看到负载均衡效果, 你需要暂时跳过查找缓存的步骤, 这是因为如果请求的资源已经被缓存了, 就会等到缓存时间结束了之后才会进行负载均衡.

另外, 关于调度器的实现语法也已经有了很大的不同, 现在我们就来看一下.

负载均衡和健康状态检测的效果实现配置在下面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
backend webserver1 {
.host = "192.168.16.101";
.port = "8080";
.probe = {
.url = "/index.html";
}
}

backend webserver2 {
.host = "192.168.16.101";
.port = "8888";
.probe = {
.url = "/index.html";
}
}

sub vcl_init {
new cluster = directors.round_robin();
cluster.add_backend(webserver1);
cluster.add_backend(webserver2);
}


sub vcl_recv {
set req.backend_hint = cluster.backend();
if (req.url ~ "(?i)\.html$") {
return(pass);
}
}

这里只是填写了必要的参数, 实现之后的效果就是在访问时候进行轮询切换, 除了round_robin算法, 同样可以使用hash, random等等.

这里访问时需要加上/index.html才可以达到轮询的效果, 这是因为只有这样才会达到if的条件判断, 进行下面的return语句, 才不会读取缓存.

看到这里, 你是不是觉得Varnish这样调度是没有意义的呀, 其实不是, 这里之所以先这样做, 是因为当我们持续访问同一个资源的时候, Varnish只会从一个位置的后端服务器去取资源, 但是如果向我们上面这样设定, 我们访问的同一个资源对于Varnish来说就是不同的资源了.

那么接下来我们再来做一个测试, 我们分别在两个backend server都生成test1-10页面, 来测试下效果.

测试效果就像这样子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
C:\Users\lenovo\Desktop
λ curl 192.168.16.100:6081/test1.html
WebPage1 on Web1.

C:\Users\lenovo\Desktop
λ curl 192.168.16.100:6081/test2.html
WebPage2 on Web2.

C:\Users\lenovo\Desktop
λ curl 192.168.16.100:6081/test3.html
WebPage3 on Web1.

C:\Users\lenovo\Desktop
λ curl 192.168.16.100:6081/test4.html
WebPage4 on Web2.

C:\Users\lenovo\Desktop
λ curl 192.168.16.100:6081/test3.html
WebPage3 on Web1.

C:\Users\lenovo\Desktop
λ curl 192.168.16.100:6081/test2.html
WebPage2 on Web2.

如果是同一个资源, 由于命中了缓存, 所以自然得到的结果就会一致. 但是放来回访问不同的资源并且不是被缓存过的资源的话, 就会进行rr轮询了.