推理缓存的租户隔离:从 Cloudflare 事故复盘看跨节点交接如何失效
推理缓存的租户隔离:从 Cloudflare 事故复盘看跨节点交接如何失效
一个推理请求超过可用资源时,最容易预期的结果是收到明确错误。Cloudflare 在 2026 年 10 月 26 日公开的事故复盘说明,在预填充和解码分别运行的系统里,拒绝本身也需要跨节点传递。如果前一个节点发现请求无法完成,却留下一个能进入下一阶段的中间状态,后一个节点就可能将未写好的缓存当作当前输入。问题的严重性来自对数据归属的错误判断,而不只是某个请求超长。
复盘披露,受影响的是 Workers AI 的 GLM-5.3 部署。该部署对外配置的上下文窗口超过预填充节点的 KV 缓存池实际容量。超长请求被引擎内部标为拒绝,预填充仍以占位输入进行部分计算并发出传输完成信号,解码则可能继续读取此前请求留下的状态。Cloudflare 报告有 155 个响应受到影响,并确认该事件未影响平台上其他模型。模型在 9 月 9 日经配置调整和引擎修补后恢复。公司称调查没有发现蓄意利用的证据。这个结论受其已审查记录与可见性的约束。
本文用公开事实定位失效机制,随后讨论如何为多租户推理服务设计可检查的资源承诺、跨节点状态以及故障演练。假设情景均为说明机制而设置。
KV 页不是普通的空闲显存
生成模型在处理输入时,会产生注意力所需的键和值。把这些中间结果保留下来,能让生成下一段输出时复用先前工作。在预填充与解码拆分的架构里,前者计算输入对应的缓存,后者拿到相应状态后生成输出。拆分可以让不同阶段按各自资源需求扩展,同时增加了缓存转移、分配和回收的协调边界。
虽然缓存页只是中间表示,服务仍应将它视为由具体请求和访问上下文拥有的数据。模型读取了另一请求留下的页,不能指望向量形式天然保护输入。模型原本就被设计成利用这些内部状态继续预测,当它把外来页当成当前上下文的一部分,就可能产出此前请求的相关信息。可访问性应由系统边界定义,不能由数据看起来是否像原始文本来决定。
这里需要分别核对两种共享。显式设计的公共前缀复用,需要有明确的复用条件和权限范围。物理页回到空闲池再分配,是资源复用,获得地址的请求并没有自动获得前任数据的读取权。若工程实现对两者使用同一个缓存概念,容易忽略从逻辑身份到物理内存的转换。复用计划必须回答,何时一页可以再次分配,何时分配后的内容已经有效,以及任何读写引用如何证明自己仍持有本次分配。
假设两个请求先后占用同一物理区域。第一个请求结束后,该区域被标为空闲。第二个请求获取它,但传输只覆盖了其中一部分。若解码根据预留容量读取整个区域,剩余部分就可能来自第一个请求。假设例子强调,已分配、已初始化、已完整交接与可供读取是四种不同的状态。它们在无故障路径上通常连续发生,异常时却不能互相替代。
资源承诺必须随部署测量
这次事故的起点是配置承诺和实际容量不一致。对外的上下文长度是产品约束,实际 KV 池能容纳的长度由模型、节点角色、权重和内存分配等条件共同决定。某个在测试机器上成立的数值,并不能自动适用于每个生产角色。工程化的部署门槛应该从实际运行的引擎获取容量,在模型、量化、并行方式或显存比例改变后重算。
即使只维护一套模型,也不宜把节点类别当作完全同质。一批节点可能使用不同硬件或分配方式,扩缩容可能在升级期间并存旧配置和新配置,预填充与解码可能有不同的约束。把集群最大值贴到入口,只能描述能力最强的某些节点。若路由没有依据实际容量选择节点,服务承诺必须覆盖它可能到达的所有相关路径。另一种设计是显式分层和调度,但这也要求验证路由信息与节点实际状态一致。
边界检查应覆盖刚好满足、刚好超过和显著超过上限的情形。测试不能只依赖响应是成功还是失败。假设一个系统接受了超出资源的请求,随后返回一段语言通顺的输出。这个现象也可能由输入截断、错误重试、缓存误用或计费字段来自入口记录造成。好的测试应检查实际计算范围、错误码、跨阶段状态以及结果中的标记。测试越接近资源边界,越需要独立证明完成了预期工作。
入口拒绝可以减少无效负载,但引擎仍需保护自身。可能有内部调用绕过网关,路由配置也可能漂移。深层校验让一次入口错误只产生拒绝或降级,而不会继续扩大到其他租户的数据。两层校验并不要求复制所有决策。入口可以基于服务契约预先筛选,引擎则检查实际持有的资源和当前操作是否安全。
交接协议要传递结果,而不仅是进度
复盘中,预填充阶段已经产生拒绝状态,但交接链路判断的是请求是否完成,拒绝状态没有被当成阻止继续执行的条件。解码看到完成信号后按最初的预留范围读取,信号的含义超过了前一阶段实际做到的事情。这里的问题可以表述为协议的成功条件不足。某个发送动作结束,并不表示接收者期待的全部数据已经准备好。
一个跨节点交接至少要让接收方知道数据属于谁、覆盖哪一段、当前请求处于何种状态。对防御性设计而言,成功必须由正面证据成立。比如预填充确认了实际写入范围,解码再按自己预期的范围和请求身份检查。缺少必要证据的状态可以等待补齐、报错或进入恢复,不能默认为已经就绪。尤其应避免只用一个容易被不同模块解释为不同含义的布尔值。
假设传输层只返回成功写入了一页,业务层把它变换为缓存就绪。对于恰好需要一页的请求,这可能没有问题。对于需要多页的请求,转换就丢失了关键范围。如果程序只覆盖常见短输入,两个接口会在多数情况下协同正常。这种错误最有效的测试方式是主动制造中间结果与预期不一致,并要求接收端拒绝,而不是仅看发送端有没有抛异常。
终止状态也要有统一的后果。拒绝、取消、超时和正常完成都意味着调用方可能不再等待,但它们对资源与通知的要求不同。取消发生时可能还有写入在途,拒绝发生时可能只分配了资源而没有有效内容,正常完成后仍可能有读取方持有引用。将它们挤进同一字段,通常会让调度、清理和返回码各自推断。协议应明确哪些状态允许交接,哪些必须通知对端,哪些状态需要等待排空后再复用。
所有权问题不应只在异常日志里出现
内存地址告诉程序到哪里读写,却不能证明它可以为某个请求读取那些字节。一个页号若可被反复分配,就可能在旧操作恢复时指向另一个请求的数据。用身份与分配世代标记表示所有权,可以让读取前的检查判断当前页仍属于预期分配。Cloudflare 在复盘中介绍了类似的实验方向,也说明其试验版由于可靠性等问题曾停用,覆盖范围还需要扩展。这个细节提醒读者,设计概念可行与生产全覆盖之间存在额外工程工作。
校验和与所有权检查有不同职责。若传输前后计算的校验和一致,接收者知道所比较的数据大概率没有在传输中变样,但仍需知道发送的是本次请求应交付的范围。如果只为一页生成正确校验和,无法证明八页都属于当前请求。相反,一个指向正确世代的页也可能因为内容不完整而不宜读取。较强的设计可以将请求身份、页索引、长度、世代和完整性一起验证,把不同错误分成可观测的失败类型。
要考虑检查的成本。每次解码都验证全部历史数据,可能带来过多内存访问。适当做法要依据后端的访问模式,判断是否能在页表映射、批次边界或复用时验证,并测量实际吞吐与尾延迟。降低验证开销的技术不能减少对哪些状态安全的解释能力。如果为了少一次验证而把风险交给人工检查日志,就应该明确只有什么条件下可接受,以及未来扩大使用范围时要补哪些证据。
清零能减少旧数据被误读的后果,也有成本。如果清零发生时仍有旧写在途,清完之后还可能被覆盖。若回收策略只在部分路径清零,测试还要能证明其他路径不会暴露数据。资源初始化、页归属和传输终止需要共同形成边界。每项措施针对一种失败路径,单一措施通过不等于其他路径已经得到保护。
取消不是让远端写入立即消失
复盘还讨论了与此次直接原因不同的跨节点竞态。传输开始后,已经发出的远端写入可能继续到达。解码一侧若在取消时立刻把缓存归还给内存池,新请求就可能拿到那些页,而旧写入随后落到新的分配上。业务层看到取消成功,不能由此推断硬件与传输层已经停止所有副作用。
假设一个长请求在客户端超时后释放连接。协调服务为了快速回收资源,将整个请求标记为已结束并通知资源池。与此同时,另一节点正在发送最后一批状态。系统可以让页暂留,直到对端确认传输已经停止,或者在有明确上限的条件下等待可能的写入排空。后一种设计需要证明超时上限覆盖实际传输批次。若没有边界,等待某个固定时长只是把问题发生的概率降低,并未建立可说明的安全条件。
延迟回收也有反面。大量取消若让页迟迟不能回收,剩余容量会下降,正常请求可能排队或被拒绝。可以用待回收页数、等待时间分布、因确认与超时分别释放的比例来判断是否出现异常。还可以让上游处理客户端断开时避免触发不必要的中途终止。无论选择哪种方式,关键是把安全责任与容量成本放在同一张运维视图中。
测试时尤其要覆盖多个并发事件的顺序变化。只测试先完成再取消,容易遗漏反向次序。可以在验证环境中让请求跨节点传输时断开客户端,等待另一个隔离身份获得资源,确认旧写入无法到达新请求所读区域。这样的测试不需要引入真实客户内容。测试输入可采用合成标记,并且只在预先限定的数据域内操作。
怎样验证受影响范围,而不扩大对数据的接触
事故核查需要区分已经复现的机制、观察到的受影响响应和对未观测请求的推断。公开复盘将范围限定在一个模型部署和一段时间内,并说明了特定超长阈值。这样的边界有助于把排查集中在可能经过问题路径的请求,同时仍应保留证据无法排除的情况。既有记录能证明的通常是记录所覆盖的请求,不能仅凭没有报案推断所有错误都没有发生。
从设计阶段开始记录与隔离有关的技术状态,可以降低事故期间读取业务内容的需要。比如记录请求是否被某阶段拒绝、预期与实际分配范围、传输完成原因、解码准入结果和页回收原因。少量结构化状态能重建失效链条。直接收集每个用户的原始提示词,会使排查本身成为额外的隐私负担。诊断需要足够信息,但应为每项数据明确用途与保留边界。
对于假设的验证流程,可以先通过运行配置和请求元数据识别可能进入异常路径的候选,再用经过授权的方式确认影响。如果一个记录出现预填充拒绝而解码成功,就应被优先复核。若请求的分配范围大于传输确认范围,也值得调查,即便响应看起来正常。另一方面,日志缺字段时,无法从缺少某个标志推断状态安全。排查报告可以将已证实、已排除和证据不足分开,让读者理解实际验证范围。
错误响应的观测也不应只以全局比例为准。超长请求在整体流量中占比可能很低,因此即使这类请求的异常集中发生,平均成功率也不容易变化。为模型、长度段、节点角色和部署版本建立有控制的分组,会比只看总量更容易定位。分组时要避免让标识粒度过细造成新的数据暴露,尤其不应将单个用户与可公开下载的诊断指标绑定。
可落地的验证顺序
采用预填充和解码拆分前,可以先列出流程中的所有权不变量。只有在持有当前请求的有效映射、范围已被初始化、对端没有拒绝且交接已经确认时,才允许解码读取。回收则要等到读写持有者都释放,或经设计证明迟到操作无法落到当前分配上。随后按照这些规则写出每条异常路径的预期结果,再决定用什么库和部署参数实现。
上线验收可以分三个层次。先检查单个节点每次启动报告的实际容量是否支持对外承诺,再验证两端协议面对超长、拒绝、取消和部分传输时的行为。最后执行多租户并发试验,在不同请求中加入可区分的合成标记,观察没有任何请求读取了另一请求的状态。测试用的合成标记只服务于判别,不承担证明所有实际负载均无问题的责任。
反对意见是,每次变更都做完整矩阵可能太慢。可以按风险分层。普通文档变更无需走推理隔离演练,影响显存池、请求路由、传输协议或页生命周期的变更则值得触发对应检查。重要的是保留一组稳定、能复现的边界测试,避免依赖一次手工压测来判断后续版本仍然正确。投入更严格检查的理由,是这类配置变化即便保持接口不变,也会改变请求能否安全通过。
最后,操作团队还可以事先定义何时关闭受影响路径、何时允许恢复。若观察到读到了无权访问的页,失败应限制在当前请求并保留可定位的证据。恢复时既要确认部署配置与补丁已生效,也要重放造成问题的边界条件。修正容量能避免进入特定路径,引擎修补则让未来某次误配置更可能安全失败。把两种结果分别确认,才知道恢复建立在什么保证上。
另一个适合在部署评审中提前讨论的条件是混合版本。预填充节点可能比解码节点先完成升级,两个节点对拒绝信号或确认范围的理解并不一定相同。假设新发送方新增了字段,旧接收方忽略该字段后仍然按预留范围读取,那么单独验证新节点就会得到错误的安全结论。更稳妥的做法是先列出会同时存在的版本组合,验证缺少必需字段时会显式失败,再扩大可互通的范围。如果设计依赖所有节点同时切换,也应让入口明确拒绝不兼容的组合,并验证回滚不会使旧协议重新接收它无法理解的数据。
一次有判别力的演练,可以把容量、交接与回收分别做成可证伪的假设。第一轮保持协议正确,仅降低一个节点的容量,预期请求在资源不足时明确失败。第二轮让容量充足,但人为让接收端只看到部分交接信息,预期解码拒绝读取。第三轮模拟取消与资源复用交错,确认同一物理页进入新的分配世代后,旧操作不能再访问它。分轮演练的价值是,即使一层校验已经阻挡最常见的故障,仍能单独证实后面的隔离边界。演练环境应事先约束租户、请求范围和数据保留期限,并在结束时清理合成标记。
还可以区分正常负载和故障负载的成本。节点正常时,增加检查可能只影响一点请求延迟。节点异常时,拒绝数量会上升,等待排空的页面占用会增加,重试也可能把压力转移给其他节点。如果只看平稳时期的吞吐,就会错过错误恢复路径的峰值。运营者可以在受控环境记录这些变化,再判断扩缩容阈值和限流策略是否能保护隔离检查持续生效。
这起事件的工程意义在于,内存隔离必须贯穿整个请求生命周期。容量校验决定请求能否开始,交接验证决定下一阶段是否能够读取,所有权与终止协议决定页面何时能再次使用。只要一个环节把缺失信息当成成功,就可能在正常输出的外观下交付错误数据。将这些边界写成可观察、可重放的条件,才能让错误在越过租户边界之前停止。
公开原文 https://blog.cloudflare.com/kv-cache-exposure-remediation/