サーバレスにおけるRustについて

この記事は、 NTT Communications Advent Calendar 2023 22日目の記事です。

はじめに

こんにちは、イノベーションセンターの鈴ヶ嶺です。普段は、クラウド・ハイブリッドクラウド・エッジデバイスなどを利用したAI/MLシステムに関する業務に従事しています。

本記事は、各クラウドベンダーのサーバレスにおけるプログラミング言語Rustについて調査・比較した結果を紹介します。 まず初めにサーバレスでRustを利用するメリットをエネルギー効率の観点から説明し、次に各クラウドベンダーの関連記事をピックアップします。 さらに、それぞれのクラウドでRustを使ったサーバレスアプリの代表的な作成方法を紹介して比較します。

Rustのエネルギー効率

Rustは、次の公式ページでも宣伝している通りパフォーマンスを強くアピールしています。

Rustは非常に高速でメモリ効率が高くランタイムやガベージコレクタがないため、パフォーマンス重視のサービスを実装できますし、組込み機器上で実行したり他の言語との調和も簡単にできます。

https://www.rust-lang.org/ja

ポルトガルのポルト大学にある研究機関INESC TECから発表されたプログラミング言語別にエネルギー効率を比較した論文からも上位に位置する性能を示していることが分かります。

Ranking programming languages by energy efficiency Table 41

多くのサーバレスの課金体系はリソースの実行時間に依存するため、エネルギー効率がよく高速に処理可能なRustはコスト的な観点から非常に有用です。

関連記事

このセクションでは各クラウドベンダーがRustに言及している記事をピックアップしました。

Why AWS loves Rust, and how we’d like to help

AWSはRustを高く評価しており、Rustコミュニティに積極的に貢献していくことを表明したブログ記事です。 具体的には、オープンソースプロジェクトへのスポンサーやTokioコミッターの雇用などを行なっています。

AWS re:Invent 2023 - “Rustifying” serverless: Boost AWS Lambda performance with Rust (COM306)

2023年のAWSの年次会議AWS re:Invent 2023で発表されたBreakout sessionです。 内容はAWS SAMとcargo-lambdaを使用してRust関数をデプロイする方法やPyO3, maturin, AWS SDK for Rustを使って既存のPythonで実装されたAWS Lambda関数にRustを組み込む方法を紹介しています。

発表中にはPythonとRustをコスト面で比較して、Rustがより経済的であることも示しています。

“Rustifying” Serverless: Boost AWS Lambda performance with Rust

Rust on Cloud Run

Google Cloudの公式YoutubeチャンネルGoogle Cloud TechでCloud Run上においてRustを動作させる方法を解説する動画を公開しています。

次のようにRustのメリットとして

  • 高速な起動による、バースト時のスケールアップ
  • シングルバイナリによる扱いやすさ
  • メモリ、CPU利用の高い効率による低コスト
  • クリティカルタスクに対するパフォーマンス

などが挙げられています。

https://www.youtube.com/watch?v=rOMroL3mhO4&t=262s

Do We Need Rust Language Support for Azure?

Microsoft Build 2023で行われたAzureにおけるRustのニーズを議論するセッションです。 残念ながらセッション内容自体は動画が配信されていないため把握できませんがRustコミュニティへのサポートや貢献がここから伺えます。

以上の様に、各クラウドベンダーはRustの高速性に伴うコストメリットなどに着目してコミュニティへの貢献やニーズ調査を積極的に行なっていることが分かりました。 次のセクションでは実際にそれぞれのクラウドでRustを使ったサーバレスを利用する代表的な方法を紹介します。

Amazon Web Services(AWS)

AWSでは、AWS Lambdaを利用してサーバレスアプリを作成します。 Lambdaは課金単位が1ミリ秒単位のため高速に実行可能なRustによる低コスト化が期待できます。

AWS Lambdaの開発を円滑に進める周辺ツールにcargo-lambdaがあるため、今回はこちらを利用します。

実際のコマンド例

# install cargo-lambda
brew tap cargo-lambda/cargo-lambda
brew install cargo-lambda

# create project
cargo lambda new new-lambda-project --http
cd new-lambda-project

# debug
cargo lambda watch
curl http://localhost:9000

# deploy
cargo lambda build --release
cargo lambda deploy --enable-function-url # function url発行 option
cargo lambda invoke --remote new-lambda-project --data-example apigw-request # デプロイされたlambdaをテスト実行

テンプレートとして以下のようなものが作成されます。

main.rs

use lambda_http::{run, service_fn, Body, Error, Request, RequestExt, Response};

