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

Varnish Cache 运维教程 / 第06章:HTTP 头部处理

第06章:HTTP 头部处理

6.1 HTTP 头部与缓存

HTTP 头部是缓存行为的核心控制机制。Varnish 通过解析和操作 HTTP 头部来决定缓存策略。

6.1.1 缓存相关头部概览

头部方向说明
Cache-Control请求/响应缓存指令(最重要)
Expires响应过期时间(旧标准)
ETag响应实体标签(内容指纹)
Last-Modified响应最后修改时间
If-None-Match请求条件请求(配合 ETag)
If-Modified-Since请求条件请求(配合 Last-Modified)
Vary响应变体标识
Age响应对象在缓存中的年龄
Set-Cookie响应设置 Cookie(影响缓存)
Authorization请求认证信息(影响缓存)

6.2 Cache-Control 头部

6.2.1 Cache-Control 指令

指令方向说明
public响应可被任何缓存存储
private响应仅限私有缓存(浏览器)
no-cache请求/响应缓存前必须验证
no-store请求/响应不存储任何缓存
max-age=<seconds>请求/响应最大新鲜时间(客户端)
s-maxage=<seconds>响应共享缓存最大新鲜时间(Varnish 优先)
must-revalidate响应过期后必须向后端验证
proxy-revalidate响应类似 must-revalidate,针对共享缓存
immutable响应内容不会改变(无需验证)
stale-while-revalidate=<seconds>响应过期后仍可使用,同时后台验证
stale-if-error=<seconds>响应后端错误时仍可使用过期内容

6.2.2 解析 Cache-Control

sub vcl_backend_response {
    # 解析 s-maxage(Varnish 优先使用)
    if (beresp.http.Cache-Control ~ "s-maxage=(\d+)") {
        set beresp.ttl = std.duration(
            regsub(beresp.http.Cache-Control, ".*s-maxage=(\d+).*", "\1") + "s",
            0s
        );
    }
    # 解析 max-age
    elseif (beresp.http.Cache-Control ~ "max-age=(\d+)") {
        set beresp.ttl = std.duration(
            regsub(beresp.http.Cache-Control, ".*max-age=(\d+).*", "\1") + "s",
            0s
        );
    }

    # 处理 no-cache 和 no-store
    if (beresp.http.Cache-Control ~ "no-cache|no-store") {
        set beresp.uncacheable = true;
        set beresp.ttl = 0s;
    }

    # 处理 private
    if (beresp.http.Cache-Control ~ "private") {
        set beresp.uncacheable = true;
        set beresp.ttl = 0s;
    }

    # 处理 must-revalidate
    if (beresp.http.Cache-Control ~ "must-revalidate|proxy-revalidate") {
        set beresp.uncacheable = false;
    }
}

6.2.3 后端缺少 Cache-Control 时的处理

sub vcl_backend_response {
    # 如果后端没有设置缓存控制头,使用 VCL 默认策略
    if (!beresp.http.Cache-Control && !beresp.http.Expires) {
        # 根据 URL 模式设置默认 TTL
        if (bereq.url ~ "\.(css|js)$") {
            set beresp.ttl = 1h;
            set beresp.http.Cache-Control = "public, max-age=3600";
        } elseif (bereq.url ~ "\.(jpg|png|gif|webp|svg)$") {
            set beresp.ttl = 7d;
            set beresp.http.Cache-Control = "public, max-age=604800";
        } elseif (bereq.url ~ "\.(html|htm)$") {
            set beresp.ttl = 5m;
            set beresp.http.Cache-Control = "public, max-age=300, s-maxage=300";
        } elseif (bereq.url ~ "^/api/") {
            set beresp.ttl = 60s;
            set beresp.http.Cache-Control = "public, max-age=60, s-maxage=60";
        } else {
            set beresp.ttl = 5m;
            set beresp.http.Cache-Control = "public, max-age=300, s-maxage=300";
        }
    }
}

6.2.4 生成正确的 Cache-Control 头部

sub vcl_backend_response {
    # 确保发送给客户端的 Cache-Control 是正确的
    # s-maxage 仅用于共享缓存,客户端应使用 max-age

    if (beresp.http.Cache-Control ~ "s-maxage=(\d+)") {
        # 设置客户端的 max-age 等于 s-maxage
        set beresp.http.Cache-Control = regsub(
            beresp.http.Cache-Control,
            "s-maxage=(\d+)",
            "max-age=\1, s-maxage=\1"
        );
    }
}

