以观察、判断和行动三个面板呈现有界陈旧缓存决策。

陈旧缓存需要有界策略

陈旧响应可以在源站故障时维持公开手册的可读性,也可能保留过时价格、已撤销权限或误导性的状态。可用性并不天然高于正确性。启用这种行为之前,应先确定哪些信息允许继续使用、什么条件下可以复用,以及最多能旧多久,而不是看到一次错误响应就无条件拿旧成功结果代替。 用可用性换新鲜度 过期和淘汰是两件事。过期响应仍可留在缓存里,用于条件验证或受到许可的陈旧响应;但冷缓存或已经淘汰条目的缓存,没有旧正文可返回。陈旧指令无法凭空创造副本,保存着字节也不会自动授予复用权限。选中的响应还必须匹配当前请求,包括相关 Vary 维度和访问边界,不能拿另一个请求留下的成功内容填补故障。 如何表示 stale-while-revalidate 允许在有界窗口内返回旧响应,同时尝试不阻塞该响应的验证。stale-if-error 则允许在尝试访问源站遭遇符合条件的错误时使用旧响应。两者触发条件不同,都不强制缓存保存内容,也不能保证每种缓存实现都支持。请求到达时若已越过允许窗口,不能因为之前有过一次许可,就继续无限期使用旧内容。 对于 max-age=30, stale-if-error=60,额外六十秒从新鲜期结束后开始计算,不是从每一次失败尝试重新开始。候选响应的总年龄窗口约为九十秒。RFC 5861 指定了会形成 500、502、503、504 的错误场景。认证失败或资源不存在,不能随手重新包装成可用性故障,再用旧的成功响应掩盖真正需要客户端看到的结果。 成功的条件验证不一定重新下载正文。三零四响应可以依据验证元数据更新存储响应,同时保留原来的表示字节。应把这种结果与替换正文的二百响应、故障期间的陈旧命中分别记录;三种情况可能显示相同页面,却对应不同的新鲜度依据,不能仅靠正文是否改变判断验证是否发生。 坏掉的样子 每次重验证失败后都把 Age 清零,会把有界例外变成无限陈旧。保留旧正文却刷新存储时间戳,也会造成同类错误。失败时应保留年龄;只有真正的新响应或成功验证,才提供更新新鲜度的依据。后台验证通常由流量触发,所以一个长期无人访问的缓存,不一定会在下一次用户访问之前主动把内容更新好。 只读或幂等并不代表可以安全返回旧内容。一个 GET 也可能涉及对时效敏感的权限或价格。RFC 9111 禁止在适用的 no-cache、must-revalidate 等指令不允许时复用陈旧响应。应避免互相矛盾的策略,并测试实际部署的缓存如何处理这些限制。另外,RFC 5861 中关于旧 Warning 字段的建议,已被废弃该字段的 RFC 9111 更新,不能把它当作当前验收必需项。 观察指标中要区分陈旧响应和新鲜命中,记录总年龄、错误类别以及验证是否成功,同时避免记录秘密信息。如果每次都返回旧内容,漂亮的 HTTP 成功率可能掩盖持续故障的源站。恢复验收必须确认发生了真实替换或成功验证,而不是只看到源站进程重新启动,就宣布数据已经恢复新鲜。 快速验证 准备 Node.js 与 npm。本轮使用 Node 24.19.0 和已有的 http-cache-semantics 4.2.0 安装运行了 JavaScript。安装固定版本后,用 NODE_PATH="$stale_lab/node_modules" node 执行 JavaScript。这是使用虚构响应和受控时钟的确定性策略与正文选择实验,不发送请求,也不制造真实宕机;只有安装阶段需要联网。 ...

二月 1, 2027 · 2 分钟 · XB Field Notes
不可变资源与内容指纹标题卡旁边排列观察、决策与行动三个面板。

不可变资源与内容指纹

