AxumとTonicのTower抽象化レイヤーを解き明かす
Olivia Novak
Dev Intern · Leapcell

はじめに
ネットワークプログラミングの急速に進化する状況において、堅牢でスケーラブル、かつ保守性の高いサービスを構築することは最重要です。パフォーマンスと安全性に重点を置くRustは、そのようなシステムを開発するための強力な候補となっています。Webアプリケーション向けのAxumとgRPCサービス向けのTonicという2つの著名なフレームワークは、Towerと呼ばれる強力な基盤となる抽象化を利用しています。Towerは、モジュール式でコンポーズ可能な方法でネットワークサービスを構築するための手段を提供し、リクエストルーティング、エラーハンドリング、ミドルウェア統合などの一般的な課題に対処します。この記事は、TowerのコアコンポーネントであるService、Layer、BoxCloneServiceを解明し、それらがAxumとTonicのバックボーンをどのように形成し、エレガントで拡張性の高いサービスアーキテクチャを可能にしているかを説明することを目的としています。これらの抽象化を理解することは、単なる学術的な演習ではありません。これらのフレームワークの可能性を最大限に引き出し、開発者が高度にカスタマイズされた効率的なサービスを作成できるようになります。
Towerコアの理解
AxumとTonicがTowerをどのように利用しているかを詳しく見る前に、その基本的な構成要素についての明確な理解を確立しましょう。
Serviceトレイト
Towerの中心にあるのはServiceトレイトです。これは、リクエストを受け取り、レスポンスまたはエラーに解決されるFutureを返す非同期関数を表します。入来するアイテムを処理し、出ていくアイテムを生成するあらゆるコンポーネントの汎用インターフェイスと考えてください。
pub trait Service<Request>: Sized { type Response; type Error; type Future: Future<Output = Result<Self::Response, Self::Error>>; fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>>; fn call(&mut self, req: Request) -> Self::Future; }
Request: このサービスが受け入れる入力の型。Response: このサービスが生成する成功した出力の型。Error: このサービスが返す可能性のあるエラーの型。Future: 最終的にResponseまたはErrorで完了する非同期操作。poll_ready: このメソッドはバックプレッシャーにとって重要です。サービスが新しいリクエストを受け入れる準備ができているかどうかをシグナルで通知できます。Poll::Pendingを返した場合、呼び出し元はcallを呼び出す前に待機する必要があります。call: これは、サービスがRequestを処理し、最終的なResponseを表すFutureを返すコアロジックです。
Axumのコンテキストでは、ServiceはしばしばHTTPハンドラーを表し、http::Requestを受け取り、http::Responseを返します。Tonicでは、gRPCリクエストをレスポンスに変換するgRPCメソッドを処理します。
Layerトレイト
Serviceは作業の単一の単位を定義しますが、Layerはサービスを合成および変更するためのメカニズムを提供します。Layerは基本的にサービスの高階関数です。内部のServiceを受け取り、横断的関心事を追加または動作を変更する新しい(おそらくラップされた)Serviceを返します。
pub trait Layer<S> { type Service: Service<S::Request, Response = S::Response, Error = S::Error>; fn layer(&self, inner: S) -> Self::Service; }
S: このレイヤーがラップする内部サービスの型。Service: このレイヤーによって生成される新しい、ラップされたサービスの型。layer: このメソッドはinnerサービスを受け取り、新しいサービスを返します。
Layerはミドルウェアの基本です。一般的な例としては、次のようなものがあります。
- ロギングレイヤー: 入ってくるリクエストと出ていくレスポンスをログに記録します。
 - レート制限レイヤー: サービスが処理できるリクエストの数に制限を強制します。
 - 認証レイヤー: 内部サービスにリクエストを転送する前に資格情報をチェックします。
 - メトリクスレイヤー: リクエスト期間などのパフォーマンスデータを収集します。
 
