この記事は「Develop fun!」を体現する! Works Human Intelligence Advent Calendar 2024 シリーズ1の22日目の記事です。
概要(TL;DR)
- RustでWebサービスのバックエンド(サーバーサイド)を作ろうとした場合、サーバーフレームワークで人気なものは下記の3つがあるよ
- Actix Web
- axum
- Rocket
- それぞれのフレームワークについて、筆者の所感と簡単なサンプルコードを載せて紹介して、ついでに簡単なパフォーマンスの比較もするよ
- それぞれのフレームワークには長所・短所があるよ
- 業務で使ったり、規模の大きなシステムを開発するなら、カスタマイズが容易でエコシステムの成熟度が高いActix Webを使うのがいいと思うよ
- 個人で開発するような小規模のちょっとしたプロジェクトだったり、プロトタイピングだったりの用途なら、比較的手軽に使えるaxumかRocketがいいと思うよ
- axumとRocketのどちらを使うかは正直好みだけど、パフォーマンスはaxumのほうが良い傾向にあったよ
- パフォーマンスを比較した場合、
- 🥇Actix Web
- 🥈axum
- 🥉Rocket
という傾向が見られたよ。
- ただし、Actix Webは処理時間にバラツキが大きく、処理によってはパフォーマンスが大きく劣化する傾向にあったよ
- axumは全体的に比較的安定してパフォーマンスを出せていたよ
はじめに
Webサービスのバックエンド、Rustで書いてみたいと思ったことはありませんか?
でも正直どんなフレームワークがあるのか知らない、どれを選べばいいのか分からない、そんな経験はありませんか?
今回はそんな方のための記事です。
RustでWebサービスのバックエンドを書く意味?
「RustでWebサービスのバックエンドを書く意味ってあるの?」
「Rustって学習コストが高いって聞くんだけど……」
「どうせフロントエンドをTypeScriptで書くなら、バックエンドもTypeScriptのほうが学習コストやスイッチングコストが低くて良いんじゃ?」
そう考える方も多いかと思います。筆者も正直そう思います。
しかし、Rustの持つ「優れた実行速度」「メモリ安全性」「静的型付けによる型安全性」といった特徴は、Webサービスのバックエンドとして使用した場合でももちろん生かすことができます。(あと個人的には、Rustはコンパイルエラーの際のエラーログが読みやすくて好きです。)
高パフォーマンスで堅牢なバックエンドを構築したい場合、「Rustで書く」というのも選択肢の1つになり得るのでないでしょうか。
代表的なサーバーフレームワーク
こちらのGitHubリポジトリに、RustにおけるWebフレームワークの一覧がまとまっています。
今回はサーバーフレームワークを探しているので、"High-Level Server Frameworks"を見ていきましょう。
フレームワークの人気度を推し量る指標の1つであるStars1を見ると、Actix Web, axum, Rocket の3つが2万以上のStarを獲得しており、ズバ抜けて人気であることが分かります(ちなみに第4位のPoemが3700程度なので、1~3位が4位に6倍近い差を付けている状況であることが分かります)。
なので今回は、それら3つのフレームワークを「Rustにおける現時点で代表的なサーバーフレームワーク」として見ていこうと思います。
サンプルとして作成するREST API
アドベントカレンダーサービス🎄のバックエンドを作ることを想定し、下記のAPIを作成します。
この程度ならフロントエンドで完結させた方が圧倒的に楽ですが、あくまでサンプルということで
-
エンドポイント:
GET /count
-
クエリパラメータ:
date
(任意、YYYY-MM-DD
形式) -
機能:
- 指定日付から、その年のクリスマスまでの日数を返す
- 日付を省略した場合、現在日付(日本時間)から今年のクリスマスまでの日数を返す
-
レスポンス: JSON形式
-
days_until_christmas
: クリスマスまでの日数(数値型)- クリスマスを過ぎている場合(12/26~12/31)、負値が入る
-
-
エラー: 無効な日付フォーマットの場合、
400 Bad Request
を返す- この場合、レスポンスボディには
Invalid date format. Please use YYYY-MM-DD.
を返す
- この場合、レスポンスボディには
例:
- リクエスト:
GET /count?date=2024-12-22
- レスポンス:
{"days_until_christmas":3}
- レスポンス:
- リクエスト:
GET /count
(※12/26にリクエストを送信した場合)- レスポンス:
{"days_until_christmas":-1}
- レスポンス:
- リクエスト:
GET /count?date=merry-x-mas
- レスポンス:
Invalid date format. Please use YYYY-MM-DD.
(※HTTPステータスコードは400
)
- レスポンス:
サンプルコードを動かすには
$ cargo new advent-server
などで新しいプロジェクトを作成します。
ディレクトリの中に下記のような内容のCargo.toml
が生成されているはずです
[package]
name = "advent-server"
version = "0.1.0"
edition = "2021"
その下に、本記事の各サンプルコードに記載されているCargo.toml
の[dependencies]
以下をコピーして貼り付けます。
src/main.rs
に、本記事の各サンプルコードに記載されているmain.rs
の内容をコピーして貼り付けます。
$ cargo run
を実行します。
http://127.0.0.1/count
こちらのURLにブラウザからにアクセスするか、curlなどでGET
リクエストを飛ばします。
なお、本記事で使用したRustのバージョンは、執筆時点で最新の安定版である1.83.0
です。
Actix Web
個人的な所感
良い点
- 細かいカスタマイズが容易
- 小回りがきいて、かゆいところに手の届きやすい
- 今回取り上げた3つのフレームワークの中で、エコシステムの成熟度が最も高い
- 何か困っても、ネット上に資料が比較的豊富
気になる点
- 他のフレームワークに比べ、書き方に癖があったりする箇所もあり、やや学習コストが高い印象
その他
このフレームワークを紹介している記事やサイトの中には「アクターモデルで設計されている」のような記述がされていることがありますが、これは古い(もしくは間違った)情報のようです。
Long ago, Actix Web was built on top of the actix actor framework. Now, Actix Web is largely unrelated to the actor framework and is built using a different system. Though actix is still maintained, its usefulness as a general tool is diminishing as the features and async/await ecosystem matures. At this time, the use of actix is only required for WebSocket endpoints.
にもあるように、アクターモデルを使用して設計されているのは派生元プロジェクトのactixであり、Actix Webがアクターモデルで設計されているという公式の記述は見つけられませんでした。一応Actix Webも昔はactixを使用していたようですが、(少なくとも現在では)ほぼ別のシステムとなっているという理解が正しいようです。
そしてActix Webを使用する上で「アクターモデルを学習や意識しておかなければならない」ということも、少なくとも筆者が使用している範囲ではありませんでした。
サンプルコード
使用したバージョンは、執筆時点で最新安定版である4.9.0
です。
[dependencies]
actix-web = "4"
serde = { version = "1.0", features = ["derive"] }
chrono = "0.4"
use actix_web::{get, web, App, HttpResponse, HttpServer, Responder};
use chrono::{Datelike, FixedOffset, Local, NaiveDate};
use serde::{Deserialize, Serialize};
#[derive(Deserialize)]
struct CountQuery {
date: Option<String>,
}
#[derive(Serialize)]
struct CountResult {
days_until_christmas: i64,
}
#[get("/count")]
async fn count_to_christmas(query: web::Query<CountQuery>) -> impl Responder {
let input_param = query.into_inner();
let target_date = if let Some(input_date) = input_param.date {
match NaiveDate::parse_from_str(&input_date, "%Y-%m-%d") {
Ok(parsed_date) => parsed_date,
Err(_) => {
return HttpResponse::BadRequest()
.body("Invalid date format. Please use YYYY-MM-DD.");
}
}
} else {
let jst_offset = FixedOffset::east_opt(9 * 3600).unwrap();
Local::now().with_timezone(&jst_offset).date_naive()
};
let christmas_date = NaiveDate::from_ymd_opt(target_date.year(), 12, 25).unwrap();
let response = CountResult {
days_until_christmas: (christmas_date - target_date).num_days(),
};
HttpResponse::Ok().json(response)
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
HttpServer::new(|| App::new().service(count_to_christmas))
.bind(("127.0.0.1", 8080))?
.run()
.await
}
axum
個人的な所感
良い点
- シンプルであまりライブラリの癖がなく、学習コストが低め
- ルーティングやリクエストの処理もシンプルで理解しやすい
気になる点
- まだバージョンが1.0に到達しておらず、破壊的変更が頻繁にある
- 下記のサンプルコードを書くだけでも、
axum::serve
回りが0.6
→0.7
でかなり変わっており、公式以外のドキュメントも少ないため、少し手こずった
- 下記のサンプルコードを書くだけでも、
サンプルコード
使用したバージョンは、執筆時点で最新の安定版である0.7.9
です。
[dependencies]
axum = "0.7"
tokio = { version = "1", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
chrono = "0.4"
use axum::http::StatusCode;
use axum::{extract::Query, routing::get, Json, Router};
use chrono::{Datelike, FixedOffset, Local, NaiveDate};
use serde::Deserialize;
use serde::Serialize;
#[derive(Deserialize)]
struct CountQuery {
date: Option<String>,
}
#[derive(Serialize)]
struct CountResponse {
days_until_christmas: i64,
}
async fn count_to_christmas(
Query(query): Query<CountQuery>,
) -> Result<Json<CountResponse>, (StatusCode, &'static str)> {
let target_date = if let Some(date_str) = query.date {
match NaiveDate::parse_from_str(&date_str, "%Y-%m-%d") {
Ok(parsed_date) => parsed_date,
Err(_) => {
return Err((
StatusCode::BAD_REQUEST,
"Invalid date format. Please use YYYY-MM-DD.",
));
}
}
} else {
let jst_offset = FixedOffset::east_opt(9 * 3600).unwrap();
Local::now().with_timezone(&jst_offset).date_naive()
};
let christmas_date = NaiveDate::from_ymd_opt(target_date.year(), 12, 25).unwrap();
Ok(Json(CountResponse {
days_until_christmas: (christmas_date - target_date).num_days(),
}))
}
#[tokio::main]
async fn main() {
let app = Router::new().route("/count", get(count_to_christmas));
let listener = tokio::net::TcpListener::bind("127.0.0.1:8080")
.await
.unwrap();
axum::serve(listener, app).await.unwrap();
}
Rocket
個人的な所感
良い点
- 様々な面でフレームワークの支援が手厚く、DX(開発者体験)が良い
- フレームワーク自体から出力されるログが豊富で読みやすい
- マクロが優れており、直感的に読み書きしやすい
-
#[get("/count?<date>")]
のようなマクロを書けるのが個人的には好み
-
- フレームワークが想定している使い方から外れなければ、シンプルかつ読みやすいコードでサクッと書くことができる
気になる点
- 少しでも複雑だったり凝ったことをしようとすると、途端に難しくなる
- まだバージョンが1.0に到達しておらず、破壊的変更が頻繁にある
- 下記のサンプルコードを書くだけでも、
rocket::Config
回りが0.4
→0.5
でかなり変わっており、公式以外のドキュメントも少ないため、少し手こずった
- 下記のサンプルコードを書くだけでも、
サンプルコード
使用したバージョンは、執筆時点で最新の安定版である0.5.1
です。
[dependencies]
rocket = { version = "0.5.1", features = ["json"] }
serde = { version = "1.0", features = ["derive"] }
chrono = "0.4"
#[macro_use]
extern crate rocket;
use std::net::{IpAddr, Ipv4Addr};
use chrono::{Datelike, FixedOffset, Local, NaiveDate};
use rocket::{response::status::BadRequest, serde::json::Json};
use serde::Serialize;
#[derive(Serialize)]
struct CountResponse {
days_until_christmas: i64,
}
#[get("/count?<date>")]
fn count_to_christmas(
date: Option<String>,
) -> Result<Json<CountResponse>, BadRequest<&'static str>> {
let target_date = if let Some(date_str) = date {
match NaiveDate::parse_from_str(date_str.as_str(), "%Y-%m-%d") {
Ok(parsed_date) => parsed_date,
Err(_) => {
return Err(rocket::response::status::BadRequest(
"Invalid date format. Please use YYYY-MM-DD.",
));
}
}
} else {
let jst_offset = FixedOffset::east_opt(9 * 3600).unwrap();
Local::now().with_timezone(&jst_offset).date_naive()
};
let christmas_date = NaiveDate::from_ymd_opt(target_date.year(), 12, 25).unwrap();
Ok(Json(CountResponse {
days_until_christmas: (christmas_date - target_date).num_days(),
}))
}
#[launch]
fn rocket() -> _ {
rocket::build()
.configure(
rocket::Config::figment()
.merge(("address", IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))))
.merge(("port", 8080)),
)
.mount("/", routes![count_to_christmas])
}
パフォーマンス比較
最後に、パフォーマンスを比較しているサイトで少しだけベンチマーク結果を見てみましょう2。
上記サイトから執筆時点での最新の結果である2024-12-16
の結果を引用させていただくと、
恐らく最も総合的にパフォーマンスを判断しやすい結果である"Total Requests per Second"(1秒間にリクエストを何回処理できるか。グラフが高いほど嬉しい)では、1位がActix Web、2位がaxum、3位がRocketという結果でした。
上記の結果を、「1位に対して何パーセントのパフォーマンスが出せているか」という観点でまとめたのが下記の表です。
フレームワーク | Requests/s (64) | 1位に対する割合 | Requests/s (256) | 1位に対する割合 | Requests/s (512) | 1位に対する割合 |
---|---|---|---|---|---|---|
Actix Web | 510,640 | 100.0% | 568,150 | 100.0% | 590,376 | 100.0% |
axum | 363,603 | 71.2% | 498,614 | 87.8% | 518,739 | 87.9% |
Rocket | 285,274 | 55.9% | 323,742 | 57.0% | 333,245 | 56.4% |
1位のActix Webと比較すると、axumは70%~90%弱、Rocketは50%台後半のパフォーマンスしか出せていないという結果になりました。ただし、Concurrency(同時実行数)を増やすとActix Webとaxumの差は15%程度改善している、という結果になっています。
"50th Percentile Latency"(個々の結果についてレイテンシを時間でソートしたときに、50%のところに来た値がどれぐらいか。グラフが低いほど嬉しい)を見ると、やはりActix Webがトップ、2位がaxum、3位がRocketという結果です。
全体のうち半数のリクエストをこの時間以下で処理できるということなので、一見するとActix Webが優秀そうに見えますね。
ところが、75, 90, 99, 99.999と、よりパーセンタイル順位が下位の結果(リクエストの処理に時間がかかったケース)を見ていくと、また違った結果になってきます。
下位のパーセンタイルになればなるほど、Actix Webのパフォーマンス低下が酷い結果になっていますね。
どうやらActix Webはリクエストの処理時間が、他のフレームワークにくらべてバラツキが大きい(もしかして極端に苦手な処理やボトルネックになっている処理がある?)ということが言えそうです。
もちろん、このサイトとは別のデータや測定方法などで測定すれば、また違う結果が出る可能性もありますのであくまで参考程度3ですが、傾向として
- パフォーマンスが最も良さそうなのはActix Web
- ただし同時実行数を増やすにつれて、リクエストの処理能力ではaxumも差を縮めてくる
- レイテンシについても、50パーセンタイルでは Actix Web > axum > Rocket の順位
- ただし、Actix Webは下位のパーセンタイルになればなるほどパフォーマンス低下が顕著になる傾向が見られた
- axumは全体的に比較的安定してパフォーマンスを出せていそう
- Rocketは3つのフレームワークの中では最もパフォーマンスが低いが、下位のパーセンタイルになると若干健闘する
ということが言えそうです。
おわりに
以上、Rustにおける現時点で代表的なサーバーフレームワークを3つ概観してきました。
現状での個人的な所感としては、次のような使い分けをすると良いんじゃないかな、と感じています。
- 業務で使いたい場合や大規模なシステムのバックエンドに使いたい場合など、堅実さで選びたい場合 → Actix Web
- 比較的エコシステムの成熟度が高く、何か困ってもネット上に情報もある
- 破壊的変更が入る頻度や可能性が他のフレームワークに比べて低い
- カスタマイズが割と容易
- 学習コストは他のフレームワークと比べて比較的高め
- パフォーマンスに関して最も優秀だが、処理によっては大幅に劣化する場合がある
- ちょっとした個人プロジェクトやプロトタイピングなど、どちらかといえばお手軽さで選びたい場合 → axum or Rocket
- Actix Webと比べて学習コストが低く、気軽に試しやすい
- フレームワークの想定した使い方から大きく外れなければ、あまり苦労せずサクッと書くことが可能
- どちらもまだバージョンが1.0に到達しておらず、破壊的変更が頻繁に入る可能性が非常に高い
- 業務だったり大規模なシステムで使う用途には不向きかも?
- axumとRocketのどちらを選ぶかは、好みの問題になってきそう
- axumはどちらかといえばシンプルで素直なフレームワークという印象
- Rocketはどちらかといえば凝ったフレームワークで手厚い支援をする代わり、型にはまった使い方をさせようとする印象
- パフォーマンスはaxumのほうが優れている傾向
今年のアドベントカレンダーも例年通り自分の趣味全開の記事になってしまいましたが、皆様のRustにおけるサーバーフレームワーク選びの参考になれば幸いです🦀
参考文献
-
そのフレームワークのGitHubリポジトリがどれだけのユーザーに「お気に入り」としてマークされたかを表す ↩
-
こちらのサイトでは表記が"actix"のみになっていますが、念のためベンチマークのコードを見るとActix Webを使用していることが分かります。 ↩
-
他に有名なサーバーフレームワークのベンチマークサイトですと、TechEmpower
Framework Benchmarksもあるのですが、こちらはRocketを計測していないため、今回は取り上げませんでした。 ↩