摘要

  • Cache-Status 以有序列表记录各层缓存对同一份响应及其请求的处理:本地命中、转发原因、上游状态、剩余新鲜期、存储与请求合并都可以分别表达。
  • 每个成员由被观察的缓存自行写入,是否披露及多数参数都由本地决定。hit 可以是陈旧响应,TTL 是该缓存自己的计算,列表顺序也不等于身份认证或路径完整性。
  • 组织应把它当作运行证词:保存原始字段,与 Cache-Control、Age、Via、跟踪和内部遥测互证,并把“交付响应”“解释响应”和“宣布业务恢复”的权力拆开。

一个全绿仪表盘里的旧价格

电商团队完成价格修正,源站接口也返回新值。监控没有报错,缓存命中率甚至接近百分之百。可用户截图仍是旧价格。值班人员抓到边缘响应:

Cache-Status: Edge; hit; ttl=376

现场很快形成结论:Edge 命中了旧对象,还有 376 秒才过期,所以责任在缓存。这个结论把三层事实压成了一层。

依照 RFC 9211,hit 的严格含义是:对这一次请求,该缓存没有继续转发,而是从自己的缓存中取得响应。它没有说明响应一定新鲜。若规则允许在无法联系源站或其他条件下直接使用陈旧对象,陈旧响应仍可算 hit。它也没有说明缓存键是否把语言、身份、租户和个性化边界表达正确,更没有说明内容是否是应用此刻想交给这名用户的版本。

ttl=376 同样不是全系统的倒计时。它是该缓存在发出响应头附近,根据自身规则计算的剩余新鲜期,其中可能包含启发式新鲜度和本地配置。

这并不削弱 Cache-Status。恰恰相反,只有承认它说得很窄,组织才能把这条证词放回正确的证据链,而不是让一个单词替代整场调查。

标准解决的是“共同说法”,不是“共同真相”

HTTP 缓存长期都会在响应里塞入调试头。不同产品可能写 HIT、MISS、内存命中代码、节点名或自定义时间。人能勉强读懂,自动作业却很难跨产品比较;同一个词在不同实现里也可能不是同一个意思。

Cache-Status 把这些说明放进 HTTP Structured Fields 的列表。每一个列表成员代表处理过请求的一层缓存;最靠近源站的成员在前,最靠近用户的成员在后。源站旁的反向代理可以先写,shield 保留前项并追加,边缘节点再追加,用户代理若选择写入则位于最后。

这个顺序很有价值。它能呈现“内层命中、外层 URI miss”,也能呈现“转发代理将并发请求合并并存下响应、浏览器端仍然 miss”。跨产品工具终于可以解析同一种布尔值、整数和 token,而不是记住几十套私有暗号。

但列表完全依赖参与者维护。RFC 让缓存自己决定何时添加字段:可以始终添加,也可以只在配置开启或请求激活调试模式时添加。缓存追加成员时应保留已有值;“应保留”是运行约定,不是防篡改机制。某一层可以不写、可以删去旧值、可以用生成字符串隐藏拓扑,也可以不透露可选参数。格式有效,不代表记录完整。

最小公共规范让彼此可理解,未来的披露范围、身份映射、留存和核验仍是本地决策。这正是标准能广泛落地的原因,也是组织不能把它误当审计链的原因。

成员有署名,但署名不是身份证明

每个成员以 String 或 Token 标识缓存。标识可以是产品、服务、主机名、IP 地址或生成字符串。这样的自由度适应了公共 CDN、企业反向代理和浏览器缓存,却没有建立“这个名字必然对应这台机器或这家公司”的认证关系。

Edge-Shanghai 可能是一组节点、一个逻辑服务,也可能只是为了不暴露真实拓扑而设的别名。RFC 9211 不要求它绑定证书、公钥或资产台账。

因此,抓包能直接证明的是:在某个时间、某个观察点到达的消息里含有这个成员。要把成员绑定到实际运行实例,还需依赖网络路径、配置版本、受控采集或密码学上下文。要证明成员描述的内部事件为真,还要用查找日志、对象指纹、上游请求或独立遥测互证。

当同一家供应商既负责交付字节,又负责写交付说明,还持有唯一内部日志时,这个边界尤其重要。供应商的自报通常非常有用,但商业争议、安全失陷或严重事故不能只剩同一控制面的单一叙事。

hit 只回答“是否转发了这次请求”

hit 是最容易画成绿灯的参数。它为 true 时说明此次请求由缓存满足,没有向上游转发,并从缓存得到响应。这个定义里没有“正确”“最新”“允许”“完整”或“用户满意”。

