一个 panic 是怎么把整个服务搞坏的——Cloudflare 修复 Rust Workers 可靠性的完整过程
Rust 以内存安全著称,但在 WebAssembly 的运行环境里,它有一个长期被低估的可靠性问题:一次 panic 或者 abort,可以把整个 Worker 实例彻底毒化,让后续所有请求都跟着失败。 这个问题在 Cloudflare 的 Rust Workers 上存在了相当长的时间。今年四月,他们系统性地解决了它,并把相关改动贡献回了 wasm-bindgen 上游项目。 这篇文章完整梳理他们是怎么一步步做到的。 原文链接:https://blog.cloudflare.com/making-rust-workers-reliable/ Rust Workers 的运行方式,是把 Rust 代码编译为 WebAssembly(Wasm),然后在 Cloudflare 的 Workers 运行时(基于 V8 引擎)里执行。 在原生 Rust 环境里,panic 分两种行为: Rust 编译到 问题在于:Wasm 实例是有状态的。一次 panic 让 Wasm 的执行中途中断,实例内部的状态可能处于不一致的中间状态。如果这个实例还在继续处理其他并发请求,那些请求读到的就是已经被污染的内存状态,产生难以预测的错误。 更严重的是:这种失效状态有时不会被立刻清理,而是持续影响后续进入的新请求,直到实例被回收。一个请求的失败,可以扩散成一片失败。 面对这个问题,Cloudflare 最初的方案是在 JavaScript 和 Rust 之间的调用边界做拦截。 具体做法是: 这套方案解决了"Worker 被彻底搞死"的问题,并从 workers-rs 0.6 版本开始默认对所有用户启用。 但它有一个明显的局限:重新初始化意味着丢弃整个 Wasm 实例的内存状态。 对于无状态的请求处理器来说,这没什么问题,反正每次请求之间也没有共享状态。但对于 Durable Objects——Cloudflare 的强一致性有状态存储原语——来说,实例内存里维护着跨请求共享的业务状态。一个请求触发了 panic,重新初始化会把其他并发请求正在使用的状态一起清掉,造成数据丢失。 治标不治本。根本的问题需要在 panic 机制本身上解决。 原生 Rust 里, Wasm 里历史上没有等价的机制,直到 WebAssembly Exception Handling 提案在 2023 年获得主流引擎的广泛支持。这个提案在 Wasm 字节码层面引入了 基于这个提案,Cloudflare 为 wasm-bindgen 实现了完整的 用 编译后的 Wasm 字节码大致为: 即使 要让整个工具链支持这套机制,需要改动多个环节: 闭包的处理还需要额外的细心。很多闭包会捕获引用,而引用在 unwind 之后可能仍然存活,这违反了 unwind 安全性(UnwindSafe)的要求。为此新增了 最终效果: 即使有了 abort 恢复的目标不是恢复状态,而是:确保 abort 之后,失败状态不会污染后续请求。 引入 Cloudflare 的解决思路是给 unwind 标记上特定的 Exception Tag(WebAssembly 异常处理提案提供的机制),让 abort 产生的错误没有这个标记,从而可以在 JavaScript 侧精确区分两种来源。 基于这套区分机制,wasm-bindgen 新增了 同时还加入了 abort 重入保护:防止在 abort 处理过程中因为深度交错的 Wasm-JS 调用栈导致 abort 处理逻辑本身被重复触发。 这套机制不只对 Rust Workers 有用。在 JavaScript Workers 里,开发者有时会直接依赖用 Rust 编写、编译为 Wasm 的 npm 包(比如 为此,wasm-bindgen 新增了实验性的 区别在于:错误发生后应用变得"出错了",但不会变成"彻底坏掉了"。 WebAssembly Exception Handling 提案经历过一次规范晚期修改,分裂成两个版本: 问题在于 Node.js 24 LTS 的发布节奏:如果不做干预,整个生态在 2028 年 4 月之前都无法切换到现代版本,因为 LTS 用户会一直停留在 Node.js 24 上。 Cloudflare 发现这个问题后,主动将现代 Exception Handling 反向移植到了 Node.js 24 和 Node.js 22 的发行版本,消除了这个长达三年的阻塞。 从 workers-rs 0.8.0 开始,构建命令支持 启用后: 仍然使用 这篇博客让人印象深刻的,不只是技术本身,而是解决问题的路径:发现问题,修平台,顺手把上游生态一起推进。 Walrus 加了对新 Wasm 指令的支持,wasm-bindgen 有了 panic/abort 恢复能力,Node.js 24/22 的 LTS 用户不用等到 2028 年才能用到现代异常处理——这些改动都不是 Cloudflare 的"私有修复",而是贡献回了对应的开源项目。 最终受益的不只是 Cloudflare Workers 的用户,而是整个 Rust + WebAssembly 的开发者社区。这是基础设施公司参与开源生态的一种比较理想的姿态。
wasm-bindgen 仓库:https://github.com/wasm-bindgen/wasm-bindgen
workers-rs 仓库:https://github.com/cloudflare/workers-rs先理解问题的根源
catch_unwind 捕获并恢复wasm32-unknown-unknown 目标时,默认是 panic=abort。这意味着一旦 Wasm 内部发生 panic,它会触发一条 unreachable 指令,抛出 WebAssembly.RuntimeError 异常,然后执行路径直接跳出 Wasm 回到 JavaScript。第一阶段:用 JavaScript 包装打补丁(workers-rs 0.6)
第二阶段:让 Wasm 真正支持栈回溯(panic=unwind)
panic=unwind 允许 panic 像异常一样向上传播,沿途执行 Drop,并允许调用方用 catch_unwind 捕获。这样可以在不丢失整体状态的前提下,隔离单次失败的影响范围。try/catch/throw 指令,让 Wasm 模块可以像宿主语言一样处理异常。panic=unwind 支持。编译层的变化
RUSTFLAGS='-Cpanic=unwind' cargo build -Zbuild-std 编译时,含有 Drop 的代码会生成对应的 Wasm 异常处理指令。例如以下 Rust 代码:fn some_func() {
let a = HasDropA;
let b = HasDropB;
imported_func(); // 可能 panic
}try
call <imported_func>
catch_all
call <drop_b>
call <drop_a>
rethrow
end
call <drop_b>
call <drop_a>imported_func 内部 panic,析构函数也能按正确顺序执行,Wasm 实例的内存状态不会停留在中间态。wasm-bindgen 工具链的改造
try/catch 指令,需要新增支持PanicError 的形式抛出给 JavaScript;对于 async 导出,panic 会以 PanicError 拒绝(reject)对应的 Promiseextern "C" 会在 unwind 穿越时触发 abort,导出函数需要改为 extern "C-unwind" 才能允许 panic 向上传播MaybeUnwindSafe trait,并提供了 Closure::new_aborting 变体——在 unwind 不安全的场景下主动触发 abort,而不是让编译器强迫用户加 AssertUnwindSafe。第三阶段:abort 恢复——兜住最坏情况
panic=unwind,abort 仍然存在。内存耗尽(OOM)是最常见的触发原因,abort 无法 unwind,没有状态恢复的可能。区分可恢复错误与不可恢复错误
panic=unwind 之后,出现了一个新问题:从 Wasm 抛出的错误,可能来自 extern "C-unwind" 的 unwind,也可能来自真正的 abort,从外部看不出区别。set_on_abort:平台级的恢复钩子
set_on_abort 钩子,允许在初始化时注册一个自定义的 abort 处理函数。Worker 实例可以在 abort 发生时执行必要的清理,然后将实例标记为不可再用,确保后续请求不会进入一个状态未知的实例。延伸:wasm-bindgen 库的自动重初始化
import { func } from 'wasm-dep')。如果这类库内部发生 abort,调用方的 JS 代码会收到一个错误,但那个 Wasm 实例可能已经处于无效状态,后续调用会持续失败。--reset-state-function 功能。它暴露一个函数,允许 Rust 应用声明"我需要重置到初始状态",而不需要调用方重新 import 或重建绑定对象。旧实例上的类实例会失效(handles 变成孤立状态),但新的类可以被构建出来,整个 JS 应用得以继续运行。推动整个生态跟上:Node.js 的贡献
运行时 支持版本 发布时间 Chrome 138 2025.06.28 Firefox 131 2024.10.01 Safari 18.4 2025.03.31 Node.js 25.0.0 2025.10.15 workerd(Workers 运行时) v1.20250620.0 2025.06.19 现在可以怎么用
--panic-unwind 标志:# 在构建时加上这个标志
workers-rs build --panic-unwindpanic=unwind 作为默认行为panic=abort 的用户,依然享有 0.6.0 引入的自定义恢复包装器,但无法做到无损恢复。这件事背后的工程文化