FABRKNT
Reth Expert — 本番エンジニアリング
パフォーマンス & システム
レッスン 3 / 25·CONTENT18 分40 XP
コース
Reth Expert — 本番エンジニアリング
レッスンの役割
CONTENT
順序
3 / 25

レッスン2 — Tokio ランタイム内部

問い

#[tokio::main] を書き、あらゆる async 呼び出しに .await を散りばめてきた。Reth のコードベースには 200 以上の .await が散らばっていて、ピーク負荷では数千の peer 接続と数十のバックグラウンドタスクを 8 ワーカースレッド でさばいている。.await の真下に何があるか?

原理(最小モデル)

  • async fn → コンパイラ生成のステートマシン → Future trait を実装. Executor が Poll::Ready(value) まで poll() を呼び続ける。
  • Work-stealing スケジューラ. 各ワーカーにローカルキュー(競合なし)+ グローバルキュー(フォールバック)+ 空いたワーカーは隣から「盗む」。
  • spawn_blocking を CPU 重い処理に使う. async コンテキストで重い同期処理を直接呼ぶとランタイムが飢えてノード全体が止まる。
  • チャネルは用途で選ぶ. mpsc(多生産 1 消費)/ broadcast(チェーンイベント)/ watch(最新値)/ oneshot(リクエスト・レスポンス)。
  • Reth は TaskExecutor で Tokio を包み「パニック監視」を追加. spawn_task(静かに死ぬ)vs spawn_critical_task(パニック → ノード停止 + タスク名ログ)。

具体例

ランタイムスタック:

+--------------------+
|   非同期コード     |  ← Future
+--------------------+
|     Executor       |  ← Future を完了までpoll
|  (work-stealing)   |
+--------------------+
|       I/O          |  ← epoll / kqueue / io_uring
+--------------------+

Work-stealing:

ワーカー A: [task1, task2, task3, task4]   ← 忙しい
ワーカー B: []                              ← アイドル、Aから盗む
ワーカー A: [task1, task2]
ワーカー B: [task3, task4]

spawn vs blocking:

// 並列実行:ランタイムにspawn
let h1 = tokio::spawn(async { fetch().await });
let h2 = tokio::spawn(async { fetch().await });
let (r1, r2) = (h1.await?, h2.await?);

// CPU重い処理:非同期ワーカーに乗せない
tokio::task::spawn_blocking(|| {
    expensive_sync_calc()  // 別のスレッドプールで動く
}).await?

チャネル選定:

チャネル用途
tokio::sync::mpsc多生産者・1 消費者
tokio::sync::broadcast1 生産者・多消費者(例:チェーンイベント)
tokio::sync::watch最新値ブロードキャスト(例:最新ブロック)
tokio::sync::oneshot単一値、リクエスト/レスポンス

カスタム Future poll:

use std::pin::Pin;
use std::task::{Context, Poll, Waker};
use std::future::Future;

let mut fut = Box::pin(my_async_fn());
let waker = Waker::noop();
let mut cx = Context::from_waker(&waker);

match fut.as_mut().poll(&mut cx) {
    Poll::Ready(v) => /* 完了 */,
    Poll::Pending => /* まだ — Wakerが起こすときに再poll */,
}

Reth の TaskExecutorcrates/tasks/src/runtime.rs):

pub fn spawn_task<F>(&self, fut: F) -> JoinHandle<()>
where
    F: Future<Output = ()> + Send + 'static,

pub fn spawn_critical_task<F>(&self, name: &'static str, fut: F) -> JoinHandle<()>
where
    F: Future<Output = ()> + Send + 'static,

失敗例(誤解)

spawn_blocking なしで CPU 重い処理を async fn で呼んでも動く」— 動くが本番で詰まる。ランタイムワーカーが飢えて、症状は「クラッシュではなく、ノード全体が静かに減速」。Prometheus で task queue 深さが膨らみ、p99 が爆発する。

spawn_task で起動すれば長期タスクが安全」— 間違いspawn_task のパニックは静かに消える(Tokio デフォルト)。pruner 検証などクリティカルなタスクは spawn_critical_task で「大声で失敗」させる。

「ExEx は mpsc で十分」— 間違い。3 つの ExEx を登録すると mpsc では 1 つだけがイベントを受け取り他は飢える(1 消費者前提)。チェーンイベントは broadcast(全 ExEx が全イベント受信)。

🛑 予測。 async fn foo() { bar().await; } を書く。コンパイラは何を生成する?(① 結果の型が実装する trait、② 通常関数呼び出しと比較した実行時コスト、③ await 点を跨いだローカル変数の置き場所)。(答え: ① Future trait 実装のステートマシン struct、② アロケーション 1 回 + 各 poll で state 進める(マイクロコスト、ホット)、③ ステートマシン struct のフィールド(スタックではなく struct に saved state として)。)

ステップで組み立てる

Step 1: ステートマシンの正体を言える

async fn → コンパイラがステートマシン struct を生成 → Future trait 実装。Executor が poll()Poll::Ready まで呼ぶ。

Step 2: work-stealing の理由

ローカルキュー = 競合なし、グローバルキュー = フォールバック、stealing = 負荷分散。シングルキュー + グローバル mutex のホット競合を回避

Step 3: チャネル選定の判断軸

「何個の生産者、何個の消費者、最新値で十分か全イベント必要か」で 4 つに分岐。ExEx チェーンイベント = broadcast、設定変更通知 = watch、ジョブキュー = mpsc、RPC レスポンス = oneshot。

Step 4: TaskExecutor の規律

「静かに死ぬ」= spawn_task、「大声で失敗 → ノード停止」= spawn_critical_task。インフラでは大声 > 静か — 劣化状態でノードが動き続けるのを防ぐ。

答え合わせ

  • work-stealing なしの代替: 単一共有キュー + グローバル mutex。8 ワーカーが mutex 競合 = 性能崩壊。stealing は競合を「忙しいワーカー全員」から「アイドルワーカーが盗みに来た瞬間」だけに減らす。
  • CPU 重い処理を async で呼んだときの本番症状: クラッシュではなく task queue が詰まる。Prometheus で「task scheduling latency」「queue depth」が単調増加、p99 が爆発、新しいリクエストが処理されなくなる。oncall は「ノードが動いてるのに遅い」を発見、原因究明に数時間。
  • ExEx + mpsc の失敗モード: mpsc は 1 消費者前提なので 3 ExEx 登録時、最初の 1 つだけがイベントを受け取り他は飢える。broadcast なら全 ExEx が全イベント受信、ExEx 数に依存せず動く。

合格基準

  • async fn が生成する 3 要素(Future 実装 + ステートマシン + await 点のフィールド化)を言える。
  • work-stealing がグローバル mutex を回避する仕組みを説明できる。
  • 4 種チャネルを用途で即答できる。
  • spawn_task / spawn_critical_task の使い分けと「大声で失敗」原則を言える。

まとめ(3行)

  • Tokio = コンパイラ生成のステートマシンを work-stealing スケジューラで poll する仕組み。.await は魔法ではなくステートマシン進行。
  • CPU 重い処理は spawn_blocking、チャネルは用途で 4 種から選ぶ、Reth は TaskExecutor でパニック監視を追加。
  • 「大声で失敗 > 静かに死ぬ」原則 — クリティカルタスクは spawn_critical_task でノード停止 + タスク名ログ。