lambda@edge上でwasmを使用した画像リサイズにおける速度

9 min read読了の目安(約8300字

はじめに

最近、lambdaでwasmを利用できることを知ったので、画像のリサイズ処理をwasm(Rust)で行った場合の速度を計測してみました。

環境構築

環境構築には下記を使用しました。

  • Terraform
  • Docker
  • Git

AWSリソースの用意

AWSリソースの作成にはTerraformで行います。
今回使用したtfファイルはこちらになります。

AWS 構成

作成する手順は下記の通り。

git clone https://github.com/takenoko-gohan/terraform-image-resizing-on-cloudfront.git
cd terraform-image-resizing-on-cloudfront
terraform init
# 問題がないか確認
# 変数 prefix は任意の値を指定してください
terraform plan -var="prefix=hoge"
# 問題がなければ AWS リソースの作成
terraform apply -var="prefix=hoge"

Lambda@edgeの用意

nodejs

デプロイパッケージの作成

比較対象のnodejsはAWSのブログを参考に下記コードを書きました。
BUCKETは自身の環境に合わせて記述します。

index.js
"use strict";

const http = require("http");
const https = require("https");
const querystring = require("querystring");
const Sharp = require("sharp");

const AWS = require("aws-sdk");
const S3 = new AWS.S3({
    signatureVersion: "v4",
});

// 自環境の S3 バケットを指定してください
const BUCKET = "";

exports.handler = (event, context, callback) => {
    let response = event.Records[0].cf.response;
 
    let request = event.Records[0].cf.request;
    let params = querystring.parse(request.querystring);

    console.log(JSON.stringify(params));

    // パラメーター d の指定がない場合、そのまま返す
    if (!params.d) {
        callback(null, response);
        return;
    }

    // リサイズ後の幅と高さを取得する
    let dimension = params.d.split("x");
    let width = parseInt(dimension[0], 10);
    let height = parseInt(dimension[1], 10);
  
    // URI から必要な情報を取得
    let path = request.uri;

    let key = path.substring(1);

    let prefix, originalKey, match, imageName, requiredFormat;

    try {
        match = key.match(/(.*)\/(.*)/);
        prefix = match[1];

        imageName = match[2];
        requiredFormat = imageName.split(".")[1];
        
        originalKey = prefix + "/" + imageName;
    }
    catch (err) {
        console.log("no prefix present..");
        match = key.match(/(.*)/);

        imageName = match[1];

        requiredFormat = imageName.split(".")[1];
        
        originalKey = imageName;
    }

    // 画像を取得し、リサイズ
    console.log("start getObject");
    S3.getObject({ Bucket: BUCKET, Key: originalKey }).promise()
        .then(data => Sharp(data.Body)
            .resize(width, height)
            .toFormat(requiredFormat)
            .toBuffer()
        )
        .then(result => {
            // generate a binary response with resized image
            response.status = 200;
            response.body = result.toString("base64");
            response.bodyEncoding = "base64";
            response.headers["content-type"] = [{ key: "Content-Type", value: "image/" + requiredFormat }];
            console.log("response :", JSON.stringify(response));
            callback(null, response);
        })
    .catch( err => {
        console.log("Exception while reading source image :%j",err);
    });
};

次に下記のDockerfileを使用してデプロイパッケージを作成します。

docker pull node:12-buster-slim
docker run --rm --volume ${PWD}/<index.jsが格納されているディレクトリ>:/build node:12-buster-slim /bin/bash -c "cd /build; npm init -f -y; npm install sharp --save; npm install querystring --save; npm install --only=prod"
cd <index.jsが格納されているディレクトリ>
zip -r app.zip ./* 
Lambda@edgeにデプロイ

作成したデプロイパッケージを{ prefix }-image-resize-nodejsにアップロードし、「トリガーを追加」からLambda@edgeにデプロイします。

wasm

リポジトリ
index.jsはnodejsと同じで画像リサイズ処理をwasmに置き換えているだけです。

@@ -3,7 +3,7 @@
 const http = require("http");
 const https = require("https");
 const querystring = require("querystring");
-const Sharp = require("sharp");
+const wasm = require("./wasm/image_resize");
 
 const AWS = require("aws-sdk");
 const S3 = new AWS.S3({
@@ -62,15 +62,11 @@
     // 画像を取得し、リサイズ
     console.log("start getObject");
     S3.getObject({ Bucket: BUCKET, Key: originalKey }).promise()
-        .then(data => Sharp(data.Body)
-            .resize(width, height)
-            .toFormat(requiredFormat)
-            .toBuffer()
-        )
+        .then(data => wasm.resize(data.Body, width, height, requiredFormat))
         .then(result => {
             // generate a binary response with resized image
             response.status = 200;
-            response.body = result.toString("base64");
+            response.body = result;
             response.bodyEncoding = "base64";
             response.headers["content-type"] = [{ key: "Content-Type", value: "image/" + requiredFormat }];
             console.log("response :", JSON.stringify(response));

wasmではネットワーク通信、ファイル操作が行えないようなので、nodejs側が取得したS3オブジェクトのバッファーを受け取って処理するようにしました。

extern crate base64;
extern crate console_error_panic_hook;
extern crate wasm_bindgen;
extern crate web_sys;

use image::{self, imageops::FilterType};
use wasm_bindgen::prelude::*;

macro_rules! log {
    ( $( $t:tt )* ) => {
        web_sys::console::log_1(&format!( $( $t )* ).into());
    }
}

#[wasm_bindgen]
pub fn resize(buf: Vec<u8>, width: u32, height: u32, format: &str) -> String {
    console_error_panic_hook::set_once();

    log!("start wasm function");

    // nodejs から渡されたバッファーを読み込む
    log!("start buffer load");
    let img = image::load_from_memory(&buf).expect("buffer load error");
    log!("end buffer load");

    // リサイズ
    log!("start image resize");
    let resize_image = img.resize_to_fill(width, height, FilterType::Triangle);
    log!("end image resize");

    // リサイズ結果を書き込む
    log!("start image write to buffer");
    let mut result: Vec<u8> = Vec::new();

    match format {
        "jpeg" | "jpg" => {
            log!("match jpeg");
            match resize_image.write_to(&mut result, image::ImageOutputFormat::Jpeg(80)) {
                Ok(_) => log!("buffer write sucess"),
                Err(err) => log!("buffer write error: {}", err),
            }
        },
        "png" => {
            log!("match png");
            match resize_image.write_to(&mut result, image::ImageOutputFormat::Png) {
                Ok(_) => log!("buffer write sucess"),
                Err(err) => log!("buffer write error: {}", err),
            }
        },
        _ => {
            log!("did not match");
        },
    }
    log!("end image write to buffer");

    log!("end wasm function");

    // BASE64 で返す
    return base64::encode(&result);
}
デプロイパッケージの作成
git clone https://github.com/takenoko-gohan/image-resizing-with-rust-wasm-on-lambda-edge.git

# wasm のビルド準備
cd image-resizing-with-rust-wasm-on-lambda-edge/image_resize
rustup target add wasm32-unknown-unknown
cargo install wasm-pack 

# wasm のビルド
wasm-pack build --target nodejs --release --out-dir ../app/wasm

# BUCKET を編集
cd ../
vim app/index.js

# デプロイパッケージの作成
docker pull node:12-buster-slim
docker run --rm --volume ${PWD}/app:/build node:12-buster-slim /bin/bash -c "cd /build; npm init -f -y; npm install querystring --save; npm install --only=prod"
cd app
zip -r app.zip ./*
Lambda@edgeにデプロイ

nodejsと同様に{ prefix }-image-resize-wasmにアップロードし、Lambda@edgeにデプロイします。

速度

それぞれのCloudfrontでa(927KB)、b(4.2MB)、c(9.9MB)の画像を400x400にリサイズした場合の速度は下記のようになりました。

nodejs

a(927KB) b(4.2MB) c(9.9MB)
1回目 573ms 1130ms 1960ms
2回目 456ms 990ms 1850ms
3回目 450ms 904ms 1780ms
4回目 494ms 950ms 1640ms
5回目 455ms 935ms 1730ms
平均 486ms 982ms 1792ms

wasm

a(927KB) b(4.2MB) c(9.9MB)
1回目 1810ms 6630ms 10340ms
2回目 1890ms 6160ms 10100ms
3回目 1840ms 6080ms 10050ms
4回目 1770ms 6060ms 10020ms
5回目 1760ms 6010ms 9950ms
平均 1814ms 6188ms 10092ms

Cloudwatch Logsでログを見てみるとwasmではnodejsから渡されたバッファーの読み込みに時間がかかっていたようでした。(画像のログはcをリサイズしたときのものになります。)

さいごに

最近Rustを学習し始めたので、こうしたほうがwasmの速度が早くなるなどのアドバイスをいただければ幸いです。