6.3 Vary 头部

6.3.1 Vary 的作用

Vary 头部告诉缓存服务器,响应的内容会根据哪些请求头变化。这使得缓存可以为不同的客户端变体存储不同的响应。

请求1: Accept-Encoding: gzip
响应1: Vary: Accept-Encoding
        → 缓存存储 gzip 版本

请求2: Accept-Encoding: (无)
响应2: Vary: Accept-Encoding
        → 缓存存储非压缩版本

请求3: Accept-Encoding: gzip
        → 返回缓存的 gzip 版本

6.3.2 常见 Vary 值

Vary 值场景影响
Accept-Encoding压缩变体正常,常见
Accept-Language多语言中等,变体较多
User-Agent设备适配高,变体极多
Cookie个性化很高,几乎无法缓存
Authorization认证很高,不应缓存
*所有变体无法缓存

6.3.3 Vary 处理策略

sub vcl_recv {
    # 标准化 Accept-Encoding,减少变体数量
    if (req.http.Accept-Encoding) {
        if (req.http.Accept-Encoding ~ "gzip") {
            set req.http.Accept-Encoding = "gzip";
        } else {
            # 删除不支持的编码
            unset req.http.Accept-Encoding;
        }
    }
}

sub vcl_backend_response {
    # 移除不必要的 Vary
    if (beresp.http.Vary ~ "User-Agent") {
        # User-Agent 变体太多,建议移除或替换
        set beresp.http.Vary = regsub(beresp.http.Vary, "(,?\s*User-Agent|User-Agent,?\s*)", "");
    }

    # 如果 Vary 为空,移除它
    if (beresp.http.Vary == "") {
        unset beresp.http.Vary;
    }

    # 确保 Vary 不包含 Cookie 或 Authorization
    if (beresp.http.Vary ~ "(?i)Cookie|Authorization") {
        set beresp.uncacheable = true;
        set beresp.ttl = 0s;
    }
}

6.3.4 Accept-Encoding 标准化

sub vcl_recv {
    # 标准化 Accept-Encoding 是最关键的 Vary 优化
    if (req.http.Accept-Encoding) {
        # 只保留 gzip(最常用)
        if (req.http.Accept-Encoding ~ "gzip") {
            set req.http.Accept-Encoding = "gzip";
        }
        # 可选:支持 br (Brotli)
        # elseif (req.http.Accept-Encoding ~ "br") {
        #     set req.http.Accept-Encoding = "br";
        # }
        else {
            # 不支持压缩,移除头部
            unset req.http.Accept-Encoding;
        }
    }
}

6.4 ETag 与条件请求

6.4.1 ETag 机制

ETag(Entity Tag)是响应内容的指纹标识。客户端可以在后续请求中使用 If-None-Match 头部发送 ETag,服务器返回 304 Not Modified 如果内容未改变。

首次请求:
Client → GET /page.html
Client ← 200 OK, ETag: "abc123", <content>

后续请求:
Client → GET /page.html, If-None-Match: "abc123"
Client ← 304 Not Modified (无内容)

6.4.2 ETag 与 Varnish

sub vcl_backend_response {
    # 保留后端的 ETag
    # Varnish 会自动处理条件请求

    # 如果后端没有 ETag,可以基于内容生成
    if (!beresp.http.ETag && beresp.http.Content-Length) {
        # 简单的 ETag 生成(基于 URL 和内容长度)
        set beresp.http.ETag = "\"" + hash_data(bereq.url + beresp.http.Content-Length) + "\"";
    }
}

sub vcl_deliver {
    # 向客户端发送 ETag
    # Varnish 会自动处理 If-None-Match 请求

    # 如果命中缓存且客户端发送了 If-None-Match
    # Varnish 内部会处理 304 响应
}

6.4.3 If-Modified-Since 处理

sub vcl_backend_response {
    # 确保后端响应包含 Last-Modified
    if (!beresp.http.Last-Modified) {
        set beresp.http.Last-Modified = now;
    }
}

