電通総研 テックブログ

電通総研が運営する技術ブログ

Rust製WebフレームワークのaxumとActix webの「Hello, world!」を比較してみた

こんにちは。電通総研の多田圭佑です。
本記事は電通総研 Advent Calendar 2024 の12月20日の記事です!
前日の記事は平岡恵里さんの「Azure SOCで活躍している便利なサービスたち」でした。

はじめに

みなさんはRustのWebフレームワークを使用したことはありますか?
RustにはいくつかのWebフレームワークがありますが、中でもaxumとActix Webは機能が豊富で開発も活発なことから人気があります。
どのような違いがあるのか、今回は簡単に「Hello, world」を比較しながら違いを見ていきたいと思います。
使用した環境およびバージョンは以下です。

項目 情報
OS Windows 11
Rust 1.83.0
axum 0.7.9
Actix Web 4.9.0

axumとActix Web

冒頭で述べた通りRustのWebフレームワークはいくつかあります。
以下がよくまとめられていて参考になります。

https://github.com/flosse/rust-web-framework-comparison?tab=readme-ov-file#server-frameworks

掲題の2つのWebフレームワークについてはGitHubから引用しつつ概要を記載します。

axumの概要

axum is a web application framework that focuses on ergonomics and modularity.

「axumは人間工学とモジュール性に重点を置いたWebアプリケーションフレームワークである」
とでも訳せるでしょうか。axumの特徴は以下のとおりです。

  • Rustの非同期ランタイムであるTokioとHyper(HTTPライブラリ)で構成されている
  • 設計の中心にRustの強力な型システムを据え、リクエストやレスポンスの処理を安全かつ明確に行える
  • Towerミドルウェアを活用することで、柔軟で再利用可能な構成が可能となっている

また、axumを人間工学やモジュール性の観点から見た特徴は以下のとおりです。

  • 人間工学
    • シンプルなAPIと型安全性、Rustエコシステムとの統合により、直感的でミスを減らす設計がなされている
  • モジュール性
    • Towerの利用や、ハンドラやミドルウェアを独立した構成単位として扱える設計により、拡張性と再利用性の高い設計がなされている

ちなみにdocs.rsはaxumで動いています。
https://docs.rs/
Cargo.toml

あと余談ですがaxumのaは小文字が正式です(私はActix Webに引きずられてずっと大文字だと思ってました)。

Actix Webの概要

Actix Web is a powerful, pragmatic, and extremely fast web framework for Rust

「Actix WebはRustで開発された高性能で拡張性の高いWebフレームワーク」といったところでしょうか。
概要は以下のとおりです。

  • Actorモデルに基づいた非同期フレームワークであるActixがベースとなっている
  • 高速かつ安全なHTTPサーバー構築が可能で、大規模なアプリケーションにも対応している
  • 非同期処理のためにTokioランタイムを使用している

全文検索で有名なmeilisearchはActix Webを使用していますね。
https://www.meilisearch.com/
Cargo.toml

Hello, world!

さて、本題です。さっそくコードを見ていきましょう。
まずはaxumです。

use axum::{extract::Path, response::IntoResponse, routing::get, Router};

async fn greet(Path(name): Path<String>) -> impl IntoResponse {
    format!("Hello, {name}!")
}

#[tokio::main]
async fn main() {
    let app = Router::new().route("/hello/:name", get(greet));
    let listener = tokio::net::TcpListener::bind("127.0.0.1:3000")
        .await
        .unwrap();
    axum::serve(listener, app).await.unwrap();
}

続いてActix Webです。

use actix_web::{get, web::Path, App, HttpServer, Responder};

#[get("/hello/{name}")]
async fn greet(name: Path<String>) -> impl Responder {
    format!("Hello, {name}!")
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| App::new().service(greet))
        .bind(("127.0.0.1", 8080))?
        .run()
        .await
}

