一.移除刷新缓存Purging and banning
提高缓存命中率的最有效途径之一是增加缓存对象的生存时间(TTL),但是这也可能会带来副作用,比如缓存对象的TTL过长使得后端源资源已经更新了,而缓存还未更新,这导致用户会一直访问过期资源。解决办法就是当资源更新时提醒varnish。相应地,Varnish为完成这类的任务提供了三种途径:HTTP修剪(HTTPpurging)、禁用某类缓存对象(banning)和强制缓存未命中(forced cache misses)。
在具体执行某清理工作时,需要事先确定如下问题:
(1)仅需要检验一个特定的缓存对象,还是多个;
(2)目的是释放内存空间,还是仅替换缓存的内容;
(3)是不是需要很长时间才能完成内容替换;
(4)这类操作是个日常工作,还是仅此一次的特殊需求;
1.HTTP Purging
purge就是当你从缓存中选出一个对象并丢弃它和它的变体,并等待下一个针对此内容的客户端请求到达时刷新此内容,通常purge是通过调用HTTP协议的PURGE方法来实现。HTTP purge和HTTP GET请求类似,只不过方法是PURGE,事实上,你可以调用任何你想要的方法,但大多数人都把这种方法称为purging。例如squid也支持相同的机制。varnish为了支持purging需要在合适的位置使用如下VCL:
acl purge { #定义可访问来源
"127.0.0.1";
"192.168.85.0"/24;
}
sub vcl_recv { #只允许purge中主机进行PURGE
if (req.method == "PURGE") {
if (!client.ip ~ purge) {
return(synth(405,"Not allowed."));
}
return (purge);
}
}
vcl_purge在执行结束vcl_recv后跳转到vcl_hash上,当vcl_hash调用return(lookup)时,varnish将purge这个对象然后调用
vcl_purge。这里你就可以选择添加任何varnish每次purge该对象的特别动作;
例如使example.com的首页无效,就可以这样:
PURGE / HTTP/1.0
Host: example.com
此时varnish会丢弃该首页,这将删除所有Vary定义的变体;
2.Bans
你可以把bans作为从已缓存对象中的筛选过滤出某特定对象并将其移除的缓存内容刷新机制,不过,它并不阻止新的内容进入缓存或响应于请求。在varnish中,可以通过CLI接口或VCL实现ban.
例如ban所有的example.com中的png对象:
ban req.http.host == "example.com" && req.url ~ "\\.png$"
bans在命中缓存中的对象使但deliver前会被检查,且对象只会被新的bans检查。
bans和obj.*相比也是会被background worker threads(也称为ban lurker)处理的。而ban lurker会遍历所有的bans去逐一
匹配,并将匹配的对象清除。而ban lurker由参数ban_lurker_sleep所控制,当该参数值设为0时,表示禁用ban lurker;
bans比缓存中最旧对象还旧的对象时,它会被直接丢弃。如果你有很多的很少访问的,TTL值较大的对象时,你就会积累很多的bans,这会影响CPU的使用以及性能;
如果你想通过HTTP添加bans到varnish,你需要如下VCL:
sub vcl_recv {
if (req.method == "BAN") {
# Same ACL check as above:
if (!client.ip ~ purge) {
return(synth(403, "Not allowed."));
}
ban("req.http.host == " + req.http.host +
" && req.url == " + req.url);
# Throw a synthetic page so the request won't go to the backend.
return(synth(200, "Ban added"));
}
}
这个VCL示例允许varnish处理HTTP BAN方法的请求,并对URL和主机部分进行ban;
ban lurker可以帮助你保持ban列表(ban-list)在可控范围内,所以建议避免在bans中使用req.*因为在ban lurker threads中
请求对象时不可用的;
你可以使用如下模板去写对bans有好的ban lurker:
sub vcl_backend_response {
set beresp.http.url = bereq.url;
}
sub vcl_deliver {
unset resp.http.url; # Optional
}
sub vcl_recv {
if (req.method == "PURGE") {
if (client.ip !~ purge) {
return(synth(403, "Not allowed"));
}
ban("obj.http.url ~ " + req.url); # Assumes req.url is a regex. This might be a bit too simple
}
}
在CLI命令行下,可以使用ban.list来查看当前ban列表中所有bans的状态,如:
0xb75096d0 1318329475.377475 10 obj.http.url ~ test
0xb7509610 1318329470.785875 20G obj.http.url ~ test
ban列表中包含了ban的ID号,ban进入ban列表时的时间戳,如果ban失效时列表中符合该ban的对象的数目(可选后缀为G表示Gone),最后是ban的表达式;如果是一个重复的ban,那么该ban可被标记为Gone,但为了优化的目的仍留在列表中;
3.Forcing a cache miss
对于一个单一请求,允许使用强制缓存未命中的方法去刷新对象,当req.hash_always_miss值设为true时,varnish会miss缓存中的当前对象,强制去到后端去取,然后将从后端取得的新鲜对象轮流添加到缓存中,于是就覆盖了当前的对象。The old object will stay in the cache until ttl expires or it is evicted by some other means(这句话不是很理解)明明说将新鲜的缓存覆盖了当前缓存,那么当前缓存就是最新的了,为什么最后还说old object还存在缓存当中呢?这个old object是哪个object呢?大胆猜测一下:这个old object是被覆盖的缓存,只不过它还存在,但是不会被响应给请求,直到它过期?但是这不又浪费内存空间了么?
以下是原话:
Forcing a cache miss
The final way to invalidate an object is a method that allows you to refresh an object by forcing a hash miss for a single request. If you set 'req.hash_always_miss' to true, Varnish will miss the current object in the cache, thus forcing a fetch from the backend. This can in turn add the freshly fetched object to the cache, thus overriding the current one. The old object will stay in the cache until ttl expires or it is evicted by some other means.
因水平有限,可能在某些地方有出入,详细信息请查看官方文档:
示例:
[root@varnish varnish]# cat test.vcl
vcl 4.0;
backend webserver {
.host="192.168.85.156";
.port="80";
}
acl purges {
"127.0.0.1";
"192.168.85.0"/24;
}
sub vcl_recv {
if (req.method == "PURGE") {
if (!client.ip ~ purges) {
return(synth(405,"dont't allow."));
}
return(purge);
}
}
sub vcl_deliver {
if (obj.hits > 0) {
set resp.http.X-Cache = "Hit from" + server.ip;
}
else {
set resp.http.X-Cache = "Miss";
}
}
连续访问主页:
[root@varnish varnish]# curl -I
X-Cache: Miss #第一次访问未命中
[root@varnish varnish]# curl -I
X-Cache: Hit from192.168.85.129 #第二次访问显示命中
此时清除该主页的缓存:
[root@varnish varnish]# curl -X PURGE
200 Purged
Error 200 Purged
Purged
Guru Meditation:
XID: 30
Varnish cache server
再次连续访问主页:
[root@varnish varnish]# curl -I
X-Cache: Miss #第一次访问未命中
[root@varnish varnish]# curl -I
X-Cache: Hit from192.168.85.129 #第二次访问显示命中
二.后端主机健康状态检查
probe会定期查询后端主机的状态,且当主机失效时会down掉它。其定义方式为:
probe probe_name {
.attribute = "value";
}
可设置的attribute有:
url:检测后端主机健康状态时请求的URL。默认为/;
request:检测后端主机健康状态时所请求内容的格式,使用多个string指定一个完整的HTTP请求且.request会在每个string后自动插入\r\n,定以后.request会优先于.url,例如:
.request =
"GET /.healthtest.html HTTP/1.1"
"Host: "
"Connection: close";
expected_response:预期的后端主机的HTTP响应码。默认为200;
timeout:每次检测请求的超时时间,默认为2s;
interval:检测请求的发送周期,默认为5s;
initial:varnish启动时对后端主机至少需要多少次的成功探测;
window:基于最近多少次的检测来判断后端主机健康,默认为8;
threshold:在.window中有多少次检测一定是成功的(此时认为后端是健康的),默认值为3;
以各个默认值为例,简单来说就是如果8个检测中大于3个是成功的,那么就认为该后端是健康的,反之,后端不健康;
官方文档:
三.多后端主机Directors
varnish中可以将一个或多个后端主机定义为一个逻辑组director中,并可以指定调度方式。varnish4.0开始将后端源服务器组VCL中的director指令改为了varnish模块(即VMODs——vmod_directors),因此先import directors然后定义各个backend,最后在vcl_init子进程中初始化directors时将backend添加到director中,注意:directors也可以使用其他directors作为后端。例如:
import directors;
backend web1 { #定义后端主机web1
.host = "192.168.85.156";
.port = "80";
.probe = probe_name;
}
backend web2 { #定义后端主机web2
.host = "192.168.85.157";
.port = "80";
.probe = {
.
url = "/healthtest.html";
.interval = 5s;
.timeout = 3s;
.window = 8;
.threshold = 3;
}
}
sub vcl_init {
new bar = directors.round_robin(); #定义后端主机组directors
bar.add_backend(web1);
bar.add_backend(web2);
}
sub vcl_recv { #将所有的用户流量发往bar的后端上
set req.backend_hint = bar.backend();
}
director的调度方法:
1.round_robin
以轮询的方式选择后端,例如new bar = directors.round_robin();
round_robin.add_backend(BACKEND):添加一个后端到director,例如,bar.add_backend(backend1);
round_robin.remove_backend(BACKEND):从该director中移除后端;例如,bar.remove_backend(backend1);
round_robin.backend():从该director中选出一个后端;例如,set req.backend_hint = bar.backend();
2.random
随机从可用后端主机(带权重)中进行挑选,例如,new bar = directors.random();
random.add_backend(BACKEND, REAL):添加一个带有权重的后端到director,每个后端将接受大约百分比(其权重/总权重)的流量发送到该director例如,2/3给backend1, 1/3给backend2,定义为bar.add_backend(backend1, 10.0); bar.add_backend(backend2, 5.0);
random.remove_backend(BACKEND):从该director中移除后端;例如,bar.remove_backend(backend1);
random.backend():从该director中选出一个后端;例如,set req.backend_hint = bar.backend();
3.fallback
创建一个后备director,该director会轮流尝试添加的后端并返回第一个健康状态的主机如new bar=directors.fallback();
fallback.add_backend(BACKEND):添加后端主机到director,例如,bar.add_backend(backend1);
fallback.remove_backend(BACKEND):从该director中移除后端,例如,bar.remove_backend(backend1);
fallback.backend():从该director中选出一个后端;例如,set req.backend_hint = bar.backend();
4.hash
创建一个hash类型的director,它使用hash数据作为挑选因子,这意味着对同一个URL的请求将被发往同一个后端主机,其常用于多级缓存的场景中,例如,new bar = directors.hash();
hash.add_backend(BACKEND, REAL):添加带有明确权重的后端主机到director,权重用在随机的director中,除非
有特殊需求,建议值设置为1.0,例如,bar.add_backend(backend1, 1.0); bar.add_backend(backend2, 1.0);
hash.remove_backend(BACKEND):从该director中移除后端,例如,bar.remove_backend(backend1);
hash.backend(STRING_LIST):从该director中选择出一个后端,使用提供的字符串或字符串列表来挑选后端,例如
基于客户端的cookie header来挑选后端set req.backend_hint = bar.backend(req.http.cookie);
官方文档:
如果这还不能满足你的要求,你也可以写一个自定义的director
(官方参考文档:),而且当有主机down掉
时,varnish总会把用户请求发给健康状态的后端;
示例:
[root@localhost varnish]# cat li.vcl
vcl 4.0;
import directors; #导入directors模块
probe backend_healthy { #定义后端状态检查(注意:我在测试时直接写的,结果总是出现503 Backend fetch failed
.url = "/healthtest.html"; #错误,后来在各后端根目录下创建了healthtest.html页面结果就正常了,而网上的资料只是
.interval = 5s; #定义,没有说要创建健康状态检查请求页面,所以要注意)
.timeout = 3s;
.window = 8;
.threshold = 3;
}
backend web1 { #定义后端主机web1
.host="192.168.85.156";
.port="80";
.probe=backend_healthy;
}
backend web2 { #定义后端主机web2
.host="192.168.85.157";
.port="80";
.probe=backend_healthy;
}
acl purges { #定义允许刷新缓存的主机
"127.0.0.1";
"192.168.85.0"/24;
}
sub vcl_init {
new webs = directors.round_robin(); #创建round_robin的directors
webs.add_backend(web1); #向directors中添加后端
webs.add_backend(web2);
}
sub vcl_recv {
set req.backend_hint = webs.backend(); #定义用户请求访问的后端
if (req.method=="PURGE") {
if(!client.ip ~ purges) {
return(synth(405,"Not Allowed"));
}
return(purge);
}
}
sub vcl_hit {
if (req.method == "PURGE") {
return(synth(200 ,"Purged"));
}
return(deliver);
}
sub vcl_miss {
if (req.method == "PURGE") {
return(synth(404 ,"Not in cache"));
}
return(fetch);
}
sub vcl_deliver {
if (obj.hits > 0) {
set resp.http.X-Cache = "Hit from" + server.ip;
}
else {
set resp.http.X-Cache = "Miss";
}
}
sub vcl_init {
return (ok);
}
sub vcl_fini {
return (ok);
}
测试:
先清除缓存:[root@localhost varnish]# curl -X PURGE
然后连续访问:
[root@localhost varnish]# curl -I
HTTP/1.1 200 OK
Date: Tue, 26 Apr 2016 12:56:37 GMT
Server: Apache/2.2.15 (CentOS)
Last-Modified: Mon, 25 Apr 2016 08:44:41 GMT
ETag: "1007c8-15-5314b30026dd1"
Content-Length: 21
Content-Type: text/html; charset=UTF-8
X-Varnish: 196629
Age: 0
Via: 1.1 varnish-v4
X-Cache: Miss
Accept-Ranges: bytes
Connection: keep-alive
[root@localhost varnish]# curl -I
HTTP/1.1 200 OK
Date: Tue, 26 Apr 2016 12:56:37 GMT
Server: Apache/2.2.15 (CentOS)
Last-Modified: Mon, 25 Apr 2016 08:44:41 GMT
ETag: "1007c8-15-5314b30026dd1"
Content-Length: 21
Content-Type: text/html; charset=UTF-8
X-Varnish: 142 196630
Age: 43
Via: 1.1 varnish-v4
X-Cache: Hit from192.168.85.129
Accept-Ranges: bytes
Connection: keep-alive
此时访问的后端主机为web2:
[root@localhost varnish]# curl
This is a backend server 192.168.85.157
然后再次清除缓存并连续访问:
[root@localhost varnish]# curl -X PURGE
[root@localhost varnish]# curl -I
HTTP/1.1 200 OK
Date: Tue, 26 Apr 2016 12:59:10 GMT
Server: Apache/2.2.15 (CentOS)
Last-Modified: Wed, 20 Apr 2016 09:12:52 GMT
ETag: "104301-31-530e6ff9a6ca8"
Content-Length: 49
Content-Type: text/html; charset=UTF-8
X-Varnish: 144
Age: 0
Via: 1.1 varnish-v4
X-Cache: Miss
Accept-Ranges: bytes
Connection: keep-alive
[root@localhost varnish]# curl -I
HTTP/1.1 200 OK
Date: Tue, 26 Apr 2016 12:59:10 GMT
Server: Apache/2.2.15 (CentOS)
Last-Modified: Wed, 20 Apr 2016 09:12:52 GMT
ETag: "104301-31-530e6ff9a6ca8"
Content-Length: 49
Content-Type: text/html; charset=UTF-8
X-Varnish: 147 145
Age: 2
Via: 1.1 varnish-v4
X-Cache: Hit from192.168.85.129
Accept-Ranges: bytes
Connection: keep-alive
[root@localhost varnish]# curl
This is a backend server 192.168.85.156
这就实现了定义directors,定义后端状态检查和轮询调度(其实不清除缓存的话也不算轮询,因为一旦选择一个后端就会一直在使用其缓存);
四.优雅模式Grace mode
当几个客户端请求同一个页面的时候,varnish只发送一个请求到后端服务器,然后让其他几个请求挂起并等待返回结果;获得结果后,其它请求再复制后端的结果发送给客户端,且varnish是自动做这些的。但如果同时有数以千计的请求,那么这个等待队列将变得很大,这将导致两类潜在问题:
惊群问题(thundering herd problem)即突然释放大量的线程去复制后端返回的结果将导致负载急速上升;
没有用户喜欢等待;
为了解决这类问题,可以配置varnish在缓存对象因超时失效后再保留一段时间,以给那些等待的请求返回过期的内容(stale content)。为了提供给用户过期的内容,我们必须先有这些内容。因此我们在VCL中配置如下,使得varnish能在内容过期过后依然保持2分钟:
sub vcl_backend_response {
set beresp.grace = 2m;
}
现在Varnish允许在对象过期后2分钟内扔可提供给客户端。同时varnish也将刷新这个对象。刷新动作是异步发生的,发生在新的对象将替换老对象的同时。
我们可以在vcl_hit中增加代码来影响这个逻辑的工作。默认VCL是这样:
sub vcl_hit {
if (obj.ttl >= 0s) {
// A pure unadultered hit, deliver it
return (deliver);
}
if (obj.ttl + obj.grace > 0s) {
// Object is in grace, deliver it
// Automatically triggers a background fetch
return (deliver);
}
// fetch & deliver once we get the result
return (fetch);
}
如果开启了优雅模式,在TTL到期后仍不将其从内存中清除掉而是要再等待一段时间才清除,这就无端的浪费了资源,但如果你开启了健康检测,要是后端有问题就能提供给客户端过期的对象且此时应设置时间长一些,如果后端没问题就设置时间短一些。用如下的if语句替换掉上面的第二个if语句块:
if (!std.healthy(req.backend_hint) && (obj.ttl + obj.grace > 0s)) {
set req.grace = 1h;
return (deliver);
} else {
set req.grace = 10s;
return (fetch);
}
总之,优雅模式解决下面2个问题:
提供过期内容给客户端以避免请求堆积;
如果你允许varnish可以提供过期的内容;
五.Hashing
实质上,当Varnish存储内容在缓存中时,它也把发现的这个对象的哈希键和对象一起存储。默认设置的hash键是基于内容的主机名或者ip地址和URL计算出的(url+host或者url+ip),默认的VCL:
sub vcl_hash {
hash_data(req.url);
if (req.http.host) {
hash_data(req.http.host);
} else {
hash_data(server.ip);
}
return (lookup);
}
你可以修改hash,这种方式可以让Varnish根据任意算法提供不同的内容给不同的客户端。
如果想基于源ip地址提供不同语言的页面给你的用户,你需要一些根据ip地址判断是哪个国家的VMOD,然后放在哈希中。看起来像这样:
In vcl_recv:
set req.http.X-Country-Code = geoip.lookup(client.ip);
然后在vcl_hash中增加:
sub vcl_hash {
hash_data(req.http.X-Country-Code);
}
六. 内建函数regsub和regsuball
regsub(str, regex, sub)
简单理解就是将str中第一次出现的regex替换为定义的sub并返回该结果,在sub中,\0(也可写为\&)在整个匹配的字符串中被替换,\n在匹配的字符串中被分组n的内容替换;
regsuball(str, regex, sub)
和regsub类似,但是替换的不是第一次出现的,而是替换所有出现的;
参考文章:
varnish翻译文章:
http://my.oschina.net/monkeyzhu/blog?catalog=3295309
完整varnish示例: