在之前的​​目前大火的 Jamstack 到底是什么?​​一文中,我们介绍了 Jamstack 的基本概念,现在让我们看看如何使用 Rust 和 WebAssembly 在 Vercel 平台构建高性能的 Jamtack 应用。


Vercel 是开发和托管 Jamstack 应用程序的领先平台。与传统 Web 应用程序在 runtime 从服务器动态生成 UI 不同,Jamstack 应用程序由静态 UI( HTML 和 JavaScript )和一组通过 JavaScript 支持动态 UI 元素的 serverless 函数组成。

Jamstack 的方式有很多好处。这其中最重要的好处之一是其强大的性能。由于 UI 不再从中心服务器的 runtime 生成,因此服务器上的负载要少得多,我们可以通过边缘网络(例如 CDN)部署 UI。

但是,边缘 CDN 只解决了分发静态 UI 文件的问题。后端的 serverless 函数可能仍然很慢。事实上,目前流行的 serverless 平台存在众所周知的性能问题,例如冷启动缓慢,对于交互式应用程序尤其如此。在这方面, WebAssembly 大有可为。

使用 WasmEdge,一个 CNCF 托管的云原生的 WebAssembly runtime , 开发者可以编写高性能 serverless 函数,部署在公共云或边缘计算节点上。本文中,我们将探索如何使用 Rust 编写的 WasmEdge 函数来驱动 Vercel 应用程序后端。

为什么在 Vercel Serverless 使用 WebAssembly ?

Vercel 平台已经有了非常易于使用的 serverless框架 ,可以部署 Vercel 中托管的函数。正如上面讨论的,使用 WebAssembly 和 WasmEdge 是为了进一步提高性能。用 C/C++、Rust 和 Swift 写的高性能函数可以轻松编译成 WebAssembly。这些 WebAssembly 函数比 serverless 函数中常用的 JavaScript 或 Python 快得多。

那么问题来了,如果原始性能是唯一的目标,为什么不直接将这些函数编译为机器本地可执行文件呢?这是因为 WebAssembly “容器”仍然提供许多有价值的服务。

首先,WebAssembly 在 runtime 层级隔离了函数。代码中的错误或内存安全问题不会传播到 WebAssembly runtime之外。随着软件供应链日渐变得愈加复杂,将代码在容器中运行,以防止别人未经授权通过依赖库访问你的数据,这点非常重要。

其次,WebAssembly 字节码是可移植的。开发者只需构建一次,无需担心未来底层 Vercel serverless 容器(操作系统和硬件)的改变或更新。它还允许开发者在相似的托管环境中重复使用相同的 WebAssembly 函数,如腾讯 Serverless Functions 的公有云中,或者在像 YoMo 这样的数据流框架中。

最后, WasmEdge Tensorflow API 提供了最符合 Rust 规范的、执行 Tensorflow 模型的方式。WasmEdge 安装了 Tensorflow 依赖库的正确组合,并为开发者提供了统一的 API。

概念和解释说了很多,趁热打铁,让我们看看示例应用程序!

准备工作

由于我们的 demo WebAssembly 函数是用 Rust 编写的,因此需要安装有 Rust 编译器。确保按如下方式安装 ​​wasm32-wasi​​ 编译器目标,以生成 WebAssembly 字节码。

$ rustup target add wasm32-wasi

Demo 应用程序的前端是用 Next.js 编写的,并部署在 Vercel 上。我们假设你已经具备使用 Vercel 的基本知识。

示例 1: 图片处理

我们的第一个 demo 应用程序是让用户上传图片,然后调用 serverless 函数将其变成黑白图片。在开始之前,你可以试一下这个部署在 Vercel 上的 demo。

用 Rust 和 WebAssembly 在 Vercel 上开发高性能的 serverless 函数_github

首先 fork demo 应用程序的 GitHub repo 。在 Vercel 上部署应用程序,只需从 Vercel for GitHub 页面点GitHub repo 导入。

