作者 Robby Qiu, Second State 開發與 WasmEdge 貢獻者
Serverless 函式為開發者節省了管理後端基礎設施的大量麻煩。Serverless 還簡化了開發過程,因為開發者只需關注業務本身的邏輯。本文是有關如何在 Amazon 的 serverless 計算平臺 AWS Lambda 上編寫和部署 WebAssembly serverless 函式的分步指南。在我們的演示中,WebAssembly 函式使用 WasmEdge runtime 執行。下圖顯示了我們解決方案的整體架構。
在本文的第一部分,我們將解釋為什麼 WebAssembly 是 serverless 函式極佳的 runtime。我們將 WebAssembly 位元組碼(通常由 Rust、C++ 編譯得來)、高階程式語言(例如 Python 和 JavaScript)以及機器本機可執行檔案(本機客戶端或 NaCl)進行比較。然後,在第二部分,我們將演示兩個 serverless 函式示例,都是用 Rust 編寫並編譯為 WebAssembly 進行部署。第一個示例展示了 WasmEdge 快速處理影象的能力,而第二個示例執行由 WasmEdge 的 TensorFlow 擴充套件提供支援的 AI 推理。
為什麼選擇 WebAssembly?
簡單回答是 WebAssembly 快速、安全且可移植。那麼具體是為什麼呢?下面是詳細回答。
WebAssembly vs. Python 和 JavaScript
DataDog 最近的一項調查發現大部分 AWS Lambda serverless 函式是用 JavaScript 和 Python 寫的。 二者是世界上最流行的兩種程式語言,所以這並不出人意料。
但是,眾所周知,高階語言執行速度非常慢。 事實上,根據發表在Science上的一篇論文 ,Python 比用 C 或 C++ 編寫的相同程式最多慢 60,000 倍。
因此,雖然 JavaScript 和 Python 非常適合簡單的函式,但它們不適合計算密集型任務,例如影象、影片、音訊和自然語言處理,這些在現代應用程式中越來越普遍。
另一方面,WebAssembly 的效能與 C/C++ 編譯的本機二進位制檔案 (NaCl) 相當,同時仍保持與高階語言 runtime 相關的可移植性、安全性和可管理性。 WasmEdge 是市場上目前最快的WebAssembly runtime 之一。
WebAssembly vs. 原生客戶端
但是,當兩者都在 Docker 容器或者 microVM 內部執行的時候, WebAssembly 相比 NaCl 的優勢有哪些呢?
我們對未來的願景是在原生基礎設施中, WebAssembly 作為一個替代輕量級 runtime,與 Docker 和 microVM 並行執行。與類似 Docker 的容器或 microVM 相比,WebAssembly 效能更加出色並且消耗的資源更少。但就目前而言,AWS Lambda 和許多其他平臺僅支援在 microVM 內執行 WebAssembly。儘管如此,與執行容器化的 NaCl 程式相比,在 microVM 中執行 WebAssembly 函式仍然具有許多優勢。
首先,WebAssembly 為單個函式提供了細粒度的 runtime 隔離。一個微服務可以有多個函式並支援在一個 microVM 中執行的服務。 WebAssembly 可以讓微服務更安全、更穩定。
其次,WebAssembly 位元組碼是可移植的。即使在容器內,NaCl 仍然依賴於安裝在 OS 上的底層 CPU、作業系統和動態庫。 而 WebAssembly 位元組碼應用程式是跨平臺的。開發者只需編寫一次即可部署在任何雲、任何容器和任何硬體平臺上。
第三,WebAssembly 應用**易於部署和管理。**與 NaCl 動態庫和可執行檔案相比,它們的平臺依賴性和複雜性要少得多。
最後,WebAssembly 是多語言的。 C/C++、Rust、Swift、Kotlin 程式都可以輕鬆編譯成 WebAssembly。 WebAssembly 甚至支援 JavaScript。 WasmEdge Tensorflow API 提供了以 Rust 程式語言執行 Tensorflow 模型的最符合習慣的方式。
我們能夠看到,WebAssembly + WasmEdge 是一個更好的選擇。為了實際見證這個結論,讓我們深入示例,親自上手吧!
前期準備
由於我們的 demo WebAssembly 函式是用 Rust 寫的,你需要安裝一個 Rust 編譯器。 確保你添加了 wasm32-wasi 編譯器目標(如下),從而生成 WebAssembly 位元組碼。
$ rustup target add wasm32-wasi
該 demo 應用程式前端是 Next.js 寫的,並部署在 AWS Lambda 上。我們假設你已經有使用 Next.js 和 Lambda 的基礎知識了。
案例1:影象處理
我們的第一個 demo 應用程是讓使用者上傳一個影象,然後使用者呼叫 serverless 函式將其變成黑白的。 你可以檢視已經透過 GitHub Pages 部署好的實時 demo。
demo 連結: https://second-state.github.io/aws-lambda-wasm-runtime/
Fork demo 應用程式的 GitHub repo ,就可以開始部署自己的函數了。將應用程式部署在 AWS Lambda 上的具體流程,請參考 repository 中的 README 教程。
模板 GitHub repo:https://github.com/second-state/aws-lambda-wasm-runtime
建立函式
模板 repo 是一個標準的 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 artifact 複製到 api 資料夾。
$ cp target/wasm32-wasi/release/grayscale.wasm ../../
當我們構建 docker 映象時,會執行 api/pre.sh。 pre.sh 安裝 WasmEdge runtime,然後將每個 WebAssembly 位元組碼程式編譯為原生 so 庫以加快執行速度。
建立服務指令碼,載入函式
api/hello.js 指令碼載入 WasmEdge runtime,在 WasmEdge 中啟動編譯了的 WebAssembly 程式,並將已上傳的圖片資料透過 STDIN傳遞。 注意 api/hello.js 執行已編譯的由 api/pre.sh 產生的 grayscale.so 檔案,以達到更佳的效能。
const { spawn } = require('child_process');const path = require('path');function _runWasm(reqBody) { return new Promise(resolve => { 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); resolve(buf); }); wasmedge.stdin.write(reqBody); wasmedge.stdin.end(''); });}
hello.js的 exports.handler 部分匯出一個非同步函式處理程式,用於每次呼叫 serverless 函式時處理不同的事件。 在這個例子中,我們只是透過呼叫上面的函式來處理影象並返回結果,但你可以根據需要定義更復雜的事件處理行為。 我們還需要返回一些 Access-Control-Allow header 以避免在從瀏覽器呼叫 servereless 時發生跨域資源共享 Cross-Origin Resource Sharing (CORS) 錯誤。 如果你在複製我們的示例時遇到 CORS 錯誤,你可以在此處檢視更多有關 CORS 錯誤的資訊。
exports.handler = async function(event, context) { var typedArray = new Uint8Array(event.body.match(/[\da-f]{2}/gi).map(function (h) { return parseInt(h, 16); })); let buf = await _runWasm(typedArray); return { statusCode: 200, headers: { "Access-Control-Allow-Headers" : "Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token", "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT" }, body: buf.toString('hex') };}
構建 Docker 映象用於 Lambda 部署
現在我們有了 WebAssembly 位元組碼函式和指令碼來載入和連線到 Web 請求。 為了將它們部署為 AWS Lambda 上的函式服務,仍然需要將整個內容打包到 Docker 映象中。
我們不會詳細介紹如何構建 Docker 映象並在 AWS Lambda 上部署,你可以參考 README 中的 deploy 部分 。 但是,我們將突出顯示 Dockerfile 中的一部分,以避免一些陷阱。
FROM public.ecr.aws/lambda/nodejs:14# Change directory to /var/taskWORKDIR /var/taskRUN yum update -y && yum install -y curl tar gzip# Bundle and pre-compile the wasm filesCOPY *.wasm ./COPY pre.sh ./RUN chmod +x pre.shRUN ./pre.sh# Bundle the JS filesCOPY *.js ./CMD [ "hello.handler" ]
首先,我們從 AWS Lambda 的 Node.js 基礎映象 構建映象。使用 AWS Lambda 基礎映象的優勢在於它包含了 Lambda Runtime 介面客戶端 (RIC),當我們在 AWS Lambda 部署 Docker 映象時需要這個。 Amazon Linux 使用 yum 作為包管理器。
這些基本映象包含 Amazon Linux Base 作業系統、給定語言的 runtime、依賴項和 Lambda runtime 介面客戶端 (RIC),它實現 Lambda Runtime API。 Lambda Runtime API 客戶端允許你的 runtime 從 Lambda 服務接收請求並向其傳送請求。
其次,我們需要將我們的函式及其所有依賴項放在 /var/task 目錄中。 AWS Lambda 不會執行其他資料夾中的檔案。
第三,我們需要在啟動容器時定義預設命令。 CMD [ "hello.handler" ] 意味著只要呼叫 serverless 函式,我們就會呼叫 hello.js 中的 handler 函式。回想一下,我們在前面的步驟中透過 hello.js 中的 exports.handler = ... 定義並匯出了 handler 函式。
可選:在本地測試 Docker 映象
你可以按照 AWS 給出的指南在本地測試從 AWS Lambda 的基礎映象中構建的 Docker 映象。 本地測試需要 AWS Lambda Runtime Interface Emulator (RIE) ,它已經安裝在所有 AWS Lambda 的基礎映象中。 要測試你的映象,首先,透過執行以下命令啟動 Docker 容器:
docker run -p 9000:8080 myfunction:latest
該命令在你的本地機器設定了一個函式端點 http://localhost:9000/2015-03-31/functions/function/invocations.
然後從一個獨立的終端視窗,執行:
curl -XPOST "http://localhost:9000/2015-03-31/functions/function/invocations" -d '{}'
你應在終端中獲得預期的輸出。
如果你不想使用來自 AWS Lambda 的基礎映象,你也可以使用自己的基礎映象並在構建 Docker 映象時安裝 RIC 和/或 RIE。 只需按照 AWS 給出的指南,從替代基礎映象部分建立映象即可。
就是這樣! 構建 Docker 映象後,你可以按照 repo 中的 README 概述的步驟將其解壓到 AWS Lambda 。 現在,你的serverless 函式已準備就緒!讓我們看看第二個高難度的函式
案例2: AI推理
第二個 demo 應用程式是讓使用者上傳圖片,然後觸發一個 serverless 函式對圖片上的主要物品進行識別。
[Image: aws-lambda-wasmedge-runtime-tensorflow.gif] 它與上一個示例位於同一 GitHub repo 中,但位於 tensorflow 分支中。 用於影象分類的後端 serverless 函式位於 tensorflow 分支的 api/functions/image-classification 資料夾中。 src/main.rs檔案包含 Rust 程式的原始碼。 Rust 程式從 STDIN 讀取影象資料,然後將指令碼輸出輸出到 STDOUT。 它利用 WasmEdge Tensorflow API 來執行 AI 推理。
AI 推理模板:https://github.com/second-state/aws-lambda-wasm-runtime/tree/tensorflow
$ 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檔案以獲得更好的效能。 Handler 函式和我們前面的例子類似,這裡不再詳述了。
const { spawn } = require('child_process');const path = require('path');function _runWasm(reqBody) { return new Promise(resolve => { 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) => { resolve(d.join('')); }); wasmedge.stdin.write(reqBody); wasmedge.stdin.end(''); });}exports.handler = ... // _runWasm(reqBody) is called in the handler
你可以按照上一個示例中講述的方式構建 Docker 映象並部署該函式。 現在你已經建立了一個用於主題分類的 Web 應用程式!
展望未來
從部署在 AWS Lambda 上的 Docker 容器執行 WasmEdge 是一種向 Web 應用程式新增高效能函式的簡單方法。 展望未來,更好的方法是使用WasmEdge作為容器本身。 這樣就無需 Docker 和 Node.js 來裝 WasmEdge。這樣一來,我們執行 serverless 函式的效率就更高了。 WasmEdge 已經與 Docker 工具相容。 如果你有興趣加入 WasmEdge 和 CNCF 一起進行這項激動人心的工作,請告訴我們!