今天在和同事谈论现网后端tomcat挂起导致前端varnish线程堆积,最终造成varnish挂掉或前端web处理繁忙的问题。由于之前详细的看过varnish的文档,当时记得有一个上帝模式还是圣杯模式来着(由于英向汉翻译的问题,可能有N种翻译方法)。由于时隔相当一段时间了,所以记得不大清楚,后来就去网上查了下,现在汉译的统称叫———— 神圣模式和优雅模式可以应用到上面提到的场景。这里再做一个tag总结成博文记录下。

一、Grace mode(优雅模式)

为什么使用Grace mode ,优雅模式的优点是什么?

如果你每秒需要相应成千上万的点击,等待的请求队列就会很巨大。这里有两个潜在问题:一个是thundering herd problem(这个无法翻译。。。wiki有专门的对应解释),突然增加一千个线程去提供内容,会让负载变得很高;第二个,没有人喜欢等。为了解决这个问题,我们指示varnish去保持缓存的对象超过他们的TTL(就是该过期的,不让它过期),并且去提供旧的内容给正在等待的请求。 既然要提供旧的内容,首先我们必须有内容去提供。所以,我们使用以下VCL,以使varnish保持所有对象超出了他们的TTL30分钟。

1sub vcl_fetch {
2  set beresp.grace = 30m;
3}

这样,varnish还不会提供旧对象。为了启用varnish去提供旧对象,我们必须在请求上开启它。下面表示,我们接收15s的旧对象:

1sub vcl_recv {
2  set req.grace = 15s;
3}

varnish的优势在于内存级的cache,所以内存的多少决定了cache的数据量的多少。如果开启了优雅模式,在TTL到期后,我们仍不将其从mem里清除掉,而是要再等待一段时间才清除,这就无端的浪费了资源。但如果你开启了健康检查,你可以检查后端是否出问题。如果出问题了,我们可以提供长点时间的旧内容。如果后端没有问题,我们可以将该时间设置的短一些。这就在保证优雅的本身,减少了资源的浪费,其配置如下:

1if (! req.backend.healthy) {
2   set req.grace = 5m;
3} else {
4   set req.grace = 15s;
5}

所以,综上所述。优雅模式的主要功能有以下两点:

1、合并请求,当N个客户端请求同一个页面的时候,varnish只发送一个请求到后端服务器,然后让其他几个请求挂起等待返回结果,返回结果后,复制请求的结果发送给客户端。。

2、通过提供旧的内容,避免请求扎堆。如果后端提供旧的内容,减少后端和前端请求的压力,而且为后端的重启和切换提供了时间。

二、神圣模式(Saint mode)

有时候,服务器很古怪,他们发出随机错误,您需要通知 varnish 使用更加优雅的方式处理 它,这种方式叫神圣模式(saint mode)。Saint mode 允许您抛弃一个后端服务器或者另一个尝试的后端服务器或者 cache 中服务陈旧的内容。如:

1sub vcl_fetch {
2    if (beresp.status = = 500) {
3        set beresp.saintmode = 10s;
4        return (restart);
5  }
6    set beresp.grace = 5m;
7}

如上面的配置,当我们设置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上的示例。

以上内容主要参看官方的如下页面:

varnish 3.0.5官网文档

varnish static book部分

四、完整示例

