Language:Chinese VersionEnglish Version

WebAssembly 最初是为浏览器设计的。到 2026 年,它最引人注目的用例之一是在服务器端。插件系统、边缘计算、多租户代码执行和多语言函数运行时——服务器端 Wasm 正在解决容器和原生代码处理不佳的实际问题。

这并非理论概述。我已经在生产环境中为两个项目部署了 Wasm:一个 SaaS 平台的插件系统和一个边缘函数运行时。本文涵盖了我构建的内容、哪些有效、哪些出了问题,以及服务器端 Wasm 是否适合您的用例。

为什么服务器端 Wasm 是有意义的

容器彻底改变了部署方式。但它们存在固有局限性:

  • 冷启动时间: 最小的 Docker 容器需要 500ms-2s 才能启动。Wasm 模块在 1ms 内即可启动。
  • 隔离开销: 每个容器都需要自己的操作系统镜像层、网络栈和进程空间。Wasm 模块是一个具有沙盒内存的单个二进制文件。
  • 大小: 一个”精简”的容器镜像为 50-100MB。Wasm 模块通常为 1-10MB。
  • 安全边界: 容器逃逸是真实存在的威胁。Wasm 的沙盒是数学定义的——模块无法访问其明确授予能力之外的任何内容。

权衡之处在于:Wasm 并非通用容器替代品。它擅长短暂的、沙盒化的计算,在这些场景中,启动时间、安全隔离或多语言支持至关重要。

2026 年的 Wasm 服务器生态系统

运行时

三种运行时主导着服务器端 Wasm:

Wasmtime (Bytecode Alliance):参考实现。生产级、文档完善,被 Fastly 和 Shopify 使用。最适合在更大的应用程序中嵌入 Wasm 执行。

Wasmer

WasmEdge:针对边缘和 IoT 优化。除了 WASI 标准外,还包括网络、数据库和 AI 扩展。最适合具有扩展能力的边缘计算。

WASI:系统接口

WASI(WebAssembly 系统接口)使服务器端 Wasm 变得实用。它定义了 Wasm 模块如何与外部世界交互——文件系统、网络套接字、时钟、随机数。没有 WASI,Wasm 模块就是一个没有 I/O 的纯计算引擎。

2025 年底稳定的 WASI Preview 2 引入了组件模型——一种让 Wasm 模块定义和使用类型化接口的方式。这是可组合、语言无关的服务器端 Wasm 的基础。

用例 1:插件系统

这正是服务器端 Wasm 发挥最大价值的地方。如果您的应用需要用户提供或第三方扩展,Wasm 提供了沙盒执行环境,无需担心运行不受信任的原生代码所带来的安全噩梦。

架构

// 使用 Wasmtime 的 Rust 主机应用
use wasmtime::*;
use wasmtime_wasi::preview1::WasiP1Ctx;
use wasmtime_wasi::WasiCtxBuilder;

// 定义插件接口
// 插件必须导出这些函数
trait Plugin {
    fn on_request(&self, request: &HttpRequest) -> PluginResult;
    fn on_response(&self, response: &mut HttpResponse) -> PluginResult;
    fn metadata(&self) -> PluginMetadata;
}

struct PluginHost {
    engine: Engine,
    plugins: Vec,
}

struct LoadedPlugin {
    name: String,
    instance: Instance,
    store: Store,
}

impl PluginHost {
    fn new() -> Result {
        let mut config = Config::new();
        config.consume_fuel(true);          // CPU 限制
        config.epoch_interruption(true);     // 超时支持

        let engine = Engine::new(&config)?;
        Ok(Self { engine, plugins: vec![] })
    }

