一、Varnish简介
Varnish 的作者Poul-Henning Kamp是FreeBSD的内核开发者之一,他认为现在的计算机比起1975年已经复杂许多。在1975年时,储存媒介只有两种:内存与硬盘。但现在计算机系统的内存除了主存外,还包括了CPU内的L1、L2,甚至有L3快取。硬盘上也有自己的快取装置,因此Squid Cache自行处理物件替换的架构不可能得知这些情况而做到最佳化,但操作系统可以得知这些情况,所以这部份的工作应该交给操作系统处理,这就是 Varnish cache设计架构。
varnish项目是2006年发布的第一个版本0.9.距今已经八年多了,此文档之前也提过varnish还不稳定,那是2007年时候编写的,经过varnish开发团队和网友们的辛苦耕耘,现在的varnish已经很健壮。很多门户网站已经部署了varnish,并且反应都很好,甚至反应比squid还稳定,且效率更高,资源占用更少。相信在反向代理,web加速方面,varnish已经有足够能力代替squid。
二、Web缓存部分首部介绍
程序能够缓存是因为程序具有局部性,时间局部性和空间局部性。缓存一般存储为键值,key为访问路径的url经过hash计算后的结果,value为服务器内容,一般缓存存储的是热点数据,切缓存中存储的数据不会大于后端web服务器的程序数据大小。缓存的对象是有生命周期的,当缓存空间耗尽的时候会根据LRU算法(最近最少使用)或其他算法对缓存空间进行清理。缓存中一般只能缓存非客户私有数据,客户私有数据和带cookie的数据可以进行缓存,但是存在风险,所以一般不对客户私有数据进行缓存。
缓存处理的步骤:接收请求 --> 解析请求 (提取请求的URL及各种首部)--> 查询缓存 --> 新鲜度检测 --> 创建响应报文 --> 发送响应 --> 记录日志(此图不一定准确,只是为了好理解)
新鲜度检测机制:
过期日期:
HTTP/1.0 Expires(Expires:Thu, 04 Jun 2015 23:38:18 GMT)
HTTP/1.1 Cache-Control: max-age(Cache-Control:max-age=600)
条件式请求首部:
If-Modified-Since:基于请求内容的时间戳作验正;
If-None-Match:基于被请求变量的实体值ETag;ETag是一个可以与Web资源关联的记号;
有效性再验正:revalidate
如果原始内容未改变,则仅响应首部(不附带body部分),响应码304 (Not Modified)
如果原始内容发生改变,则正常响应,响应码200;
如果原始内容消失,则响应404,此时缓存中的cache object也应该被删除;
缓存控制机制(Cache-Control):
请求:
no-cache:不要给我缓存的内容,要从Web服务器现取的内容
max-age:只接受Age值小于max-age的值,并且没有过期的缓存对象
(Age:当代理服务器用自己缓存的实体去响应请求时,用该头部表明该实体从产生到现在经过多长时候)
max-statle:可以接受过期的缓存对象,但是过期时间必须小于此值
min-fresh:接收其新鲜生命期大于当前Age和min-fresh值之和的缓存对象
响应:
public:可以用Cache内容回应任何用户
private:只能缓存内容回应先前请求该内容的那个用户
no-cache:可以缓存,但是只有在跟Web服务器验证了其有效后,才能返回客户端
no-store:不允许缓存
三、Varnish的结构和工作流程
①、Management进程
Management进程主要实现应用新的配置、编译VCL、监控varnish、初始化varnish以及提供一个命令行接口等。Management进程会每隔几秒钟探测一下Child进程以判断其是否正常运行,如果在指定的时长内未得到Child进程的回应,Management将会重启此Child进程。
②、Child/Cache 进程
Commad line 线程 : 管理接口
Storage/hashing 线程 :完成hash并进行缓存存储
Log/stats 线程:查看记录日志并统计各种状态
Accept 线程:接收新的连接请求并响应;
Backend Communication 线程:管理后端主机线程,当缓存中没有缓存需要有此进程交由后端服务器处理
Worker 线程:child进程会为每个会话启动一个worker线程,因此,在高并发的场景中可能会出现数百个worker线程甚至更多;
Object Expiry 线程:从缓存中清理过期内容;
③、Varnish日志
为了与系统的其它部分进行交互,Child进程使用了可以通过文件系统接口进行访问的共享内存日志(shared memory log),因此,如果某线程需要记录信息,其仅需要持有一个锁,而后向共享内存中的某内存区域写入数据,再释放持有的锁即可。而为了减少竞争,每个worker线程都使用了日志数据缓存。共享内存日志大小一般为90M,其分为两部分,前一部分为计数器,后半部分为客户端请求的数据。varnish提供了多个不同的工具如varnishlog、varnishncsa或varnishstat等来分析共享内存日志中的信息并能够以指定的方式进行显示。
④、VCL(Varnish Configuation Language)简介
Varnish Configuration Language (VCL)是varnish配置缓存策略的工具,它是一种基于“域”(domain specific)的简单编程语言,它支持有限的算术运算和逻辑运算操作、允许使用正则表达式进行字符串匹配、允许用户使用set自定义变量、支持if判断语句,也有内置的函数和变量等。使用VCL编写的缓存策略通常保存至.vcl文件中,其需要编译成二进制的格式后才能由varnish调用。事实上,整个缓存策略就是由几个特定的子例程如vcl_recv、vcl_fetch等组成,它们分别在不同的位置(或时间)执行,如果没有事先为某个位置自定义子例程,varnish将会执行默认的定义。
VCL策略在启用前,会由management进程将其转换为C代码,而后再由gcc编译器将C代码编译成二进制程序。编译完成后,management负责将其连接至varnish实例,即child进程。正是由于编译工作在child进程之外完成,它避免了装载错误格式VCL的风险。因此,varnish修改配置的开销非常小,其可以同时保有几份尚在引用的旧版本配置,也能够让新的配置即刻生效。编译后的旧版本配置通常在varnish重启时才会被丢弃,如果需要手动清理,则可以使用varnishadm的vcl.discard命令完成。
⑤、Varnish 的后端存储
Varnish支持多种不同类型的后端存储,这可以在varnishd启动时使用-s选项指定。后端存储的类型包括:
file:使用特定的文件存储全部的缓存数据,并通过操作系统的mmap()系统调用将整个缓存文件映射至内存区域(如果条件允许);
malloc:使用malloc()库调用在varnish启动时向操作系统申请指定大小的内存空间以存储缓存对象;
persistent(experimental):与file的功能相同,但可以持久存储数据(即重启varnish数据时不会被清除);仍处于测试期;
Varnish无法追踪某缓存对象是否存入了缓存文件,从而也就无从得知磁盘上的缓存文件是否可用,因此,file存储方法在varnish停止或重启时会清除数据。而persistent方法的出现对此有了一个弥补,但persistent仍处于测试阶段,例如目前尚无法有效处理要缓存对象总体大小超出缓存空间的情况,所以,其仅适用于有着巨大缓存空间的场景。
一个Management可以启动多个child/cache,每个child/cache子进程内部生成多个worker threads响应用户请求。Varnish是单进程多线程模式的,每个线程处理一个用户请求,但是不是所有的线程都用于用户请求,部分线程工作于Backend Communication、Log/stats等
三、Varnish的安装和配置
这里使用Centos 7通过epel源安装Varnish
[root@C7node1 /]# yum install varnish jemalloc.x86_64 0:3.6.0-1.el7 #jemalloc 一个性能非常卓越的内存分配器,由于C提供的内存分配效率过低,所以这里使用了更高性能的内存管理器 #用户malloc内存缓存对象使用 [root@C7node1 /]# rpm -ql varnish /etc/logrotate.d/varnish #日志回滚 /etc/varnish/default.vcl #varnish的配置文件 /etc/varnish/varnish.params #varnish的参数,用于配置varnish作为缓存的工作属性 /usr/lib/systemd/system/varnishlog.service #从共享内存中抽取第二段数据日志 /usr/lib/systemd/system/varnishncsa.service #从共享内存中抽取第二段数据日志
配置varnish的三种应用:
①、varnishd应用程序的命令行参数(/etc/varnish/varnish.params)
Varnishd常用选项
-p param=value # set parameter。配置参数
-r param[,param...] # make parameter read-only。设定只读参数列表
-f file # VCL script,读取VCL配置文件
-a address:port # HTTP listen address and port。指定http地址端口
-d # debug。运行于调试模式
-s [name=]kind[,options] # Backend storage specification。指定存储类型
# -s malloc[,<size>]
# -s file,<dir_or_file>,<size>,<granularity>
# -s persist{experimental}
-T address:port # Telnet listen address and port。指定管理接口
-S secret-file # Secret file for CLI authentication。指定管理密钥文件
-t # Default TTL。前端服务器varnish连接各后端服务器时超时时间
②、varnishd Child/cahce:实时参数(child的进程数,worker的线程数)
varnishadm
param.set
③、vcl:配置缓存系统的缓存机制;
通过vcl配置文件进行配置;先编译,后应用;依赖于c编译器;
四、Varnish工具的使用
①、varnishadm
[root@C7node1 varnish]# varnishadm -S /etc/varnish/secret -T 127.0.0.1:6082 help [<command>] #查询帮助 ping [<timestamp>] #测试ping通信 auth <response> quit #退出varnishadm status #查看Child的状态 start #开启Child stop #停止Child vcl.load <configname> <filename> #编译加载指定vcl vcl.inline <configname> <quoted_VCLstring> vcl.use <configname> #使用指定vcl vcl.discard <configname> #删除指定vcl vcl.list #列出所有vcl vcl.show <configname> #查看编译好的vcl的配置文件 param.show [-l] [<param>] #查看运行参数 param.set <param> <value> #设置运行参数 panic.show #查看Child恐慌挂掉的信息 panic.clear #清除信息 storage.list #查看使用的storage列表 backend.list [<backend_expression>] #查看backed后端服务器 backend.set_health <backend_expression> <state> #调整backed状态 ban <field> <operator> <arg> [&& <field> <oper> <arg>]... #清理缓存中的缓存对象 ban.list #列出定义的ban规则
②、varnishlog
③、varnishncsa
④、varnishtop
⑤、varnishstat
①、VCL的状态引擎
state engine:各引擎之间存一定程度上的相关性;前一个engine如果可以有多种下游engine,则上游engine需要用return指明要转移的下游engine;
vcl_recv:接收客户请求
vcl_hash:当域名做泛解析的时候,对同一类的url做规范化
vcl_hit:命中缓存
vcl_miss:没有命中缓存
vcl_fetch:去后端服务器请求数据
vcl_deliver:投递给客户端
vcl_pipe:
vcl_pass:
vcl_error:
vcl_backend_fetch(V4新添加的)
vcl_backend_response(V4新添加的)
vcl_backend_error(V4新添加的)
vcl_purge(V4新添加的)
vcl_synth(V4新添加的)
②、VCL语法
//、#或/* comment */ 用于注释;会被编译器忽略
sub $name 定义函数 ,sub vcl_recv { }
不支持循环,有众多内置变量 ,变量的可调用位置与state engine有密切相关性
使用终止语句,return(action),没有返回值
域专用
操作符:=(赋值)、==(等值比较)、~(模式匹配)、!(取反)、&&(逻辑与)、||(逻辑或)
条件判断语句:if(comment){ }else{ }
变量赋值和撤销变量:set name=value,unset name
③、varnish中的内置变量:
client
client.ip:客户端IP地址
server
server.ip:varnish ip地址
server.hostname:varnish主机名
req
req.http.HEADERS:客户端发往varnish的请求报文的指定首部
req.method:请求方法
req.proto:请求协议版本
req.ttl:请求ttl
req.url:请求url
resp
resp.http.HEADERS:varnish发往客户端的响应报文的指定首部
resp.proto:响应协议版本
resp.reason:响应的原因短语
resp.status:响应的状态码
bereq
bereq.http.HEADERS: 由varnish发往backend server的请求报文的指定首部;
bereq.request:请求方法;
bereq.url:请求的url
bereq.proto:请求的协议版本
bereq.backend:指明要调用的后端主机;
beresp
beresp.proto:又backend server响应的协议版本
beresp.status:后端服务器的响应的状态码
beresp.reason:原因短语;
beresp.backend.ip:后端服务器的IP地址
beresp.backend.name:后端服务器的主机名
beresp.http.HEADER: 从backend server响应的报文的首部;
beresp.ttl:后端服务器响应的内容的余下的生存时长;
obj
obj.ttl: 对象的ttl值;
obj.hits:此对象从缓存中命中的次数;
storage
官方文档:https://www.varnish-cache.org/docs/4.0/reference/vcl.html#varnish-configuration-language
④、varnish中的内置方法:
return():终止vcl引擎
hash_data(input):进行hash计算并放回对应的hash值
regsub(str, regex, sub):正则表达式查找替换,之查找替换一次
regsuball(str, regex, sub):正则表达式查找替换,替换所有
ban(expression):缓存对象清理
c7node1.wlw.com 192.168.0.56 Varnish服务器
C6node1.wlw.com 192.168.0.66 backend server
C6node2.wlw.com 192.168.0.76 backend server
定义在vcl_deliver中,向响应给客户端的报文添加一个自定义首部X-Cache
[root@C7node1 varnish]# vim /etc/varnish/wlw.vcl 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); } sub vcl_deliver { if(obj.hits > 0) { set resp.http.X-Cache = "HIT"; }else{ set resp.http.X-Cache = "Miss"; } } [root@C7node1 varnish]# varnishadm vcl.load wlw wlw.vcl 200 VCL compiled. vcl.list 200 active 0 boot available 0 wlw vcl.use wlw 200 VCL 'wlw' now active [root@wlw ~]# curl -I http://192.168.0.56 X-Cache: Miss [root@wlw ~]# curl -I http://192.168.0.56 X-Cache: HIT
让Varnish支持虚拟主机
sub vcl_recv { if (req.http.host == "www.wlw.com") { return (pipe); } } sub vcl_pipe{ set bereq.http.host = "www.wlw.com"; }
强制对某资源的请求,不检查缓存
sub vcl_recv { if (req.url ~ "^/test1.html$") { return(pass); } } [root@C6node1 dz]# curl -I http://192.168.0.56/test1.html HTTP/1.1 200 OK X-Cache: Miss [root@C6node1 dz]# curl -I http://192.168.0.56/test1.html HTTP/1.1 200 OK X-Cache: Miss #可以看到多次请求都没有命中缓存 sub vcl_recv { if (req.url ~ "(?i)^/login" || req.url ~ "(?i)^/admin") { return(pass); } } #对/login或者/admin的目录都不查询缓存,(?i) 表示对正则匹配不区分大小写
对特定类型的资源(公共可用)取消其私有的cookie标识,并强行设定其可以varnish缓存的时长
sub vcl_backend_response { if (beresp.http.cache-control !~ "s-maxage") { 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; } } } #veresp.ttl为设置在varnish上的缓存时长
定义backend server和后端主机的健康状态检测,并实现资源分别分配
backend websrv1 { .host = "192.168.0.66"; .port = "80"; .probe = { .url = "/test5.html"; } } backend websrv2 { .host = "192.168.0.76"; .port = "80"; .probe = { .url = "/test5.html"; } } sub vcl_recv { if (req.url ~ "(?i)\.(jpg|png|gif)$") { set req.backend_hint = websrv1; } else { set req.backend_hint = websrv2; } } #定义两台后端主机
测试查看结果
backend.list Backend name Refs Admin Probe websrv1(192.168.0.66,,80) 1 probe Healthy 8/8 websrv2(192.168.0.76,,80) 1 probe Sick 0/8 backend.list 200 Backend name Refs Admin Probe websrv1(192.168.0.66,,80) 1 probe Healthy 8/8 websrv2(192.168.0.76,,80) 1 probe Healthy 6/8 #这里因为192.168.0.76忘记关闭防火墙导致请求test5.html检测失败,关闭防火墙后检测通过并恢复
backend server的定义:
backend name {
.attribute = "value";
}
.host: BE主机的IP;
.port:BE主机监听的PORT;
.probe: 对BE做健康状态检测;
.max_connections:并连接最大数量;
后端主机的健康状态检测方式:
probe name {
.attribute = "value";
}
.url: 判定BE健康与否要请求的url;
.request:手动定义请求报文的内容
.expected_response:期望响应状态码;默认为200;
.timeout:健康状态检测的超时时间,默认2s
.interval:每隔多长时间检测一次,默认5s
.window:最近检测的窗口,默认为8
.threshold:默认为3,只要在window定义的次数中有threshold次通过即为正常
.initial:当Varnish主机刚启动的时候检测后端主机多少次成功即为正常,默认为1
基于Varnish的负载均衡
backend websrv1 { .host = "192.168.0.66"; .port = "80"; .probe = { .url = "/test5.html"; } } backend websrv2 { .host = "192.168.0.76"; .port = "80"; .probe = { .url = "/test5.html"; } } import directors; sub vcl_init { new mycluster = directors.round_robin(); mycluster.add_backend(websrv1); mycluster.add_backend(websrv2); } sub vcl_recv { if (req.url ~ "(?i)\.(jpg|png|gif)$") { set req.backend_hint = websrv1; } else { set req.backend_hint = websrv2; } set req.backend_hint = mycluster.backend(); } [root@C6node1 dz]# curl http://192.168.0.56/test1.html Page 1 Web2 [root@C6node1 dz]# curl http://192.168.0.56/test2.html Page 2 Web1 [root@C6node1 dz]# curl http://192.168.0.56/test3.html Page 3 Web2 [root@C6node1 dz]# curl http://192.168.0.56/test4.html Page 4 Web1 #基于资源的负载均衡,负载均衡算法:fallback, random, round_robin, hash