此 GitHub repo 的内容是一个 Vercel 平台的标准 Next.js 应用程序。其后端 serverless 函数在 ​​api/functions/image_grayscale​​​ 文件夹中。​​src/main.rs​​​ 文件包含 Rust 程序的源代码。Rust 程序从 ​​STDIN​​​ 读取图片数据,然后将黑白图片输出到 ​​STDOUT​​。

use hex;
use std::io::{self, Read};
use image::{ImageOutputFormat, ImageFormat};

fn main() {
let mut buf = Vec::new();
io::stdin().read_to_end(&mut buf).unwrap();

let image_format_detected: ImageFormat = image::guess_format(&buf).unwrap();
let img = image::load_from_memory(&buf).unwrap();
let filtered = img.grayscale();
let mut buf = vec![];
match image_format_detected {
ImageFormat::Gif => {
filtered.write_to(&mut buf, ImageOutputFormat::Gif).unwrap();
},
_ => {
filtered.write_to(&mut buf, ImageOutputFormat::Png).unwrap();
},
};
io::stdout().write_all(&buf).unwrap();
io::stdout().flush().unwrap();
}

使用 Rust 的 ​​cargo​​ 工具构建 Rust 程序为 WebAssembly 字节码或者原生代码。

$ cd api/functions/image-grayscale/
$ cargo build --release --target wasm32-wasi

将 build artifacts 复制到 ​​api​​文件夹。

$ cp target/wasm32-wasi/release/grayscale.wasm ../../

Vercel 在设置 serverless 环境时会运行 ​​api/pre.sh​​​ 。这时会安装 WasmEdge runtime,然后将 WebAssembly 字节码程序编译为一个本地的 ​​so​​ 库,从而更快地执行。

​api/hello.js​​​ 文件符合 Vercel 的 serverless 规范。它加载 WasmEdge runtime,在 WasmEdge 中启动已编译的 WebAssembly 程序,并通过 STDIN 传递上传的图像数据。注意这里 ​​api/hello.js​​​ 运行由 ​​api/pre.sh​​​ 生成的已编译的 ​​grayscale.so​​ 文件从而得到更好的性能。

const fs = require('fs');
const { spawn } = require('child_process');
const path = require('path');

module.exports = (req, res) => {
const wasmedge = spawn(
path.join(__dirname, 'WasmEdge-0.8.1-Linux/bin/wasmedge'),
[path.join(__dirname, 'grayscale.so')]);

let d = [];
wasmedge.stdout.on('data', (data) => {
d.push(data);
});

wasmedge.on('close', (code) => {
let buf = Buffer.concat(d);

res.setHeader('Content-Type', req.headers['image-type']);
res.send(buf);
});

wasmedge.stdin.write(req.body);
wasmedge.stdin.end('');
}

这样就完成了。接下来将 repo 部署到 Vercel ,就可以得到一个 Jamstack 应用程序。该应用程序具有高性能的基于 Rust 和 WebAssembly 的 serverless 后端。

示例 2: AI 推理

第二个 demo 应用程序是让用户上传图像,然后调用 serverless 函数来识别图片中的主要物体。

用 Rust 和 WebAssembly 在 Vercel 上开发高性能的 serverless 函数_应用程序_02

它与上一个示例在同一个 GitHub repo ,但是在 ​​tensorflow​​​ 分支。注意:将此 GitHub repo 导入到 Vercel 网站上时,Vercel 将为每个分支创建一个预览 URL。​​tensorflow​​ 分支将会有自己的部署 URL 。

用于图像分类的后端 serverless 函数位于 ​​tensorflow​​​ 分支中的 ​​api/functions/image-classification​​​ 文件夹中。​​src/main.rs​​​ 文件包含 Rust 程序的源代码。Rust 程序从 ​​STDIN​​​ 读取图像数据,然后将文本输出输出到 ​​STDOUT​​。它用 WasmEdge Tensorflow API 来运行 AI 推理。