sub vcl_recv {
    # Varnish 自动处理 If-Modified-Since
    # 可以添加调试信息
    if (req.http.If-Modified-Since) {
        set req.http.X-Debug-IMS = "true";
    }
}

6.4.4 条件请求优化

sub vcl_backend_fetch {
    # 如果有缓存的 ETag,发送给后端进行条件请求
    # Varnish 自动处理此逻辑

    # 可以在 Keep 期间使用条件请求
    if (bereq.http.If-None-Match || bereq.http.If-Modified-Since) {
        # 设置较短的超时,因为条件请求应该很快
        set bereq.first_byte_timeout = 5s;
    }
}

sub vcl_backend_response {
    # 处理 304 响应
    if (beresp.status == 304) {
        # 内容未改变,更新缓存对象的 TTL
        # Varnish 自动处理此逻辑
    }
}

Cookie 是缓存的主要敌人之一。带有 Set-Cookie 的响应默认不会被缓存,而请求中的 Cookie 通常表示个性化内容。

sub vcl_recv {
    # 策略 1:对静态资源完全剥离 Cookie
    if (req.url ~ "\.(css|js|jpg|png|gif|webp|svg|ico|woff2)$") {
        unset req.http.Cookie;
        return (hash);
    }

    # 策略 2:仅保留特定 Cookie
    if (req.http.Cookie) {
        # 保留 session cookie,移除其他
        set req.http.X-Temp-Cookie = req.http.Cookie;
        unset req.http.Cookie;

        # 只恢复需要的 Cookie
        if (req.http.X-Temp-Cookie ~ "session_id=") {
            set req.http.Cookie = "session_id=" + regsub(
                req.http.X-Temp-Cookie,
                ".*session_id=([^;]+).*",
                "\1"
            );
        }
        unset req.http.X-Temp-Cookie;
    }

    # 策略 3:对已登录用户不缓存
    if (req.http.Cookie ~ "logged_in=true") {
        return (pass);
    }
}

sub vcl_backend_response {
    # 移除后端的 Set-Cookie(对于可缓存内容)
    if (bereq.url ~ "\.(css|js|jpg|png|gif|webp|svg|ico|woff2)$") {
        unset beresp.http.Set-Cookie;
    }

    # 对于特定页面,移除 Set-Cookie 以启用缓存
    if (bereq.url ~ "^/public/") {
        unset beresp.http.Set-Cookie;
    }
}
sub vcl_recv {
    # 分析 Cookie 中的会话信息
    if (req.http.Cookie ~ "session_id=") {
        # 提取 session_id
        set req.http.X-Session-ID = regsub(
            req.http.Cookie,
            ".*session_id=([^;]+).*",
            "\1"
        );

        # 对于 API 请求,使用 session 区分
        if (req.url ~ "^/api/user/") {
            # 用户特定 API,不缓存
            return (pass);
        }

        # 对于其他请求,忽略 session 进行缓存
        unset req.http.Cookie;
    }
}

6.6 自定义头部

6.6.1 添加调试头部

sub vcl_deliver {
    # 缓存状态
    if (obj.hits > 0) {
        set resp.http.X-Cache = "HIT (" + obj.hits + ")";
    } else {
        set resp.http.X-Cache = "MISS";
    }

    # 处理时间
    set resp.http.X-Cache-TTL = obj.ttl;
    set resp.http.X-Cache-Age = obj.age;

    # 请求标识
    set resp.http.X-Varnish = req.xid;

    # 后端信息
    set resp.http.X-Backend = req.backend_hint;
}

6.6.2 安全头部

sub vcl_deliver {
    # 添加安全头部
    set resp.http.X-Content-Type-Options = "nosniff";
    set resp.http.X-Frame-Options = "SAMEORIGIN";
    set resp.http.X-XSS-Protection = "1; mode=block";
    set resp.http.Referrer-Policy = "strict-origin-when-cross-origin";

    # 移除敏感头部
    unset resp.http.Server;
    unset resp.http.X-Powered-By;
    unset resp.http.X-AspNet-Version;

    # 移除内部头部
    unset resp.http.X-Varnish;
    unset resp.http.Via;
    unset resp.http.X-Cache-TTL;
    unset resp.http.X-Cache-Age;
}

6.6.3 CORS 头部

