我的 CMS 生成相当复杂的页面,因此需要一点时间(大约 2 秒),这远远超出了我向客户提供页面的时间预算。
但是,对我来说,了解给定页面的当前版本非常容易,因此我可以很容易地判断给定版本是否仍然是最新的。因此,我希望能够实施基于 ETag 的策略,其中对页面的每一个请求都需要重新验证,但如果内容没有变化,服务器将在 10ms 内回复。
为了使此方法有效,我需要在所有客户端之间共享此缓存。只要 ETag 重新验证,我的所有页面对于所有用户来说都将保持相同,因此我可以安全地共享他们的内容。
为了做到这一点,我的页面发出了一个:
Cache-Control: public, no-cache, must-revalidate
ETag: W/"xxx"
当从浏览器测试时,它运行良好:页面保留在缓存中,每次刷新页面时都会根据服务器重新验证,大多数情况下会得到 304,或者当我更改内容版本时会得到 200。
我现在需要做的就是在客户端之间共享这个缓存。本质上:
- A 阶段
- 客户端 A 向代理发送请求
- 代理没有缓存,因此询问后端
- 后端使用 ETag 回复 200
- 代理使用 ETag 回复 200
- B 阶段
- 客户端 B 向代理发送相同请求
- 代理已在缓存中但必须重新验证(因为无缓存和必须重新验证和 ETag)
- 后端回复 304(因为重新验证请求包含带有缓存 ETag 的 If-None-Match 标头)
- 代理使用 Etag 回复 200
- C阶段
- 客户端 A 再次发送相同的请求,这次使用 If-None-Match
- 代理使用提供的 If-None-Match 标头(而不是缓存的标头)向后端询问
- 后端服务器回复304
- 代理回复 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
相同的方式处理该指令:内容将不会进入缓存中。private
no-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 有一组计时器,用于决定过期、重新验证和容忍的陈旧程度。
我的建议是增加keep
VCL 中的计时器。此计时器可确保保留过期和超出正常范围的对象,而不会冒提供过时内容的风险。
计时器存在的唯一原因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
值。