资源指纹把文件字节的摘要放进地址。样式表发生变化,构建就为新内容生成另一个地址,页面因此能够选择新版本,而不用等待所有旧缓存同时过期。这项方法依赖发布流程遵守承诺:某个地址一旦对外提供,就不能随意替换成与名称摘要不符的内容。文件名看起来像摘要,不代表服务器已经落实这种约束。 名称配合新鲜度契约 构建程序可以对生成文件计算完整的 SHA-256,再把页面中的逻辑名称替换成相应地址。同样的字节保留相同名称,变化的字节通常得到不同名称;摘要截断越短,碰撞风险越需要认真评估。浏览器不会因为名称含有一串十六进制字符,就自动计算文件摘要验证完整性。实际缓存键还受到请求上下文和 Vary 等因素影响,命名方法不能取代 HTTP 缓存规则。 公开指纹资源常用的响应头是 Cache-Control: public, max-age=31536000, immutable。前者给出一年新鲜期,后者承诺该表示在新鲜期内不会改变,因此 RFC 8246 建议客户端避免不必要的条件请求。这不是永久保存许可:缓存可能被淘汰,用户可以强制刷新,客户端也可能不支持扩展。规范还建议在 HTTPS 等经过认证的上下文之外忽略 immutable,不能把它当作内容可信性的替代品。 负责引用这些资源的 HTML 需要独立缓存策略,例如较短新鲜期或合适的重新验证,让用户能够发现新版本。发布顺序应先让新资源可用,再暴露引用它们的页面或清单。旧页面、旧标签页和回退版本仍可能使用旧地址,资源保留期限必须覆盖这些真实使用方式。仅有指纹,不能让整个多文件发布自动成为原子事务。 public 只适合允许共享的表示。某份个性化下载即使带有内容摘要,也不会因此变成可供共享缓存保存的公开资源。响应策略仍然必须服从身份鉴别与保密要求;摘要只是命名依据,不能充当授权或隔离用户数据的措施。 坏掉的样子 覆盖旧指纹路径会制造两套互相矛盾的事实:已有用户继续从缓存读取红色样式,新客户端却在同一个地址读到蓝色字节。清理一个 CDN 节点,也不能把所有浏览器里的私有副本同步删除。发布检查应核对真实文件字节和记录的摘要,在开放流量之前拒绝这种变更,而不是依赖上线后提醒用户刷新。 立即删除上一版资源会产生另一类故障。缓存中的页面还可能引用昨日脚本,回退后的 HTML 也可能依赖已经移除的文件。保留源码提交不等于保留可直接恢复的构建产物。即使所有摘要都正确,先发布页面、后上传文件仍会造成暂时的缺失资源;多节点传播期间也需要检查依赖是否已经就绪。 快速验证 把下面代码保存为 immutable.py,使用 Python 3 执行 python3 immutable.py。程序启动回环服务,检查样式字节的摘要和缓存响应头,故意改写旧地址背后的内容,确认摘要不匹配,然后恢复原文并关闭服务。预期三个契约检查标志为真,而 browser_cache_tested_by_cli 为假。这个命令读取了真实 HTTP 响应,但没有实现浏览器缓存,因此不能单凭命令结果宣称已经验证客户端复用行为。 ...

十一月 25, 2026 · 2 分钟 · XB Field Notes
观察、决策、行动三个面板,旁边的标题说明Age 头估算响应年龄。

Age 头告诉我们缓存已存在多久