sub vcl_recv {
    # 处理 CORS 预检请求
    if (req.method == "OPTIONS" && req.http.Origin) {
        return (synth(200, "CORS OK"));
    }
}

sub vcl_synth {
    # CORS 预检响应
    if (resp.status == 200 && resp.reason == "CORS OK") {
        set resp.http.Access-Control-Allow-Origin = req.http.Origin;
        set resp.http.Access-Control-Allow-Methods = "GET, POST, OPTIONS";
        set resp.http.Access-Control-Allow-Headers = "Content-Type, Authorization";
        set resp.http.Access-Control-Max-Age = "86400";
        set resp.http.Content-Length = "0";
        return (deliver);
    }
}

sub vcl_deliver {
    # 添加 CORS 头部到正常响应
    if (req.http.Origin) {
        set resp.http.Access-Control-Allow-Origin = req.http.Origin;
        set resp.http.Access-Control-Allow-Credentials = "true";
    }
}

6.6.4 请求 ID 追踪

sub vcl_recv {
    # 传递或生成请求 ID
    if (!req.http.X-Request-ID) {
        # 生成唯一的请求 ID
        set req.http.X-Request-ID = req.xid;
    }

    # 传递给后端
    set req.http.X-Forwarded-For = client.ip;
}

sub vcl_deliver {
    # 返回请求 ID 给客户端
    set resp.http.X-Request-ID = req.http.X-Request-ID;
}

6.7 头部修改最佳实践

6.7.1 头部操作语法

sub vcl_recv {
    # 设置头部(覆盖现有值)
    set req.http.X-Custom = "value";

    # 删除头部
    unset req.http.X-Unwanted;

    # 追加头部值(仅限部分头部)
    # 不支持 append,需要手动拼接
    if (req.http.X-Forwarded-For) {
        set req.http.X-Forwarded-For = req.http.X-Forwarded-For + ", " + client.ip;
    } else {
        set req.http.X-Forwarded-For = client.ip;
    }

    # 条件设置
    if (req.http.Host ~ "^api\.") {
        set req.http.X-Backend-Type = "api";
    }
}

6.7.2 响应头部清理

sub vcl_deliver {
    # 清理后端暴露的信息
    unset resp.http.Server;
    unset resp.http.X-Powered-By;
    unset resp.http.X-AspNet-Version;
    unset resp.http.X-AspNetMvc-Version;

    # 生产环境移除调试头部
    unset resp.http.X-Debug;
    unset resp.http.X-Backend;
    unset resp.http.X-Varnish;

    # 保留必要的头部
    # Cache-Control
    # Content-Type
    # ETag
    # Last-Modified
    # Set-Cookie(仅在需要时)
}

6.8 注意事项

重要

  1. Vary: CookieVary: Authorization 会严重降低缓存命中率,应该避免
  2. 标准化 Accept-Encoding 是提高缓存命中率的关键步骤
  3. 不要缓存带有 Set-Cookie 的响应,除非明确知道后果
  4. 生产环境应移除调试头部(X-Varnish 等),避免信息泄露
  5. ETag 和 Last-Modified 需要后端正确设置才能生效
  6. CORS 头部需要根据实际的域名配置,不要使用 *

6.9 业务场景

场景一:多语言网站头部处理

sub vcl_recv {
    # 根据 Accept-Language 标准化
    if (req.http.Accept-Language ~ "^zh") {
        set req.http.X-Language = "zh";
    } elseif (req.http.Accept-Language ~ "^en") {
        set req.http.X-Language = "en";
    } else {
        set req.http.X-Language = "en";
    }

    # 将语言信息加入缓存键
    # (在 vcl_hash 中处理)
}

sub vcl_hash {
    hash_data(req.url);
    if (req.http.host) {
        hash_data(req.http.host);
    }
    # 添加语言变体
    if (req.http.X-Language) {
        hash_data(req.http.X-Language);
    }
    return (lookup);
}

场景二:API 版本头部

sub vcl_recv {
    # API 版本路由
    if (req.http.X-API-Version) {
        set req.http.X-API-Version = req.http.X-API-Version;
    } elseif (req.url ~ "^/api/v(\d+)/") {
        set req.http.X-API-Version = regsub(req.url, "^/api/v(\d+)/.*", "\1");
    } else {
        set req.http.X-API-Version = "1";
    }
}

6.10 扩展阅读