pub fn main() {
// Step 1: Load the TFLite model
let model_data: &[u8] = include_bytes!("models/mobilenet_v1_1.0_224/mobilenet_v1_1.0_224_quant.tflite");
let labels = include_str!("models/mobilenet_v1_1.0_224/labels_mobilenet_quant_v1_224.txt");

// Step 2: Read image from STDIN
let mut buf = Vec::new();
io::stdin().read_to_end(&mut buf).unwrap();

// Step 3: Resize the input image for the tensorflow model
let flat_img = wasmedge_tensorflow_interface::load_jpg_image_to_rgb8(&buf, 224, 224);

// Step 4: AI inference
let mut session = wasmedge_tensorflow_interface::Session::new(&model_data, wasmedge_tensorflow_interface::ModelType::TensorFlowLite);
session.add_input("input", &flat_img, &[1, 224, 224, 3])
.run();
let res_vec: Vec<u8> = session.get_output("MobilenetV1/Predictions/Reshape_1");

// Step 5: Find the food label that responds to the highest probability in res_vec
// ... ...
let mut label_lines = labels.lines();
for _i in 0..max_index {
label_lines.next();
}

// Step 6: Generate the output text
let class_name = label_lines.next().unwrap().to_string();
if max_value > 50 {
println!("It {} a <a href='https://www.google.com/search?q={}'>{}</a> in the picture", confidence.to_string(), class_name, class_name);
} else {
println!("It does not appears to be any food item in the picture.");
}
}

使用 ​​cargo​​ 工具将 Rust 程序构建为 WebAssembly 字节码或原生代码。

$ cd api/functions/image-grayscale/
$ cargo build --release --target wasm32-wasi

将 build artifacts 复制到 ​​api​​ 文件夹

$ cp target/wasm32-wasi/release/classify.wasm ../../

同样,​​api/pre.sh​​​ 脚本在此应用程序中安装 WasmEdge runtime 及其 Tensorflow 依赖项。它还在部署时将 ​​classify.wasm​​​ 字节码程序编译为 ​​classify.so​​原生共享库。

​api/hello.js​​​ 文件符合 Vercel serverless 规范。它加载 WasmEdge runtime,在 WasmEdge 中启动已编译的 WebAssembly 程序,并通过 ​​STDIN​​​传递上传的图像数据。注意 ​​api/hello.js​​​ 运行由 ​​api/pre.sh​​​ 产生的编译过的 ​​classify.so​​ 文件,以达到更好的性能。

const fs = require('fs');
const { spawn } = require('child_process');
const path = require('path');

module.exports = (req, res) => {
const wasmedge = spawn(
path.join(__dirname, 'wasmedge-tensorflow-lite'),
[path.join(__dirname, 'classify.so')],
{env: {'LD_LIBRARY_PATH': __dirname}}
);

let d = [];
wasmedge.stdout.on('data', (data) => {
d.push(data);
});

wasmedge.on('close', (code) => {
res.setHeader('Content-Type', `text/plain`);
res.send(d.join(''));
});

wasmedge.stdin.write(req.body);
wasmedge.stdin.end('');
}

现在可以将 forked 的repo 部署到 vercel, 就会得到一个识别物体的 Jamstack 应用。

只需更改模板里的 Rust 函数,你就可以部署自己的高性能 Jamstack 应用了!

展望

从 Vercel 当前的 serverless 容器运行 WasmEdge 是一种向 Vercel 应用程序添加高性能函数的简单方法。

如果你用 WasmEdge 开发了有趣的 Vercel 函数或者应用,可以添加微信 h0923xw,就可以领取一份 WasmEdge 周边。

展望未来,更好的方法是将 WasmEdge 本身作为容器使用,而不是今天这样用 Docker 和 Node.js 来启动 WasmEdge。这样,我们可以以更高效率运行 serverless 函数。WasmEdge 已经与 Docker 工具兼容。如果有兴趣加入 WasmEdge 和 CNCF 进行这项激动人心的工作,欢迎加入我们的 channel。

入口:https://github.com/WasmEdge/WasmEdge#community