Vary 告诉缓存:同一个 URL 其实有好几份响应,按这里点名的首部来区分。
它就是缓存的 key 扩展——漏写一次,就会把 gzip 版发给不会解压的客户端。
上一节讲服务端自报家门,这一节讲服务端提醒缓存别乱来。内容协商那一章埋的伏笔,到这里收。
一、它解决的是「同 URL 多份」
同一份 /app.js,会解压的客户端拿 gzip,不会的拿原文。两部字节流完全一样吗?不一样。能混用吗?
不能。可它们的 URL 是同一个——缓存只按 URL 存的话就会混。
Vary: Accept-Encoding 就是在说「存的时候把 Accept-Encoding 也算进 key」。
于是同一 URL 下面挂两个条目,各服务各自的客户端。
能被点名的首部不止这一个。按语言分的写 Vary: Accept-Language,按 UA 分的写 Vary: User-Agent。
多个就逗号分隔,意思是要同时匹配所有。
二、漏写会怎样
Demo 跑了紧密对照的两组。
带 Vary 的一组:第一次(会 gzip)收到 151 字节,第二次(不会 gzip)收到 117 字节的干净原文,
缓存命中 0 次、源服务器被访问 2 次。
不带 Vary 的一组:第二次那个客户端收到 151 字节,还带着 Content-Encoding: gzip。
缓存命中 1 次,源服务器只被访问 1 次——省了回源,代价是把压缩过的二进制塞给了一个不会解压的人。
缓存并没有做错什么,它只是「按 URL 存」而已。错在响应没说清「这个 URL 有好几份」。
这一条错误极难排查:浏览器显示乱码,你查正文、查编码、查响应头,最后才发现是 CDN 上少配了一行。
三、Vary: * 是「谁都别存」
星号表示「这份响应依赖的东西没法用首部表达」,于是任何缓存都不许复用。
用在按 IP、按登录态动态生成的页面上。
它等于把缓存关掉,所以别拿它当万能药。
真需要「按人区分」,标准做法是 Cache-Control: private,让中间缓存不存、只许浏览器存。
还有一个现实问题:Vary 点名的首部越多,缓存条目涨得越快。
Vary: User-Agent 尤其糟——UA 组合几乎无限,缓存命中率会掉到接近 0。
四、动手 Demo:同一份 /app.js,带 Vary 和不带 Vary 的两组对照
先做一个按 Accept-Encoding 决定发 gzip 还是原文的源服务器,再让一个极简缓存站在前面,跑两遍:
一遍把 Accept-Encoding 算进 key,一遍只按 URL 存。
def through(raw, st, vary_on):
hd = {}
for ln in X.head_lines(raw)[1:]:
if u':' in ln:
k, v = ln.split(u':', 1)
hd[k.lower()] = v.strip()
key = hd.get(vary_on, u'') if vary_on else u'*' # 关键:key 怎么算
if key in cache:
st[u'hit'] = st.get(u'hit', 0) + 1
return cache[key]
r = X.ask(p_origin[0], raw)
cache[key] = r
return r
实跑输出:
--------------------------------------------------------------------
Vary:告诉缓存「同一 URL 其实有几份」
--------------------------------------------------------------------
同一份资源,会压缩的客户端拿 gzip,不会的拿原文。
缓存怎么知道这两份不能混?靠 Vary 里点名的那个首部。
Vary: Accept-Encoding
第一次(会 gzip)收到 151 字节,带 Content-Encoding: gzip
第二次(不会 gzip)收到 117 字节,是干净的原文
缓存命中 0 次,源服务器被访问 2 次
没有 Vary(缓存只看 URL)
第一次(会 gzip)收到 151 字节,带 Content-Encoding: gzip
第二次(不会 gzip)收到 151 字节,却带着 Content-Encoding: gzip
缓存命中 1 次,源服务器被访问 1 次
第二组就是事故现场:不会解压的客户端拿到一坨 gzip 二进制,
它按 UTF-8 去解,得到乱码。缓存没做错什么——它只是按 URL 存的。
还有第三种取值 `Vary: *`,意思是「这份响应谁都别复用」。
用在按 IP、按 Cookie 定制的页面上,等于关掉缓存。
带 Vary:151 字节(gzip)和 117 字节(原文)各走各的,缓存命中 0 次、源服务器被访问 2 次。
多花一次回源,换来正确。
不带 Vary:第二次那个客户端收到 151 字节,还带着 Content-Encoding: gzip,缓存命中 1 次、
源服务器只被访问 1 次。省了回源,换来乱码。
151 和 117 只差 34 字节——这是「省一次回源」的全部收益。为这 34 字节冒乱码的风险,怎么算都不划算。
所以结论不是「有 Vary 更好」,而是:只要响应会随首部变,就必须写 Vary,不写等于让缓存替你猜,
它猜不对。
30 秒实验
- 把
vary_on改成user-agent,看两组的命中次数会不会变化。 - 给响应加
Vary: Accept-Encoding, Accept-Language,想想要几个条目。 - 想一想:为什么
Vary: User-Agent会把命中率打到接近 0?
常见误区
- 「Vary 是给客户端看的」——它是给缓存看的,客户端基本不关心。
- 「不写 Vary 只是缓存效率低」——会串版本,把 gzip 发给不会解压的客户端。
- 「
Vary: *是好办法」——它等于关掉缓存,按人区分该用private。
自测三问(点开对答案)
1. `Vary: Accept-Encoding` 到底改了什么? —— 缓存条目的 key,让它按该首部的值分开存多份。 2. 漏写 Vary 会造成什么具体故障? —— 把压缩版发给不支持压缩的客户端,正文变乱码。 3. 为什么 `Vary: User-Agent` 很危险? —— UA 组合几乎无限,缓存条目爆炸,命中率趋近于 0。Vary 是提醒缓存。最后一个是回到认证:401 里的 WWW-Authenticate,和前面 407 那个正好成对。
- ☐ (本节完,下一节 6.5.9 WWW-Authenticate)