簡単なロギングレイヤーで説明しましょう。
use std::future::Future; use std::pin::Pin; use std::task::{Context, Poll}; use tower::{Service, Layer}; // デモンストレーション用のダミーリクエストとレスポンス #[derive(Debug)] struct MyRequest(String); struct MyResponse(String); type MyError = std::io::Error; // シンプルなエラー型 // 例示的なサービス struct MyService; impl Service<MyRequest> for MyService { type Response = MyResponse; type Error = MyError; type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>; fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> { Poll::Ready(Ok(())) } fn call(&mut self, req: MyRequest) -> Self::Future { println!(" (Inner Service) Processing request: {}", req.0); Box::pin(async move { Ok(MyResponse(format!("Response to {}", req.0))) }) } } // 私たちのロギングミドルウェア型 struct LogLayer; impl<S> Layer<S> for LogLayer where S: Service<MyRequest, Response = MyResponse, Error = MyError> + Send + 'static, S::Future: Send + 'static, { type Service = LogService<S>; fn layer(&self, inner: S) -> Self::Service { LogService { inner } } } // LogLayerによって生成されるサービス #[derive(Clone)] struct LogService<S> { inner: S, } impl<S> Service<MyRequest> for LogService<S> where S: Service<MyRequest, Response = MyResponse, Error = MyError> + 'static, S::Future: Send + 'static, { type Response = MyResponse; type Error = MyError; type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>; fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> { self.inner.poll_ready(cx) } fn call(&mut self, req: MyRequest) -> Self::Future { println!("(Log Layer) Incoming request: {:?}", req); let fut = self.inner.call(req); Box::pin(async move { let res = fut.await; println!("(Log Layer) Outgoing response: {:?}", res.as_ref().map(|r| r.0.clone())); res }) } } #[tokio::main] async fn main() { let my_service = MyService; let logged_service = LogLayer.layer(my_service); let res1 = logged_service.call(MyRequest("hello".to_string())).await.unwrap(); println!("Main received: {} ", res1.0); // 注: `MyService` が `logged_service.call` によって消費されるため、`logged_service` はここでは再度呼び出せません。 // これが `BoxCloneService` につながります。 }
この例は、LogLayerがMyServiceをラップしてLogServiceを作成し、内部サービスの前後にロギングを追加する方法を示しています。LogServiceのCloneに注意してください。これは、Layer::layerが新しいServiceインスタンスを返すため、実際のアプリケーションでは複数のリクエストを同時に処理するためにClone可能であることがしばしば必要とされるため、重要です。
BoxCloneService型
Serviceトレイト自体は、多くの場合オブジェクトセーフではありません。これは、型を消去するために直接Box<dyn Service<...>>を使用できないことを意味し、多相性と動的ディスパッチを制限します。実世界のサービスは、並列処理のために、またはさまざまなデータ構造に保存するために、しばしばクローン可能である必要があります。Towerはこれらの課題に対処するためにBoxCloneServiceを提供しています。
BoxCloneServiceは、Send、Sync、CloneであるServiceをラップするBoxの型エイリアスであり、そのFutureもSendかつstaticであるものです。これにより、動的ディスパッチとサービス(ドット)のクローンが可能になり、ルーティングと並列実行に不可欠です。
//Simplified representation pub type BoxCloneService<Request, Response, Error> = Box<dyn Service<Request, Response = Response, Error = Error, Future = Pin<Box<dyn Future<Output = Result<Response, Error>> + Send>>> + Send + Sync + Clone>;
主な側面:
Box: ヒープ割り当てと動的ディスパッチを可能にします。dyn Service<...> + Send + Sync + Clone: 基になる具体的なサービス型が動的にディスパッチされ、スレッド間で安全に送信され、スレッド間で共有され、クローン可能であることを意味します。Future = Pin<Box<dyn Future<...> + Send>>:callによって返されるFutureも動的にディスパッチされ、Sendであることを保証します。
BoxCloneServiceを使用するのはどのような場合ですか?
- ルーティング: 特定の基準に基づいて異なるサービスにリクエストをルーティングしたい場合で、これらのサービスが異なる具体的な型を持つ場合、
BoxCloneServiceにより、共通のコレクションにそれらを保存できます。 - ミドルウェアチェーン: 各レイヤーがボクシングされたサービスを返す必要がある複雑なミドルウェアチェーンを構築します。
 - フレームワーク内部: AxumとTonicは、ハンドラー関数とgRPCメソッドの実装を管理するために内部で
