Varnish Cache 运维教程 / 第04章:缓存策略与机制
第04章:缓存策略与机制
4.1 缓存基础概念
4.1.1 HTTP 缓存模型
HTTP 缓存遵循一套标准化的控制机制,Varnish 作为反向代理缓存,严格遵循 HTTP 缓存规范。
客户端请求
│
▼
┌──────────────┐ 缓存命中 ┌──────────┐
│ Varnish │ ──────────────────▶│ 返回 │
│ 缓存查找 │ (Cache HIT) │ 响应 │
└──────────────┘ └──────────┘
│
│ 缓存未命中 (Cache MISS)
▼
┌──────────────┐ ┌──────────┐
│ 后端请求 │──────────────────▶│ 后端 │
│ │◀──────────────────│ 服务器 │
└──────────────┘ └──────────┘
│
│ 缓存响应
▼
┌──────────────┐ ┌──────────┐
│ 存储缓存 │───────────────────▶│ 返回 │
│ 并返回 │ │ 响应 │
└──────────────┘ └──────────┘
4.1.2 可缓存性判断
并非所有响应都应该被缓存。默认情况下,Varnish 不会缓存以下内容:
| 条件 | 原因 |
|---|---|
| 请求方法不是 GET/HEAD | POST 等方法通常有副作用 |
| 响应包含 Set-Cookie | 可能包含会话信息 |
| 响应包含 Authorization | 认证响应通常不应缓存 |
| 响应 Cache-Control 包含 no-store | 明确禁止缓存 |
| 响应 Cache-Control 包含 private | 仅限私有缓存 |
| Vary: * | 无法有效缓存 |
4.2 TTL 机制
TTL(Time To Live)是缓存对象的生存时间,决定了对象在缓存中保存多久。
4.2.1 TTL 的组成
┌─────────────────────────────────────────────────────┐
│ 对象生命周期 │
├──────────┬──────────┬──────────┬───────────────────┤
│ fresh │ grace │ keep │ 删除 │
│ (新鲜) │ (宽限) │ (保留) │ │
├──────────┼──────────┼──────────┼───────────────────┤
│ 0 → ttl │ ttl→grace│grace→keep│ keep → 删除 │
│ 命中返回│ 后台刷新│ 等待 │ │
└──────────┴──────────┴──────────┴───────────────────┘
4.2.2 TTL 的计算
Varnish 按以下优先级确定 TTL:
- 后端响应的
s-maxage指令 - 后端响应的
max-age指令 - 后端响应的
Expires头部 - VCL 中
set beresp.ttl的设置 - Varnish 默认 TTL(
default_ttl参数,默认 120 秒)
sub vcl_backend_response {
# 方式 1:VCL 中直接设置 TTL
set beresp.ttl = 300s;
# 方式 2:根据响应头决定 TTL
if (beresp.http.Cache-Control ~ "s-maxage=(\d+)") {
# 使用 s-maxage(优先级最高)
set beresp.ttl = std.duration(regsub(beresp.http.Cache-Control, ".*s-maxage=(\d+).*", "\1") + "s", 0s);
}
# 方式 3:根据 URL 模式设置
if (bereq.url ~ "\.(css|js)$") {
set beresp.ttl = 1h;
} elseif (bereq.url ~ "\.(jpg|png|gif)$") {
set beresp.ttl = 7d;
} else {
set beresp.ttl = 5m;
}
}
4.2.3 不缓存处理
sub vcl_backend_response {
# 不缓存后端明确标记的响应
if (beresp.http.Cache-Control ~ "no-cache|no-store|private") {
set beresp.uncacheable = true;
set beresp.ttl = 0s;
return (deliver);
}
# 不缓存错误响应
if (beresp.status >= 400) {
set beresp.uncacheable = true;
set beresp.ttl = 10s; # 短暂缓存错误,避免后端过载
return (deliver);
}
# 不缓存带 Set-Cookie 的响应(除非特别指定)
if (beresp.http.Set-Cookie) {
set beresp.uncacheable = true;
set beresp.ttl = 0s;
return (deliver);
}
}
4.3 Grace 机制
Grace 允许在缓存对象过期后继续提供旧的缓存内容,同时在后台异步更新缓存。这对用户体验至关重要——用户永远不需要等待后端响应。
4.3.1 Grace 工作原理
时间线:
──────────────────────────────────────────────────────────▶
│←── TTL ──→│←── Grace ──→│←── Keep ──→│
0s 300s 900s 3600s
│ │ │ │
│ 缓存新鲜 │ Grace 期间 │ Keep 期间 │ 对象删除
│ 直接返回 │ 后台更新 │ 仅用于 │
│ │ 同时返回旧 │ IMS 304 │
4.3.2 Grace 配置
sub vcl_recv {
# 启用 Grace 模式
# 当后端不健康时,允许使用过期缓存
if (!std.healthy(req.backend_hint)) {
# 后端不健康时,使用更长的 grace
set req.grace = 1h;
} else {
# 后端健康时,允许短时间的 grace
set req.grace = 30s;
}
}
sub vcl_backend_response {
# 设置 Grace 时长
set beresp.grace = 1h;
# 设置 Keep 时长
set beresp.keep = 24h;
}
4.3.3 Grace 触发条件
Grace 在以下情况会被触发:
| 条件 | 说明 |
|---|---|
req.grace > 0s | 客户端请求允许 grace |
obj.ttl + obj.grace > 0s | 对象仍在 grace 期间 |
| 后端不健康 | 后端探针失败时 |
sub vcl_hit {
# 检查对象是否需要 grace
if (obj.ttl >= 0s) {
# 对象仍然新鲜,直接返回
return (deliver);
}
if (obj.ttl + obj.grace > 0s) {
# 对象已过期但在 grace 期间
# 返回旧对象,同时触发后台更新
return (deliver);
}
# 对象已超出 grace 期间
return (miss);
}
4.3.4 业务场景
# 场景:电商网站商品详情页
# 需求:即使后端响应慢,也要快速返回页面
sub vcl_recv {
if (req.url ~ "^/products/[0-9]+") {
# 商品详情页允许较长的 grace
set req.grace = 2m;
}
}
sub vcl_backend_response {
if (bereq.url ~ "^/products/[0-9]+") {
# 商品详情页缓存 5 分钟
set beresp.ttl = 5m;
# Grace 30 分钟
set beresp.grace = 30m;
# Keep 24 小时
set beresp.keep = 24h;
}
}
4.4 Keep 机制
Keep 是比 Grace 更长的保留期。在 Keep 期间,过期的对象不会被主动提供给客户端,但可以用于条件请求(If-Modified-Since / 304 响应)。
4.4.1 Keep 工作流程
请求到达
│
▼
缓存查找
│
├── 对象新鲜(TTL 未过期)→ 直接返回
│
├── 对象在 Grace 期间 → 返回旧对象 + 后台更新
│
├── 对象在 Keep 期间 → 发送条件请求到后端
│ │
│ ├── 后端返回 304 → 更新 TTL,返回缓存对象
│ │
│ └── 后端返回 200 → 存储新对象,返回新响应
│
└── 对象已过期 → 从后端获取新对象
4.4.2 Keep 配置
sub vcl_backend_response {
# 标准缓存策略
set beresp.ttl = 5m; # 5 分钟新鲜
set beresp.grace = 30m; # 30 分钟 grace
set beresp.keep = 24h; # 24 小时 keep
}
sub vcl_backend_fetch {
# 如果有缓存对象,添加条件请求头
# Varnish 自动处理 IMS 请求
}
4.5 缓存键与 Hash
4.5.1 默认缓存键
默认情况下,Varnish 使用以下信息计算缓存键:
- 请求的 URL
- 请求的 Host 头部(或服务器 IP)
# 默认的 vcl_hash 实现等价于:
sub vcl_hash {
hash_data(req.url);
if (req.http.host) {
hash_data(req.http.host);
} else {
hash_data(server.ip);
}
return (lookup);
}
4.5.2 自定义缓存键
sub vcl_hash {
# 默认 hash 数据
hash_data(req.url);
if (req.http.host) {
hash_data(req.http.host);
} else {
hash_data(server.ip);
}
# 自定义:根据 Accept-Encoding 区分压缩
if (req.http.Accept-Encoding ~ "gzip") {
hash_data("gzip");
}
# 自定义:根据设备类型区分
if (req.http.User-Agent ~ "Mobile|Android|iPhone") {
hash_data("mobile");
} elseif (req.http.User-Agent ~ "Tablet|iPad") {
hash_data("tablet");
} else {
hash_data("desktop");
}
# 自定义:根据语言区分
if (req.http.Accept-Language ~ "^zh") {
hash_data("zh");
} elseif (req.http.Accept-Language ~ "^en") {
hash_data("en");
}
return (lookup);
}
4.5.3 缓存键优化
sub vcl_recv {
# 标准化 URL,提高缓存命中率
# 移除追踪参数
set req.url = regsub(req.url, "[?&](utm_[^&]*|fbclid|gclid|_ga)=[^&]*", "");
set req.url = regsub(req.url, "\?&?$", "");
# 移除尾部斜杠(非根路径)
if (req.url != "/" && req.url ~ "/$") {
set req.url = regsub(req.url, "/+$", "");
}
# 移除片段标识符
set req.url = regsub(req.url, "#.*$", "");
}
4.6 ESI(Edge Side Includes)
ESI 是一种用于在边缘(CDN/缓存层)组装页面片段的语言。它允许将一个页面分解为多个可独立缓存的片段。
4.6.1 ESI 工作原理
<!-- 主页面 HTML -->
<html>
<body>
<header>
<!-- ESI 片段:导航栏(缓存 1 小时) -->
<esi:include src="/fragment/header" />
</header>
<main>
<!-- ESI 片段:商品内容(缓存 5 分钟) -->
<esi:include src="/fragment/product/123" />
</main>
<footer>
<!-- ESI 片段:页脚(缓存 24 小时) -->
<esi:include src="/fragment/footer" />
</footer>
</body>
</html>
4.6.2 ESI 配置
sub vcl_recv {
# 检测 ESI 标记
if (req.url ~ "^/page/") {
set req.http.X-ESI = "true";
}
}
sub vcl_backend_response {
# 主页面启用 ESI
if (bereq.url ~ "^/page/") {
set beresp.do_esi = true;
}
# ESI 片段配置
if (bereq.url ~ "^/fragment/header") {
set beresp.ttl = 1h;
} elseif (bereq.url ~ "^/fragment/product/") {
set beresp.ttl = 5m;
} elseif (bereq.url ~ "^/fragment/footer") {
set beresp.ttl = 24h;
}
}
4.6.3 ESI 条件处理
<!-- ESI 条件注释 -->
<esi:choose>
<esi:when test="$(HTTP_COOKIE{user_type})=='premium'">
<div class="premium-content">
<!-- 会员专属内容 -->
</div>
</esi:when>
<esi:otherwise>
<div class="standard-content">
<!-- 标准内容 -->
</div>
</esi:otherwise>
</esi:choose>
<!-- ESI 变量 -->
<p>欢迎,$(HTTP_COOKIE{username})</p>
<!-- ESI 尝试/回退 -->
<esi:try>
<esi:attempt>
<esi:include src="/fragment/recommendations" />
</esi:attempt>
<esi:except>
<p>推荐内容暂时不可用</p>
</esi:except>
</esi:try>
4.6.4 ESI 与 Ajax 对比
| 特性 | ESI | Ajax |
|---|---|---|
| 渲染位置 | 服务端(边缘) | 客户端(浏览器) |
| SEO 友好 | 是(内容在 HTML 中) | 否(需要额外处理) |
| 首屏加载 | 快(一次请求) | 慢(多次请求) |
| 灵活性 | 中等 | 高 |
| 缓存控制 | 细粒度(每个片段独立) | 粗粒度 |
| 安全性 | 较高 | 较低(暴露 API) |
4.6.5 ESI 最佳实践
# 最佳实践配置
sub vcl_backend_response {
# 仅在需要时启用 ESI
if (beresp.http.Content-Type ~ "text/html") {
# 检查响应内容是否包含 ESI 标记
if (beresp.body ~ "<esi:include") {
set beresp.do_esi = true;
}
}
# ESI 片段不应该包含 ESI
if (bereq.url ~ "^/fragment/") {
set beresp.do_esi = false;
}
# ESI 片段应该设置合理的缓存头
if (bereq.url ~ "^/fragment/") {
# 移除可能干扰缓存的头部
unset beresp.http.Set-Cookie;
unset beresp.http.Cache-Control;
}
}
4.7 缓存清除策略
缓存清除是维护缓存有效性的重要操作。Varnish 提供多种清除机制。
4.7.1 清除方式对比
| 方式 | 机制 | 适用场景 | 性能影响 |
|---|---|---|---|
| Purge | 精确删除单个对象 | 已知 URL 的精确清除 | 低 |
| Ban | 正则匹配标记删除 | 批量清除、模式匹配 | 中-高 |
| TTL 到期 | 自然过期 | 通用策略 | 无 |
4.7.2 基于 TTL 的自然过期
sub vcl_backend_response {
# 根据内容类型设置不同 TTL
switch -regsub (bereq.url, "\?.*$", "") {
case "\.(css|js)$":
set beresp.ttl = 1h;
case "\.(jpg|png|gif|webp)$":
set beresp.ttl = 7d;
case "\.(html|htm)$":
set beresp.ttl = 5m;
case "^/api/":
set beresp.ttl = 60s;
default:
set beresp.ttl = 5m;
}
}
4.7.3 多层级缓存策略
# 完整的缓存策略示例
sub vcl_recv {
# 标准化请求
set req.url = regsub(req.url, "#.*$", "");
set req.url = regsub(req.url, "[?&](utm_[^&]*|fbclid|gclid)=[^&]*", "");
# 不缓存的方法
if (req.method != "GET" && req.method != "HEAD") {
return (pass);
}
# 不缓存认证请求
if (req.http.Authorization) {
return (pass);
}
# 不缓存管理后台
if (req.url ~ "^/admin" || req.url ~ "^/wp-admin") {
return (pass);
}
return (hash);
}
sub vcl_backend_response {
# 不缓存后端明确标记的内容
if (beresp.http.Cache-Control ~ "no-cache|no-store|private") {
set beresp.uncacheable = true;
set beresp.ttl = 0s;
return (deliver);
}
# 不缓存带 Set-Cookie 的响应
if (beresp.http.Set-Cookie) {
set beresp.uncacheable = true;
set beresp.ttl = 0s;
return (deliver);
}
# 根据 URL 模式设置缓存策略
if (bereq.url ~ "^/api/") {
# API 响应缓存较短
set beresp.ttl = 60s;
set beresp.grace = 30s;
} elseif (bereq.url ~ "\.(css|js)$") {
# 静态资源长缓存
set beresp.ttl = 1h;
set beresp.grace = 1h;
} elseif (bereq.url ~ "\.(jpg|png|gif|webp|svg|ico)$") {
# 图片资源长缓存
set beresp.ttl = 7d;
set beresp.grace = 1d;
} elseif (bereq.url ~ "^/products/") {
# 产品页缓存
set beresp.ttl = 5m;
set beresp.grace = 30m;
set beresp.keep = 24h;
} else {
# 默认缓存策略
set beresp.ttl = 5m;
set beresp.grace = 5m;
}
# 始终启用 Grace
set beresp.grace = 1h;
}
4.8 缓存状态标识
4.8.1 添加缓存状态头部
sub vcl_deliver {
# 标准缓存状态标识
if (obj.hits > 0) {
set resp.http.X-Cache = "HIT";
set resp.http.X-Cache-Hits = obj.hits;
} else {
set resp.http.X-Cache = "MISS";
}
# 添加调试信息
set resp.http.X-Cache-TTL = obj.ttl;
set resp.http.X-Cache-Grace = obj.grace;
set resp.http.X-Varnish = req.xid;
}
4.8.2 缓存命中率监控
# 使用 varnishstat 查看缓存命中率
varnishstat -1 | grep -E "MAIN.cache_hit|MAIN.cache_miss"
# 计算命中率
# 命中率 = cache_hit / (cache_hit + cache_miss) * 100%
4.9 注意事项
重要
- 永远不要缓存包含敏感信息的响应(密码、token、个人数据)
- 设置合理的 TTL,避免缓存过期时间过长导致内容更新延迟
- Grace 机制是保证用户体验的关键,建议始终启用
- ESI 片段的缓存时间应该比主页面更长
- 缓存键的设计直接影响缓存命中率,需要仔细规划
- 删除
Set-Cookie头部前确保该响应确实可以公开缓存- 监控缓存命中率,低于 70% 说明缓存策略需要优化
4.10 业务场景
场景一:新闻网站缓存策略
# 新闻网站缓存需求:
# - 首页:高并发、快速更新
# - 文章页:中等并发、较长缓存
# - 热点榜:高并发、频繁更新
sub vcl_backend_response {
if (bereq.url == "/" || bereq.url ~ "^/index") {
# 首页:缓存 60 秒,Grace 30 秒
set beresp.ttl = 60s;
set beresp.grace = 30s;
} elseif (bereq.url ~ "^/article/[0-9]+") {
# 文章页:缓存 5 分钟,Grace 30 分钟
set beresp.ttl = 5m;
set beresp.grace = 30m;
set beresp.keep = 24h;
} elseif (bereq.url ~ "^/hot") {
# 热点榜:缓存 30 秒
set beresp.ttl = 30s;
}
}
场景二:SaaS 应用缓存策略
# SaaS 应用缓存需求:
# - 公共资源(文档、帮助):长缓存
# - 用户数据:不缓存
# - API:根据端点策略化缓存
sub vcl_recv {
# 用户数据不缓存
if (req.http.Cookie ~ "session=") {
return (pass);
}
# API 路径
if (req.url ~ "^/api/") {
# 公共 API 可以缓存
if (req.url ~ "^/api/public/") {
return (hash);
}
# 其他 API 不缓存
return (pass);
}
}