Varnish调优手记

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介:

   最近公司做活动推广,流量暴增,后端服务器压力山大,导致用户的请求响应时间延长,客户因此抱怨声音很大。 

  为尽快解决问题,在安排人员不断优化后端代码的同时,考虑在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 
此时新的配置文件已经生效! 









本文转自 nmshuishui 51CTO博客,原文链接:http://blog.51cto.com/nmshuishui/1407604,如需转载请自行联系原作者
相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
目录
相关文章
|
7月前
|
缓存 前端开发 应用服务中间件
Nginx多方面调优策略
Nginx多方面调优策略
94 0
|
缓存 监控
varnish缓存初探(2)—基础配置
varnish缓存学习的第二步
1673 0
|
Web App开发 应用服务中间件 Linux
|
存储 消息中间件
|
Web App开发 测试技术 应用服务中间件
|
Web App开发 缓存 测试技术
|
Web App开发 缓存 监控