响应原本来自源站,缓存把它用于 304 或 206,也可能仍是 hit,只要此次处理没有向上游转发。缓存里有对象但因陈旧或不完整而必须上行,就不是 hit。反过来,陈旧对象若在规则允许下不转发而被直接使用,仍可报告 hit。

真正决定能否存储、选择、复用、校验或在异常时提供陈旧对象的是 RFC 9111 的缓存语义、Cache-Control、Expires、验证器、请求指令以及 stale-if-error 等扩展。Cache-Status 报告决定后的部分结果,不能替一项本来不合法的复用补上授权。

治理上必须维持四个问题:规范是否允许;运行配置是否正确实现;字段是否如实报告;应用与用户是否得到正确结果。命中率只触及其中一角。

fwd 把 MISS 拆成了不同故障面

请求向源站方向转发时,fwd 可以说明原因。

uri-miss 表示缓存中没有与请求 URI 匹配的响应;vary-miss 表示存在 URI 匹配,但依据请求头与已存 Vary 无法选出适用表示;miss 是无法进一步区分时的泛化说法。request 表示可选出新鲜响应,但请求语义禁止使用;stale 表示选中的响应已经陈旧;partial 表示已有部分响应不能覆盖所需范围;method 和 bypass 则分别暴露方法语义与配置绕过。

这些原因对应完全不同的动作。URI miss 激增可能来自路径改版;Vary miss 激增可能来自新增语言或编码维度;request 增多可能是客户端强制重校验;bypass 可能是策略正确生效,并非缓存失效。若把它们重新聚合为 MISS,仪表盘等于主动抹去标准提供的决策信息。

RFC 要求在实现能够区分时使用最具体的原因。采购与平台治理也应把“能够区分什么”当作能力要求,而不能只接受一个命中率数字。

每个可选参数都属于不同证据层

fwd-status 记录下一跳对转发请求返回的状态码。缓存可以因为陈旧而上行验证,下一跳回 304,最终再向用户交付 200。fwd=stale; fwd-status=304 能说明验证交换,却不能证明验证器对应的是正确业务版本。

ttl 是缓存自己计算的剩余新鲜期,可以为负数。它不同于 Age:Age 估算从源站生成或验证响应以来的时间,包含各缓存驻留和传输;TTL 则表示当前缓存认为还剩多少新鲜时间。调查需要同时看 Date、Age、控制指令与 TTL,任何一个都不应单独充当过期判决。

stored 表示转发所得响应被该缓存存储。它不承诺对象不会马上被淘汰,也不承诺下一次请求会选中它。collapsed 表示这次请求是否与其他上行请求合并。成功合并能避免源站被击穿,但也意味着一个上游结果会同时影响许多等待者,风险被集中放大。

key 可以透露缓存所用键的某种表示。这对排查查询规范化、Vary、语言和租户隔离极其重要,也可能向攻击者揭示键变换,为缓存投毒提供线索。detail 是实现本地信息;两个缓存即便都写 MEMORY,也不必有相同语义。若要跨产品互操作,应该注册扩展参数,而不是让值班人员猜。

多数参数是可选的。未出现表示“消息中没有这项断言”,不能自动转成 false。证据系统若把 unknown 压成 no,就会生产一套比缓存本身更肯定、也更错误的数据。

这是一条披露链,不是一份路径发现清单

理想路径可能有源站缓存、shield、edge、企业代理与浏览器五层,客户端看到的列表却未必有五项。浏览器可能不写,edge 可能只向授权调试请求披露,安全网关可能删除诊断头,旧版本也可能没有保留上游成员。

因此,客户端看到的是“成功抵达这个观察点的有序自报”,不是“路径上所有缓存的完整枚举”。Via 能补充转发接收者和协议版本,Proxy-Status 能说明更广的代理处理与错误,但它们各有自己的生成和隐私规则,彼此不会自动担保完整性。

组织必须测试真实路径,而不是看架构图。让一条可控请求穿过预期各层,在多个观察点抓取;分别制造 hit、URI miss、Vary miss、校验、允许的陈旧使用、bypass 与请求合并;核对成员顺序、保留、标识符和参数。每次更换网关、CDN 规则或缓存版本都要重复。文档中的链路只属于符号层,运行字段才属于执行层。

状态说明不能反过来授权缓存键

缓存键至少包含请求方法与目标 URI,Vary 可以加入请求头维度,其他机制还可调整选择规则。例如 No-Vary-Search 能让响应声明某些查询组件不影响缓存键匹配,这是关于 URI 等价关系的控制输入。

