当前位置: 首页 > news >正文

一个 panic 是怎么把整个服务搞坏的——Cloudflare 修复 Rust Workers 可靠性的完整过程

Rust 以内存安全著称,但在 WebAssembly 的运行环境里,它有一个长期被低估的可靠性问题:一次 panic 或者 abort,可以把整个 Worker 实例彻底毒化,让后续所有请求都跟着失败

这个问题在 Cloudflare 的 Rust Workers 上存在了相当长的时间。今年四月,他们系统性地解决了它,并把相关改动贡献回了 wasm-bindgen 上游项目。

这篇文章完整梳理他们是怎么一步步做到的。

原文链接:https://blog.cloudflare.com/making-rust-workers-reliable/
wasm-bindgen 仓库:https://github.com/wasm-bindgen/wasm-bindgen
workers-rs 仓库:https://github.com/cloudflare/workers-rs


先理解问题的根源

Rust Workers 的运行方式,是把 Rust 代码编译为 WebAssembly(Wasm),然后在 Cloudflare 的 Workers 运行时(基于 V8 引擎)里执行。

在原生 Rust 环境里,panic 分两种行为:

  • panic=unwind:触发栈回溯(unwinding),析构函数(Drop)正常执行,程序可以通过catch_unwind捕获并恢复
  • panic=abort:直接终止,没有任何清理,进程退出

Rust 编译到wasm32-unknown-unknown目标时,默认是panic=abort。这意味着一旦 Wasm 内部发生 panic,它会触发一条unreachable指令,抛出WebAssembly.RuntimeError异常,然后执行路径直接跳出 Wasm 回到 JavaScript。

问题在于:Wasm 实例是有状态的。一次 panic 让 Wasm 的执行中途中断,实例内部的状态可能处于不一致的中间状态。如果这个实例还在继续处理其他并发请求,那些请求读到的就是已经被污染的内存状态,产生难以预测的错误。

更严重的是:这种失效状态有时不会被立刻清理,而是持续影响后续进入的新请求,直到实例被回收。一个请求的失败,可以扩散成一片失败。


第一阶段:用 JavaScript 包装打补丁(workers-rs 0.6)

面对这个问题,Cloudflare 最初的方案是在 JavaScript 和 Rust 之间的调用边界做拦截。

具体做法是:

  • 自定义 panic handler:在 Rust 侧植入一个全局 panic 跟踪器,记录失败状态
  • Proxy 包装层:在 JavaScript 侧用 Proxy 对所有 Rust-JS 入口点做统一拦截,一旦检测到 Wasm 处于失效状态,触发全量重新初始化
  • 模块重初始化:失败后重新加载 Wasm 模块,让实例回到干净状态

这套方案解决了"Worker 被彻底搞死"的问题,并从 workers-rs 0.6 版本开始默认对所有用户启用。

但它有一个明显的局限:重新初始化意味着丢弃整个 Wasm 实例的内存状态

对于无状态的请求处理器来说,这没什么问题,反正每次请求之间也没有共享状态。但对于 Durable Objects——Cloudflare 的强一致性有状态存储原语——来说,实例内存里维护着跨请求共享的业务状态。一个请求触发了 panic,重新初始化会把其他并发请求正在使用的状态一起清掉,造成数据丢失。

治标不治本。根本的问题需要在 panic 机制本身上解决。


第二阶段:让 Wasm 真正支持栈回溯(panic=unwind)

原生 Rust 里,panic=unwind允许 panic 像异常一样向上传播,沿途执行 Drop,并允许调用方用catch_unwind捕获。这样可以在不丢失整体状态的前提下,隔离单次失败的影响范围。

Wasm 里历史上没有等价的机制,直到WebAssembly Exception Handling 提案在 2023 年获得主流引擎的广泛支持。这个提案在 Wasm 字节码层面引入了try/catch/throw指令,让 Wasm 模块可以像宿主语言一样处理异常。

基于这个提案,Cloudflare 为 wasm-bindgen 实现了完整的panic=unwind支持。

编译层的变化

RUSTFLAGS='-Cpanic=unwind' cargo build -Zbuild-std编译时,含有 Drop 的代码会生成对应的 Wasm 异常处理指令。例如以下 Rust 代码:

