varnish介绍
Varnish Cache是一个web应用程序加速器,也是一个HTTP反向代理软件。放在HTTP或NGINX服务器前端,缓存内容。
一、varnish软件包中的关键程序:
1.varnishd
2.varnishadmin
varnishd:
varnishd是主程序,启动后生成两个进程,Manager Process(父进程)和Cacher Process(子进程),前者在启动时读入配置文件(稍后会讲到)并fork()出后者,同时具有CLI接口接收管理员的指令以用于管理后者,后者是真正处理缓存业务的进程,其下又可以生成若干线程,这些线程各司其职,完成缓存业务的处理并记录日志。这里的Manager进程和Cacher进程类似于Nginx的Master和Worker
varnishadm:
varnishadm可以用来建立一个CLI来连接varnishd,使用-n名称或者-T和-S参数。如果提供了-n参数,密钥文件和address:port会在共享内存中被查找。如果没有提供的话,那么varnishadm将会查找一个实例而不需要指定一个名字。如果给出一个命令,命令和参数会通过CLI连接发送出去,并且结果会返回给stdout。如果没有命令参数提供,varnishadm会通过cli socket和stdin/stdout来传递命令和输出。
-n ident
通过这个名字连接varnishd
-S secretfile
指定认证的密钥文件。这个需要和提供给varnishd的-S参数一致。只有它可以读取该文件的内容,并验证这个CLI连接。
-t timeout
操作的超时时间。
-T
连接指定地址和端口的管理接口
二、varnish配置文件
/etc/varnish/any_name.vcl
varnish是如何制定缓存策略的呢?(所谓缓存策略就是控制http请求要如何拿到所请求的资源,查不查缓存,怎么查,查不到怎么办等)就是通过varnish自己的语言VCL(Varnish Configuration Language)来控制的,我们使用这种语言把要对报文如何处理的动作写到 .vcl 文件中,然后让 varnishd 加载这个文件就ok了,下文会详细介绍如何使用。使用yum安装varnish后,系统会在/etc/varnish下生成一个default.vcl文件,我们可以把vcl控制语句写在这里面,也可以写在该目录下自建的 .vcl 文件中。该文件的具体配置将在后文详细说明,需要额外强调的是 .vcl 文件需要被gcc编译之后才能够被加载使用,所以要一定要预先安装gcc。
/etc/varnish/varnish.params
主要定义了启动的时候使用哪个vcl作为启动时的缓存策略,作为反向代理监听在哪个IP的哪个Port上,开启的CLI接口监听在哪个IP的哪个Port上。启动的时候设定Cacher Process有多少线程池,每个线程池有多少线程。还有很多与缓存业务处理相关的参数(如 default_grace),这些参数可以在启动时设定(有缺省值),也可以在启动后修改(启动时也可以指定哪些参数是启动后只读的)。
VCL4相比VCL3语法的改变点:
要在配置文件中指定版本:即在第一行写上 vcl 4.0;
vcl_fetch函数被vcl_backend_response代替,且req.*不再适用vcl_backend_response;
后端源服务器组director成为varnish模块,需import directors后再在vcl_init子例程中定义;
vcl_error变更为vcl_backend_error,必须使用beresp.*,而不是obj.*。
req.request变更为req.method,obj为只读对象了。
自定义的子例程(即一个sub)不能以vcl_开头,调用使用call sub_name;
error()函数被synth()替代;
return(lookup)被return(hash)替代;
使用beresp.uncacheable创建hit_for_pss对象;
变量req.backend.healty被std.healthy(req.backend)替代;
变量req.backend被req.backend_hint替代;
关键字remove被unset替代;
关键字"purge;"命令,已被去除。在vcl_recv使用return(purge)。
vcl_synth采用resp.*,而非原来的obj.* 。
三、VCL内置state engine
vcl_recv
vcl_deliver
vcl_synth
vcl_hash
vcl_hit
vcl_miss
vcl_pass
vcl_pipe
vcl_purge
vcl_backend_fetch
vcl_backend_response
vcl_backend_error
----------------------------------------------------------------------------------------------------------------------------------
vcl_recv
vcl_recv是客户端请求报文被解析后第一个被执行的subroutine,在该subroutine我们可以添加vcl代码完成诸多功能,如:
1.缓存未命中的话去后端哪台主机请求资源 2.控制缓存策略,如仅仅针对某些url做缓存 3.完成url重写 等功能。
在vcl_recv中我们可以return如下action:
pass:对于http请求,跳过缓存查找这一步去后端server请求资源,虽然没有跳过了查缓存这一步,但是后续的步骤该走的还是得走,该过的subroutine还得过。这种情况下从后端server拿到的资源不会被缓存。
pipe:对于http请求,跳过所有步骤,也就是不用过任何subtoutine的处理,直接去后端server请求资源,这种情况下拿到的资源也不缓存,就像经过varnish搭建了一条客户端到服务器的一条管道。且后续同一个tcp连接的所有request都会直接被送进管道处理。被pipe处理的请求不记录日志。
hash:进行哈希计算并查找缓存。
purge:在缓存中查找缓存对象并清除之。
synth:合成http响应报文,通常是错误报文,此外,synth也可以用于重定向客户端请求。
实例A:
sub vcl_recv {
if (req.httpd.User-Agent ~ "iPad"
req.httpd.User-Agent ~ "iPhone"
req.httpd.User-Agent ~ "Android") {
set req.http.X-Device = "mobile";
} else {
set req.http.X-Device = "desktop";
}
}
作用:根据用户请求报文中的User-Agent判断用户的浏览器类型为mobile或desktop,然后在请求报文中加入名为X-Device的头部,并设置其值为mobile或desktop,表示客户端平台。响应报文(无论来自缓存还是后端server)可以根据客户端平台类型构造并发送给客户端。
实例B:
sub vcl_recv {
set req.http.host = regsub(req.http.host,"^www\.","");
}
作用:把 www.xxx.yyy 按照xxx.yyy处理,如 www.web1.com 按照 web1.com 处理
实例C:
sub vcl_recv {
if (req.http.host == "sport.web1.com") {
set req.http.host = "web1.com";
set req.url = "/sport" + req.url;
}
}
作用:重写 http://sport.web1.com/ 到 http://web1.com/soprt/
实例D:
sub vcl_recv {
if (req.http.host ~ "^sport\.") {
set req.http.host = regsub(req.http.host,"^sport\.","");
set req.url = regsub(req.url,"^","/sport");
}
}
作用:重写 http://sport.xxxx/ 到 http://xxxx/sport/
--------------------------------
vcl_pass
当一个subroutine执行return(pass)时,即跳到vcl_pass执行处理逻辑,vcl_pass会把一个请求设置为pass模式,在vcl_pass中我们可以return: 1.fetch 2.synth 3.restart。 当return一个fetch时,被设置为pass模式的请求得到的对象不会被缓存而直接响应给客户端。返回synth时进入vcl_synth合成响应报文,返回restart则从状态机开始处再开启一轮处理请求报文的动作。
--------------------------------
补充:hit-for-pass
有些请求得到的响应对象不应该被缓存,一个典型的例子就是当响应报文中含有Set-Cookie头部时,因为该响应对象仅仅是针对单个用户的。这种场景下,我们可以设置varnish生成一个hit-for-pass对象,然后缓存之,而不是直接缓存响应对象。
当一个从后端server拿到的对象不需要被缓存时,我们可以 set bereq.uncacheable = true 这样的话Cacher Process就会维护一个指向hit-for-pass的键值对,当再次有请求查到该哈希键时,会找到一个hit-for-pass的缓存对象,然后跳转到vcl_pass处理,在vcl_pass中请求被设置为pass模式。
和正常的缓存对象一样,hit-for-pass也有其ttl,当ttl一过时一样会被从缓存区清理掉。
--------------------------------
vcl_backend_fetch
sub vcl_backend_fetch {
return (fetch);
}
vcl_backend_fetch可以在vcl_miss或vcl_pass中被调用(当然是通过return),当其被vcl_miss调用时在后端server拿到的对象会被缓存,而当其被vcl_pass调用时在后端拿到的对象就不会被缓存,即使对象的obj.ttl和obj.keep变量值大于0。
还有一个与缓存相关的变量 bereq.uncacheable, 该变量指明了后端返回的被请求的资源对象是否被缓存。然而,从pass中的请求得到的对象会忽略bereq.uncacheable的值,而不缓存之,上文也提到了。
vcl_backend_fetch中我们自己加入的代码可以return到fetch或者abandon。前者会将请求代理发送到后端,而后者会调用vcl_synth。vcl_backend_fetch中的缺省代码return的是fetch。来自后端的响应会被vcl_backend_response或vcl_backend_error处理,这取决于响应报文。
如果varnish接收到一个语法正确的http响应(含5xx错误码的http响应),则进入vcl_backend_response处理。若varnish没有收到http响应则进入vcl_backend_error处理。
--------------------------------
vcl_hash
vcl_hash的作用是对一个http请求做哈希计算。
缺省的vcl_recv代码是跳到vcl_hash的,任何subroutine都可以通过return(hash)跳转到vcl_hash。
内置的vcl_hash代码如下:
sub vcl_hash {
hash_date(req.url);
if (req.http.host) {
hash_data(req.http.host);
}
else {
hash_data(server.ip);
}
return (lookup);
}
vcl_hash为将要缓存的对象定义一个哈希键,缓存对象的键是特有的,不同的缓存对象具有不同的键。vcl_hash中的内置代码使用请求的url和hostname/IP来计算哈希键。
vcl_hash的一个用途就是使用user-name来计算哈希键以标识一个特定用户的数据,然而此功能应该谨慎使用。一个更好的替代方案是基于session来哈希某个对象。
vcl_hash运行到最后会return到lookup,lookup并不是subroutine,而只是一个操作。vcl_hash之后进入哪个subroutine处理,取决于lookup在缓存中找到了什么。
当lookup没有匹配到任何哈希键,它会创建一个具有busy标志的对象并把它扔到缓存区。之后跳到vcl_miss处理http请求,当http请求被后端处理后,缓存对象内容就会被后端返回的内容更新,同时busy标志也会被移除。
若一个请求命中了有busy标志的缓存对象,则该请求会被送到waiting list,waiting list是为了提高响应性能而设计的。
--------------------------------
vcl_hit
lookup若匹配到哈希键,则会跳转到vcl_hit。vcl_hit的默认代码如下:
sub vcl_hit {
if (obj.ttl >= 0s) {
return (deliver);
}
if (obj.ttl + obj.grace > 0s) {
return (deliver);
}
return (fetch);
}
vcl_hit执行到最后一般都是return: deliver,restart或synth。
deliver会使处理流程跳转到vcl_deliver,如果该对象的ttl+grace没有过时的话。
restart是把http请求扔到状态机的入口当作一个新的http请求重新处理,同时restart counter这个计数器加一,还记得存放log的内存分为两部分吗?所有的counter都存储在第一部分。当restart counter的值高于 max_restarts 时,varnish会抛出一个guru meditation错误。(max_restarts是一个参数,可以通过varnishadm param.show max_restarts查看其值)
synth(status_code,reason)会丢弃本次request,并返回一个指定的http状态码给客户端。
--------------------------------
vcl_miss
lookup若没有匹配到哈希键,则会跳转到vcl_miss。
vcl_miss中可以加入代码,以决定是否去后端server请求资源,去哪台后端server请求资源。
vcl_miss的默认代码如下:
sub vcl_miss {
return (fetch);
}
我们很少在vcl_hit和vcl_miss这两个subroutine中添加自己的处理逻辑,因为对http请求头部的修改通常都是在vcl_recv中完成的。然而,如果不想让X-Varnish这个http头部发送给后端server的花,可以在vcl_miss或vcl_pass中通过 unset bereq.http.x-varnish; 来实现。
--------------------------------
vcl_deliver
通常来说,对于一个http请求流程,vcl_deliver都是最后一个处理动作。但是经由vcl_pipe代理转发到后端server的请求除外。
该subroutine常常被用来删除debug-headers。
vcl_deliver的默认代码如下:
sub vcl_deliver {
return (deliver);
}
如果我们想修改响应给客户端的报文头部,如删除或增加一个新头部且不想改动后被缓存,则可以在此操作。在vcl_deliver中我们常常用到 rest.http.* , resp.status , resp.reason , obj.hits , req.restarts
------------------------------
vcl_synth
生成含有指定内容的http响应报文,并通过return(deliver)发送给客户端。vcl_synth的默认代码如下:
sub vcl_synth {
set resp.http.Content-Type = "text/html; charset=utf-8";
set resp.http.Retry-After = "5";
synthetic( {"<!DOCTYPE html>
<html>
<head>
<title>"} + resp.status + " " + resp.reason + {"</title>
</head>
<body>
<h1>Error "} + resp.status + " " + resp.reason + {"</h1>
<p>"} + resp.reason + {"</p>
<h3>Guru Meditation:</h3>
<p>XID: "} + req.xid + {"</p>
<hr>
<p>Varnish cache server</p>
</body>
</html> "} );
return (deliver);
}
解释:设置http头部,然后调用synthetic()函数合成一个页面,然后通过return到deliver把页面发送到客户端。我们可以通过在指定的subroutine中return(synth(status_code,"reason_phrase")); 来调用vcl_synth并设置resp.http.status和resp.http.reason。需要注意的是这里return的不是keyword,而是一个内置的具有参数的函数。
{“ 和 ”}用来指示多行字符串
vcl_synth定义的页面对象不会被缓存,而vcl_backend_error定义的页面对象会被缓存。
default.vcl:
probe backend_healthcheck { #健康状况监测
.url = "/test1.html";
.timeout = 1s;
.interval = 10s;
.window = 5;
.threshold = 2;
}
backend web1 { #创建后端主机
.host = "192.168.50.138";
.port = "80";
.probe = backend_healthcheck;
}
backend web2 {
.host = "192.168.50.139";
.port = "80";
.probe = backend_healthcheck;
}
import directors;
sub vcl_init { #创建后端主机组,基于round_robin轮转
new web_cluster = directors.round_robin();
web_cluster.add_backend(web1);
web_cluster.add_backend(web2);
}
acl purgers { #定义PURGE方法访问来源IP
"localhost";
"127.0.0.1";
"192.168.50.0"/24;
}
sub vcl_recv {
if (req.url ~ "test.html") { #测试页面不缓存
return(pass);
}
if (req.method == "PURGE") { #当发送PURGE请求的客户端不再acl中指定地址时,返回405状态代码,并提示Not allowed.
if (!client.ip ~ purgers) {
return(synth(405,"Not allowed"));
}
return(hash);
}
if (req.http.X-Forward-For) { #为后端主机添加X-Forward-For首部
set req.http.X-Forward-For = req.http.X-Forward-For + "," +client.ip;
} else {
set req.http.X-Forward-For = client.ip;
}
set req.backend_hint = web_cluster.backend();
return(hash);
}
sub vcl_hit { #如果请求的是PURGE方法,命中的话返回200状态码。
if (req.method == "PURGE") {
return(synth(200,"Purged"));
}
}
sub vcl_miss {
if (req.method == "PURGE") {
return(synth(404,"Not in cache"));
}
}
sub vcl_pass {
if (req.method == "PURGE") {
return(synth(502,"PURGE on a passed object"));
}
}
sub vcl_backend_response {
if (bereq.url ~ "\.(jpg|jpeg|gif|png)$") {
set beresp.ttl = 6000s;
}
if (bereq.url ~ "\.(html|css|js)$") {
set beresp.ttl = 6000s;
}
if (beresp.http.Set-Cookie) {
return(deliver);
}
}
sub vcl_deliver {
if (obj.hits >0) { #判断请求的资源是否缓存命中
set resp.http.X-Cache="Hit from"+" "+server.ip;
}
else {
set resp.http.X-Cache="Miss from"+" "+server.ip;
}
}