第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 自动处理此逻辑
}
}
6.5 Cookie 处理
6.5.1 Cookie 对缓存的影响
Cookie 是缓存的主要敌人之一。带有 Set-Cookie 的响应默认不会被缓存,而请求中的 Cookie 通常表示个性化内容。
6.5.2 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;
}
}
6.5.3 会话 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 注意事项
重要
Vary: Cookie或Vary: Authorization会严重降低缓存命中率,应该避免- 标准化
Accept-Encoding是提高缓存命中率的关键步骤- 不要缓存带有
Set-Cookie的响应,除非明确知道后果- 生产环境应移除调试头部(X-Varnish 等),避免信息泄露
- ETag 和 Last-Modified 需要后端正确设置才能生效
- 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";
}
}