订阅以接收新文章的通知:

提高 Rust Workers 可靠性:wasm-bindgen 中的 panic 错误与中止恢复机制

2026-04-22

阅读时间:7 分钟
本文同时提供 English日本語한국어繁體中文 版本。

Rust Workers 是 Cloudflare Workers 平台上运行的一个工具,它将 Rust 代码编译为 WebAssembly 格式,但我们发现 WebAssembly 存在一些缺陷。当出现 panic 错误或意外中止时,运行时可能处于未定义状态。对于 Rust Workers 用户而言,panic 往往会产生致命影响:不仅污染实例,甚至可能导致 Worker 在一段时间内无法响应。

虽然我们能够检测并缓解这些问题,但 Rust Worker 仍然有可能意外失败,并导致其他请求也随之失败。Worker 中未处理的 Rust 中止会影响单个请求,可能升级为影响同级请求的更大故障,甚至持续影响新的传入请求。问题的根源在于 wasm-bindgen,这是生成 Rust worker 所依赖的 Rust-to-JavaScript 绑定的核心项目,而 wasm-bindgen 缺乏内置的恢复机制。

在这篇文章中,我们将分享最新版 Rust Workers 如何处理全面的 Wasm 错误恢复,以解决这种由中止引起的沙箱污染问题。作为我们去年在 wasm-bindgen 组织内部合作的一部分,我们已将这项工作贡献融入 wasm-bindgen。首先,我们添加了 panic=unwind 支持,确保单个失败的请求不会影响其他请求;其次,我们添加了中止恢复机制,保证 Wasm 中的 Rust 代码在中止后绝不会再次执行。

初始恢复缓解措施

我们最开始尝试解决这方面的可靠性问题时,侧重于理解和控制生产环境中的 Rust Worker 因 Rust panic 和中止引起的故障。我们引入了自定义 Rust panic 处理程序来跟踪 Worker 中的故障状态,并在处理后续请求之前触发了完整的应用重新初始化。在 JavaScript 端,这需要使用基于代理的间接寻址来封装 Rust-JavaScript 调用边界,以确保以一致的方式封装所有入口点。我们还对生成的绑定进行了针对性修改,以便在故障发生后正确地重新初始化 WebAssembly 模块。

虽然这种方法依赖于自定义 JavaScript 逻辑,但它证明了可靠的恢复是可以实现的,并且排除了我们在实践中遇到的持续性故障模式。从 0.6 版本开始,此解决方案已默认提供给所有 workers-rs 用户,并为下文所述的更普遍的、上游中止恢复机制奠定了基础。

使用 WebAssembly Exception Handling,实施 panic=unwind

上文描述的中止恢复机制可确保 Worker 能够在出现故障时继续运行,但这些机制是通过重新初始化整个应用来实现这个目标。对于无状态请求处理程序来说,这没有问题。但对于在内存中保存有意义状态的工作负载(例如 Durable Objects)来说,重新初始化意味着完全丢失该状态。一个请求中的单个 panic 可能会清除其他并发请求正在使用的内存状态。

在大多数原生 Rust 环境中,可以进行 panic unwind 处理,从而允许析构函数运行,程序在不丢失状态的情况下恢复。在 WebAssembly 中,情况历来截然不同。通过 wasm32-unknown-unknown 编译成 Wasm 的 Rust 默认使用 panic=abort,因此,Rust Worker 内部的 panic 会突然生成 unreachable 指令,导致 Wasm 退出执行并抛出 WebAssembly.RuntimeError 错误给 JS。

为了从 panic 中恢复且不丢弃实例状态,我们需要 wasm-bindgen 中对 wasm32-unknown-unknownpanic=unwind 支持。WebAssembly Exception Handling 提案使这成为可能,该提案在 2023 年获得了广泛的引擎支持。

我们首先使用 RUSTFLAGS='-Cpanic=unwind' cargo build -Zbuild-std 进行编译,这重新构建支持 unwind 的标准库,并生成具备适当 panic unwind 处理策略的代码。例如:

struct HasDropA;
struct HasDropB;
extern "C" {
    fn imported_func();
}

fn some_func() {
    let a = HasDropA;
    let b = HasDropB;
    imported_func();
}

编译为 WebAssembly 格式的代码如下:

try
  call <imported_func>
catch_all
  call <drop_b>
  call <drop_a>
  rethrow
end
call <drop_b>
call <drop_a>

这可确保即使 imported_func() panic 错误,析构函数仍然会运行。类似地,std::panic::catch_unwind(|| some_func()) 编译后的格式为:

try
  call <some_func>
  ;; set result to Ok(return value)
catch
  try
    call <std::panicking::catch_unwind::cleanup>
    ;; set result to Err(panic payload)
  catch_all
    call <core::panicking::cannot_unwind>
    unreachable
  end
end