由于版本和参数变更的问题,示例中的配置并不保证能完全适用您所用的版本,不过具体可以在该示例的基础上做下适当的增减。其中一些涉及到的参数也可以对比官方文档做下适当更改。

  1backend web1 {
  2    .host = "172.16.2.31";
  3    .port = "80";
  4    .probe = {
  5        .url = "/";
  6        .interval = 10s;
  7        .timeout = 2s;
  8        .window = 3;
  9        .threshold = 3;
 10    }
 11}
 12backend web2 {
 13    .host = "172.16.2.32";
 14    .port = "80";
 15    .probe = {
 16        .url = "/";
 17        .interval = 10s;
 18        .timeout = 2s;
 19        .window = 3;
 20        .threshold = 3;
 21    }
 22}
 23# 定义负载均衡组
 24director webgroup random {
 25    {
 26       .backend = web1;
 27       .weight = 1;
 28    }
 29    {
 30       .backend = web2;
 31       .weight = 1;
 32    }
 33}
 34# 允许刷新缓存的ip
 35acl purgeAllow {
 36     "localhost";
 37     "172.16.2.5";
 38}
 39sub vcl_recv {
 40    # 刷新缓存设置
 41    if (req.request == "PURGE") {
 42        #判断是否允许ip
 43        if (!client.ip ~ purgeAllow) {
 44             error 405 "Not allowed.";
 45        }
 46        #去缓存中查找
 47        return (lookup);
 48    }
 49    std.log("LOG_DEBUG: URL=" + req.url);
 50    set req.backend = webgroup;
 51    if (req.request != "GET" && req.request != "HEAD" && req.request != "PUT" && req.request != "POST" && req.request != "TRACE" && req.request != "OPTIONS" && req.request != "DELETE") {
 52         /* Non-RFC2616 or CONNECT which is weird. */
 53         return (pipe);
 54    }
 55    # 只缓存 GET 和 HEAD 请求
 56    if (req.request != "GET" && req.request != "HEAD") {
 57        std.log("LOG_DEBUG: req.request not get!  " + req.request );
 58        return(pass);
 59    }
 60    # http 认证的页面也 pass
 61  if (req.http.Authorization) {
 62        std.log("LOG_DEBUG: req is authorization !");
 63        return (pass);
 64    }
 65    if (req.http.Cache-Control ~ "no-cache") {
 66        std.log("LOG_DEBUG: req is no-cache");
 67        return (pass);
 68    }
 69    # 忽略admin、verify、servlet目录,以.jsp和.do结尾以及带有?的URL,直接从后端服务器读取内容
 70    if (req.url ~ "^/admin" || req.url ~ "^/verify/" || req.url ~ "^/servlet/" || req.url ~ ".(jsp|php|do)($|?)") {
 71        std.log("url is admin or servlet or jsp|php|do, pass.");
 72        return (pass);
 73    }
 74    # 只缓存指定扩展名的请求, 并去除 cookie
 75    if (req.url ~ "^/[^?]+.(jpeg|jpg|png|gif|bmp|tif|tiff|ico|wmf|js|css|ejs|swf|txt|zip|exe|html|htm)(?.*|)$") {
 76        std.log("*** url is jpeg|jpg|png|gif|ico|js|css|txt|zip|exe|html|htm set cached! ***");
 77        unset req.http.cookie;
 78        # 规范请求头,Accept-Encoding 只保留必要的内容
 79        if (req.http.Accept-Encoding) {
 80            if (req.url ~ ".(jpg|png|gif|jpeg)(?.*|)$") {
 81                remove req.http.Accept-Encoding;
 82            } elsif (req.http.Accept-Encoding ~ "gzip") {
 83                set req.http.Accept-Encoding = "gzip";
 84            } elsif (req.http.Accept-Encoding ~ "deflate") {
 85                set req.http.Accept-Encoding = "deflate";
 86            } else {
 87                remove req.http.Accept-Encoding;
 88            }
 89        }
 90        return(lookup);
 91    } else {
 92        std.log("url is not cached!");
 93        return (pass);
 94    }
 95}
 96sub vcl_hit {
 97    if (req.request == "PURGE") {
 98        set obj.ttl = 0s;
 99        error 200 "Purged.";
100    }
101    return (deliver);
102}
103sub vcl_miss {
104    std.log("################# cache miss ################### url=" + req.url);
105    if (req.request == "PURGE") {
106        purge;
107        error 200 "Purged.";
108    }
109}
110sub vcl_fetch {
111    # 如果后端服务器返回错误,则进入 saintmode
112    if (beresp.status = = 500 || beresp.status = = 501 || beresp.status = = 502 || beresp.status = = 503 || beresp.status = = 504) {
113        std.log("beresp.status error!!! beresp.status=" + beresp.status);
114        set req.http.host = "status";
115        set beresp.saintmode = 20s;
116        return (restart);
117    }
118    # 如果后端静止缓存, 则跳过
119    if (beresp.http.Pragma ~ "no-cache" || beresp.http.Cache-Control ~ "no-cache" || beresp.http.Cache-Control ~ "private") {
120        std.log("not allow cached!   beresp.http.Cache-Control=" + beresp.http.Cache-Control);
121    return (hit_for_pass);
122    }
123    if (beresp.ttl <= 0s || beresp.http.Set-Cookie || beresp.http.Vary == "*") {
124        /* Mark as "Hit-For-Pass" for the next 2 minutes */
125        set beresp.ttl = 120 s;
126        return (hit_for_pass);
127    }
128    if (req.request == "GET" && req.url ~ ".(css|js|ejs|html|htm)$") {
129        std.log("gzip is enable.");
130        set beresp.do_gzip = true;
131        set beresp.ttl = 20s;
132    }
133    if (req.request = = "GET" && req.url ~ "^/[^?]+.(jpeg|jpg|png|gif|bmp|tif|tiff|ico|wmf|js|css|ejs|swf|txt|zip|exe)(?.*|)$") {
134        std.log("url css|js|gif|jpg|jpeg|bmp|png|tiff|tif|ico|swf|exe|zip|bmp|wmf is cache 5m!");
135        set beresp.ttl = 5m;
136    } elseif (req.request = = "GET" && req.url ~ ".(html|htm)$") {
137        set beresp.ttl = 30s;
138    } else {
139        return (hit_for_pass);
140    }
141    # 如果后端不健康,则先返回缓存数据1分钟
142    if (!req.backend.healthy) {
143        std.log("eq.backend not healthy! req.grace = 1m");
144        set req.grace = 1m;
145    } else {
146        set req.grace = 30s;
147    }
148     return (deliver);
149}
150# 发送给客户端
151sub vcl_deliver {
152    if ( obj.hits > 0 ) {
153    set resp.http.X-Cache = "has cache";
154    } else {
155    #set resp.http.X-Cache = "no cache";
156    }
157    return (deliver);
158}