Varnish: 让客户端强制刷新更新服务器缓存

从 RFC2616 的要求来看,缓存服务器是需要对客户端发过来的”Cache-Control”头作出回应的

Sometimes a user agent might want or need to insist that a cache revalidate its cache entry with the origin server (and not just with the next cache along the path to the origin server), or to reload its cache entry from the origin server. End-to-end revalidation might be necessary if either the cache or the origin server has overestimated the expiration time of the cached response. End-to-end reload may be necessary if the cache entry has become corrupted for some reason.

http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.4
其中说到了一种叫做”End-to-end reload”的:

End-to-end reload
The request includes a “no-cache” cache-control directive or, for compatibility with HTTP/1.0 clients, “Pragma: no-cache”. Field names MUST NOT be included with the no-cache directive in a request. The server MUST NOT use a cached copy when responding to such a request.

也就是说,当客户端(通常来说是浏览器)发过来”Cache-Control: no-cache”头(通常当我们按 Ctrl+f5 或者 Shift+鼠标点击刷新按钮的时候就会发出这个 HTTP 头)的时候,缓存服务器绝不能响应之前被缓存的拷贝,而是要重新去从源服务器获取一份发送给客户端。
但是在现实中往往不是这样:当我们在浏览器进行强制刷新的时候,只是将浏览器本地的缓存给清除掉了,缓存服务器的可能并没有更新。造成这个现象的原因主要有两个:

  1. 缓存服务器的性能可能满足不了这样的要求。
    在一个繁忙的缓存服务器,尤其是以磁盘作为主要缓存存储设备的服务器上面,限于磁盘IO,如果每次都必须将缓存对象彻底清除(实际上应该是可以讨巧的不去碰旧的缓存对象,然后去后端请求一个新的,然后替代旧的),即使再好的算法恐怕也无法应对大量的刷新缓存需求。
  2. 忽略客户端发过来的”Cache-Control”头,可以达到更高的缓存命中率。
    之所以要做缓存服务器,就是要让缓存率越高越好,开放让用户端去刷新缓存会降低缓存命中率。

但是现在也许该用新一点的思维来思考这个问题了:如果我们的服务器可以承受这样的刷新量,我们也许就可以让用户拥有刷新缓存服务器的能力。毕竟这是个信息迅速过期的时代,”Cache-Control: max-age=31536000″ 的作用也许已并不重要了。
我们可以用 Varnish 缓存服务器来做到这点:
前提要求是最好用纯内存作为缓存。添加如下的配置

vcl 4.0;

acl purge {
    "127.0.0.1";
}

sub vcl_recv {
    if (req.http.Cache-Control ~ "no-cache" && client.ip ~ purge) {
        # Force a cache miss
        set req.hash_always_miss = true;
    }
}

req.hash_always_miss 让 Varnish 去后端取新的内容,在 hash 里面替代旧的缓存对象,但是旧的并不会被立即清除,要等待它的 ttl 到了或者其它方法才会清除掉。这个做法的缺点是会在内存里面留下大量的旧的无用的缓存对象拷贝。
另一个方法是使用 ban 规则去清除缓存:

vcl 4.0;

acl purge {
    "127.0.0.1";
}

sub vcl_backend_response {
    # 后端响应添加到缓存时,把 Host 和 Url 添加到缓存里面,以方便后续可以根据 obj. 对象进行缓存清除。
    set beresp.http.X-Cache-Host = bereq.http.Host;
    set beresp.http.X-Cache-Url = bereq.url;
}

sub vcl_recv {
    # purge 地址内的客户端发过来"Cache-Control: no-cache"头,则添加一条 ban 规则,清除缓存
    if (req.http.Cache-Control ~ "no-cache" && client.ip ~ purge) {
        ban ("obj.http.X-Cache-Host == " + req.http.Host + " && obj.http.X-Cache-Url == " + req.url);
    }
}

sub vcl_backend_response {
        # 设置 Varnish 最长缓存保留时间为一天
        if (beresp.ttl > 1d) {
                set beresp.ttl = 1d;
        }
}

sub vcl_deliver {
    # 在发送给客户端之前,我们可以将只用于清除缓存的头去掉,没必要发给客户。
    unset resp.http.X-Cache-Host;
    unset resp.http.X-Cache-Url;
}

可以根据需求修改要不要对可以进行刷缓存的客户端 IP 进行限制。
最后,为什么要设置 Varnish 的最长保留缓存时间为一天?这是因为如果刷新请求量一旦比较大的话,会累积大量的 ban 规则(ban 规则只有当缓存里面最老的缓存对象都比他要新的时候才会被删除,否则就会一直累积着。因此,即使只有一个缓存对象保存时间为一年的话,都会导致 Varnish 累积一年的 ban 规则),这仍然还是会对性能产生很大的影响,所以为了不至于累积过多的 ban 规则,拖累 ban lurker 线程和整个 Varnish 的性能,直接设置最多保存一天就够了。(实际上,需要被缓存一天以上的东西本来就不多,可以通过 varnishtop -i TTL 查看到。)
更新:后面使用 ban 规则的方法似乎设置过最长 ttl 为一天后,还是会出现大量 ban 规则累积的情况,暂时没时间研究了。。。 🙁