基于 ETag 的内容重新验证

基于 ETag 的内容重新验证

我的 CMS 生成相当复杂的页面,因此需要一点时间(大约 2 秒),这远远超出了我向客户提供页面的时间预算。

但是,对我来说,了解给定页面的当前版本非常容易,因此我可以很容易地判断给定版本是否仍然是最新的。因此,我希望能够实施基于 ETag 的策略,其中对页面的每一个请求都需要重新验证,但如果内容没有变化,服务器将在 10ms 内回复。

为了使此方法有效,我需要在所有客户端之间共享此缓存。只要 ETag 重新验证,我的所有页面对于所有用户来说都将保持相同,因此我可以安全地共享他们的内容。

为了做到这一点,我的页面发出了一个:

Cache-Control: public, no-cache, must-revalidate
ETag: W/"xxx"

当从浏览器测试时,它运行良好:页面保留在缓存中,每次刷新页面时都会根据服务器重新验证,大多数情况下会得到 304,或者当我更改内容版本时会得到 200。

我现在需要做的就是在客户端之间共享这个缓存。本质上:

  1. A 阶段
    1. 客户端 A 向代理发送请求
    2. 代理没有缓存,因此询问后端
    3. 后端使用 ETag 回复 200
    4. 代理使用 ETag 回复 200
  2. B 阶段
    1. 客户端 B 向代理发送相同请求
    2. 代理已在缓存中但必须重新验证(因为无缓存和必须重新验证和 ETag)
    3. 后端回复 304(因为重新验证请求包含带有缓存 ETag 的 If-None-Match 标头)
    4. 代理使用 Etag 回复 200
  3. C阶段
    1. 客户端 A 再次发送相同的请求,这次使用 If-None-Match
    2. 代理使用提供的 If-None-Match 标头(而不是缓存的标头)向后端询问
    3. 后端服务器回复304
    4. 代理回复 304

我尝试过 nginx,但即使要让它远程工作也需要进行大量调整。然后我尝试了 Traefik,才意识到缓存中间件是企业版的一部分。然后我发现 Varnish似乎实现了我想要的是。

以下是我的 Varnish 配置:

vcl 4.0;

backend default {
    .host = "localhost";
    .port = "3000";
}

backend api {
    .host = "localhost";
    .port = "8000";
}

sub vcl_recv {
    if (req.url ~ "^/back/" || req.url ~ "^/_/") {
        set req.backend_hint = api;
    } else {
        set req.backend_hint = default;
    }
}

当然...它没有起作用。

当改变Cache-Control标题时,我要么从共享缓存中获取结果,但不会重新验证,要么只是传递到客户端,但似乎从来没有像我希望的那样将内容保存在缓存中。

我缺少什么才能实现此共享缓存/ETag 重新验证逻辑?我想我缺少了一些明显的东西,但不知道是什么。

答案1

设置 TTL

正如内置 VCL:Varnish以与&no-cache相同的方式处理该指令:内容将不会进入缓存中。privateno-store

对于基于 ETag 的重新验证,这会带来问题,因为没有什么可比较的。

我的建议是设置较低的 TTL 以确保它最终进入缓存。我建议使用以下Cache-Control标头:

Cache-Control: public, s-maxage=3, must-revalidate

让 Varnish 理解 must-revalidate

另一个问题是 Varnish 不理解该must-revalidate指令,但它确实支持该stale-while-revalidate指令。

添加must-revalidate具有与以下相同的效果stale-while-revalidate=0:当对象过期时我们无法提供过时的内容并需要立即进行同步验证。

这会将内部grace计时器设置为零。

但是,使用以下 VCL 代码,您可以让 Varnish 遵守该must-revalidate指令:

sub vcl_backend_response {
    if(beresp.http.Cache-Control ~ "must-revalidate") {
        set beresp.grace = 0s;
    }
}

增加保持时间

通过将宽限期设置为零并分配较低的 TTL,对象将很快过期,并且不会存在足够长的时间进行重新验证。

正如解释的那样https://stackoverflow.com/questions/68177623/varnish-default-grace-behavior/68207764#68207764Varnish 有一组计时器,用于决定过期、重新验证和容忍的陈旧程度。

我的建议是增加keepVCL 中的计时器。此计时器可确保保留过期和超出正常范围的对象,而不会冒提供过时内容的风险。

计时器存在的唯一原因keep是为了 ETag 重新验证,所以这正是您所需要的。

我建议使用以下 VCL 来支持must-revalidate和增加keep计时器:

sub vcl_backend_response {
    set beresp.keep = 1d;
    if(beresp.http.Cache-Control ~ "must-revalidate") {
        set beresp.grace = 0s;
    }
}

此代码片段将keep计时器延长至一天,允许过期内容在重新验证期间在缓存中保留一天。

警惕 Cookie(和授权标头)

