自 2019 年发布以来, Dapr (分布式应用程序运行时)迅速成为非常流行的用于构建微服务的开源框架。它提供了分布式应用程序中常用的构建块和预打包的服务,例如服务调用、状态管理、消息队列、资源绑定和触发器、mTLS 安全连接和服务监控。分布式应用程序开发人员可以利用和使用这些构建块在运行时公开的基于 Web 的 API。这些应用程序通常称为微服务并作为 sidecar 运行。正如 InfoQ 作者Bilgin Ibryam所描述的, Dapr 是 多运行时微服务架构的一个示例。
Dapr 的 sidecar 模式非常类似于服务网格。然而,与旨在在不更改任何代码的情况下管理应用程序的传统服务网格不同,Dapr 应用程序需要集成并积极利用外部 Dapr 构建块服务。
Dapr sidecars 中的微服务应用程序可以是从 Go 和 Rust 等语言编译的本机客户端 (NaCl) 应用程序,也可以是用 Python/ target=_blank class=infotextkey>Python 或 JavaScript 编写的托管语言应用程序。换句话说,sidecar 应用程序可以有自己的语言运行时。Sidecar 模型允许 Dapr 为其应用程序支持“任何语言、任何框架、任何地方”。
Dapr 可以直接在操作系统上运行 Sidecar 应用程序,也可以通过 Docker 等应用程序容器运行。容器提供了可移植性、易于部署和安全性等优点,但也带来了巨大的开销。
在本文中,我们介绍了一种运行 Dapr Sidecar 应用程序的新方法。我们使用一个用 Rust 或 Go 编写的简单 NaCl 来监听微服务的 API 请求。它将请求数据传递给 WebAssembly 运行时进行处理。微服务的业务逻辑是由应用程序开发人员创建和部署的 WebAssembly 功能。
图 1. 具有 WebAssembly 功能的 Dapr 微服务。
WebAssembly 运行时非常适合执行业务逻辑功能。
下表总结了 Sidecar 应用程序不同方法的优缺点。
WasmEdge是由 CNCF (云原生计算基金会)/linux 基金会托管 的领先的云原生 WebAssembly 运行时 。它是当今市场上最快的 WebAssembly 运行时。WasmEdge 支持所有标准 WebAssembly 扩展以及 Tensorflow 推理、KV 存储和图像处理等专有扩展。其编译器工具链不仅支持 C/C++、Rust、Swift、Kotlin 和 AssemblyScript 等 WebAssembly 语言,还支持 常规 JavaScript .
WasmEdge 应用程序可以嵌入到 C 程序、 Go 程序、 Rust 程序、 JavaScript 程序或操作系统的 CLI中。运行时可以由 Docker 工具(例如 CRI-O)、编排工具(例如 K8s)、无服务器平台(例如 Vercel、.NETlify、 AWS Lambda、 腾讯 SCF)和数据流框架(例如 YoMo 和 Zenoh)来管理。
在本文中,我将演示如何使用 WasmEdge 作为 Dapr 的 sidecar 应用程序运行时。
首先,您需要安装 Go、 Rust、 Dapr、 WasmEdge和 rustwasmc 编译器工具。
接下来,从 Github 复制或克隆演示应用程序。您可以将此 repo 用作您自己的应用程序模板。
$ git clone https://github.com/second-state/dapr-wasm
该演示有 3 个 Dapr sidecar 应用程序。
图 2. 演示应用程序中的 Dapr sidecar 微服务。
您可以按照 README 中的说明启动 sidecar 服务。以下是构建 WebAssembly 功能和启动 3 个 sidecar 服务的命令。
# Build the classify and grayscale WebAssembly functions, and deploy them to the sidecar projects
$ cd functions/grayscale
$ ./build.sh
$ cd ../../
$ cd functions/classify
$ ./build.sh
$ cd ../../
# Build and start the web service for the Application UI
$ cd web-port
$ go build
$ ./run_web.sh
$ cd ../
# Build and start the microservice for image processing (grayscale)
$ cd image-api-rs
$ cargo build
$ ./run_api_rs.sh
$ cd ../
# Build and start the microservice for tensorflow-based image classification
$ cd image-api-go
$ go build --tags "tensorflow image"
$ ./run_api_go.sh
$ cd ../
最后,您应该能够在浏览器中看到 Web UI。
图 3. 运行中的演示应用程序。
我们有两个用 Rust 编写并编译成 WebAssembly 的函数。它们部署在 sidecar 微服务中,以执行图像处理和分类的实际工作。
虽然我们的示例 WebAssembly 函数是用 Rust 编写的,但您可以将用 C/C++、Swift、Kotlin 和 AssemblyScript 编写的函数编译为 WebAssembly。WasmEdge 还支持用 JavaScript 和 DSL 编写的函数。
grayscale函数是一个 Rust 程序, 它读取图像数据 STDIN 并将灰度图像写入 STDOUT.
use image::{ImageFormat, ImageOutputFormat};
use std::io::{self, Read, Write};
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();
}
我们使用 rustwasmc 构建它,然后将其复制到 image-api-rs sidecar。
$ cd functions/grayscale
$ rustup override set 1.50.0
$ rustwasmc build --enable-ext
$ cp ./pkg/grayscale.wasm ../../image-api-rs/lib
分类函数是一个 Rust 函数,它将图像数据的 字节数组作为输入,并返回一个用于分类的字符串。它使用 WasmEdge TensorFlow API。
use wasmedge_tensorflow_interface;
pub fn infer_internal(image_data: &[u8]) -> String {
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");
let flat_img = wasmedge_tensorflow_interface::load_jpg_image_to_rgb8(image_data, 224, 224);
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");
// ... Map the probabilities in res_vec to text labels in the labels file ...
if max_value > 50 {
format!(
"It {} a <a href='https://www.google.com/search?q={}'>{}</a> in the picture",
confidence.to_string(),
class_name,
class_name
)
} else {
format!("It does not appears to be any food item in the picture.")
}
}
我们使用 rustwasmc 构建它,然后将其复制到 image-api-go sidecar。
$ cd functions/classify
$ rustup override set 1.50.0
$ rustwasmc build --enable-ext
$ cp ./pkg/classify_bg.wasm ../../image-api-go/lib/classify_bg.wasm
在接下来的三个部分中,我们将研究这三个 sidecar 服务。
image-api-rs sidecar 应用程序是用 Rust 编写的。 它应该已经从上一步安装了 WebAssembly 函数 lib/grayscale.wasm。请参考 functions/bin/install.sh 脚本来安装 WasmEdge 运行时二进制文件 lib/wasmedge-tensorflow-lite 及其依赖项。
Sidecar 微服务运行一个基于 Tokio 的事件循环,该循环在路径上侦听传入的 HTTP 请求 /api/image。
#[tokio::main]
pub async fn run_server(port: u16) {
pretty_env_logger::init();
let home = warp::get().map(warp::reply);
let image = warp::post()
.and(warp::path("api"))
.and(warp::path("image"))
.and(warp::body::bytes())
.map(|bytes: bytes::Bytes| {
let v: Vec<u8> = bytes.iter().map(|&x| x).collect();
let res = image_process(&v);
Ok(Box::new(res))
});
let routes = home.or(image);
let routes = routes.with(warp::cors().allow_any_origin());
let log = warp::log("dapr_wasm");
let routes = routes.with(log);
warp::serve(routes).run((Ipv4Addr::UNSPECIFIED, port)).await
}
一旦它在 HTTP POST 请求中接收到图像文件,它就会调用 WasmEdge 中的 WebAssembly 函数来执行图像处理任务。它创建了一个 WasmEdge 实例来与 WebAssembly 程序交互。
pub fn image_process(buf: &Vec<u8>) -> Vec<u8> {
let mut child = Command::new("./lib/wasmedge-tensorflow-lite")
.arg("./lib/grayscale.wasm")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
.expect("failed to execute child");
{
// limited borrow of stdin
let stdin = child.stdin.as_mut().expect("failed to get stdin");
stdin.write_all(buf).expect("failed to write to stdin");
}
let output = child.wait_with_output().expect("failed to wait on child");
output.stdout
}
以下 Dapr CLI 命令在 Dapr 运行时环境中启动微服务。
$ cd image-api-rs
$ sudo dapr run --app-id image-api-rs
--app-protocol http
--app-port 9004
--dapr-http-port 3502
--components-path ../config
--log-level debug
./target/debug/image-api-rs
$ cd ../
image-api-go sidecar 应用程序是用 Go 编写的。 它应该已经 lib/classify_bg.wasm 安装了上一步中的 WebAssembly 功能。请参考 functions/bin/install.sh 脚本来安装 WasmEdge Runtime Go SDK。
Sidecar 微服务运行一个事件循环,在路径上侦听传入的 HTTP 请求 /api/image。
func main() {
s := daprd.NewService(":9003")
if err := s.AddServiceInvocationHandler("/api/image", imageHandlerWASI); err != nil {
log.Fatalf("error adding invocation handler: %v", err)
}
if err := s.Start(); err != nil && err != http.ErrServerClosed {
log.Fatalf("error listenning: %v", err)
}
}
一旦它在 HTTP POST 请求中接收到图像文件,它就会调用 WasmEdge 中的 WebAssembly 函数来执行基于 Tensorflow 的图像分类任务。它利用 WasmEdge 的 Go API 与 WebAssembly 程序进行交互。
func imageHandlerWASI(_ context.Context, in *common.InvocationEvent) (out *common.Content, err error) {
image := in.Data
var conf = wasmedge.NewConfigure(wasmedge.REFERENCE_TYPES)
conf.AddConfig(wasmedge.WASI)
var vm = wasmedge.NewVMWithConfig(conf)
var wasi = vm.GetImportObject(wasmedge.WASI)
wasi.InitWasi(
os.Args[1:], /// The args
os.Environ(), /// The envs
[]string{".:."}, /// The mapping directories
[]string{}, /// The preopens will be empty
)
/// Register WasmEdge-tensorflow and WasmEdge-image
var tfobj = wasmedge.NewTensorflowImportObject()
var tfliteobj = wasmedge.NewTensorflowLiteImportObject()
vm.RegisterImport(tfobj)
vm.RegisterImport(tfliteobj)
var imgobj = wasmedge.NewImageImportObject()
vm.RegisterImport(imgobj)
vm.LoadWasmFile("./lib/classify_bg.wasm")
vm.Validate()
vm.Instantiate()
res, err := vm.ExecuteBindgen("infer", wasmedge.Bindgen_return_array, image)
ans := string(res.([]byte))
vm.Delete()
conf.Delete()
out = &common.Content{
Data: []byte(ans),
ContentType: in.ContentType,
DataTypeURL: in.DataTypeURL,
}
return out, nil
}
以下 Dapr CLI 命令在 Dapr 运行时环境中启动微服务。
$ cd image-api-go
$ sudo dapr run --app-id image-api-go
--app-protocol http
--app-port 9003
--dapr-http-port 3501
--log-level debug
--components-path ../config
./image-api-go
$ cd ../
Web UI 服务 web-port 是一个用 Go 编写的简单 Web 服务器。它提供静态文件夹中的静态 HTML 和 JavaScript 文件,并将上传到 /api/hello 的图像发送到 灰度 或 分类 边车的 /api/image 端点。
func main() {
http.HandleFunc("/static/", staticHandler)
http.HandleFunc("/api/hello", imageHandler)
println("listen to 8080 ...")
log.Fatal(http.ListenAndServe(":8080", nil))
}
func staticHandler(w http.ResponseWriter, r *http.Request) {
// ... read and return the contents of HTML css and JS files ...
}
func imageHandler(w http.ResponseWriter, r *http.Request) {
// ... ...
api := r.Header.Get("api")
if api == "go" {
daprClientSend(body, w)
} else {
httpClientSend(body, w)
}
}
// Send to the image-api-go sidecar (classify) via the Dapr API
func daprClientSend(image []byte, w http.ResponseWriter) {
// ... ...
resp, err := client.InvokeMethodWithContent(ctx, "image-api-go", "/api/image", "post", content)
// ... ...
}
// Send to the image-api-rs sidecar (grayscale) via the HTTP API
func httpClientSend(image []byte, w http.ResponseWriter) {
// ... ...
req, err := http.NewRequest("POST", "http://localhost:3502/v1.0/invoke/image-api-rs/method/api/image", bytes.NewBuffer(image))
// ... ...
}
page.js中的 JavaScript 只是将图像上传到 web-port sidecar 的 /api/hello 端点, web-port 将根据请求标头请求分类或灰度微服务 api。
function runWasm(e) {
const reader = new FileReader();
reader.onload = function (e) {
setLoading(true);
var req = new XMLHttpRequest();
req.open("POST", '/api/hello', true);
req.setRequestHeader('api', getApi());
req.onload = function () {
// ... display results ...
};
const blob = new Blob([e.target.result], {
type: 'application/octet-stream'
});
req.send(blob);
};
console.log(image.file)
reader.readAsArrayBuffer(image.file);
}
以下 Dapr CLI 命令启动静态 UI 文件的 Web 服务。
$ cd web-port
$ sudo dapr run --app-id go-web-port
--app-protocol http
--app-port 8080
--dapr-http-port 3500
--components-path ../config
--log-level debug
./web-port
$ cd ../
而已。您现在有一个用两种语言编写的由三部分组成的分布式应用程序!
正如我们所展示的,Dapr 的分布式网络运行时和 WasmEdge 的通用语言运行时之间有很多协同作用。这种方法可以推广并应用于其他服务网格或分布式应用程序框架。与 Dapr 不同,许多服务网格只能在 Kubernetes 作为其控制平面中运行,因此依赖于 Kubernetes API。WasmEdge 是与 Kubernetes 兼容的运行时,可以作为运行微服务的轻量级容器替代品发挥重要作用。敬请关注!