Age 告诉客户端:这份响应在缓存里已经待了多少秒。它只由缓存添加,源服务器自己不会发。
多级缓存时它是累加的。
上一节讲资源能不能切,这一节讲副本有多旧。 缓存能省往返,代价就是「你拿到的是旧东西」,Age 就是这个代价的刻度。
一、只有缓存才会加这一行
源服务器刚生出来的响应,年龄就是 0,没必要说。所以你在源站抓包,几乎永远看不到 Age;
一看到它,说明中间至少有一个人替你存过。
这也解释了一个常见困惑:本地测试时怎么都不出现 Age,一上 CDN 就有了。
不是你的代码问题,是路径上多了缓存。
二、和 max-age 一起算剩多久
最简的算法是:还能用多久 = max-age − Age。Demo 里拿 max-age=600 配五组 Age:
| Age | max-age | 判定 |
|---:|---:|---|
| 0 | 600 | 新鲜,还能用 600 秒 |
| 100 | 600 | 新鲜,还能用 500 秒 |
| 599 | 600 | 新鲜,还能用 1 秒 |
| 600 | 600 | 正好到期 |
| 650 | 600 | 过期,必须回源 |
严格的算法还要加上「本机收到时刻减去 Date」这一段,因为 Age 只统计到缓存发给你那一刻。
RFC 7234 里把它拆成四项,还要处理时钟不同步,实际实现大多用上面这个简化式。
要注意 Age 和 Date 不重复计算:Date 是源服务器生出来的时刻,
Age 是「从那个时刻到现在过了多久」的合计,两者相加才是完整的账。
三、多级缓存时是累加的
Demo 里搭了两层代理,走三步:直接问内层(Age 0)、睡 2 秒后问外层让它去问内层(Age 2)、 再睡 1.2 秒问外层(Age 3)。 第三行是 3 而不是 1,因为内层那份在第 2 步时已经 2 秒了,外层把这个 2 存下来当底数, 自己又放了 1 秒,报出去就是 2+1。 整个过程中源服务器只被访问 1 次,内层被访问 2 次,外层被访问 2 次。 这就是缓存的账本:每一次命中都在 Age 上记一笔,回源次数却不变。
四、动手 Demo:两级缓存,Age 从 0 累加到 3
先做一个只报 max-age=600 的源服务器,再套两层各带一份副本的代理。 每层都把自己这份「放了多久」写进 Age,客户端只看最后一行的数。
def strip_age(raw):
base, keep = 0, []
for ln in raw.split(b'\r\n\r\n', 1)[0].decode('latin-1').split(u'\r\n'):
if ln.lower().startswith(u'age:'):
base = int(ln.split(u':', 1)[1]) # 上游报的,留着当底数
else:
keep.append(ln)
return keep, base
def outer(raw, st):
if u'copy' not in st:
st[u'copy'] = X.ask(p1, X.req(u'GET', u'/doc', u'127.0.0.1'))
st[u'birth'] = time.time()
keep, base = strip_age(st[u'copy'])
keep.append(u'Age: %d' % (base + int(time.time() - st[u'birth'])))
return (u'\r\n'.join(keep) + u'\r\nConnection: close\r\n\r\n').encode('utf-8') + X.body_of(st[u'copy'])
实跑输出:
--------------------------------------------------------------------
Age:这份副本在缓存里放了多少秒
--------------------------------------------------------------------
场景:源服务器给的 `Cache-Control: max-age=600`,
缓存把副本放了不同时长,然后把 Age 报给客户端。
Age max-age 判定
0 600 新鲜,还能用 600 秒
100 600 新鲜,还能用 500 秒
599 600 新鲜,还能用 1 秒
600 600 正好到期
650 600 过期,必须回源
客户端自己算的公式是:还剩 = max-age − Age − (收到时刻 − Date)。
第二项是本机时钟和源服务器时钟的差,Age 已经把缓存那一段算进去了,
所以两者不重复。
两级缓存时,Age 是累加的。真跑一遍,中间各睡一段时间:
第 1 步 直接问内层 Age: 0
第 2 步 睡 2 秒后问外层 Age: 2(外层没有,去问内层)
第 3 步 再睡 1.2 秒问外层 Age: 3(外层自己有,直接给)
源服务器总共被访问 1 次
内层代理总共被访问 2 次
外层代理总共被访问 2 次
第 3 步那行不是 1 而是 3,因为它不在一个地方变老的:
内层那份出生在第 1 步,到第 2 步时它已经 2 秒了,这个 2 写进 Age;
外层把这个 2 存下来当底数,自己在第 2 步到第 3 步之间又放了 1 秒,
于是报给客户端的是 2+1=3。Age 从来不是「某一个人的年龄」,
而是「这份副本从出生到现在,一共被转了多久」的合计值。
第 1 步直接问内层,Age 是 0——副本刚出生。 第 2 步睡 2 秒后问外层,外层手上没有,去问内层,拿到 Age: 2。 第 3 步再睡 1.2 秒问外层,外层这次自己有,直接把存的 2 加上自己的 1,报 Age: 3。 三个计数也对得上:源服务器 1 次、内层 2 次、外层 2 次。 每多一层缓存,Age 就多一段,回源却一次都不多。
30 秒实验
- 把第 2 步的 2 秒改成 5 秒,看第 3 步会不会变成 6。
- 把 max-age 改成 2,看 Age 3 那一行是不是该判过期。
- 想一想:如果两层缓存的时钟差了 10 秒,Age 会偏大还是偏小?
常见误区
- 「源服务器也会发 Age」——不会,Age 只由缓存添加。
- 「Age 就是 Date 到现在的时间」——Age 是逐跳累加的合计,不是单一差值。
- 「Age 超过 max-age 就该删副本」——是「不能直接给」,通常还得回源验证一次。
自测三问(点开对答案)
1. 为什么源站抓包看不到 Age? —— 因为它刚生成、年龄为 0,规范上只有缓存才加这一行。 2. 两级缓存各放了 2 秒和 1 秒,客户端收到的 Age 是多少? —— 3,Age 是逐跳累加的。 3. max-age=600、Age=650,这份副本还能直接用吗? —— 不能,已经过期,必须回源或者走协商验证。Age 说「有多旧」,可「旧」到底按什么判?下一节的 ETag 给了另一条尺子:按内容判。
- ☐ (本节完,下一节 6.5.3 ETag)