今天在和同事谈论现网后端tomcat挂起导致前端varnish线程堆积,最终造成varnish挂掉或前端web处理繁忙的问题。由于之前详细的看过varnish的文档,当时记得有一个上帝模式还是圣杯模式来着(由于英向汉翻译的问题,可能有N种翻译方法)。由于时隔相当一段时间了,所以记得不大清楚,后来就去网上查了下,现在汉译的统称叫———— 神圣模式和优雅模式可以应用到上面提到的场景。这里再做一个tag总结成博文记录下。
一、Grace mode(优雅模式)
为什么使用Grace mode ,优雅模式的优点是什么?
如果你每秒需要相应成千上万的点击,等待的请求队列就会很巨大。这里有两个潜在问题:一个是thundering herd problem(这个无法翻译。。。wiki有专门的对应解释),突然增加一千个线程去提供内容,会让负载变得很高;第二个,没有人喜欢等。为了解决这个问题,我们指示varnish去保持缓存的对象超过他们的TTL(就是该过期的,不让它过期),并且去提供旧的内容给正在等待的请求。 既然要提供旧的内容,首先我们必须有内容去提供。所以,我们使用以下VCL,以使varnish保持所有对象超出了他们的TTL30分钟。
sub vcl_fetch { set beresp.grace = 30m; }
这样,varnish还不会提供旧对象。为了启用varnish去提供旧对象,我们必须在请求上开启它。下面表示,我们接收15s的旧对象:
sub vcl_recv { set req.grace = 15s; }
varnish的优势在于内存级的cache,所以内存的多少决定了cache的数据量的多少。如果开启了优雅模式,在TTL到期后,我们仍不将其从mem里清除掉,而是要再等待一段时间才清除,这就无端的浪费了资源。但如果你开启了健康检查,你可以检查后端是否出问题。如果出问题了,我们可以提供长点时间的旧内容。如果后端没有问题,我们可以将该时间设置的短一些。这就在保证优雅的本身,减少了资源的浪费,其配置如下:
if (! req.backend.healthy) { set req.grace = 5m; } else { set req.grace = 15s; }
所以,综上所述。优雅模式的主要功能有以下两点:
1、合并请求,当N个客户端请求同一个页面的时候,varnish只发送一个请求到后端服务器,然后让其他几个请求挂起等待返回结果,返回结果后,复制请求的结果发送给客户端。。
2、通过提供旧的内容,避免请求扎堆。如果后端提供旧的内容,减少后端和前端请求的压力,而且为后端的重启和切换提供了时间。
二、神圣模式(Saint mode)
有时候,服务器很古怪,他们发出随机错误,您需要通知 varnish 使用更加优雅的方式处理 它,这种方式叫神圣模式(saint mode)。Saint mode 允许您抛弃一个后端服务器或者另一个尝试的后端服务器或者 cache 中服务陈旧的内容。如:
sub vcl_fetch { if (beresp.status == 500) { set beresp.saintmode = 10s; return (restart); } set beresp.grace = 5m; }
如上面的配置,当我们设置beresp.saintmode为10秒时,varnish会不请求该服务器10秒。或多或少可以算是一个黑名单。restart被执行时,如果我们有其他后端可以提供该内容,varnish会请求它们。当没有其他后端可用,varnish就会提供缓存中的旧内容。
三、grace和saint模式的局限性
以上两种模式也并非是万能的,如当请求正在被获取时,如果你的请求失败,会被扔到vcl_error中。由于vcl_error对数据集的访问会在前端显示,所以你不能启用优雅模式和神圣模式。在以后发布的版本中会解决这个问题,但是这里我们还是可以做些尽量避免该问题,官方给出的原文是:
Declare a backend that is always sick. Set a magic marker in vcl_error Restart the transaction Note the magic marker in vcl_recv and set the backend to the one mentioned Varnish will now serve stale data is any is available
这段话理解上比较费力,也有人做了一个中文翻译版是:
1、声明总是出状况的后端 2、在vcl_error中设置magic marker 3、重启事务 4、注意vcl_recv中的magic marker,并设置后端为之前提到的。 5、varnish现在将提供旧任何可获得的数据
注:其中magic marker是其参数中的一部分,具体可以参看官方wiki上的示例。
以上内容主要参看官方的如下页面:
四、完整示例
由于版本和参数变更的问题,示例中的配置并不保证能完全适用您所用的版本,不过具体可以在该示例的基础上做下适当的增减。其中一些涉及到的参数也可以对比官方文档做下适当更改。
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); }