强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

Varnish Cache 运维教程 / 第04章:缓存策略与机制

第04章:缓存策略与机制

4.1 缓存基础概念

4.1.1 HTTP 缓存模型

HTTP 缓存遵循一套标准化的控制机制,Varnish 作为反向代理缓存,严格遵循 HTTP 缓存规范。

客户端请求
    │
    ▼
┌──────────────┐     缓存命中      ┌──────────┐
│   Varnish    │ ──────────────────▶│  返回    │
│   缓存查找   │     (Cache HIT)    │  响应    │
└──────────────┘                   └──────────┘
    │
    │ 缓存未命中 (Cache MISS)
    ▼
┌──────────────┐                   ┌──────────┐
│   后端请求   │──────────────────▶│  后端    │
│              │◀──────────────────│  服务器  │
└──────────────┘                   └──────────┘
    │
    │ 缓存响应
    ▼
┌──────────────┐                   ┌──────────┐
│   存储缓存   │───────────────────▶│  返回    │
│   并返回     │                   │  响应    │
└──────────────┘                   └──────────┘

4.1.2 可缓存性判断

并非所有响应都应该被缓存。默认情况下,Varnish 不会缓存以下内容:

条件原因
请求方法不是 GET/HEADPOST 等方法通常有副作用
响应包含 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:

  1. 后端响应的 s-maxage 指令
  2. 后端响应的 max-age 指令
  3. 后端响应的 Expires 头部
  4. VCL 中 set beresp.ttl 的设置
  5. 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 使用以下信息计算缓存键:

  1. 请求的 URL
  2. 请求的 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 对比

特性ESIAjax
渲染位置服务端(边缘)客户端(浏览器)
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 注意事项

重要

  1. 永远不要缓存包含敏感信息的响应(密码、token、个人数据)
  2. 设置合理的 TTL,避免缓存过期时间过长导致内容更新延迟
  3. Grace 机制是保证用户体验的关键,建议始终启用
  4. ESI 片段的缓存时间应该比主页面更长
  5. 缓存键的设计直接影响缓存命中率,需要仔细规划
  6. 删除 Set-Cookie 头部前确保该响应确实可以公开缓存
  7. 监控缓存命中率,低于 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);
    }
}

4.11 扩展阅读