この記事は、 NTT Communications Advent Calendar 2023 22日目の記事です。
はじめに
こんにちは、イノベーションセンターの鈴ヶ嶺です。普段は、クラウド・ハイブリッドクラウド・エッジデバイスなどを利用したAI/MLシステムに関する業務に従事しています。
本記事は、各クラウドベンダーのサーバレスにおけるプログラミング言語Rustについて調査・比較した結果を紹介します。 まず初めにサーバレスでRustを利用するメリットをエネルギー効率の観点から説明し、次に各クラウドベンダーの関連記事をピックアップします。 さらに、それぞれのクラウドでRustを使ったサーバレスアプリの代表的な作成方法を紹介して比較します。
Rustのエネルギー効率
Rustは、次の公式ページでも宣伝している通りパフォーマンスを強くアピールしています。
Rustは非常に高速でメモリ効率が高くランタイムやガベージコレクタがないため、パフォーマンス重視のサービスを実装できますし、組込み機器上で実行したり他の言語との調和も簡単にできます。
ポルトガルのポルト大学にある研究機関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利用の高い効率による低コスト
- クリティカルタスクに対するパフォーマンス
などが挙げられています。
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
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を使ったサーバレスアプリの代表的な作成方法を紹介して比較しました。
それでは、明日の記事もお楽しみに!
- Pereira, Rui, et al. "Ranking programming languages by energy efficiency." Science of Computer Programming 205 (2021): 102609.↩