    fn load_plugin(&mut self, name: &str, wasm_bytes: &[u8])
        -> Result<()>
    {
        let module = Module::new(&self.engine, wasm_bytes)?;

        // 为此插件创建沙盒化的 WASI 上下文
        let wasi = WasiCtxBuilder::new()
            .inherit_stdio()            // 允许 stdout/stderr
            // 无文件系统访问
            // 无网络访问
            // 无环境变量
            .build_p1();

        let mut store = Store::new(&self.engine, PluginState {
            wasi,
            fuel_limit: 1_000_000,     // 最多 1M 条指令
            memory_limit: 16 * 1024 * 1024, // 最多 16MB 内存
        });

        // 设置资源限制
        store.set_fuel(1_000_000)?;
        store.epoch_deadline_trap();

        let instance = Instance::new(&mut store, &module, &[])?;

        self.plugins.push(LoadedPlugin {
            name: name.to_string(),
            instance,
            store,
        });

        Ok(())
    }
}

使用任何语言编写插件

Wasm 插件的力量:开发者可以用他们偏好的语言编写插件,而主机则以相同的方式运行它们。

// 用 Rust 编写的插件
// 编译命令:cargo build --target wasm32-wasip1 --release

#[no_mangle]
pub extern "C" fn on_request(request_ptr: *const u8, len: usize) -> i32 {
    let request = unsafe {
        let slice = std::slice::from_raw_parts(request_ptr, len);
        serde_json::from_slice::(slice).unwrap()
    };

    // 添加自定义头
    if request.path.starts_with("/api/") {
        // 返回 0 = 继续,1 = 阻止
        return 0;
    }

    0
}

#[no_mangle]
pub extern "C" fn metadata() -> *const u8 {
    let meta = PluginMetadata {
        name: "rate-limiter",
        version: "1.0.0",
        description: "Rate limits API requests",
    };
    let json = serde_json::to_vec(&meta).unwrap();
    let ptr = json.as_ptr();
    std::mem::forget(json); // 防止释放
    ptr
}
// 相同的 Go 插件
// 编译命令:tinygo build -o plugin.wasm -target=wasip1

package main

import "encoding/json"

//export on_request
func onRequest(ptr *byte, length int) int32 {
    data := unsafe.Slice(ptr, length)
    var request Request
    json.Unmarshal(data, &request)

    if strings.HasPrefix(request.Path, "/api/") {
        return 0 // 继续
    }
    return 0
}

//export metadata
func metadata() *byte {
    meta := PluginMetadata{
        Name:        "rate-limiter",
        Version:     "1.0.0",
        Description: "Rate limits API requests",
    }
    data, _ := json.Marshal(meta)
    ptr := &data[0]
    return ptr
}

func main() {}

两者都编译成主机以相同方式加载的 .wasm 文件。主机不知道也不关心插件是用什么语言编写的。

用例 2:边缘函数

Cloudflare Workers、Fastly Compute 和 Fermyon Cloud 都在边缘运行 Wasm。亚毫秒级的启动时间使 Wasm 成为请求级计算的理想选择,在这种情况下,容器冷启动是不可接受的。

构建边缘函数

// 使用 WASI HTTP 接口的边缘函数
// 这可以在任何 WASI 兼容的边缘平台上运行

use wasi::http::types::{IncomingRequest, ResponseOutparam, Headers, OutgoingResponse, OutgoingBody};