/// This is the main body for the function.
/// Write your code inside it.
/// There are some code example in the following URLs:
/// - https://github.com/awslabs/aws-lambda-rust-runtime/tree/main/examples
async fn function_handler(event: Request) -> Result<Response<Body>, Error> {
    // Extract some useful information from the request
    let who = event
        .query_string_parameters_ref()
        .and_then(|params| params.first("name"))
        .unwrap_or("world");
    let message = format!("Hello {who}, this is an AWS Lambda HTTP request");

    // Return something that implements IntoResponse.
    // It will be serialized to the right response event automatically by the runtime
    let resp = Response::builder()
        .status(200)
        .header("content-type", "text/html")
        .body(message.into())
        .map_err(Box::new)?;
    Ok(resp)
}

#[tokio::main]
async fn main() -> Result<(), Error> {
    tracing_subscriber::fmt()
        .with_max_level(tracing::Level::INFO)
        // disable printing the name of the module in every log line.
        .with_target(false)
        // disabling time is handy because CloudWatch will add the ingestion time.
        .without_time()
        .init();

    run(service_fn(function_handler)).await
}

また、他AWSサービスと連携する場合は、AWS SDK for Rustを用います。 注目するポイントしてAWS SDK for Rustは2023年11月27にGAとなっています。 今まで性能は良い一方で、SDKがプレビューという理由で採用を見送ってきたケースを解決すると思われます。

AWS SDK for Rust の一般提供を開始

https://aws.amazon.com/jp/about-aws/whats-new/2023/11/aws-sdk-rust/

Microsoft Azure

Azureでは、Azure Functionsカスタム ハンドラーを利用してサーバレスアプリを作成します。 Azure Functionsは課金単位が1ミリ秒単位のためAWS Lambdaと同様に低コスト化が期待できます。

ツールとしてAzure Functions Core Toolsを利用します。

プロジェクト構成は次の様に準備します。

host.json ファイル: アプリのルート local.settings.json ファイル: アプリのルート function.json ファイル: 関数ごとに必要 (関数名と一致する名前のフォルダー内) コマンド、スクリプト、または実行可能ファイル: Web サーバーを実行

| /MyQueueFunction
|   function.json
|
| host.json
| local.settings.json
| handler.exe

https://learn.microsoft.com/ja-jp/azure/azure-functions/functions-custom-handlers#application-structure

HttpExample/function.json

