Actix Webは、Rust製の非同期Webフレームワークであり、高スループットと低レイテンシを両立させたマイクロサービス基盤として注目を集めている。本稿では、実践的なコード例とパフォーマンスチューニング手法を交えながら、分散環境で活きる設計パターンを解説する。
非同期ランタイムの内部構造
Actix WebはTokioランタイムの上に独自のActorレイヤーを載せており、リクエストごとに独立した「Context」を生成することで競合を回避する。以下は最小構成のサーバ起動例である。
use actix_web::{web, App, HttpServer};
#[actix_web::main]
async fn main() -> std::io::Result<()> {
HttpServer::new(|| {
App::new()
.route("/health", web::get().to(health_check))
})
.bind(("0.0.0.0", 3000))?
.workers(num_cpus::get())
.run()
.await
}
async fn health_check() -> impl actix_web::Responder {
"OK"
}
workers()にCPUコア数を渡すことで、ワーカーごとに独立したイベントループが生成され、ロックフリーなスケーリングを実現する。
ミドルウェアチェーンの組み立て方
Actix Webのミドルウェアは「Transform」トレイトを実装した構造体として定義される。以下はカスタムタイムアウトミドルウェアの実装例。
use std::time::{Duration, Instant};
use actix_web::{
dev::{Service, ServiceRequest, ServiceResponse, Transform},
Error,
};
use futures_util::future::{ok, Ready};
pub struct TimeoutMiddleware {
pub limit: Duration,
}
impl<S, B> Transform<S, ServiceRequest> for TimeoutMiddleware
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
B: 'static,
{
type Response = ServiceResponse<B>;
type Error = Error;
type InitError = ();
type Transform = TimeoutService<S>;
type Future = Ready<Result<Self::Transform, Self::InitError>>;
fn new_transform(&self, service: S) -> Self::Future {
ok(TimeoutService {
service,
limit: self.limit,
})
}
}
pub struct TimeoutService<S> {
service: S,
limit: Duration,
}
impl<S, B> Service<ServiceRequest> for TimeoutService<S>
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
B: 'static,
{
// ...省略...
}
このミドルウェアをApp::wrap()で登録するだけで、全エンドポイントに横断的にタイムアウトを適用できる。
型安全ルーティングと抽出器
パスパラメータやクエリ文字列はコンパイル時に検証されるため、ランタイムエラーがゼロに近づく。以下は複合的な抽出器の使用例。
use actix_web::{get, web, Result};
use serde::Deserialize;
#[derive(Deserialize)]
struct PageQuery {
offset: Option<usize>,
limit: Option<usize>,
}
#[get("/repos/{owner}/{name}/issues")]
async fn list_issues(
path: web::Path<(String, String)>,
query: web::Query<PageQuery>,
) -> Result<String> {
let (owner, repo) = path.into_inner();
let PageQuery { offset, limit } = query.into_inner();
Ok(format!(
"owner={}, repo={}, offset={:?}, limit={:?}",
owner, repo, offset, limit
))
}
抽出器は自動的にバリデーションを実行し、不正なリクエストに対しては400レスポンスを返す。
負荷分散とサービスメッシュ連携
Actix Web単体でも高い性能を発揮するが、KubernetesやConsulなどのサービスディスカバリ基盤と組み合わせることで水平スケーラビリティが飛躍的に向上する。以下はEnvoyサイドカー構成でのヘルスチェックエンドポイント例。
use actix_web::{middleware::Logger, web, App, HttpResponse, HttpServer};
#[actix_web::main]
async fn main() -> std::io::Result<()> {
std::env::set_var("RUST_LOG", "actix_web=info");
env_logger::init();
HttpServer::new(|| {
App::new()
.wrap(Logger::default())
.service(
web::scope("/ready")
.route("", web::get().to(ready))
.route("/live", web::get().to(live)),
)
})
.bind(("0.0.0.0", 8080))?
.run()
.await
}
async fn ready() -> HttpResponse {
// DB接続などの準備チェック
HttpResponse::Ok().body("ready")
}
async fn live() -> HttpResponse {
HttpResponse::Ok().body("live")
}
/readyはPodがトラフィックを受け入れられる状態かを判定し、/liveはプロセス生存確認に使われる。
メモリ効率を高めるレスポンスストリーミング
巨大なJSONやCSVを返す際、チャンク単位でストリーミングすることでメモリ占有量を大幅に削減できる。
use actix_web::{get, web, HttpResponse};
use futures_util::stream::{repeat, StreamExt};
#[get("/stream/{n}")]
async fn counter_stream(path: web::Path<usize>) -> HttpResponse {
let n = path.into_inner();
let body = repeat(())
.take(n)
.enumerate()
.map(|(i, _)| Ok(web::Bytes::from(format!("{}\n", i + 1))));
HttpResponse::Ok()
.content_type("text/plain")
.streaming(body)
}
このエンドポイントはn個の数字を逐次送信し、サーバ側は全体をメモリに載せないため、GB単位のデータでも安定して配信できる。
まとめ
Actix Webは単なるHTTPサーバではなく、Actorモデル、ゼロコピーI/O、型安全APIを統合した分散システムの要石である。上記パターンを踏襯することで、Rustのメモリ安全性とActixの非同期性能を最大限に活用したサービスを構築できる。