Netlify 是一个开发和托管 Jamstack 应用的平台。实际上,Jamstack 是Netlify 的创始人 Mathias Biilmann 于 2015年造出来的一个词。Netlify 也是 JamstackConf 的主要组织者。
Jamstack 应用程序由一个静态 UI (HTML 和 JavaScript) 和一系列 serverless 函数组成。动态 UI 元素由 JavaScript 向 serverless 函数获取数据生成。Jamstack 的好处有很多,但这其中最重要的好处之一是性能绝佳。由于 UI 不再从中心服务器的 runtime 生成,因此服务器上的负载要少得多,我们可以通过边缘网络(例如 CDN)部署 UI。
但是边缘 CDN 只解决了分发静态 UI 文件的问题。后端 serverless 函数可能仍然很慢。事实上,流行的 Serverless 平台存在众所周知的性能问题,例如冷启动缓慢,尤其是对于交互式应用程序而言。在这方面, WebAssembly 大有可为。
使用由 CNCF 托管的云原生 WebAssembly runtime WasmEdge ,开发者可以编写部署在公共云或边缘计算节点上的高性能serverless 函数。本文中,我们将探索如何使用 Rust 编写的 WasmEdge 函数来支持 Netlify 应用程序后端。
为什么用 WebAssembly 实现 Netlify 函数
Netlify 平台已经有一个非常易于使用的用于部署函数的 serverless 框架。正如上面讨论的,使用 WebAssembly 以及 WasmEdge 是为了进一步提高性能。用 C/C 、Rust 和 Swift 写的高性能函数可以轻松编译成 WebAssembly。这些 WebAssembly 函数比 serverless 函数中常用的 JavaScript 或 Python 快得多。
可是,如果原始性能是唯一的目标,为什么不直接将这些函数编译为机器本地可执行文件呢(本地客户端或者 NaCl)?这是因为 Netlify 已经在使用 Firecracker microVM 在 AWS Lambda 中安全地运行这些函数。
我们对未来的愿景是在云原生基础架构中将 WebAssembly 作为轻量级的 runtime 与 Docker 、 microVM 并列运行。与类似 Docker 容器或 microVM 相比,WebAssembly 提供更高的性能并消耗更少的资源。但就目前而言,Netlify 仅支持在 microVM 中运行 WebAssembly。
相比运行容器化 NaCl 程序,在 microVM 中运行 WebAssembly 函数有很多优势。
首先,WebAssembly 为独立的函数提供了细颗粒度的 runtime 隔离。一个微服务可以有多个函数,并支持在 microVM 中运行的服务。WebAssembly 可以让微服务更安全、更稳定。
第二,WebAssembly 字节码是可移植的。开发者只需构建一次,无需担心未来底层 Netlify serverless runtime 的改变或更新。它还允许开发者在其它云环境中重复使用相同的 WebAssembly 函数。
第三, WebAssembly 应用很容易部署和管理。与 NaCl 动态库和可执行文件相比,它们具有更少的平台依赖性和复杂性。
最后, WasmEdge Tensorflow API 提供了最符合 Rust 规范的、执行 Tensorflow 模型的方式。WasmEdge 安装了 Tensorflow 依赖库的正确组合,并为开发者提供了统一的 API。
概念和解释说了很多,趁热打铁,我们来看看示例应用吧!
准备工作
由于我们的 demo WebAssembly 函数是用 Rust 编写的,因此您需要安装 Rust 编译器。确保按如下方式安装 wasm32-wasi
编译器目标,以生成 WebAssembly 字节码。
$ rustup target add wasm32-wasi
demo 应用前端是用 Next.js 写的,并且部署在 Netlify 上。我们假设你已经有使用 Next.js 和 Netlify 的基本知识了。
示例 1: 图片处理
我们的第一个 demo 应用程序是让用户上传图片,然后调用 serverless 函数将其变成黑白图片。开始之前,你可以试用一下这个部署在 Netlify 上的 demo。
代码链接:https://github.com/second-state/netlify-wasm-runtime
首先 fork demo 应用的 GitHub repo 。要想把这个应用部署到 Netlify,只需将你的 GitHub repo 添加到 Netlify 上就行了。
这个 repo 是 Netlify 平台的标准 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 ../../
Netlify 函数在设置 serverless 函数时运行
api/pre.sh
。这时会安装 WasmEdge runtime,然后将 WebAssembly 字节码程序编译为一个本地的so
库,从而更快地执行。
api/hello.js
文本加载 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'),
[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 部署到Netlify ,就得到了一个 Jamstack 应用。该应用有着基于 Rust 和 WebAssembly 的高性能 serverless 后端。
示例 2: AI 推理
第二个demo应用让用户上传图像,然后调用 serverless 函数来识别图片中的主要物体。
源代码:https://github.com/second-state/netlify-wasm-runtime/tree/tensorflow
它与上一个示例在同一个 GitHub repo ,但是在 tensorflow
分支。用于图片识别的后端 serverless 函数在该分支的 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-classification/
$ 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
脚本加载 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('');
}
现在可以将你fork 的 repo 部署到 Netlify 上,并得到一个可以进行物体识别的 web 应用。
接下来呢?
在 Netlify 目前的 serverless 容器中运行 WasmEdge 是目前将高性能函数添加到 Netlify 应用中的简单方式。未来更好的方法是将WasmEdge作为容器本身使用,这样就无须 Docker 与 Node.js,我们可以以更高的效率运行 serverless 函数。WasmEdge 已经与 Docker 工具兼容。如果你有兴趣加入 WasmEdge 和 CNCF 进行这个激动人心的工作,欢迎加入我们的 channel!