要使这种编译方式能够端到端正常发挥作用,我们对 wasm-bindgen 工具链进行了一些更改。WebAssembly 解析器 Walrus 无法处理 try/catch 指令,因此,我们添加了对它们的支持。描述符解释器还需要学会如何评估包含异常处理块的代码。就在这时,可以使用 panic=unwind 构建完整的应用。

最后一步是修改 wasm-bindgen 生成的导出,以便在 Rust-JavaScript 边界处捕获 panic,并将其显示为 JavaScript PanicError 异常。需要注意的一点是:Rust 会捕获外部异常,并在通过 extern "C" 函数进行 unwind 时终止,因此,需要将导出标记为 extern "C-unwind",以明确支持跨边界进行 unwind 处理。如果使用 futures 库,panic 会拒绝 JavaScript Promise,并抛出 PanicError

闭包问题需要特别注意,确保通过新的 MaybeUnwindSafe trait 来正确检查 unwind 安全性,该 trait 仅在使用 panic=unwind 进行构建时才会检查 UnwindSafe。但这很快暴露了一个问题:许多闭包捕获了 unwind 处理后仍然存在的引用,这使得它们本质上不安全。为避免出现用户错误地将闭包包装在 AssertUnwindSafe 中只为满足编译器要求这种情况,我们添加了 Closure::new_aborting 变体,在无法保证 unwind 安全性的情况下,这些变体会在发生 panic 时终止程序,而不是进行 unwind 处理。

启用 panic unwind 时:

  • wasm-bindgen 会捕获已导出 Rust 函数中的 panic

  • panic 会作为 PanicError 异常抛给 JavaScript

  • 异步导出会拒绝其返回的 Promise,并抛出 PanicError

  • Rust 析构函数正常运行

  • WebAssembly 实例仍然有效且可重用

有关这种方法的详细信息以及在 wasm-bindgen 中的使用方式,请参阅 Wasm Bindgen:捕获 panic 最新指南页面。

中止恢复

即便启用 panic=unwind 支持,也仍然会出现中止,而内存溢出错误是常见原因之一。由于无法对中止进行 unwind 处理,因此完全无法恢复状态,但我们至少可以检测中止并从中恢复,以执行后续操作,避免无效状态导致后续请求出错。

Panic unwind 支持为中止恢复引入了新问题。当我们收到源自 Wasm 的错误时,我们无法确定它是源自 extern “C-unwind”的错误,还是真正的中止。WebAssembly 中的中止可能以多种形式出现。

有两种技术方案来解决这个问题:标记所有明确的中止错误,或者标记所有明确的 unwind 错误。两种方案都可行,但我们选择了后者。由于我们的外部异常处理已直接使用原始的 WAT 级 Exception Handling (WebAssembly 文本格式)指令,因此,我们发现可以更轻松地为外部异常添加异常标记,将它们与中止 non-unwind-safe 异常区分开来。

借助 WebAssembly Exception Handling 中的 Exception.Tag 特性,我们能够清楚地区分可恢复错误与不可恢复错误,然后集成新的中止处理程序以及中止重入防护。 新的中止 hook set_on_abort 可用于在初始化时附加处理程序,该处理程序会根据平台嵌入的需求进行相应的恢复。

强化 panic 和中止处理是避免无效执行状态的关键。WebAssembly 支持调用栈深度交错,也就是说,Wasm 可以调用 JavaScript,JavaScript 可以重新进入 Wasm,无论嵌套调用有多深;除此之外,多个任务可以在同一个 WebAssembly 实例中运行。之前,某个任务或嵌套栈中发生的中止并不一定能通过 JS 导致更高层级的栈失效,从而引发未定义的行为。我们需要谨慎地确保执行模型的可靠性,并且这方面的工作仍在持续进行。

虽然中止并非理想情况,故障后重新初始化更是极端情况,但将关键错误恢复作为最后一道安全防线可确保执行正确无误,以及后续操作能够成功。无效状态不会持续存在,从而确保单个故障不会引发多个故障。

扩展:wasm-bindgen 库的中止后重新初始化

在开发过程中,我们意识到这是使用 wasm-bindgen 构建 JS 库的常见问题,以及添加一个中止处理程序进行恢复,也会让这些库从中受益。

但是,当以 ES 模块的形式构建 Wasm 并直接导入(例如,使用 import { func } from ‘wasm-dep’)时,如果用户 JS 应用中已链接并初始化的库在调用 func() 函数时发生 Wasm 中止,尚不清楚其恢复机制是什么。

虽然这并非严格意义上的 Rust Workers 用例,但我们团队也支持基于 JS 的 Workers 用户,此类用户运行 Rust 支持的 Wasm 库依赖项。如果我们能够同时解决这个问题,可能会间接推动 Cloudflare Workers 平台上的 Wasm 使用。

为了支持 Wasm 库用例的自动化中止恢复,我们在 wasm - bindgen 中添加了试验性重新初始化机制 --reset-state-function 支持。该机制提供一个函数,让 Rust 应用能够有效地请求将其内部 Wasm 实例重置回初始状态以备下一次调用,而无需生成的绑定的用户重新导入或重新创建实例。旧实例中的类实例会抛出异常,因为其句柄已变为孤立类,但此后可以构造新的类。使用 Wasm 库的 JS 应用会出现错误,但不是完全无响应。