fn handle_request(request: IncomingRequest, response_out: ResponseOutparam) {
    let path = request.path_with_query().unwrap_or_default();
    let method = request.method();

    // 路由处理
    let (status, body) = match (method, path.as_str()) {
        (Method::Get, "/health") => {
            (200, r#"{"status":"healthy"}"#.to_string())
        }
        (Method::Get, path) if path.starts_with("/api/transform/") => {
            let input = path.strip_prefix("/api/transform/").unwrap();
            let result = transform_data(input);
            (200, serde_json::to_string(&result).unwrap())
        }
        _ => (404, r#"{"error":"not found"}"#.to_string()),
    };

    // 构建响应
    let headers = Headers::new();
    headers.set(&"content-type".to_string(),
        &[b"application/json".to_vec()]).unwrap();

    let response = OutgoingResponse::new(headers);
    response.set_status_code(status).unwrap();

    let outgoing_body = response.body().unwrap();
    ResponseOutparam::set(response_out, Ok(response));

    let stream = outgoing_body.write().unwrap();
    stream.blocking_write_and_flush(body.as_bytes()).unwrap();
    drop(stream);
    OutgoingBody::finish(outgoing_body, None).unwrap();
}

性能:真实数据

来自我的生产部署的基准测试(处理 HTTP 中间件的插件系统):

指标 Docker 容器 Wasm 模块
冷启动 1,200ms 0.8ms
热调用 2ms 0.1ms
每个实例内存 45MB 2MB
二进制文件大小 85MB (镜像) 3.2MB (.wasm)
启动到第一个请求 3.5s 5ms
最大并发实例数 (4GB RAM) ~80 ~1,800

在多租户场景中,数据不言自明。在 4GB RAM 中运行 1,800 个隔离的插件实例,使用容器是不可能实现的。

目前还不适用的功能

诚实地说明局限性很重要,因为 Wasm 的炒作周期是真实存在的:

  • 网络:WASI 套接字已标准化但尚未得到普遍支持。在许多运行时中,从 Wasm 模块发出 HTTP 请求仍然需要主机提供的函数。
  • 线程:Wasm 线程(共享内存和原子操作)在浏览器中可用,但服务器端支持不一致。CPU 密集型并行工作负载目前不是 Wasm 的强项。
  • 调试:对 Wasm 模块进行源码级调试很痛苦。虽然存在 DWARF 调试信息支持,但相关工具尚不成熟。你将花时间阅读十六进制转储。
  • 生态系统:并非所有库都能干净地编译为 Wasm。任何使用特定于平台的系统调用的库(许多 C 库、大多数 FFI 绑定)都需要移植工作。
  • 垃圾回收:使用 GC 的语言(Go、Java、C#)会产生更大的 Wasm 二进制文件,因为它们捆绑了运行时。TinyGo 中的 “hello world” 是 250KB;而在 Rust 中是 15KB。

何时使用服务器端 Wasm

适用场景:

  • 插件/扩展系统,第三方代码在你的进程中运行
  • 需要亚毫秒级启动时间的边缘计算
  • 隔离密度重要的多租户平台
  • 多语言函数运行时(FaaS 平台)
  • 安全关键的沙箱环境(运行用户上传的代码)

不适用场景:

  • 长时间运行的服务(使用容器)
  • 需要大量操作系统交互的应用程序(使用原生代码)
  • GPU 工作负载(Wasm 无法访问 GPU)
  • 冷启动无关紧要的应用程序(使用容器)

入门:一个最小的插件主机

如果你想尝试,这里是最简单的路径:

# 安装 Wasmtime CLI
curl https://wasmtime.dev/install.sh -sSf | bash

# 用 Rust 编写一个简单的 Wasm 模块
cargo new --lib my-plugin
cd my-plugin

# 添加到 Cargo.toml:
# [lib]
# crate-type = ["cdylib"]

# 为 WASI 构建
cargo build --target wasm32-wasip1 --release

# 运行它
wasmtime target/wasm32-wasip1/release/my_plugin.wasm

对于 JavaScript/TypeScript 开发者,Extism 提供了一个更高级的 SDK,抽象了 Wasm 运行时的细节:

// 主机(Node.js)使用 Extism
import Extism from "@extism/extism";

const plugin = await Extism.createPlugin(
  "./my-plugin.wasm",
  { useWasi: true }
);

const result = await plugin.call("process", JSON.stringify({
  input: "hello",
  transform: "uppercase"
}));

console.log(result.text()); // {"output":"HELLO"}

服务器端 Wasm 并不是在取代容器。它正在为容器过于笨重的场景开辟一个利基市场:沙箱插件、边缘函数和高密度多租户执行。如果你的应用程序有这些需求中的任何一项,Wasm 值得认真评估。生态系统在 2025 年跨越了”可用于生产”的门槛,而 2026 年将是基于它构建的一年。

By

Leave a Reply

Your email address will not be published. Required fields are marked *

You missed