BoxCloneServiceを広範囲に使用しており、APIの柔軟性を高めています。 
ロギング例を再考すると、MyServiceがレイヤーリング後に複数回呼び出される必要がある場合:
use std::future::Future; use std::pin::Pin; use std::task::{Context, Poll}; use tower::{Service, Layer}; use tower::ServiceBuilder; // より簡単なレイヤーリングのため // ... (MyRequest, MyResponse, MyError, MyService, LogLayer, LogService は前と同じ) ... #[tokio::main] async fn main() { let my_service = MyService; // ServiceBuilderを使用してクローンを容易にし、レイヤーを合成します let layered_service = ServiceBuilder::new() .layer(LogLayer) // ロギングレイヤーを追加します .service(my_service); // ベースサービス // now, we can call it multiple times, because ServiceBuilder ensures `Clone` // (or consumes and re-produces clonable services if needed) let res1 = layered_service.call(MyRequest("hello".to_string())).await.unwrap(); println!("Main received: {} ", res1.0); let res2 = layered_service.call(MyRequest("world".to_string())).await.unwrap(); println!("Main received: {} ", res2.0); // 型消去のために(例:ルーター内で)それをボクシングしたい場合 let boxed_service = ServiceBuilder::new() .boxed_clone() // サービスをBoxCloneServiceにボクシングします .service(MyService); // ベースサービス let res3 = boxed_service.call(MyRequest("boxed".to_string())).await.unwrap(); println!("Main received: {} ", res3.0); }
ServiceBuilder::boxed_clone()メソッドが鍵です。これは、具体的なサービス(以降のレイヤーの後)を取得し、BoxCloneServiceにボクシングして、多相的に使用し、必要に応じてクローンできるようにします。これは、各ルートが異なる基盤となるService型でリクエストを処理する可能性があるAxumのルーティングにおいて、すべてのリクエストがルーターによって均一に扱われる必要があるため、不可欠です。
AxumとTonicがTowerを利用する方法
Axum: Tower上に構築されたWebフレームワーク
Axumのコア哲学は、Tower上に直接構築することで、複雑さを最小限に抑え、柔軟性を最大化することです。
- ハンドラーとしてのサービス: Axumでは、ルートハンドラーは本質的に
Serviceです。axum::Router::get("/", handler_fn)を定義すると、handler_fnはServiceインスタンスに変換されます。Axumのエクストラクタとレスポンダー(Json、Path、Htmlなど)は、Serviceトレイトまたはその関連型で消費可能な型で動作するか、それらを生成するロジックを実装することによって機能します。 - レイヤーとしてのミドルウェア: Axumのミドルウェア関数(
axum::Router::layer、axum::Router::fallback_service)はLayerを期待します。これにより、ロギング、認証、圧縮などのTower互換ミドルウェアを簡単にプラグインできます。 - BoxCloneServiceによるルーティング: Axumの
Routerは、内部でサービス(ルートハンドラーとその関連ミドルウェア)のコレクションを管理します。これらの多様なサービスを多相的に保存するために、BoxCloneServiceまたは類似のボクシングされたコンストラクトを使用し、着信リクエストを正しいハンドラーに一致させてから、そのハンドラーをcallできるようにします。 
// Towerの概念を暗黙的に実証するAxumの例 use axum::{ routing::{get}, response::IntoResponse, Router, }; use tower_http::trace::TraceLayer; // 一般的なTowerレイヤー async fn hello_world() -> impl IntoResponse { "Hello, Axum!" } #[tokio::main] async fn main() { // hello_worldは暗黙的にサービスに変換されます let app = Router::new() .route("/", get(hello_world)) // TraceLayerはTower Layerです .layer(TraceLayer::new_for_http()); let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap(); axum::serve(listener, app).await.unwrap(); }
ここでは、TraceLayerはLayerとして機能し、hello_world()から作成されたサービスをラップしてリクエストトレースを追加します。Router自体は、パスに基づいて正しい内部サービスにリクエストをルーティングするServiceです。
Tonic: RustのためのgRPCフレームワーク
Tonic、RustのためのgRPCフレームワークも、Towerに大きく依存しています。
- サービスとしてのgRPCメソッド: Tonicサービスで実装する各gRPCメソッドは、本質的に
Serviceインスタンスです。Tonicは、Rust関数をHTTP/2フレームを消費する単一のServiceを公開する前に、プロトコル処理(HTTP/2など)のためのレイヤー、そしてカスタムミドルウェアを適用し、最終的にTower互換サービスに変換するマクロとコード生成を提供します。 - gRPCのためのミドルウェア: Axumと同様に、Tonicの
tonic::transport::Serverは、gRPCサービスにLayerを適用するためのメソッドを提供します。これは、認証、承認、またはgRPC呼び出しのためのカスタムメトリクス収集のためのインターセプターなどの、gRPC固有のミドルウェアを実装するのに非常に役立ちます。 - サービススタック: TonicはTowerサービススタックを構築し、gRPCメソッド実装から開始し、プロトコル処理(HTTP/2など)のためのレイヤー、そしてカスタムミドルウェアを適用し、最終的にHTTP/2フレームを消費する単一の
Serviceを公開します。 
// Towerレイヤーを実証するTonicの例 use tonic::{transport::Server, Request, Response, Status}; use hello_world::greeter_server::{Greeter, GreeterServer}; use hello_world::{HelloReply, HelloRequest}; use tower_http::trace::TraceLayer; pub mod hello_world { tonic::include_proto!("helloworld"); } #[derive(Debug, Default)] pub struct MyGreeter; #[tonic::async_trait] impl Greeter for MyGreeter { async fn say_hello( &self, request: Request<HelloRequest>, ) -> Result<Response<HelloReply>, Status> { println!("Got a request from {:?}", request.remote_addr()); let reply = hello_world::HelloReply { message: format!("Hello {}!", request.into_inner().name), }; Ok(Response::new(reply)) } } #[tokio::main] async fn main() -> Result<(), Box<dyn std::error::Error>> { let addr = "[::1]:50051".parse()?; let greeter = MyGreeter::default(); println!("GreeterServer listening on {}", addr); Server::builder() // gRPCサービスにTraceLayerを適用します .layer(TraceLayer::new_for_grpc()) .add_service(GreeterServer::new(greeter)) .serve(addr) .await?; Ok(()) }
このTonicの例では、TraceLayer::new_for_grpc()は再びLayerとして機能し、GreeterServer(自体が各gRPCメソッドのServiceを実装しています)をラップしています。Server::builder().layer(...)構文は、Tower Layerの適用を直接反映しています。
結論
Tower抽象化レイヤーは、そのコアコンポーネントであるService、Layer、BoxCloneServiceにより、Rustでネットワークアプリケーションを構築するための信じられないほど強力で柔軟な基盤を提供します。これらの概念を理解することで、開発者はAxumやTonicのようなフレームワークを効果的に使用できるだけでなく、カスタムミドルウェアで拡張したり、多様なサービスコンポーネントをシームレスに統合したりできます。Towerは、Rustのコンポーザビリティと型安全性の哲学を体現し、高性能で堅牢、かつ保守性の高いネットワークサービスの作成を可能にします。これは、回復力のある分散システムを構築するという複雑なタスクを根本的に簡素化します。