Age 是从源站生成或成功验证响应以来,已经经过多少秒的估计值,包含缓存驻留和网络传输时间。它不是对象的创建时间戳,也不是访问次数,更不只是最后一台代理在磁盘上保存副本的时长。要判断这个数字意味着什么,必须同时查看响应的新鲜度策略和实际请求上下文,不能把年龄直接等同于是否允许复用。 合并估计时不能重复计时 RFC 9111 定义了接收响应时的两种年龄估计。表观年龄用接收缓存的时钟减去 Date,如果得到负数,就按零处理。校正年龄从收到的 Age 数值出发,再加上发出请求到收到响应之间的时间。缺少 Age 时,这个输入按零参与计算,但并不意味着应当丢掉基于 Date 的另一种估计。 当缓存链满足相关 HTTP 行为要求时,RFC 允许直接使用校正年龄;更保守的计算则取表观年龄与校正年龄的较大值,再加本地驻留时间。下面的实验采用后者。把这两种估计直接相加,会重复计算互相重叠的时间;而每经过一个缓存就把 Age 清零,又会丢掉上游已经积累的历史。 请求、接收和本地驻留时间应使用一致的测量方法,Date 比较还依赖时钟同步。因此结果仍然是估计,而不是精确秒表。生产实现需要恰当处理时钟和输出整数值。下面只使用固定的整秒 UTC 时间与合法头字段,展示指定计算步骤,不构成一套完整的 HTTP 缓存实现。 新鲜度是另一个判断 当前年龄为四十五秒,寿命为六十秒时仍在新鲜期;同样的年龄放在三十秒寿命下,就已经陈旧。相等时也算陈旧,因为新鲜要求年龄严格小于寿命。不过,这个比较本身并不授权复用。请求限制、缓存键、Vary、存储规则和要求验证的指令,都可能让一份年龄很小的响应仍然不能直接返回。 对于共享缓存,s-maxage 覆盖 max-age,其数值既可能缩短,也可能延长适用寿命。stale-while-revalidate 并不是把旧响应重新变成新鲜,而是在策略与实现允许的条件下,一边验证一边有限使用陈旧副本。应把“允许服务旧内容”视为另一种状态,不要把所有宽限期都混进新鲜期的定义。 坏掉的样子 Age 很大,不自动证明发布出了问题。缓存寿命为一年的指纹资产,即使年龄很大也可能仍然新鲜。正文没有改变的表示,还可以在后来成功通过验证,只更新元数据而不替换字节。此后 Age 对应的是较新的验证历史,不一定反映这些字节最初被作者写出的时间,不能据此计算内容存在了多久。 同一 URL 的每次请求也不保证 Age 单调递增。请求可能到达不同边缘、选择不同表示、遇到条目淘汰,或经过成功的重新验证。Age 为零也可能来自刚缓存的响应;没有这个头,同样不能单独证明一定由源站直接应答。应对照匹配的请求、正文身份、验证器、Date、缓存策略和服务商诊断字段一起判断。 ...

十一月 6, 2026 · 2 分钟 · XB Field Notes
以观察、判断和行动三个面板区分单用户缓存与共享缓存的存储范围。

Cache-Control:private 还是 shared

存储范围不是身份系统。Cache-Control: private 要求遵守协议的共享缓存不存储这条响应,却不会教会浏览器识别正文属于哪个应用账户。当一个人退出登录,另一个账户在同一浏览器中登录,然后访问相同网址时,这个区别就会变成实际的数据隔离问题,不能只凭响应头里出现 private 就判定安全。 有资格被缓存不代表能共享 不带字段名参数的 private 允许单用户缓存保存响应,但仍受其他缓存条件约束。浏览器是常见实现,并不是唯一实现。public 则明确允许某些原本可能受到限制的响应进入缓存,例如满足条件的带授权请求响应。已经符合缓存条件的普通响应不一定需要 public;出现 public 也不意味着所有缓存都必须保存它,存储许可和实际是否存储是不同事实。 不带参数的 no-cache 与 no-store 回答的是另一个问题。前者允许存储,但每次复用前必须成功验证;后者禁止私有和共享 HTTP 缓存存储及随后复用该响应。它们都不是加密、访问控制检查,也不承诺清除应用日志或用户之前保存的副本。本文讨论不带字段名参数的形式,不把实现支持较弱的按字段限定形式混进实验。 新鲜度还需要单独判断。错误的个人响应如果按共享键保存,即使只给五秒钟有效期,也可能有五秒钟把数据交给另一个人。缩短 max-age 不能修复身份边界。反过来,s-maxage 只是控制共享缓存的新鲜度,并不是 private 的另一种写法。先决定响应可在哪一层保存,再讨论可直接复用多久,才能避免用性能参数代替安全条件。 坏掉的样子 HTTP 的通用规则不会因为请求里有 Cookie 就自动禁止共享缓存。Authorization 确实有专门的共享缓存限制,但 public 等指令能够明确允许存储。为了提高命中率而添加 public,可能恰好撤掉应用之前依赖的限制。修改指令前应检查实际响应体和认证路径,不能只查看页面看起来是不是一份普通文档。 Vary 细化的是请求匹配条件,不授予存储许可。增加 Vary: Cookie 不会让 private 响应变得可以进入共享缓存。在私有缓存内,它可以区分不同 Cookie 字段,但这仍然只是字段匹配,不是授权判断。假设令牌文本没有变化,而账户权限已经被撤销,仅凭请求字段仍匹配,不能证明当前用户继续有权读取旧内容。 ...

