このページの目次
Tower とミドルウェア
Tower は Service トレイト(リクエストを処理して Future を返す)と Layer(Service をラップして新しい Service を生成する)という 2 層の抽象化により、非同期ミドルウェアスタックを構築します。ミドルウェアは積み重ね可能(タイムアウト→リトライ→レート制限→ビジネスロジック)であり、バックプレッシャは readiness の待機を通じて上位層へ伝播します。これは Rust 非同期エコシステムで最もエレガントなオニオンアーキテクチャの実装です。
Tower が解決する課題
ネットワークサービスを作成する際には、いくつかの横断的関心事(cross-cutting concerns)があります。具体的には、タイムアウト、レート制限、並行性制御、リトライ、ログ/metrics です。これらのロジックを各サービスにハードコードしてしまうと、コードの重複と組み合わせ可能性の欠如を招きます。例えば、gRPC ハンドラはビジネスロジックの処理とタイムアウト処理の両方を担わなければならず、HTTP ハンドラも同様のレート制限ロジックを必要とします。
Tower の答えは、サービスを Service トレイトとして抽象化し、Layer を用いてデコレータパターンでこれらの関心事を組み合わせることです。各ミドルウェアは1つのことだけを責任として持ち、Layer スタックがそれらを組み合わせて完全なサービスへと仕上げます。
Service trait: 非同期の request → response
このトレイトの2つのメソッドには明確な役割分担があります。
poll_ready: サービスが新しいリクエストを受け付けることができるかどうかを確認します。レートリミッターは残りのクォータをチェックし、並行性リミッターは空いているスロットをチェックします。サービスがまだ受信できない場合、Poll::Pending を返します。呼び出し側は、次にウェイクアップされるまで待つ必要があります。これが Tower に内蔵された バックプレッシャ(背圧)メカニズムです。下位層サービスの負荷が poll_ready を通じて上位層へ伝達され、追加のメッセージチャネルは不要です。
call: リクエストを実行し、Future を返します。call の &mut self シグネチャは、同じ Service インスタンスへの呼び出しが本質的に直列化されることを意味します。同じインスタンス上で並列実行が必要な場合は、Buffer ミドルウェアを使用して Clone 可能にします。
この2段階の分離は、Tower と他の多くのミドルウェアフレームワーク(Express や actix のミドルウェアなど)の核心的な違いです。それらのフレームワークは通常 fn handle(&self, req) -> Future という1つのメソッドしか持たず、バックプレッシャのセマンティクスを持ちません。
Layer: デコレータパターンの関数化された表現
Layer は Service ではなく、Service のファクトリです。内側のサービス(inner service)を受け取り、それをラップしたラッパーサービス(wrapper service)を返します。最大の利点は、Layer を組み合わせられることです。
let svc = new
.layer
.layer
.layer
.service;
ServiceBuilder::new().layer(A).layer(B).service(S) は実際には以下を構築します。
A::layer(B::layer(S))
つまり A << B << S です。各 Layer が内側のサービスをラップします。実際にリクエストが到来したときの実行順序は外側から内側へ向かいます。リクエスト → A → B → S → B → A → レスポンス。
バックプレッシャの理解
バックプレッシャがない場合、並列リクエストがサービスに直接押し寄せ、サービスがオーバーロードされる可能性があります。Tower の poll_ready により、ミドルウェアはリクエストを受け取る前に拒否することができます。
つまり、ConcurrencyLimitLayer が並行性の上限に達すると、新しい poll_ready は Pending を返します。パニックしたり、拒絶したりするのではなく、優雅に延期されます。呼び出し側の Future はスロットが空いた後にウェイクアップされ、処理を続行します。この一連のプロセスは Future の poll モデル内で自然に表現され、追加のセマフォやチャネルは不要です。
一般的なミドルウェア
Timeout:callが返す Future が指定時間後にまだ Ready にならない場合 → タイムアウトエラーを返します。注意:これは内部の実行を停止しません(tokio ではtokio::select!やJoinHandle::abortを使わない限り、基盤となるタスクを強制的にキャンセルできません)。ConcurrencyLimit: 内部カウンターで同時に処理されるリクエスト数を制限します。カウンターが上限に達すると poll_ready は Pending を返します。RateLimit: トークンバケットアルゴリズムでリクエストレートを制限します。トークンが不足している場合、poll_ready は Pending を返します。Buffer:call(&mut self)が共有できない問題を解決します。内部でチャネルを使用してリクエストをワーカースレッドへ転送し、外部には Clone 可能なインターフェースを公開します。Retry: 失敗したリクエストを自動的にリトライします。リトライ条件と指数バックオフを設定可能です。ただし、冪等性に注意が必要です。リトライは冪等な操作(GET, PUT)に対してのみ安全です。Trace: 各リクエストの所要時間を自動的に記録します。ビジネスロジックに手動で時刻計測コードを挿入する必要はありません。
tonic は tower のネイティブな消費者です
builder
.layer
.add_service
.serve.await?;
各 gRPC メソッドは自動的に Service となります。tonic の codegen は、あなたのハンドラに対して Service impl を生成します。つまり、tower のすべてのミドルウェアを、変更を加えることなく任意の gRPC サービスに適用できるということです。
参考
- tower: docs.rs/tower (README に設計哲学の説明があります)
- tonic: github.com/hyperium/tonic
Keywords: tower, Service, Layer, middleware, tonic, poll_ready, backpressure