最近公司做活动推广,流量暴增,后端服务器压力山大,导致用户的请求响应时间延长,客户因此抱怨声音很大。
为尽快解决问题,在安排人员不断优化后端代码的同时,考虑在nginx前增加varnish缓存层,只透传部分动态请求过去,直接减少后端服务器的压力。
在实际使用中,真正感受到了varnish服务器强大的威力!在不断的调优缓存命中率后,后端服务器cpu直接从80%降到了20%,再大的并发前端也可以直接消化,后端服务器表示毫无压力。有了这玩意,可以再也不用在后台写定时任务,不断重新生成静态页面了,直接丢缓存里完事!此外,varnish还支持一种叫“神圣模式”,在后端服务器报错返回500的时候,varnish还能继续优先返回过去缓存的内容,为用户屏蔽部分错误,这东东有时真算是救命稻草啊。
但同时,也趟了n多的坑,varnish中的VCL语言太过强大和灵活,稍微运用不好就会中枪。而网上公开的大多数varnish配置文件都是一大抄,根本无法直接用于生产。在研究了几天,翻阅了大量各种资料后,才总算把遇到的问题都解决了。
现将调优心得记录如下:
一、介绍
Varnish是一种专业的网站缓存软件(其实就是带缓存的反向代理服务),它可以把整个HTTP响应内容缓存到内存或文件中,从而提高Web服务器的响应速度。
Varnish内置强大的VCL(Varnish Configuration Language)配置语言,允许通过各种条件判断来灵活调整缓存策略。在程序启动时,varnish就把VCL转换成二进制代码,因此性能非常高。
二、安装
epel源里也有varnish,但是却2.x版本的。
因为 varnish 3.0的配置文件与 2.x 的存在很大不同,因此varnish团队不能再更新epel里的软件源。如果你想安装最新版本,推荐使用 rpm 方式。
RPM安装
在redhat系服务器上可以很容易的直接通过rpm包安装:
1
2
3
4
|
wget http:
//repo
.varnish-cache.org
/redhat/varnish-3
.0
/el6/x86_64/varnish/varnish-libs-3
.0.4-1.el6.x86_64.rpm
wget http:
//repo
.varnish-cache.org
/redhat/varnish-3
.0
/el6/x86_64/varnish/varnish-3
.0.4-1.el6.x86_64.rpm
wget http:
//repo
.varnish-cache.org
/redhat/varnish-3
.0
/el6/x86_64/varnish/varnish-docs-3
.0.4-1.el6.x86_64.rpm
yum localinstall *.rpm
|
varnish的安装和配置路径
1
2
3
|
/etc/varnish/default
.vcl
#默认配置文件存文件
/etc/sysconfig/varnish
#服务启动参数脚本
/etc/init
.d
/varnish
#服务控制脚本
|
可以通过调整 /etc/sysconfig/varnish 配置文件中的参数来调整启动参数,设置线程池、缓存到内存还是文件等。当然如果你乐意也可以在varnishd后面带上启动参数手工启动服务和管理。
现在能通过服务的方式启动 varnish了:
1
|
service varnish start (stop
/restart
)
|
将varnish设为开机自启动:
1
|
chkconfig varnish on
|
三、VCL执行过程
先介绍一下Varnish处理请求的主要处理方法和流程
VCL 需定义几个默认的函数,在Varnish处理HTTP请求的各个阶段会回调这些函数进行处理:
-
vcl_recv,请求入口,判断是否要进一步处理,还是直接转发给后端(pass)。 此过程中可以使用和请求相关的变量,例如客户端请求的url,ip,user-agent,cookie等,此过程中可以把不需缓存的地址,通过判断(相等、不相等、正则匹配等方法)透传给后端,例如POST请求,及jsp、asp、do等扩展名的动态内容;
-
vcl_fetch,当从后端服务器获取内容后会进入此阶段,除了可以使用客户端的请求变量,还可以使用从后端获取的信息(bersp),如后端返回的头信息,具体指定此信息的缓存时间TTL;
-
vcl_miss 缓存未命中时中要做的处理
-
vcl_hit 缓存命中后做的处理
-
vcl_delever 发送给客户端前的处理
-
vcl_pass 交给后端服务器
-
vcl_hash 设置缓存的键值key
首次请求时过程如下:
recv->hash->miss->fetch->deliver
缓存后再次请求:
recv->hash->hit->deliver(fetch的过程没了,这就是我们要做的,把要缓存的页面保存下来)
直接交给后端pass的情况:
recv->hash->pass->fetch->deliver(直接从后端获取数据后发送给客户端,此时Varnish相当于一个中转站,只负责转发)
四、通过日志调优
安装完成后,默认的配置文件位于
/etc/varnish/default.vcl
我们可以参考缺省配置项学习vcl语言的使用,并进行不断的调优。
但直接修改配置,不断的重启调优效率非常低下痛苦!经过不断摸索,我发现其实varnish里内置了日志模块,我们可以在 defalut.vcl 最上边引用std库,以便输出日志:
1
|
import
std;
|
在需要输出日志的地方,使用 std.log 即可:
1
|
std.log(
"LOG_DEBUG: URL="
+ req.url);
|
这样的话,就可以通过日志了解varnish的工作流程,很方便的优化啦,效率何止提高十倍!
类似于你想跟踪哪些连接没有命中缓存,可以在vcl_miss函数中这样写:
1
2
3
4
|
sub vcl_miss {
td.log(
"url miss!!! url="
+ req.url);
return
(fetch);
}
|
启动varnish 后,通过 varnishlog工具跟踪打印出的日志
1
|
varnishlog -I LOG
|
五、负载均衡
Varnish可以挂载多个后端服务器,并进行权重、轮询,将请求转发到后端节点上,以达到避免单点的问题。
举例如下:
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
|
backend web1 {
.host =
"172.16.2.31"
;
.port =
"80"
;
.probe = {
.url =
"/"
;
.interval = 10s;
.timeout = 2s;
.window = 3;
.threshold = 3;
}
}
backend web2 {
.host =
"172.16.2.32"
;
.port =
"80"
;
.probe = {
.url =
"/"
;
.interval = 10s;
.timeout = 2s;
.window = 3;
.threshold = 3;
}
}
# 定义负载均衡组
director webgroup random {
{
.backend = web1;
.weight = 1;
}
{
.backend = web2;
.weight = 1;
}
}
|
其中,在backend 中添加 probe 选项,将可以对后端节点进行健康检查。如果后端节点无法访问,将会自动摘除掉该节点,直到这个节点恢复。
需要注意window 和threshold 两个参数。当有后端服务器不可达时,varnish会时不时的报503错误。网上查出的资料都是改线程组什么的,经测试完全无效。后来发现,只要将 window 和threshold 两个参数的值设成一样的,503现象就再没有发生了。
六、优雅模式和神圣模式
Grace mode
如果后端需要很长时间来生成一个对象,这里有一个线程堆积的风险。为了避免这 种情况,你可以使用 Grace。他可以让 varnish 提供一个存在的版本,然后从后端生成新 的目标版本。当同时有多个请求过来的时候,varnish只发送一个请求到后端服务器,在
set beresp.grace = 30m;
时间内复制旧的请求结果给客户端。
Saint mode
有时候,服务器很古怪,他们发出随机错误,您需要通知 varnish 使用更加优雅的方式处理 它,这种方式叫神圣模式(saint mode)。Saint mode 允许您抛弃一个后端服务器或者另一个尝试的后端服务器或者 cache 中服务陈旧的内容。
例如:
1
2
3
4
5
6
7
|
sub vcl_fetch {
if
(beresp.status == 500) {
set
beresp.saintmode = 10s;
return
(restart);
}
set
beresp.grace = 5m;
}
|
七、完整示例
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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
|
import
std;
backend web1 {
.host =
"172.16.2.31"
;
.port =
"80"
;
.probe = {
.url =
"/"
;
.interval = 10s;
.timeout = 2s;
.window = 3;
.threshold = 3;
}
}
backend web2 {
.host =
"172.16.2.32"
;
.port =
"80"
;
.probe = {
.url =
"/"
;
.interval = 10s;
.timeout = 2s;
.window = 3;
.threshold = 3;
}
}
# 定义负载均衡组
director webgroup random {
{
.backend = web1;
.weight = 1;
}
{
.backend = web2;
.weight = 1;
}
}
# 允许刷新缓存的ip
acl purgeAllow {
"localhost"
;
"172.16.2.5"
;
}
sub vcl_recv {
# 刷新缓存设置
if
(req.request ==
"PURGE"
) {
#判断是否允许ip
if
(!client.ip ~ purgeAllow) {
error 405
"Not allowed."
;
}
#去缓存中查找
return
(lookup);
}
std.log(
"LOG_DEBUG: URL="
+ req.url);
set
req.backend = webgroup;
if
(req.request !=
"GET"
&& req.request !=
"HEAD"
&& req.request !=
"PUT"
&& req.request !=
"POST"
&& req.request !=
"TRACE"
&& req.request !=
"OPTIONS"
&& req.request !=
"DELETE"
) {
/* Non-RFC2616 or CONNECT
which
is weird. */
return
(pipe);
}
# 只缓存 GET 和 HEAD 请求
if
(req.request !=
"GET"
&& req.request !=
"HEAD"
) {
std.log(
"LOG_DEBUG: req.request not get! "
+ req.request );
return
(pass);
}
# http 认证的页面也 pass
if
(req.http.Authorization) {
std.log(
"LOG_DEBUG: req is authorization !"
);
return
(pass);
}
if
(req.http.Cache-Control ~
"no-cache"
) {
std.log(
"LOG_DEBUG: req is no-cache"
);
return
(pass);
}
# 忽略admin、verify、servlet目录,以.jsp和.do结尾以及带有?的URL,直接从后端服务器读取内容
if
(req.url ~
"^/admin"
|| req.url ~
"^/verify/"
|| req.url ~
"^/servlet/"
|| req.url ~
"\.(jsp|php|do)($|\?)"
) {
std.log(
"url is admin or servlet or jsp|php|do, pass."
);
return
(pass);
}
# 只缓存指定扩展名的请求, 并去除 cookie
if
(req.url ~
"^/[^?]+\.(jpeg|jpg|png|gif|bmp|tif|tiff|ico|wmf|js|css|ejs|swf|txt|zip|exe|html|htm)(\?.*|)$"
) {
std.log(
"*** url is jpeg|jpg|png|gif|ico|js|css|txt|zip|exe|html|htm set cached! ***"
);
unset
req.http.cookie;
# 规范请求头,Accept-Encoding 只保留必要的内容
if
(req.http.Accept-Encoding) {
if
(req.url ~
"\.(jpg|png|gif|jpeg)(\?.*|)$"
) {
remove req.http.Accept-Encoding;
} elsif (req.http.Accept-Encoding ~
"gzip"
) {
set
req.http.Accept-Encoding =
"gzip"
;
} elsif (req.http.Accept-Encoding ~
"deflate"
) {
set
req.http.Accept-Encoding =
"deflate"
;
}
else
{
remove req.http.Accept-Encoding;
}
}
return
(lookup);
}
else
{
std.log(
"url is not cached!"
);
return
(pass);
}
}
sub vcl_hit {
if
(req.request ==
"PURGE"
) {
set
obj.ttl = 0s;
error 200
"Purged."
;
}
return
(deliver);
}
sub vcl_miss {
std.log(
"################# cache miss ################### url="
+ req.url);
if
(req.request ==
"PURGE"
) {
purge;
error 200
"Purged."
;
}
}
sub vcl_fetch {
# 如果后端服务器返回错误,则进入 saintmode
if
(beresp.status == 500 || beresp.status == 501 || beresp.status == 502 || beresp.status == 503 || beresp.status == 504) {
std.log(
"beresp.status error!!! beresp.status="
+ beresp.status);
set
req.http.host =
"status"
;
set
beresp.saintmode = 20s;
return
(restart);
}
# 如果后端静止缓存, 则跳过
if
(beresp.http.Pragma ~
"no-cache"
|| beresp.http.Cache-Control ~
"no-cache"
|| beresp.http.Cache-Control ~
"private"
) {
std.log(
"not allow cached! beresp.http.Cache-Control="
+ beresp.http.Cache-Control);
return
(hit_for_pass);
}
if
(beresp.ttl <= 0s || beresp.http.Set-Cookie || beresp.http.Vary ==
"*"
) {
/* Mark as
"Hit-For-Pass"
for
the next 2 minutes */
set
beresp.ttl = 120 s;
return
(hit_for_pass);
}
if
(req.request ==
"GET"
&& req.url ~
"\.(css|js|ejs|html|htm)$"
) {
std.log(
"gzip is enable."
);
set
beresp.do_gzip =
true
;
set
beresp.ttl = 20s;
}
if
(req.request ==
"GET"
&& req.url ~
"^/[^?]+\.(jpeg|jpg|png|gif|bmp|tif|tiff|ico|wmf|js|css|ejs|swf|txt|zip|exe)(\?.*|)$"
) {
std.log(
"url css|js|gif|jpg|jpeg|bmp|png|tiff|tif|ico|swf|exe|zip|bmp|wmf is cache 5m!"
);
set
beresp.ttl = 5m;
} elseif (req.request ==
"GET"
&& req.url ~
"\.(html|htm)$"
) {
set
beresp.ttl = 30s;
}
else
{
return
(hit_for_pass);
}
# 如果后端不健康,则先返回缓存数据1分钟
if
(!req.backend.healthy) {
std.log(
"eq.backend not healthy! req.grace = 1m"
);
set
req.grace = 1m;
}
else
{
set
req.grace = 30s;
}
return
(deliver);
}
# 发送给客户端
sub vcl_deliver {
if
( obj.hits > 0 ) {
set
resp.http.X-Cache =
"has cache"
;
}
else
{
#set resp.http.X-Cache = "no cache";
}
return
(deliver);
}
|
八、管理命令
跟随varnish会一起安装一些方便的调试工具,用好这些工具,对你更好的应用varnish有很大的帮助。
varnishncsa(以 NCSA 的格式显示日志)
通过这个命令,可以像类似于 nginx/apache一样的显示出用户的访问日志来。
varnishlog(varnish详细日志)
如果你想跟踪varnish处理每个请求时的详细处理情况,可以使用此命令。
直接使用这个命令,显示的内容非常多,通常我们可以通过一些参数,使它只显示我们关心的内容。
-
-b \\只显示varnish和backend server之间的日志,当您想要优化命中率的时 候可以使用这个参数。
-
-c \\和-b差不多,不过它代表的是 varnish和 client端的通信。
-
-i tag \\只显示某个 tag,比如“varnishlog –i SessionOpen”将只显示新会话,注意,这个地方的tag名字是不区分大小写的。
-
-I \\通过正则表达式过滤数据,比如“varnishlog -c -i RxHeader -I Cookie”将显示所有接到到来自客户端的包含 Cookie 单词的头信息。
-
-o \\聚合日志请求 ID
例如:
varnishlog -c -o /auth/login 这个命令将告诉您来自客户端(-c)的所有包含”/auth/login” 字段(-o)请求。
varnishlog -c -o ReqStart 192.168.1.100 只跟踪一个单独的client请求
varnishtop
您可以使用varnishtop 确定哪些URL经常被透传到后端。
适当的过滤使用 –I,-i,-X 和-x 选项,它可以按照您的要求显示请求的内容,客
户端,浏览器等其他日志里的信息。
varnishtop -i rxurl \\您可以看到客户端请求的 url次数。
Varnishtop -i txurl \\您可以看到请求后端服务器的url次数。
Varnishtop -i Rxheader -I Accept-Encoding \\可以看见接收到的头信息中有有多少次
包含Accept-Encoding。
varnishstat
显示一个运行varnishd实例的相关统计数据。
Varnish 包含很多计数器,请求丢失率,命中率,存储信息,创建线程,删除对象等,几乎所有的操作。通过跟踪这些计数器,可以很好的了解varnish运行状态。
varnishadm
通过命令行,控制varnish服务器。可以动态的删除缓存,重新加载配置文件等。
管理端口有两种链接方式:
1,telnet方式,可以通过telnet来连接管理端口.如:"telnet localhost 6082"
2,varnishadm方式,可以通过varnish自带的管理程序传递命令.如: varnishadm -n vcache -T localhost:6082 help
动态清除缓存
varnishadm -S /etc/varnish/secret -T 127.0.0.1:6082 ban.url /2011111.png
其中:ban.url 后的路径一定不要带abc.xxx.com域名之类的,否则缓存清除不了。
清除包含某个子目录的URL地址:
/usr/local/varnish/bin/varnishadm -S /etc/varnish/secret -T 127.0.0.1:6082 url.purge /a/
不重启加载配置文件
登陆到管理界面
/usr/local/varnish/bin/varnishadm -S /etc/varnish/secret -T 127.0.0.1:6082
加载配置文件
vcl.load new.vcl /etc/varnish/default.vcl
编译出错的话会有提示,成功会返回200
加载新配置文件
vcl.use new.vcl
此时新的配置文件已经生效!