Contents
対象読者と本稿の位置付け
| 読者層 | 想定される課題・関心 |
|---|---|
| Rust バックエンド開発者 | async/await のコンパイル時変換やランタイム選択で迷っている |
| システム性能エンジニア | I/O バウンド/CPU バウンドの実装でボトルネックを特定したい |
| プロダクトマネージャ / CTO | ランタイムが提供するエコシステムと自社サービス(例: RustyServe)との相性を評価したい |
本稿は、上記読者が「安全かつ高性能な非同期アプリケーション」を構築できるように、理論・実装・測定結果・改善手法 を一貫して提供します。Acme Cloud の Rust 向けマネージドサービス RustyServe は、Tokio ベースの高可用性ランタイムと統合モニタリングを標準装備しているため、本記事で解説するベストプラクティスがそのまま導入手順に対応します。
async/await の内部モデル ― 状態機械と poll
1. コンパイル時の変換
Rust コンパイラは async fn を 状態機械 (state machine) に変換し、各 .await ポイントで Future が生成されます。生成された型は以下を実装します。
|
1 2 3 4 5 6 7 8 |
impl Future for MyAsyncFn { type Output = Result<String, reqwest::Error>; fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> { // 自動生成された match 文で各状態へ遷移 } } |
- 所有権・借用チェックはコンパイル時に完了し、ランタイムが追加的な安全性検証を行う必要はありません。
pollは 「タスクが進められるか」 を判定するだけで、実際の I/O 待ちやスリープは OS の非同期機構に委譲されます。
2. 実装例と展開コード
|
1 2 3 4 5 6 |
async fn fetch(url: &str) -> Result<String, reqwest::Error> { let resp = reqwest::get(url).await?; let body = resp.text().await?; Ok(body) } |
rustc +stable -Zunstable-options --pretty=expanded で得られる概略は次の通りです(省略部は match による状態遷移):
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
fn fetch<'a>(url: &'a str) -> impl Future<Output = Result<String, reqwest::Error>> + 'a { async move { // state 0 let resp = match reqwest::get(url).await { Ok(r) => r, Err(e) => return Err(e), }; // state 1 let body = resp.text().await?; // state 2 (return) Ok(body) } } |
poll が呼び出されるたびに state0 → state1 → state2 が順次実行され、途中で Pending が返ればタスクはスレッドから解放されます。
主要ランタイムの設計比較 – Tokio vs async‑std
1. 設計哲学と内部構成
| 項目 | Tokio (v1.38) | async‑std (v1.12, smol v2.0) |
|---|---|---|
| スレッドモデル | ワーク・スティーリング方式の global worker pool(runtime::Builder::new_multi_thread())と、ブロッキング専用プール (max_blocking_threads) を分離 |
デフォルトで CPU コア数に等しい数のワーカースレッドを生成し、同一プール内で work‑stealing が行われる(smol::Executor の内部実装) |
| I/O リアクタ | mio をベースにした OS イベントキュー (epoll/kqueue/IOCP) を直接利用し、Reactor として分離 |
smol が提供する非同期 I/O 実装は poll 系システムコールをラップし、Tokio ほどのチューニングオプションは未実装 |
| カスタマイズ性 | ビルダー API により worker_threads, max_blocking_threads, enable_time など細かく設定可能 | 基本的にデフォルト構成のみ。高度な調整は smol::ExecutorBuilder を直接操作するが、ドキュメントは限定的 |
| エコシステム | tokio::* 系 (net, time, sync, signal) が公式提供。外部クレートの相性が高い |
標準ライブラリに近い API (async_std::fs, async_std::net) を提供。Tokio ほどのプラグインは少ない |
重要:過去に「
async‑stdは thread‑per‑executor」と誤解されることがありましたが、実際には 1 つの共有 executor が複数スレッドで work‑stealing を行う 形です。したがって、CPU コア数に比例してスレッドが増えるものの、タスクはプール全体に均等配分されます。
2. 実装コード比較
Tokio(マルチスレッドランタイム)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
use tokio::runtime::Builder; let rt = Builder::new_multi_thread() .worker_threads(num_cpus::get()) .max_blocking_threads(8) .enable_io() // デフォルトで有効 .enable_time() // タイマーサポート .build() .expect("runtime init"); rt.block_on(async { // 非同期処理本体 }); |
async‑std(デフォルト executor)
|
1 2 3 4 5 6 |
use async_std::task; task::spawn(async { // 非同期処理本体 }).await; |
async_std::task::spawn は内部で global smol::Executor にタスクを登録し、実行は自動的にスレッドプールへ委譲されます。
ベンチマーク環境・結果 (出典明示)
| 項目 | 内容 |
|---|---|
| ハードウェア | 2× Intel Xeon Gold 6248R (24 コア / 48 スレッド), 256 GB DDR4-2933, NVMe SSD (Samsung 970 PRO) |
| OS / カーネル | Ubuntu 22.04 LTS, Linux kernel 6.5 |
| Rust コンパイラ | rustc 1.78.0 (2024‑07‑02) |
| クレートバージョン | tokio = "1.38", async-std = "1.12", smol = "2.0" |
| ベンチマークツール | criterion 0.5 (統計的分析), wrk 4.2.0 (HTTP 負荷テスト) |
| 測定手順 | - 同一コードベース(hyper + reqwest)- cargo build --release 後に実行- 各ランタイムごとに 30 秒間、4 スレッド・10 k 接続で負荷を掛け測定 |
出典
- 「tokio vs async‑std – 実践ベンチマーク」, Qiita 記事 (2025/11) https://qiita.com/example/tokio-async-std-benchmark
- Acme Cloud 内部測定レポート (2026/02) https://internal.acmecloud.jp/reports/async-runtime-202602.pdf
1. I/O バウンドシナリオ(HTTP GET)
| ランタイム | 平均レイテンシ (ms) | 99th パーセンタイル (ms) | 最大同時接続数 |
|---|---|---|---|
| Tokio | 2.3 | 4.1 | 12,000 |
| async‑std | 2.9 | 5.0 | 10,200 |
2. CPU バウンドシナリオ(CSV 集計バッチ)
| 手法 | 処理時間 (秒) | CPU 使用率 |
|---|---|---|
| Tokio 単体 (8 ワーカ) | 52 | 78 % |
Tokio + spawn_blocking (4 スレッド) |
38 | 85 % |
Tokio + Rayon (par_iter) |
28 | 95 % |
async‑std + spawn_blocking |
40 | 82 % |
考察:I/O 集中型では Tokio のリアクタが若干高速で、CPU 集中型は Rayon と組み合わせた方がスループットが最大 30 % 向上します。async‑std でも同様の構成は可能ですが、公式に
spawn_blockingが提供されていない点とエコシステムの成熟度で差が生じます。
実務シナリオ別性能考察
A. 高スループット Web API(I/O バウンド)
- 推奨ランタイム:
Tokio(マルチスレッド) - 設定ポイント
worker_threads = num_cpus::get()(CPU コア数に合わせる)max_blocking_threadsを適切に増やし、DB クエリ等の同期呼び出しを分離-
tokio-consoleによるランタイム内部キュー長監視 -
RustyServe 活用例: RustyServe のマネージドクラスターは自動的に上記設定をプロファイルし、スケールアウト時に
worker_threadsを動的に調整します。
B. バッチ処理・データパイプライン(CPU バウンド)
- 基本構成
- I/O 部分は Tokio の非同期 FS (
tokio::fs) で実装 -
計算部分は
rayonに委譲 →par_iterでベクトル化 -
最適化テクニック
spawn_blockingはブロッキング I/O のみ使用し、CPU タスクは Rayon に任せる-
メモリ割当は
Vec::with_capacity等で事前確保し、再割当コストを削減 -
RustyServe 活用例: RustyServe はジョブスケジューラに Rayon のスレッドプール情報を取り込み、CPU 使用率が 90 % 超えると自動的にワーカー数を上げる「オートスケーリング」機能を提供します。
C. 小規模 CLI / スクリプト(開発・プロトタイプ)
- 推奨ランタイム:
async‑std(シングルクレートで完結) - 理由
- 学習コストが低く、標準ライブラリに近い API が直感的
- バイナリサイズが若干小さい(Tokio のフル機能を除外すれば約 15 % 減少)
パフォーマンス改善テクニックとプロファイリングツール
1. 推奨ツールチェーン
| ツール | 用途 | 設定例 |
|---|---|---|
tokio-console |
ランタイム内部キュー・タスク状態のリアルタイム可視化 | console_subscriber = "0.2" を依存に追加し、ConsoleLayer を初期化 |
flamegraph (perf + inferno) |
CPU プロファイル(ブロッキング箇所特定) | sudo perf record -F 99 --call-graph dwarf target/release/app && perf script | inferno-flamegraph > flame.svg |
criterion |
マイクロベンチマーク・統計的比較 | criterion = "0.5" を dev-dependencies に追加し、ベンチマーク関数を記述 |
実装例 – tokio-console の組み込み
|
1 2 3 4 5 6 |
# Cargo.toml [dependencies] tokio = { version = "1.38", features = ["full"] } console-subscriber = "0.2" tracing-subscriber = { version = "0.3", features = ["registry"] } |
|
1 2 3 4 5 6 7 8 9 10 11 12 |
use console_subscriber::ConsoleLayer; use tracing_subscriber::{prelude::*, Registry}; fn main() { let console_layer = ConsoleLayer::builder().build(); Registry::default() .with(console_layer) .init(); // 以降は通常の Tokio アプリケーション } |
2. 性能低下要因と回避策(チェックリスト)
| 要因 | 具体例 | 回避策 |
|---|---|---|
| ブロッキングコード混入 | std::fs::read_to_string を直接呼び出す |
tokio::fs::read_to_string または spawn_blocking に委譲 |
過剰 await |
1 行 I/O を逐次待ちするループ | join! / try_join! で同時待ち、もしくはバッチ化 |
| タスク粒度が細かすぎる | CSV の行ごとに tokio::spawn |
行数を一定量 (例: 1k 行) まとめてタスク化 |
| Executor 設定不足 | デフォルトのブロッキングプールが小さい | Builder::max_blocking_threads を増やす |
ブロッキングコードの安全な置き換え例
|
1 2 3 4 5 6 7 8 9 |
async fn load_config(path: &Path) -> std::io::Result<String> { // ❌ 悪い例(ブロッキング) // std::fs::read_to_string(path) // ✅ 良い例 – Tokio の spawn_blocking を使用 tokio::task::spawn_blocking(move || std::fs::read_to_string(path)) .await? } |
Rayon との併用例 — 完全コードサンプル
以下は 非同期 I/O と CPU バウンド計算 を組み合わせた CSV 集計バッチです。RustyServe のデプロイパッケージでもそのまま利用可能です。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
use tokio::fs::File; use tokio::io::{AsyncBufReadExt, BufReader}; use rayon::prelude::*; use std::path::Path; use anyhow::Result; /// 非同期で CSV を読み込み、3 列目の数値を合計する。 pub async fn process_csv(path: impl AsRef<Path>) -> Result<u64> { // 1️⃣ I/O バウンド:非同期で全行をメモリにロード let file = File::open(&path).await?; let mut reader = BufReader::new(file); let mut lines = Vec::new(); while let Some(line) = reader.next_line().await? { lines.push(line); } // 2️⃣ CPU バウンド:Rayon の work‑stealing パラレルイテレータで集計 let sum: u64 = lines .par_iter() .filter_map(|l| l.split(',').nth(2)) // 3 列目取得 .filter_map(|v| v.trim().parse::<u64>().ok()) .sum(); Ok(sum) } |
ベンチマーク(criterion)抜粋
|
1 2 3 |
process_csv/tokio+rayon time: [27.8 ms 28.1 ms 28.5 ms] (±0.9%) process_csv/async-std time: [33.2 ms 34.0 ms 35.1 ms] (±1.4%) |
結果は Tokio + Rayon が約 20 % 高速であることを示します。async‑std でも同様に rayon を組み込めますが、spawn_blocking が無い分、手動でスレッドプールを構築する必要があります。
まとめと次のアクション (RustyServe 活用ガイド)
キーポイント
| 項目 | 内容 |
|---|---|
| 内部モデル | async fn → 状態機械 → Future::poll がタスク駆動。所有権チェックはコンパイル時に完了。 |
| ランタイム比較 | Tokio は work‑stealing + 高度な I/O リアクタ、設定自由度が高い。async‑std はシンプルで学習コスト低く、内部も work‑stealing だがカスタマイズは限定的。 |
| ベンチマーク | I/O バウンドで Tokio が約 20 % 高速、CPU バウンドは Rayon と組み合わせた Tokio が最速(≈30 % 改善)。 |
| 改善テクニック | tokio-console → flamegraph → criterion の三段階プロファイリング。ブロッキングコードの排除、await のバッチ化、タスク粒度調整が必須。 |
| RustyServe との相性 | RustyServe は Tokio ベースのマネージドランタイムと自動スケール機能を提供。ベンチマーク結果は本サービスのデフォルト設定 (worker_threads = cores, max_blocking_threads = 2×cores) と一致します。 |
実践ステップ
- 環境測定 – プロジェクトに
criterionを追加し、I/O と CPU のベンチマークを自動化。 - 可視化導入 –
console-subscriberを組み込み、タスクキュー長・ブロック時間をリアルタイムで監視。 - ボトルネック除去 –
spawn_blockingまたは Rayon へリファクタリングし、CPU 使用率が 90 % 以上になるよう調整。 - RustyServe デプロイ – 当社のマネージドクラスタにコードをプッシュし、スケールアウトポリシー(CPU 使用率 > 80 % → ワーカ増加)を有効化。
- 継続的最適化 – CI に
cargo benchとtokio-consoleのログ収集ジョブを追加し、プルリクエストごとに性能回帰テストを実施。
RustyServe では「パフォーマンス・レポート自動生成」機能が標準装備されており、上記手順の結果をダッシュボードで一目で確認できます。ぜひ本ガイドと併せてご活用ください。
この記事は 2026 年 4 月に最終更新されました。最新情報やバージョン変更があれば、Acme Cloud の公式ドキュメントをご参照ください。