尽管已经准备好了所有支持 ETag 重新验证和将对象存储在缓存中的功能,但重要的是相关请求不会绕过缓存。这与标头无关Cache-Control,而是与请求标头有关。

如果你看一下vcl_recv子程序的内置 VCLAuthorization,您会注意到,默认情况下 Varnish 会绕过包含标头或标头的请求的缓存Cookie

如果您的网站使用 Cookie,请阅读以下教程,了解如何删除可能影响您的点击率的跟踪 Cookie:https://www.varnish-software.com/developers/tutorials/removing-cookies-varnish/

Varnish 测试用例作为概念验证

将以下 VTC 内容存储etag.vtc并运行varnishtest etag.vtc以验证测试用例:

varnishtest "Testing ETag & 304 status codes in Varnish"

server s1 {
    # First backend request is not a conditional one
    # Return 200 with the ETag
    rxreq
    expect req.method == "GET"
    expect req.http.If-None-Match == <undef>
    txresp -status 200 \
     -hdr "Cache-Control: public, s-maxage=3, must-revalidate" \
     -hdr "ETag: abc123" -bodylen 7


    # Second backend request is a conditional one with a matching ETag
    # Return a 304
    rxreq
    expect req.method == "GET"
    expect req.http.If-None-Match == "abc123"
    txresp -status 304 \
     -hdr "Cache-Control: public, s-maxage=3, must-revalidate" \
     -hdr "ETag: abc123"

    # Third backend request is a conditional where the Etag doesn't match
    # The content has been updated
    # Return a 200 with  the updated content and the new Etag
    rxreq
    expect req.method == "GET"
    expect req.http.If-None-Match == "abc123"
    txresp -status 200 \
     -hdr "Cache-Control: public, s-maxage=3, must-revalidate" \
     -hdr "ETag: xyz456" -bodylen 8

} -start

varnish v1 -vcl+backend {
    sub vcl_backend_response {
        set beresp.keep = 1d;
        if(beresp.http.Cache-Control ~ "must-revalidate") {
            set beresp.grace = 0s;
        }
    }
} -start

client c1 {
    # Send a regular request
    # Expect a cache miss
    # Trigger the first backend response
    # Return 200 with a response body
    txreq -url "/"
    rxresp
    expect resp.status == 200
    expect resp.http.Age == "0"
    expect resp.http.ETag == "abc123"
    expect resp.http.Cache-Control == "public, s-maxage=3, must-revalidate"
    expect resp.bodylen == 7

    # Wait for 1 second
    delay 1

    # Send a conditional request with an If-None-Match header
    # Should be a cache hit
    # Should return a 304 without a response body
    # Varnish is responsible for this 304
    txreq -url "/" -hdr "If-None-Match: abc123"
    rxresp
    expect resp.status == 304
    expect resp.http.Age != "0"
    expect resp.http.ETag == "abc123"
    expect resp.http.Cache-Control == "public, s-maxage=3, must-revalidate"
    expect resp.bodylen == 0

    # Wait for 3 seconds
    delay 3

    # Send a conditional request with an If-None-Match header
    # Should be a cache miss
    # Trigger the second backend response
    # Should return a 304 without a response body
    # The server is responsible for this 304
    txreq -url "/" -hdr "If-None-Match: abc123"
    rxresp
    expect resp.status == 304
    expect resp.http.Age == "0"
    expect resp.http.ETag == "abc123"
    expect resp.http.Cache-Control == "public, s-maxage=3, must-revalidate"
    expect resp.bodylen == 0

    # Wait for 4 seconds
    delay 4

    # Send a conditional request with an If-None-Match header
    # Should be a cache miss
    # Trigger the third backend response
    # Should return a 200 with a response body
    # Content has been updated: a new Etag is returned
    txreq -url "/" -hdr "If-None-Match: abc123"
    rxresp
    expect resp.status == 200
    expect resp.http.Age == "0"
    expect resp.http.ETag == "xyz456"
    expect resp.http.Cache-Control == "public, s-maxage=3, must-revalidate"
    expect resp.bodylen == 8

} -run

结论

如果您按照我描述的步骤操作,您的 Varnish 设置将通过两种方式支持 ETag 重新验证:

  • 从客户端到 Varnish 中的缓存对象(该对象仍然新鲜)
  • keep当对象已过期,但由于计时器的原因,它仍然保留在缓存中时,从 Varnish 发送到原始服务器

如果您的内容允许,请考虑增加缓存的 TTL。这完全取决于页面/资源的变化程度。

还请注意,如果对象保留时间过长(由于keep值增加),缓存就会被填满。当缓存已满时,将应用 LRU 策略从缓存中删除最不受欢迎的对象。

如果set beresp.keep = 1d;太多并且 Varnish 因为缓存已满而开始从缓存中删除对象,请考虑降低该keep值。

相关内容