Cloudflare 已在其边缘网络中加入对 HTTP Vary 响应头的原生支持,并覆盖所有客户套餐。它解决的是一个长期存在的缓存难题:同一个 URL 可能需要根据语言、压缩格式或其他请求头返回不同内容,但如果把请求头不加选择地纳入缓存键,缓存对象数量会迅速膨胀,命中率反而下降。
这项能力的价值不只是“识别 Vary”,还在于运营者能够针对特定请求头定义缓存行为,在正确返回内容变体的同时,限制无意义的缓存碎片。
Vary 到底改变了什么
源站可以通过响应头告诉中间缓存:除了 URL,还要参考哪些请求头来区分响应。例如:
HTTP/1.1 200 OK
Cache-Control: public, max-age=300
Vary: Accept-Encoding, Accept-Language
Content-Type: text/html; charset=utf-8
这表示以下请求不一定共享同一份缓存对象:
GET /docs HTTP/1.1
Accept-Encoding: gzip
Accept-Language: zh-CN
GET /docs HTTP/1.1
Accept-Encoding: br
Accept-Language: en-US
没有正确处理 Vary 时,边缘节点可能把中文响应返回给英文请求,或者把不合适的编码版本发给客户端。反过来,如果缓存系统对任意请求头值都创建新对象,攻击者或普通浏览器产生的大量不同头部组合又会导致缓存抖动:对象不断写入、淘汰,命中率降低,源站请求量上升。
Cloudflare 的可配置支持让运营者可以更明确地决定哪些请求头应影响缓存,以及如何处理它们,而不是在“忽略所有差异”和“无限拆分缓存”之间二选一。
真正的风险是基数,而不是请求头数量
评估某个请求头是否适合参与内容协商时,关键指标是它可能出现多少种有效值。
Accept-Encoding 通常比较安全。虽然原始字符串可能不同,但实际响应往往只落在 Brotli、gzip 和未压缩等少量类别中。
Accept-Language 就复杂得多。浏览器可能发送:
zh-CN,zh;q=0.9,en;q=0.8
zh-CN,zh-TW;q=0.8,en-US;q=0.6
zh-Hans-CN,en;q=0.5
如果边缘缓存按原始字符串逐一建立变体,即使应用最终只输出中文和英文,也可能生成成百上千个缓存对象。
更危险的候选项包括:
User-Agent:取值基数极高,而且经常包含版本号。Cookie:组合数量近乎无界,还可能包含会话或用户身份。- 自定义追踪头:通常每个请求都不同,不应进入公共缓存键。
Vary: *:意味着响应依赖未明确列出的请求因素,通常无法被共享缓存安全复用。
因此,合理策略不是把所有可能改变响应的头部全部加入 Vary,而是先把业务输出归一化为少量、可枚举的表示形式。
可运行实验:观察语言变体如何形成
下面的 Python 服务只输出中文或英文,并通过 Vary: Accept-Language 声明内容协商。它使用标准库,可以直接运行。
将以下内容保存为 server.py:
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
if self.path != "/docs":
self.send_error(404)
return
accept_language = self.headers.get("Accept-Language", "")
language = "zh" if accept_language.lower().startswith("zh") else "en"
if language == "zh":
body = "你好,这是中文文档。\n".encode("utf-8")
content_language = "zh-CN"
else:
body = b"Hello, this is the English documentation.\n"
content_language = "en"
self.send_response(200)
self.send_header("Content-Type", "text/plain; charset=utf-8")
self.send_header("Content-Language", content_language)
self.send_header("Cache-Control", "public, max-age=300")
self.send_header("Vary", "Accept-Language")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
if __name__ == "__main__":
server = ThreadingHTTPServer(("0.0.0.0", 8000), Handler)
print("Listening on http://127.0.0.1:8000")
server.serve_forever()
运行并发送两个请求:
python3 server.py
在另一个终端执行:
curl -i -H 'Accept-Language: zh-CN,zh;q=0.9' http://127.0.0.1:8000/docs
curl -i -H 'Accept-Language: en-US,en;q=0.9' http://127.0.0.1:8000/docs
把这个服务部署到 Cloudflare 代理后的域名时,可以进一步查看边缘缓存状态。将域名替换成自己的地址:
curl -sS -D - -o /dev/null \
-H 'Accept-Language: zh-CN,zh;q=0.9' \
https://example.com/docs
重点检查 Vary、Cache-Control、Age 和 Cloudflare 返回的缓存状态头。连续发送相同请求,应当观察它是否逐渐命中缓存;随后切换语言,确认不会错误复用另一种语言的响应。
这个示例也暴露了一个边界:应用虽然只产生两种语言,但客户端仍能发送大量不同的 Accept-Language 字符串。如果缓存行为基于原始值,仍可能发生碎片化。实际配置中应利用 Cloudflare 提供的请求头行为控制,把输入归并到有限类别,或者把语言直接编码进 URL,例如 /zh/docs 与 /en/docs。
更稳妥的设计:让缓存变体可枚举
对于可控的新系统,路径或主机名通常比高基数请求头更容易观察和调试:
/docs/zh/getting-started
/docs/en/getting-started
如果必须使用内容协商,可以这样实践:
- 只选择确实改变响应正文、编码或格式的请求头。
- 将输入归一化为少量类别,例如
zh、en、other。 - 对每种变体设置一致且明确的
Cache-Control。 - 用真实流量样本估算每个 URL 会生成多少变体。
- 同时监控缓存命中率、源站请求量、缓存对象数和淘汰频率。
下面是一份概念性策略,用来表达期望行为;它不是 Cloudflare 官方 API 字段,实际部署时应映射到账号中可用的规则配置界面或 API:
# conceptual-vary-policy.yaml
route: /docs/*
cache:
enabled: true
vary_by:
Accept-Encoding:
allowed_values: [br, gzip, identity]
Accept-Language:
normalize:
zh-CN: zh
zh-TW: zh
en-US: en
en-GB: en
fallback: en
ignore_headers:
- User-Agent
- X-Request-ID
这里的核心不是 YAML 语法,而是三项约束:允许列表、归一化和回退值。它们共同把理论上无限的请求空间压缩成有限缓存集合。
上线前的检查清单
- 源站是否稳定返回正确的
Vary,而不是不同实例各自为政? - 每个参与协商的请求头有多少种真实取值?
- 是否误把
Cookie、请求 ID 或完整User-Agent纳入公共缓存? - 私有数据是否使用了
private、no-store等适当指令?不要依赖Vary隔离用户数据。 - 切换语言、编码或格式后,是否仍会命中错误的旧响应?
- 开启规则后,缓存命中率和源站负载是否改善,而不是恶化?
Cloudflare 的原生 Vary 支持补齐了边缘缓存中的重要语义,但它不会自动消除错误的变体设计。最有效的采用方式,是先减少表示形式的数量,再用可配置规则精确描述必要差异。缓存键越稳定、变体越可枚举,这项能力越能真正降低源站压力,而不是制造新的缓存抖动。