Cache-Status 位于决定之后。hit 表明该缓存选中了某个已存响应,key 可能展示它怎样表示所用键。两者都不能证明源站有权声明某种等价,也不能证明缓存实现无误,更不能证明两次请求对应用而言确实可互换。

如果组织只保留 Cache-Status 而不保留键规则、配置版本、安全指纹与租户测试,就可能得到一份“执行完全符合配置”的报告,而配置本身正在跨用户泄露内容。运行代码优先,并不意味着只看代码自报;它意味着观察从规则到实际交付的整条执行链。

自报可以被认证,但认证要单独设计

RFC 9211 没有为成员定义签名或 MAC。裸字段自身不防篡改。HTTP Message Signatures 可以覆盖指定 HTTP 字段,但应用必须定义完整配置:哪些组件必须签、允许哪些算法、如何找验证密钥、谁有签名权、时间怎样处理、验证失败如何处置。

配置必须明确把 Cache-Status 纳入覆盖范围,还要说明签名发生在多层路径的哪一点。后续缓存可能在早期签名之后追加成员。即便签名有效,它证明的也是特定签名者与被覆盖消息组件的关系,并不会进入缓存内部证明查找、存储或合并事件真的发生。

许多业务并不需要对响应字段做密码学签名。受认证的调试通道、可控采集、跨层 trace ID 与独立保存的内部事件可以提供足够保证。关键不是一律上签名,而是准确说明保证从何而来、由谁提供、最多支持什么结论。

透明度本身会形成攻击面

RFC 9211 的安全章节明确警告:攻击者可利用字段探测缓存行为和其他组件,并推断共享缓存用户的活动。知道某响应是否已存储,可以辅助针对敏感数据的时序攻击;暴露缓存键,会帮助理解键变换和缓存投毒。单纯把键做模糊处理不能消除底层风险。

标准给出三种选择:不发字段;只向有权限客户端发送;或只对有权限客户端发送敏感参数。也就是说,Cache-Status 不是“加一个头”的小配置,而是一项披露权限设计。

公共最小档可以只给稳定的匿名层级标识和 hit/具体转发原因。授权调试档可以增加 TTL、上游状态、存储、合并、安全键表示与版本化 detail。高敏系统也可以对外完全不发,而把丰富事件送到独立观察平台。

最危险的两端,一端是为了透明把缓存键和拓扑向所有人公开,另一端是为了安全删除一切外部证据,让客户只能接受提供商单方面复盘。合理设计应把差异化访问与独立留存结合起来。

一次尊重证据边界的调查

第一步保存原始响应,而不是抄下仪表盘的 HIT。记录完整有序字段、观察点、时间、方法、目标 URI 和影响选择的请求头,同时保存状态码、Date、Age、Cache-Control、Expires、ETag、Last-Modified、Vary、Via 与 Proxy-Status。

第二步绑定成员。将标识符映射到预期层、软件版本和配置纪元,确认该层是否应该向当前受众发字段。若需要键证据,优先保存带密钥摘要或授权后的脱敏表示,不要把可复用原始键丢进大群和工单。

第三步互证本地声明。hit 应对应内部查找与对象身份;fwd=stale; fwd-status=304 应对应条件请求;collapsed 应能找到合并组与等待者数量;stored 应有存储事件,但仍需承认后续淘汰可能发生。

最后回到应用与用户。把交付内容与该身份、语言、租户本应得到的源站版本比较;在清理或策略修复后从用户路径重新观察。字段从 HIT 变成 MISS 只说明缓存决策改变,不说明价格、权限或页面已经正确。

把共同词汇变成运行证据

成熟部署应能主动生成已知 hit 并证明没有上游请求,分别制造 URI 与 Vary miss,强制验证并区分上游 304 与最终状态,在获准场景使用陈旧对象并识别负 TTL,用并发 miss 测试合并与源站扇入。

还要测试披露:公共响应不得出现敏感 key 和 detail,授权调试只多出批准字段;edge、shield、反向代理与网关升级后成员顺序和上游值仍被保留;未知扩展必须原样保存而不是被随意翻译。若使用签名,篡改覆盖字段必须验证失败,并能证明 Cache-Status 确实在签名范围。

IANA Cache-Status 注册表 负责协调参数名与类型,HTTP 字段名注册表 把它记录为永久 List 字段。这些登记证明公共词汇存在,不证明任何产品已经实现,更不证明一份具体响应说了真话。后者只能从运行系统与独立观察中得到。

证据记录