いかがでしょうか。雰囲気はとても似ていますね。
両者の細かな違いとして以下をピックアップします。

  • mainマクロ
  • ルーティング
  • リクエストとレスポンス

mainマクロ

  • axum: #[tokio::main]
  • Actix Web: #[actix_web::main]

となっています。
それぞれmain関数を非同期化するためのマクロを使用していますが、利用しているライブラリが違います。
マクロを展開した後のmain関数を見てみましょう。
cargo expand を使用すると展開されたコードを簡単に確認できます。
まずはaxumです。

fn main() {
    let body = async {
        let app = Router::new().route("/hello/:name", get(greet));
        let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await.unwrap();
        axum::serve(listener, app).await.unwrap();
    };
    #[allow(clippy::expect_used, clippy::diverging_sub_expression)]
    {
        return tokio::runtime::Builder::new_multi_thread()
            .enable_all()
            .build()
            .expect("Failed building the Runtime")
            .block_on(body);
    }
}

続いてActix Webです。

fn main() -> std::io::Result<()> {
    <::actix_web::rt::System>::new()
        .block_on(async move {
            {
                HttpServer::new(|| App::new().service(greet))
                    .bind(("127.0.0.1", 8080))?
                    .run()
                    .await
            }
        })
}

axumの#[tokio::main]Tokioのmain関数を非同期化するマクロでaxum固有のものではありません。
そのため詳細はTokioの公式ドキュメントなどを参照していただければと思いますが
今回のコードはサーバーを起動し、サーバーが停止するまでmain関数の終了をブロックしている、というイメージです。

Actix Web側も同様に block_on(...) がありますね。
実はこちらも裏でTokioが使用されているため、コードのイメージはaxumとほぼ同じです。
Actix Webはactix-rtというActixエコシステムの非同期ランタイム(以降、Actixランタイム)を使用しています。
ActixランタイムはもともとTokioを使用しておらず、独自の非同期ランタイムでした。
しかし、TokioがRustエコシステムの標準ランタイムとして確立されたことなどから、Tokioベースにリライトされました。
そのためActix WebはTokioに対応しており、実は#[tokio::main]も使用できます。
ただしActixのActorの機能を使う場合はActixランタイムが必要なため、#[actix_web::main]を使用する必要があります。

各サーバー処理(axum::serveHttpServer::new...)の裏側はかなり異なるのですが、長くなってしまうため今回は軽くご紹介する程度にとどめます。
別の機会に当ブログでご紹介したいと思っていますが、興味のある方は調べてみてください。

axumの場合

axumのサーバーはシンプルな構成でTokioランタイムとHyperがリクエストを非同期タスクとして処理します。
コードもかなりシンプルです。
https://github.com/tokio-rs/axum/blob/9983bc1da460becd3a0f08c513d610411e84dd43/axum/src/serve.rs#L224

Actix Webの場合

Actix Webのサーバーは起動時にワーカースレッド(OSスレッド)を用意します。
ワーカースレッドの数はデフォルトだと論理CPU分ですが、コードで数を設定することも可能です。
このワーカースレッドたちがリクエストを非同期タスクとして処理する形になります。
Actixランタイムの場合とTokioランタイムの場合で内部的な動作が若干異なりますが、こちらも詳細に入ると長くなってしまうため別の機会にします。
コードは以下のあたりです。
Actixランタイムの場合
Tokioランタイムの場合

axumもnew_multi_thread()でOSスレッドが起動しますが、管理はTokioランタイムにお任せでActix Webのように独自の管理はしていません。

ルーティング

Actix Webはマクロベースのルーティングを提供していますが、実はaxumのようなルーティングも可能です。
axumと比較すると、axumの方がto(...)が無い分ほんのわずかにシンプルですが、ほぼ変わりませんね。

use actix_web::{web::Path, web::get, App, HttpServer, Responder};

