用 Rust 写 Serverless:Cloudflare Workers + WebAssembly 实践
Cloudflare Workers 是 Cloudflare 提供的 Serverless 运行平台,代码运行在全球 150+ 个数据中心的边缘节点上。它原生支持 JavaScript,而随着 WebAssembly(WASM)支持的加入,Rust 开发者也可以把自己的代码编译成 WASM,部署到这套平台上运行。 这篇文章以一个实际项目为例,介绍如何把 Rust 代码编译为 WASM,先在本地浏览器中跑通,再上传到 Workers 作为 Serverless 函数对外提供服务。 原文地址:https://blog.cloudflare.com/cloudflare-workers-as-a-serverles... 在开始之前,有一点需要明确:WASM 不是万能的。 Cloudflare 官方文档中有一段很务实的说法:对于轻量任务,比如做一次请求重定向、校验一个 Token,纯 JavaScript 往往比 WASM 更快、更简单。原因在于 WASM 运行在独立的内存空间里,数据进出都需要拷贝,如果代码本身没有密集的计算,引入 WASM 反而会带来额外开销。 WASM 真正发挥优势的场景是计算密集型任务:图像处理、加解密、复杂的字符串操作等。 Rust 的 WASM 工具链目前已经相当成熟,核心工具是 模板项目的核心模式是这样的: 这里做了两件事: 第一,通过 第二,用 编译命令: 编译产物在 编译完成后,先不急着上线,在本地浏览器里跑通是个好习惯。 npm 上有一个 浏览器访问 把自己的 wasm 包用 作者在实际开发中踩了一个典型的坑:用 Rust 的随机数库 原因是 解决方案是换掉系统级调用,改用 JavaScript 宿主提供的 Web API。Rust 有一个 用 这个经验值得记住:在 WASM 环境下,任何涉及 I/O、系统熵、文件系统、时间获取的操作,都需要通过宿主环境(JS)来代理,不能直接走 Rust 标准库的对应实现。 本地浏览器跑通之后,接下来把 上传时把 WASM 绑定到一个全局变量(比如 但这里有一个需要手动处理的地方: 改造完成后,在 Worker 脚本里调用 Rust 函数的方式和调用普通 JavaScript 函数没有区别: 请求进来,Rust 函数被调用,结果直接返回——运行在全球 150+ 个边缘节点上。 这篇博客展示的是一条完整的路径:Rust 代码 → WASM → Cloudflare Workers。整个工具链在当时(2018年)还比较初期,需要手动处理 JS 胶水代码的适配。现在 Cloudflare 已经提供了 Wrangler CLI,把这些工作都封装进去了,流程更加顺滑。 几个值得关注的核心点: WASM 不是银弹,轻量逻辑用 JS 就好,WASM 适合计算密集型场景。 系统调用在 WASM 里不可用,需要通过 胶水代码是关键中间层, Serverless + Rust + WASM 这条路是通的,而且随着工具链的持续完善,门槛在逐步降低。背景
什么时候适合用 WASM
环境搭建
wasm-pack。# 安装 wasm-pack
curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
# 安装 cargo-generate,用于基于模板创建项目
cargo install cargo-generate
# 用官方模板创建一个新项目
cargo generate --git https://github.com/rustwasm/wasm-pack-templatewasm-pack 的作用是把 Rust 代码编译成 WebAssembly,同时生成 JavaScript 和 Rust 之间的类型绑定(binding)。这个绑定层很关键,后面会详细说。代码结构:
#[wasm_bindgen]#[wasm_bindgen]
extern {
fn alert(s: &str);
}
#[wasm_bindgen]
pub fn greet() {
alert("Hello from Rust!");
}extern 块声明外部函数——也就是宿主环境(浏览器或 Workers)提供的函数,比如 alert。#[wasm_bindgen] 标注的 pub fn,会被暴露出去,让 JavaScript 侧可以直接调用,就像调用普通 JS 函数一样。wasm-pack buildpkg/ 目录下,包含 .wasm 二进制文件和自动生成的 JS 胶水代码。本地验证
create-wasm-app 模板,提供了一个预配置好 webpack 的测试页面,可以直接 import WASM 模块:npm init wasm-app www
cd www
npm install
npm starthttp://localhost:8080,如果看到页面正常渲染,说明 WASM 模块加载成功。npm link 关联进来,修改 www/index.js,调用自己的函数:import * as wasm from "my-wasm-module";
let result = wasm.get_phrase_text(100, 10);
console.log(result);一个坑:系统调用在 WASM 里不可用
SmallRng::from_entropy() 时,本地 cargo test 完全正常,但在浏览器里跑 WASM 时直接崩溃。from_entropy() 底层依赖系统调用来获取熵值,而 WASM 的编译目标是 wasm32-unknown-unknown——这个 unknown 意味着目标平台不保证提供任何系统调用。编译器不报错,但运行时会直接失败。js-sys crate,封装了标准 ECMAScript 提供的所有全局对象,其中包括 Date:fn get_rng() -> SmallRng {
use js_sys::Date;
use rand::SeedableRng;
let ticks = Date::now();
let tick_bytes = transmute(ticks as u128);
SmallRng::from_seed(tick_bytes)
}Date.now() 作为种子,绕开了系统调用,问题解决。上传到 Workers
.wasm 文件上传到 Cloudflare Workers。BOBROSS_WASM),Workers 运行时会在 Worker 脚本启动时自动实例化这个模块。wasm-pack build 生成的 JS 胶水代码是为浏览器环境写的(使用了 ES module 的 import/export 语法),Workers 的 WASM 实例化方式和浏览器略有不同,需要做几处改造:import 语句export 关键字importObject,把需要注入的外部函数传进去WebAssembly.Instance 时传入这个 importObjectasync function handleRequest(request) {
let url = new URL(request.url);
let phraseCount = parseInt(url.searchParams.get("phrases") || 100);
let newLine = parseInt(url.searchParams.get("newline") || 0);
// 调用 Rust 编译的 WASM 函数
let phraseText = mod.get_phrase_text(phraseCount, newLine);
return new Response(phraseText);
}整体流程回顾
Rust 源码
↓ wasm-pack build
.wasm 二进制 + JS 胶水代码
↓ 本地 npm link + webpack 测试页
浏览器验证通过
↓ 手动改造 JS 胶水代码(适配 Workers)
上传 .wasm + Worker 脚本到 Cloudflare
↓
全球边缘节点运行小结
js-sys 等工具桥接宿主环境的 Web API。wasm-bindgen 自动处理了 JS 和 Rust 之间的类型转换和内存管理,理解它的工作方式对排查问题很有帮助。