fnsome_func(){leta=HasDropA;letb=HasDropB;imported_func();// 可能 panic}

编译后的 Wasm 字节码大致为:

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 工具链的改造

要让整个工具链支持这套机制,需要改动多个环节:

  • Walrus(Wasm 解析器):原来不认识try/catch指令,需要新增支持
  • 描述符解释器:需要能正确解析包含异常处理块的代码
  • Rust-JS 边界:wasm-bindgen 生成的导出函数需要在最外层捕获 panic,以PanicError的形式抛出给 JavaScript;对于 async 导出,panic 会以PanicError拒绝(reject)对应的 Promise
  • extern “C-unwind”:Rust 的extern "C"会在 unwind 穿越时触发 abort,导出函数需要改为extern "C-unwind"才能允许 panic 向上传播

闭包的处理还需要额外的细心。很多闭包会捕获引用,而引用在 unwind 之后可能仍然存活,这违反了 unwind 安全性(UnwindSafe)的要求。为此新增了MaybeUnwindSafetrait,并提供了Closure::new_aborting变体——在 unwind 不安全的场景下主动触发 abort,而不是让编译器强迫用户加AssertUnwindSafe

最终效果:

  • panic 被 wasm-bindgen 捕获,作为 JavaScript 异常抛出
  • Rust 析构函数正确执行
  • Wasm 实例继续有效,可以处理后续请求
  • Durable Objects 的内存状态不再因为单个请求的 panic 而丢失

第三阶段:abort 恢复——兜住最坏情况

即使有了panic=unwind,abort 仍然存在。内存耗尽(OOM)是最常见的触发原因,abort 无法 unwind,没有状态恢复的可能。

abort 恢复的目标不是恢复状态,而是:确保 abort 之后,失败状态不会污染后续请求

区分可恢复错误与不可恢复错误

引入panic=unwind之后,出现了一个新问题:从 Wasm 抛出的错误,可能来自extern "C-unwind"的 unwind,也可能来自真正的 abort,从外部看不出区别。

Cloudflare 的解决思路是给 unwind 标记上特定的Exception Tag(WebAssembly 异常处理提案提供的机制),让 abort 产生的错误没有这个标记,从而可以在 JavaScript 侧精确区分两种来源。

set_on_abort:平台级的恢复钩子

基于这套区分机制,wasm-bindgen 新增了set_on_abort钩子,允许在初始化时注册一个自定义的 abort 处理函数。Worker 实例可以在 abort 发生时执行必要的清理,然后将实例标记为不可再用,确保后续请求不会进入一个状态未知的实例。

同时还加入了 abort 重入保护:防止在 abort 处理过程中因为深度交错的 Wasm-JS 调用栈导致 abort 处理逻辑本身被重复触发。


延伸:wasm-bindgen 库的自动重初始化

这套机制不只对 Rust Workers 有用。在 JavaScript Workers 里,开发者有时会直接依赖用 Rust 编写、编译为 Wasm 的 npm 包(比如import { func } from 'wasm-dep')。如果这类库内部发生 abort,调用方的 JS 代码会收到一个错误,但那个 Wasm 实例可能已经处于无效状态,后续调用会持续失败。

为此,wasm-bindgen 新增了实验性的--reset-state-function功能。它暴露一个函数,允许 Rust 应用声明"我需要重置到初始状态",而不需要调用方重新 import 或重建绑定对象。旧实例上的类实例会失效(handles 变成孤立状态),但新的类可以被构建出来,整个 JS 应用得以继续运行。

区别在于:错误发生后应用变得"出错了",但不会变成"彻底坏掉了"。


推动整个生态跟上:Node.js 的贡献

WebAssembly Exception Handling 提案经历过一次规范晚期修改,分裂成两个版本:

  • legacy 版本:已被广泛支持,但已被标记为废弃
  • 现代版本(with exnref):才是正式规范,各引擎支持时间如下:
运行时支持版本发布时间
Chrome1382025.06.28
Firefox1312024.10.01
Safari18.42025.03.31
Node.js25.0.02025.10.15
workerd(Workers 运行时)v1.20250620.02025.06.19

问题在于 Node.js 24 LTS 的发布节奏:如果不做干预,整个生态在 2028 年 4 月之前都无法切换到现代版本,因为 LTS 用户会一直停留在 Node.js 24 上。

Cloudflare 发现这个问题后,主动将现代 Exception Handling 反向移植到了Node.js 24Node.js 22的发行版本,消除了这个长达三年的阻塞。


现在可以怎么用

workers-rs 0.8.0开始,构建命令支持--panic-unwind标志:

# 在构建时加上这个标志workers-rs build --panic-unwind

启用后:

  • panic 完全可恢复,不会丢失 Durable Objects 状态
  • abort 会触发新的恢复钩子,失败状态不再扩散
  • 下一个版本计划将panic=unwind作为默认行为

仍然使用panic=abort的用户,依然享有 0.6.0 引入的自定义恢复包装器,但无法做到无损恢复。


这件事背后的工程文化

这篇博客让人印象深刻的,不只是技术本身,而是解决问题的路径:发现问题,修平台,顺手把上游生态一起推进

Walrus 加了对新 Wasm 指令的支持,wasm-bindgen 有了 panic/abort 恢复能力,Node.js 24/22 的 LTS 用户不用等到 2028 年才能用到现代异常处理——这些改动都不是 Cloudflare 的"私有修复",而是贡献回了对应的开源项目。

最终受益的不只是 Cloudflare Workers 的用户,而是整个 Rust + WebAssembly 的开发者社区。这是基础设施公司参与开源生态的一种比较理想的姿态。


http://www.jsqmd.com/news/746369/

相关文章:

  • 终极指南:如何用免费开源工具释放AMD Ryzen处理器的隐藏性能
  • DLSS Swapper终极教程:5分钟学会智能管理游戏DLSS文件,告别手动替换的烦恼
  • Fluent Bit的‘瑞士军刀’:手把手教你用Record Modifier和Nest插件玩转日志字段
  • League Akari:英雄联盟玩家的智能游戏助手完全指南
  • 20.人工智能实战:大模型项目如何从 Demo 走向生产?一套可落地的上线验收清单与工程治理方案
  • 互联网大厂 Java 求职者面试:音视频场景与 Spring Boot
  • LIVE-SWE-AGENT:实时自进化软件工程代理实践
  • 别再只会画直线了!用Mermaid时序图的alt、loop、par语法,5分钟画出复杂业务流程图
  • 别再死记硬背了!用Python算一算,你的摄像头到底需要多大带宽?
  • 开源硬件控制工具OmenSuperHub:终极暗影精灵性能优化指南
  • 从数据标注到模型迭代:Label Studio如何重塑AI数据流水线
  • STM32L051C8T6 ADC采集电压不准?手把手教你用HAL库实现内部基准电压校准(附源码)
  • 嵌入式USB接口技术:设计原理与工程实践
  • 终结公会运营乱象!V4.0全景游戏电竞护航陪玩源码系统小程序,TP8.1+全自动裂变引擎重塑数千俱乐部盈利基因 - 壹软科技
  • 惠普OMEN游戏本终极性能解锁:OmenSuperHub深度技术解析与专业配置指南
  • 三分钟上手:跨平台Steam创意工坊下载器WorkshopDL完全指南
  • Java 25密封类必须掌握的4种组合模式,错过将无法适配2025年主流框架演进路线
  • 互联网大厂Java求职者面试:技术栈与场景探讨
  • Cacao部署与发布指南:从开发到上架App Store的完整流程
  • 别再只用While循环了!LabVIEW FPGA单周期定时循环(SCTL)保姆级避坑指南
  • 3步快速解决ComfyUI组件冲突:新手必看的完整指南
  • Steam成就管理神器:如何轻松掌控你的游戏成就
  • 实战应用:构建可部署的带水印与多尺寸输出的代码转图应用
  • AI偏好学习系统:精准报告生成与动态评分适配
  • 人工智能篇---Flask 和 FastAPI
  • 在Hermes Agent框架中配置Taotoken作为自定义Codex模型提供商
  • MagicWorld视频世界模型:解决动态场景运动漂移与误差累积
  • 5分钟掌握D3KeyHelper:暗黑破坏神3终极技能连点器完整指南
  • 独立开发者如何利用 Taotoken 模型广场高效进行模型选型
  • 告别迷茫!手把手教你用PCAN-Explorer 5和TSMaster玩转汽车CAN总线(从收发报文到DBC解析)