十月 23, 2026 · 2 分钟 · XB Field Notes
标题卡旁有三个方框,分别标注 Observe、Decide 和 Act。

负向 DNS 回答会把不存在的数据记到缓存寿命结束

DNS 可以通过说明数据缺失提供有效信息。NXDOMAIN 表示名称错误,NODATA 则描述成功响应中没有所请求类型的数据。对于没有别名的直接查询,两者区分名称不存在,以及名称存在但缺少该类数据。缓存这些结果,可以避免每次客户端请求都重新进行相同的无结果上游查询。负向回答也是有内容、有寿命的状态,并不是“没有收到任何东西”。 坏掉的样子 部署已经创建正确记录,先前缓存过缺失结果的客户端却可能仍然失败。权威返回新数据,缓存则在寿命结束前继续返回旧否定结果。反复删除重建记录,不会使远端缓存自动失效。这种情况常出现在应用提前查询尚未配置完成的服务名称时;排障应对照查询发生时间和缓存寿命,而不是只检查现在的区域文件是否包含记录。 仅凭回答区为空,不能完整分类 DNS 结果。需要检查响应码、权威区和可能存在的别名链。转介与权威否定回答不同,NXDOMAIN 也可能指向 CNAME 最终目标,而非最初请求的别名本身。把所有空结果都说成“域名不存在”,可能让调查去改错误的记录。应保留原始问答的类型、名称及各区数据,避免过早简化成一句错误描述。 对于 RFC 2308 规定的权威 NXDOMAIN 和 NODATA,负向寿命取 SOA.MINIMUM 与 SOA 记录 TTL 中的较小值。该值放在否定回答的 SOA TTL 上,再由缓存继续递减。MINIMUM 不是所有正向记录 TTL 的统一下限;只改变将来正向记录的 TTL,也不会缩短已经缓存的不存在结论。操作时应确认正在调整哪类数据的哪一代寿命。 快速验证 历史解析器集成示例保持原样,本次没有重跑;当前没有 Docker,也不能建立网络监听。 将脚本保存为 check-negative-dns.py,准备 Docker 后执行 python3 check-negative-dns.py。固定环境使用 BIND 9.18.49 和 dnsmasq 2.90,只有隔离回环网络。第一个区域的 SOA TTL 为八秒、minimum 为四秒,并有一条现存 A 记录。实验在新增缺失数据前后检查真实响应与权威查询次数,不通过模拟字典代替解析器缓存行为: ...

十月 22, 2026 · 3 分钟 · XB Field Notes
两行标题旁边的三个相连方块,依次标注 Observe、Decide 与 Act。

Content-Type 与 Vary 要一起工作

同一个 URL 可以向 API 客户端返回 JSON,也可以向另一个读者返回纯文本。Content-Type 说明这次选中了什么表示,Vary 则说明哪些请求字段参与了选择。如果缓存不知道应该怎样匹配后续请求,即使正文的类型标签完全正确,也可能把属于另一种偏好的响应送给当前读者。 分清格式、选择与复用 如果服务器根据请求的 Accept 字段进行协商,就应在实际响应类型之外返回 Vary: Accept。写成 Vary: Content-Type 指定的是请求里的类型字段,并不是响应的媒体类型;它通常不能表达客户端希望收到哪一种格式。缓存无法仅通过查看已存响应的类型,倒推出服务器选择该表示时使用的规则。目标 URI 与相关方法语义本来就参与普通缓存匹配,Vary 额外约束的是请求头字段。 压缩还涉及另一组容易混淆的概念。经过 gzip 的 JSON 仍然是 JSON 媒体类型,Content-Encoding: gzip 描述对表示施加的内容编码,不是传输编码或消息分帧。如果源站依据 Accept-Encoding 选择是否压缩,这个请求字段就是有关的 Vary 维度。只有真正参与选择的维度才需要纳入;浏览器的实际视口宽度不会自动变成一个请求头,也不能仅凭“这是移动端页面”就推导出缓存键应该包含什么。 Vary 不授予缓存资格,也不让一个过期响应重新变新。存储和复用还必须满足状态码、方法、认证以及缓存指令等独立条件。它更不是访问控制边界:增加 Vary: Cookie 并不能证明个人账户数据适合进入共享缓存。应先确定谁有资格接收响应、哪些缓存允许保存,再描述这些可复用表示之间的差异,不能把权限问题缩减成几个字段名。 坏掉的样子 当响应本身可以缓存且仍然新鲜时,漏写 Vary 可能让第一个请求决定后续读者拿到哪一种内容。直接访问源站一切正常,但走缓存时,后来的客户端不再回源,直接收到不兼容的表示。状态码仍是成功,Content-Type 也如实描述正文,却没有告诉读者这份响应属于另一种请求偏好。诊断时需要同时对照请求 Accept、响应类型和缓存复用证据,不能只看接口是否返回成功。 ...