{
  "bindings": [
    {
      "authLevel": "function",
      "type": "httpTrigger",
      "direction": "in",
      "name": "req",
      "methods": [
        "get"
      ]
    },
    {
      "type": "http",
      "direction": "out",
      "name": "res"
    }
  ]

host.json defaultExecutablePath で実行バイナリを指定しています。

{
  "version": "2.0",
  "logging": {
    "applicationInsights": {
      "samplingSettings": {
        "isEnabled": true,
        "excludedTypes": "Request"
      }
    }
  },
  "extensionBundle": {
    "id": "Microsoft.Azure.Functions.ExtensionBundle",
    "version": "[3.*, 4.0.0)"
  },
  "customHandler": {
      "description": {
        "defaultExecutablePath": "handler",
        "workingDirectory": "",
        "arguments": []
      },
      "enableForwardingHttpRequest": true
  }
}

local.settings.json

{
  "IsEncrypted": false,
  "Values": {
    "FUNCTIONS_WORKER_RUNTIME": "Custom"
  }
}

今回は、次の様なRustアプリを用意します。 環境変数 FUNCTIONS_CUSTOMHANDLER_PORT でポートを指定するところがポイントになります。

HTTP イベントを処理するために Web サーバーを実行し、FUNCTIONS_CUSTOMHANDLER_PORT を介して要求をリッスンするように設定されています。

https://learn.microsoft.com/ja-jp/azure/azure-functions/functions-custom-handlers

main.rs

use actix_web::{get, web, App, HttpServer, Responder};
use serde::Deserialize;
use std::env;

#[derive(Deserialize)]
struct Info {
    name: String,
}

#[get("/api/HttpExample")]
async fn greet(info: web::Query<Info>) -> impl Responder {
    format!("Hello {}!", info.name)
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    let port_key = "FUNCTIONS_CUSTOMHANDLER_PORT";
    let port: u16 = match env::var(port_key) {
        Ok(val) => val.parse().expect("Custom Handler port is not a number!"),
        Err(_) => 8080,
    };
    HttpServer::new(|| App::new().service(greet))
        .bind(("127.0.0.1", port))?
        .run()
        .await
}

実際のコマンド例

# install Azure Functions Core Tools
brew tap azure/functions
brew install azure-functions-core-tools@4

# create project
cargo new handler
cd handler

cargo add actix-web                                                                                                                                                                                                                   
cargo add serde --features derive
## edit src/main.rs

# project setup

mkdir HttpExample
## edit HttpExample/function.json

touch host.json
## edit host.json

touch local.settings.json
## edit local.settings.json

# debug
cargo build
cp target/debug/handler .
func start --custom --verbose
curl "http://localhost:7071/api/HttpExample?name=World"

# deploy

## Linux muslでcross build
brew tap messense/macos-cross-toolchains
brew install x86_64-unknown-linux-musl
export CC_x86_64_unknown_linux_musl=x86_64-unknown-linux-musl-gcc
export CXX_x86_64_unknown_linux_musl=x86_64-unknown-linux-musl-g++
export AR_x86_64_unknown_linux_musl=x86_64-unknown-linux-musl-ar
export CARGO_TARGET_X86_64_UNKNOWN_LINUX_MUSL_LINKER=x86_64-unknown-linux-musl-gcc
cargo build --release --target=x86_64-unknown-linux-musl
cp target/x86_64-unknown-linux-musl/release/handler .

## Resource Group, Storage Account, Azure Functionを事前に作成
az group create --name xxxxxxxxxxxxxxxxxx --location japaneast
az storage account create --name yyyyyyyyyyyyyyy --location japaneast --resource-group xxxxxxxxxxxxxxxxxx --sku Standard_LRS
az functionapp create -g xxxxxxxxxxxxxxxxxx -n zzzzzzzzzzzzzzzzz -s yyyyyyyyyyyyyyy --functions-version 4 --consumption-plan-location japaneast --os-type Linux --runtime custom

## Azure Functions Core Toolsでdeploy
func azure functionapp publish zzzzzzzzzzzzzzzzz --custom

また、他Azureサービスと連携する場合は、Azure SDK for Rustを利用するのが一般的かと思います。 こちらはunofficialなSDKのため継続した利用や新しいサービスへの対応については注意が必要です。

Google Cloud

Google Cloudでは、Cloud Runを利用してサーバレスアプリを作成します。

今回は次のようなRustアプリを用意しました。 環境変数 PORT でポートを設定します。デフォルトは8080を利用します。

use actix_web::{get, web, App, HttpServer, Responder};
use serde::Deserialize;
use std::env;

#[derive(Deserialize)]
struct Info {
    name: String,
}

#[get("/")]
async fn greet(info: web::Query<Info>) -> impl Responder {
    format!("Hello {}!", info.name)
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    let port_key = "PORT";
    let port: u16 = match env::var(port_key) {
        Ok(val) => val.parse().expect("PORT is not a number!"),
        Err(_) => 8080,
    };
    HttpServer::new(|| App::new().service(greet))
        .bind(("0.0.0.0", port))?
        .run()
        .await
}

Cloud Runではコンテナを利用します。

FROM rust:1.74.1

WORKDIR /usr/src/app
COPY . .

RUN cargo install --path .
CMD ["helloworld-rust"]%   

実際のコマンド例

# create project
cargo new helloworld-rust
cd helloworld-rust

cargo add actix-web                                                                                                                                                                                                                   
cargo add serde --features derive
## edit src/main.rs

tourch Dockerfile
## edit Dockerfile

# build
docker build --platform=linux/amd64 -t gcr.io/xxxxxxxxxxxxxxxxx/helloworld-rust .

# debug
docker run -it --rm -p 8080:8080 gcr.io/xxxxxxxxxxxxxxxxx/helloworld-rust .
curl "http://localhost:8080/?name=world"

# deploy
docker push gcr.io/xxxxxxxxxxxxxxxxx/helloworld-rust
gcloud run deploy --image=gcr.io/xxxxxxxxxxxxxxxxx/helloworld-rust .

また、他Google Cloudサービスと連携する場合は、Google Cloud SDK for Rustを利用するのが一般的かと思います。 こちらはMicrosoftと同様にunofficialなSDKのため継続した利用や新しいサービスへの対応については注意が必要です。

比較

それぞれのサーバレスアプリの作成方法を比較した表が次の様になります。

Amazon Web Services Microsoft Azure Google Cloud
サーバレスサービス AWS Lambda Azure Functions Cloud Run
課金単位 1ミリ秒単位 1ミリ秒単位 100ミリ秒単位
SDK(他クラウドサービスとの連携) AWS SDK for Rust(2023-11-27 GA) Azure SDK for Rust(unofficial) Google Cloud SDK for Rust(unofficial)

AWS Lambda, Azure Functionsについては、1ミリ秒単位で課金がされるためエネルギー効率の高いRustによるコストメリットを受けやすいのかと思います。 SDKについては唯一AWSが公式ツールを提供している様な状況です。今まで性能面ではRustのメリットを理解はしていたが、他クラウドサービスとの連携面で採用が難しいと考えていた面を再考しても良いのではと思いました。

まとめ

本記事では、Rustの高いエネルギー効率によるサーバレスの低コスト化のメリットや各クラウドベンダーのRustへの取り組みを紹介しました。 また、それぞれのクラウドでRustを使ったサーバレスアプリの代表的な作成方法を紹介して比較しました。

それでは、明日の記事もお楽しみに!


  1. Pereira, Rui, et al. "Ranking programming languages by energy efficiency." Science of Computer Programming 205 (2021): 102609.
© NTT Communications Corporation 2014