有关此项功能的完整技术详情以及在 wasm-bindgen 中的使用方式,请参阅新的 wasm-bindgen 指南中的 Wasm Bindgen:处理中止部分。

完善 Rust Wasm Exception Handling 生态系统

对这项工作的上游贡献并不仅限于 wasm-bindgen 项目。使用 panic=unwind 进行 Wasm 构建仍然需要采用试验性 Nightly Rust 目标,因此,我们也一直在努力推进 Rust Wasm 对 WebAssembly Exception Handling 的支持,以便将其引入稳定的 Rust 版本。

在开发 WebAssembly Exception Handling 功能的过程中,后期规范变更导致了两种变体:传统异常处理以及最终的现代异常处理(使用 exnref)。目前,Rust 的 WebAssembly 目标仍然会默认生成传统异常处理的代码。虽然传统异常处理仍然得到广泛支持,但它如今已被弃用。

以下 JS 平台版本开始支持现代 WebAssembly Exception Handling:

运行时

版本

发布日期

v8

13.8.1

2025 年 4 月 28 日

workerd

v1.20250620.0

2025 年 6 月 19 日

Chrome

138

2025 年 6 月 28 日

Firefox

131

2024 年 10 月 1 日

Safari

18.4

2025 年 3 月 31 日

Node.js

25.0.0

2025 年 10 月 15 日

在调查支持矩阵的过程中,我们发现最大的问题是 Node.js 24 LTS 的发布计划,这将导致整个生态系统只能继续使用旧版 WebAssembly Exception Handling 直至 2028 年 4 月。

发现这一差异后,我们成功地将现代异常处理机制移植到 Node.js 24 版本,甚至还移植了必要的修复程序,使其能够在 Node.js 22 系列版本上运行,以确保支持这个目标。如此一来,现代异常处理提案应该在明年会成为默认目标。

在未来几个月,我们将努力让最终用户顺畅地过渡到稳定的 panic=unwind 支持和现代异常处理机制。

虽然对完善生态系统的这些长期投入需要时间才能见效,但它们有助于为整个 Rust WebAssembly 社区奠定更坚实的基础,Cloudflare 很高兴能够为这些改进贡献一份力量。

在 Rust Workers 中使用 panic unwind

从 Rust Workers 0.8.0 版本开始,我们新增了一个 --panic-unwind 标志,用户可以按照此处的说明将其添加到 build 命令中。

使用该标志,可以完全恢复 panic 错误,中止恢复机制将使用新的中止分类和恢复 hook 机制。我们强烈建议用户升级并试用新版本,获得更稳定的 Rust Workers 体验;另外,我们还计划在后续版本中将 panic=unwind 设置为默认值。继续使用 panic=abort 方法的用户,将继续受益于 0.6.0 版本中之前的自定义恢复封装器处理功能。

确保 Rust Workers 的稳定性

这项工作是我们持续努力的一部分,旨在推出稳定版 Rust Workers。Cloudflare 通过从根本上解决 Wasm 平台基础架构中的这些棘手问题,并在适当的时候回馈生态系统,我们不仅为自己的平台,也为整个 Rust、JS 和 Wasm 生态系统构建了更坚实的基础。

我们计划对 Rust Workers 进行一系列改进,并很快分享这项额外工作的最新进展,包括 wasm-bindgen 泛型和自动化 bindgen。上个月,我们团队的 Guy Bedford 在 Wasm.io 大会上关于 Rust 与 JS 互操作性的一场演讲中预告了这方面的信息。

请关注我们在 Cloudflare Discord#rust‑on‑workers 频道。我们也欢迎用户提供反馈并展开讨论,尤其是所有新加入 workers-rswasm-bindgen GitHub 项目的贡献者。

我们保护整个企业网络,帮助客户高效构建互联网规模的应用程序,加速任何网站或互联网应用程序抵御 DDoS 攻击,防止黑客入侵,并能协助您实现 Zero Trust 的过程

从任何设备访问 1.1.1.1,以开始使用我们的免费应用程序,帮助您更快、更安全地访问互联网。要进一步了解我们帮助构建更美好互联网的使命,请从这里开始。如果您正在寻找新的职业方向,请查看我们的空缺职位
Cloudflare WorkersRustRust WorkersWebAssemblyWASM可靠性工程开源开发人员平台开发人员

在 X 上关注

Cloudflare|@cloudflare

相关帖子

2026年4月30日

Agents can now create Cloudflare accounts, buy domains, and deploy

Starting today, agents can now be Cloudflare customers. They can create a Cloudflare account, start a paid subscription, register a domain, and get back an API token to deploy code right away. Humans can be in the loop to grant permission, but there’s no need to go to the dashboard, copy and paste API tokens, or enter credit card details. ...