async fn greet(name: Path<String>) -> impl Responder {
    format!("Hello {name}!")
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
//  HttpServer::new(|| App::new().service(web::resource("/hello/{name}").to(greet)))
//  ↑も可能
    HttpServer::new(|| App::new().route("/hello/{name}", get().to(greet)))
        .bind(("127.0.0.1", 8080))?
        .run()
        .await
}

リクエストとレスポンス

リクエストはどちらも関数の引数として受けています。
axumはデストラクチャリングする形で受け取っています。
JavaScriptのスプレッド構文と同じような感じです)
パスパラメーターが増えた場合は以下のように拡張可能です。

async fn greet(Path((name, friend_name)): Path<(String, String)>) -> impl IntoResponse {
    format!("Hello {name} and {friend_name}!")
}

タプル構造体であるPathはメンバーを公開しているため(pub Tの部分)、上記のようにデストラクチャリングできます。

// axumのPath
pub struct Path<T>(pub T);

あるいは構造体を使用することも可能です。

#[derive(Deserialize)]
pub struct PathInfo {
    pub name: String,
    pub friend_name: String,
}

async fn greet(Path(p): Path<PathInfo>) -> impl IntoResponse {
    format!("Hello {} and {}!", p.name, p.friend_name)
}

一方、Actix Webはaxumのようにデストラクチャリングする形で増やせません。
下記はコンパイル時にエラーとなります。

// コンパイルできません
#[get("/hello/{name}/{friend_name}")]
async fn greet(Path((name, friend_name)): Path<(String, String)>) -> impl Responder {
    format!("Hello {} and {}!", name, friend_name)
}

これはActix WebのPathがメンバーをaxumのように公開していないためです。

// Actix WebのPath
pub struct Path<T>(T);

しかしできなくても全く問題ありません。構造体は問題なく使用できるからです。

// 構造体はaxumと同様
#[get("/hello/{name}/{friend_name}")]
async fn greet(p: Path<PathInfo>) -> impl Responder {
    format!("Hello {} and {}!", p.name, p.friend_name)
}

pPath<PathInfo>型であるものの、Rustの型推論などにより自動的にPathInfo型への参照に変換されます。
そのためPathInfoのフィールドにアクセスできています。

レスポンスはほぼ同じですね。
レスポンスのトレイトであるIntoResponse(axum)、Responder(Actix Web)を実装すればOKのようです。
主要な型(今回のStringなど)はWebフレームワーク側でだいたい実装済です。
https://docs.rs/axum/latest/axum/response/trait.IntoResponse.html
https://docs.rs/actix-web/latest/actix_web/trait.Responder.html

コンパイル後のバイナリについて

ここまでコードの違いを見てきましたが、最後にコンパイル後のバイナリについて触れます。
それぞれコンパイル後のバイナリサイズを比較したところ、Actix Webはaxumと比べて大きくなりました。
これはActix Webで使用しているライブラリがaxumに比べて多いためと推測されます。
ただ、Hello, worldで使用しているルーティングのマクロだけに機能を絞るとほぼ同じになりました。
Cargo.tomlでdefault-featuresを無効化し、featuresを設定することで絞れます。

最適化なし 最適化あり
axum 2476kb 486kb
Actix Web 9417kb 1798kb
Actix Web(絞った後) 3642kb 592kb

使用した最適化オプションは以下のとおりです。

[profile.release]
strip = true
opt-level = 's'
lto = true
codegen-units = 1
panic = "abort"

まとめ

今回はaxumとActix WebのHello, worldを比較してみました。
雰囲気は似ているものの、細かな部分で違いがありました。
mainマクロで触れた各フレームワークのサーバー処理の詳細についてはまた別途ブログにまとめたいと思います。

コードは以下にまとめました。
https://github.com/keisuketada/axum-actixweb-hello

ここまでお読みいただきありがとうございました。

私たちは一緒に働いてくれる仲間を募集しています!

電通総研 キャリア採用サイト 電通総研 新卒採用サイト

執筆:@tada.keisuke、レビュー:@nagamatsu.yuji
Shodoで執筆されました