十月 12, 2026 · 2 分钟 · XB Field Notes
标题为「ETag 与条件请求避免多余工作」的封面,下方是观察、判断与行动三个面板。

ETag 与条件请求避免多余工作

只有旧表示确实仍可替代当前表示,304 才有价值。ETag 提供的是源站对版本的声明,不会独立检查这个声明是否诚实。错误的标签会让条件请求高效地保留错误内容。因此在把传输减少当作性能改善之前,既要测试未变化的数据,也要测试有意义的内容变更,不能只追求更多的 304。 校验器标识版本 实体标签是带引号的不透明标识。客户端应按收到的形式带回它,不应擅自当作时间戳解释,也不应移除弱标签标记。生成器可以使用版本编号或抗碰撞摘要。当最终字节已经存在时,摘要比较方便;如果每次为了返回 304,仍要先完整渲染并计算一份昂贵文档的哈希,源站的计算成本可能并没有明显下降。省掉正文传输不等于省掉所有工作。 强与弱是不同的承诺 所选表示的数据变化时,强验证器必须变化。带缩进的 JSON 与紧凑 JSON 即使解析成同一个对象,字节也不同;gzip 与未编码形式同样具有不同表示数据,强标签必须保留这种区别。长度相同,或者底层数据库的粗粒度时间戳相同,都不能证明逐字节一致。因此验证器的设计要贴近真正返回的表示,而不是只观察某个间接指标。 带 W/ 的弱标签可以把源站认为可互换的表示归为一组。若 JSON 接口契约不关心排版,一个语义版本可以同时覆盖紧凑和缩进序列化。实际字段值变化后,如果旧对象已不再是可接受的替代品,就必须更新该版本。弱验证表达的是明确的等价策略,不是任意变化都允许沿用标签,更不是可以永远不维护版本信息。 If-None-Match 使用弱比较完成缓存验证。条件 GET 匹配时返回不带正文的 304,不匹配时通常返回当前表示与 200。If-Match 则使用强比较,这关系到更新前置条件;客户端也不能用弱标签作为 If-Range 的验证器。这几个字段的契约不同,先去掉 W/ 再统一做字符串比较,会把本来不同的语义混成一件事。 坏掉的样子 变化数据搭配固定标签,可能产生错误的 304,让客户端继续使用旧副本。另一个方向的错误,是完全相同的数据每次生成新标签,导致多余正文传输。添加很长的 max-age 无法修复这两种问题。新鲜度决定何时需要验证,验证决定已有响应是否仍然可用;成功的 304 也不保证应用解析、渲染或业务计算自然消失。 比较必须发生在选择表示和正常访问检查之后,匹配的验证器不是授权凭证。协商响应还需要保留对应的 Vary 维度,并正确区分表示数据。标签列表应交给真正的解析器,因为合法的带引号标签内部可以包含逗号;直接按逗号拆字符串,会破坏本来有效的标识。下面的测试会同时检查这一点,而不仅验证最简单的单个标签。 快速验证 准备 Python 3.11 或更新版本和 Werkzeug 3.1.8,创建临时环境后,用 "$etag_lab/venv/bin/python" 执行 Python 代码。测试客户端在进程内执行 WSGI 应用,不监听端口,也不发网络请求;只有安装依赖需要联网。故意写错的端点是反例测试,不能作为实际部署实现。 ...

九月 26, 2026 · 2 分钟 · XB Field Notes
两只分别标注新鲜度与校验的钟,之间是一个 ETag。

两只钟模型中的缓存新鲜度与校验

缓存的连贯性取决于你让哪一对条件显式化。

八月 14, 2026 · 1 分钟 · XB Field Notes