Rust Tokioグレースフルシャットダウン実戦:プロダクションレディ非同期サービスライフサイクルの5つのコアパターン

编程语言

クラウドネイティブアーキテクチャが主流の2026年、Rust非同期サービスはもう「起動して終わり」の単純なプログラムではありません。Kubernetesのローリングアップデート、コンテナオーケストレータからのSIGTERMシグナル、長時間接続のグレースフルドレイン—すべての环节でTokioランタイムのライフサイクルを正確に制御する必要があります。乱暴なctrl+cはデータ損失、リクエスト中断、さらにはカスケード障害を引き起こす可能性があります。今日は、シグナルキャプチャからタスクキャンセル、コネクションドレインからマルチサービス調整まで、Rust Tokioグレースフルシャットダウンの5つのコアパターンを徹底解説します。

コア概念一覧

概念 説明 主要タイプ/関数
シグナルキャプチャ OS終了シグナルを監視してシャットダウンをトリガー tokio::signal, ctrl_c()
キャンセレーショントークン 協調的タスクキャンセル機構 tokio_util::sync::CancellationToken
タスク管理 全spawnedタスクの追跡と待機 JoinHandle, JoinSet
コネクションドレイン 新規接続の停止、既存接続の完了待ち graceful_shutdown(), タイムアウト制御
シャットダウンマネージャ 複数コンポーネントのシャットダウン順序を統一管理 カスタム ShutdownManager

問題分析:5つのペインポイント

  1. シグナルロスト:mainでのみシグナルをリッスンし、サブタスクがシャットダウンイベントを検知できず、バックグラウンドタスクが永遠に実行される
  2. タスクリーク:100個のタスクをspawnしたが、シャットダウン時に3個しか待機せず、残り97個が孤児タスクになる
  3. 接続の強制切断:SIGTERM受信時に即座に終了し、処理中のHTTPリクエストが切断され、クライアントが接続リセットを受ける
  4. シャットダウン順序の混乱:DB接続プールが先に閉じられ、まだデータを書き込んでいるタスクがあり、panicやデータ損失が発生
  5. タイムアウト制御不能:スタックしたタスクが永遠に終了せず、シャットダウンプロセス全体がブロックされ、KubernetesがSIGKILLを送信

パターン1:tokio::signalシグナルキャプチャとCancellationToken

グレースフルシャットダウンの出発点—まずシャットダウンシグナルを「聞き」、それを全タスクにブロードキャストする必要があります。

use tokio::signal;
use tokio_util::sync::CancellationToken;
use tokio::time::{sleep, Duration};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let token = CancellationToken::new();

    // 各タスクにキャンセレーショントークンをクローン
    let t1 = token.clone();
    let handle1 = tokio::spawn(async move {
        tokio::select! {
            _ = t1.cancelled() => {
                println!("タスク1がキャンセルシグナルを受信、クリーンアップ開始...");
            }
            _ = async {
                loop {
                    println!("タスク1作業中...");
                    sleep(Duration::from_secs(1)).await;
                }
            } => {}
        }
    });

    let t2 = token.clone();
    let handle2 = tokio::spawn(async move {
        tokio::select! {
            _ = t2.cancelled() => {
                println!("タスク2がキャンセルシグナルを受信、クリーンアップ開始...");
            }
            _ = async {
                loop {
                    println!("タスク2作業中...");
                    sleep(Duration::from_secs(2)).await;
                }
            } => {}
        }
    });

    // 複数のシグナルタイプをリッスン
    println!("サービス起動、Ctrl+CまたはSIGTERMでグレースフルシャットダウン...");
    tokio::select! {
        _ = signal::ctrl_c() => {
            println!("\nCtrl+Cシグナル受信");
        }
        _ = signal::unix::signal(signal::unix::SignalKind::terminate())?.recv() => {
            println!("SIGTERMシグナル受信");
        }
    }

    // キャンセルをブロードキャスト
    token.cancel();

    // 全タスクの完了を待機
    let _ = handle1.await;
    let _ = handle2.await;

    println!("全タスクがグレースフルに終了");
    Ok(())
}

重要ポイント

  • CancellationTokenは協調的キャンセルであり、強制終了ではない—タスクは自らcancelled()をチェックする必要がある
  • tokio::select!を使用してタスクがキャンセルシグナルと通常作業を同時にリッスン
  • token.clone()はゼロコストクローン、内部でArcによる状態共有

パターン2:タスクキャンセルとJoinHandle管理

プロダクション環境では数十〜数百の並行タスクがあります。各JoinHandleを手動で管理するのは非現実的です。JoinSetが頼もしい味方です。

use tokio::task::JoinSet;
use tokio_util::sync::CancellationToken;
use tokio::time::{sleep, Duration};
use std::sync::Arc;

#[derive(Debug)]
struct TaskResult {
    name: String,
    status: String,
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let token = CancellationToken::new();
    let mut join_set: JoinSet<TaskResult> = JoinSet::new();

    // タスクを一括起動
    for i in 0..5 {
        let t = token.clone();
        join_set.spawn(async move {
            let name = format!("worker-{}", i);
            tokio::select! {
                _ = t.cancelled() => {
                    sleep(Duration::from_millis(100 * i as u64)).await;
                    TaskResult {
                        name: name.clone(),
                        status: "cancelled_and_cleaned".into(),
                    }
                }
                _ = async {
                    loop {
                        println!("{} 作業中...", name);
                        sleep(Duration::from_secs(1)).await;
                    }
                } => {
                    TaskResult {
                        name,
                        status: "completed".into(),
                    }
                }
            }
        });
    }

    // 一定時間実行後にシャットダウンをトリガー
    sleep(Duration::from_secs(2)).await;
    println!("グレースフルシャットダウンをトリガー...");
    token.cancel();

    // 全タスク結果を収集
    while let Some(result) = join_set.join_next().await {
        match result {
            Ok(task_result) => {
                println!("タスク完了: {} -> {}", task_result.name, task_result.status);
            }
            Err(e) => {
                println!("タスクエラー: {}", e);
            }
        }
    }

    println!("全タスク収集完了");
    Ok(())
}

重要ポイント

  • JoinSetは複数のJoinHandleを自動管理、drop時に全タスクをabort
  • 各タスクは構造化されたTaskResultを返し、シャットダウン状態の追跡に便利
  • join_next().awaitは完了順に結果を収集、他のタスクをブロックしない

パターン3:コネクションドレインとタイムアウト制御

HTTPサービスのシャットダウンで最も重要なステップ:新規接続の停止、既存リクエストの完了待ち、しかし無限待機はしない。

use axum::{Router, routing::get, extract::State};
use tokio::net::TcpListener;
use tokio::signal;
use tokio::time::{timeout, Duration};
use tokio_util::sync::CancellationToken;
use std::sync::Arc;

#[derive(Clone)]
struct AppState {
    token: CancellationToken,
    active_requests: Arc<std::sync::atomic::AtomicU64>,
}

async fn handler(State(state): State<AppState>) -> &'static str {
    state.active_requests.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
    tokio::time::sleep(Duration::from_secs(2)).await;
    state.active_requests.fetch_sub(1, std::sync::atomic::Ordering::SeqCst);
    "Hello from ToolsKu!"
}

async fn health_handler(State(state): State<AppState>) -> String {
    let active = state.active_requests.load(std::sync::atomic::Ordering::SeqCst);
    if state.token.is_cancelled() {
        format!("draining: {} active requests", active)
    } else {
        format!("healthy: {} active requests", active)
    }
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let token = CancellationToken::new();
    let state = AppState {
        token: token.clone(),
        active_requests: Arc::new(std::sync::atomic::AtomicU64::new(0)),
    };

    let app = Router::new()
        .route("/", get(handler))
        .route("/health", get(health_handler))
        .with_state(state);

    let listener = TcpListener::bind("0.0.0.0:3000").await?;
    println!("サーバー起動: http://0.0.0.0:3000");

    let server = axum::serve(listener, app)
        .with_graceful_shutdown(shutdown_signal(token.clone()));

    let t = token.clone();
    tokio::spawn(async move {
        signal::ctrl_c().await.ok();
        println!("\nシャットダウンシグナル受信、コネクションドレイン開始...");
        t.cancel();
    });

    match timeout(Duration::from_secs(30), server).await {
        Ok(Ok(())) => println!("サーバーグレースフルシャットダウン完了"),
        Ok(Err(e)) => println!("サーバーエラー: {}", e),
        Err(_) => println!("シャットダウンタイムアウト(30s)、強制終了!"),
    }

    let active = state.active_requests.load(std::sync::atomic::Ordering::SeqCst);
    if active > 0 {
        println!("警告:{}個のアクティブリクエストが強制中断されました", active);
    }

    Ok(())
}

async fn shutdown_signal(token: CancellationToken) {
    token.cancelled().await;
}

重要ポイント

  • with_graceful_shutdown()はaxumにシグナル受信後の新規接続停止を指示
  • AtomicU64でアクティブリクエスト数を追跡、シャットダウン時のドレイン状態判断に有用
  • 外側のtimeoutは最後のセーフティネット—30秒以内に終了しなければKubernetesがSIGKILLを送信

パターン4:マルチサービス調整シャットダウン

実際のサービスは通常複数のコンポーネントで構成:HTTPサーバー、gRPCサービス、バックグラウンドワーカー、DB接続プール。シャットダウン順序が極めて重要です。

use tokio_util::sync::CancellationToken;
use tokio::time::{sleep, Duration};
use tokio::task::JoinSet;

/// グレースフルサービスtrait
#[async_trait::async_trait]
trait GracefulService: Send + Sync + 'static {
    fn name(&self) -> &str;
    async fn run(&self, token: CancellationToken);
    async fn shutdown_timeout(&self) -> Duration {
        Duration::from_secs(10)
    }
}

struct HttpServer;

#[async_trait::async_trait]
impl GracefulService for HttpServer {
    fn name(&self) -> &str { "HTTPサーバー" }

    async fn run(&self, token: CancellationToken) {
        tokio::select! {
            _ = token.cancelled() => {
                println!("[HTTP] シャットダウンシグナル受信、新規接続停止...");
                sleep(Duration::from_secs(2)).await;
                println!("[HTTP] 全リクエスト処理完了");
            }
            _ = async {
                loop {
                    println!("[HTTP] リクエスト処理中...");
                    sleep(Duration::from_secs(1)).await;
                }
            } => {}
        }
    }

    async fn shutdown_timeout(&self) -> Duration {
        Duration::from_secs(15)
    }
}

struct GrpcServer;

#[async_trait::async_trait]
impl GracefulService for GrpcServer {
    fn name(&self) -> &str { "gRPCサービス" }

    async fn run(&self, token: CancellationToken) {
        tokio::select! {
            _ = token.cancelled() => {
                println!("[gRPC] シャットダウンシグナル受信、ストリーミングリクエストドレイン...");
                sleep(Duration::from_secs(3)).await;
                println!("[gRPC] ストリーミングリクエストドレイン完了");
            }
            _ = async {
                loop {
                    println!("[gRPC] ストリーミングリクエスト処理中...");
                    sleep(Duration::from_secs(2)).await;
                }
            } => {}
        }
    }
}

struct BackgroundWorker;

#[async_trait::async_trait]
impl GracefulService for BackgroundWorker {
    fn name(&self) -> &str { "バックグラウンドワーカー" }

    async fn run(&self, token: CancellationToken) {
        tokio::select! {
            _ = token.cancelled() => {
                println!("[Worker] シャットダウンシグナル受信、現在のバッチ完了...");
                sleep(Duration::from_secs(1)).await;
                println!("[Worker] 現在のバッチ処理完了");
            }
            _ = async {
                loop {
                    println!("[Worker] バックグラウンドタスク実行中...");
                    sleep(Duration::from_secs(3)).await;
                }
            } => {}
        }
    }
}

struct DatabasePool;

#[async_trait::async_trait]
impl GracefulService for DatabasePool {
    fn name(&self) -> &str { "DB接続プール" }

    async fn run(&self, token: CancellationToken) {
        token.cancelled().await;
        println!("[DB] 全クエリ完了待ち...");
        sleep(Duration::from_secs(1)).await;
        println!("[DB] 接続プールクローズ");
    }
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let token = CancellationToken::new();

    let services: Vec<Box<dyn GracefulService>> = vec![
        Box::new(HttpServer),
        Box::new(GrpcServer),
        Box::new(BackgroundWorker),
    ];
    let db_pool: Box<dyn GracefulService> = Box::new(DatabasePool);

    let mut join_set: JoinSet<()> = JoinSet::new();
    for svc in &services {
        let t = token.clone();
        let svc_name = svc.name().to_string();
        join_set.spawn(async move {
            svc.run(t).await;
            println!("[{}] 終了", svc_name);
        });
    }

    println!("全サービス起動、Ctrl+Cでシャットダウン...");
    tokio::signal::ctrl_c().await?;
    println!("\nグレースフルシャットダウンをトリガー...");
    token.cancel();

    while join_set.join_next().await.is_some() {}

    println!("全ビジネスサービス終了、DBクローズ開始...");
    db_pool.run(token.clone()).await;

    println!("システム完全シャットダウン");
    Ok(())
}

重要ポイント

  • GracefulService traitで統一シャットダウンインターフェースを定義
  • シャットダウン順序:まずトラフィック入口(HTTP/gRPC)、次にバックグラウンドタスク、最後にDB
  • 各サービスはshutdown_timeoutをカスタマイズ可能

パターン5:プロダクション級ShutdownManagerカプセル化

全パターンを再利用可能なShutdownManagerに統合。優先度、タイムアウト、進捗追跡をサポート。

use tokio_util::sync::CancellationToken;
use tokio::time::{timeout, Duration, Instant};
use tokio::task::JoinSet;
use std::sync::{Arc, Mutex};

/// シャットダウンフェーズ
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
enum ShutdownPhase {
    Draining = 0,    // トラフィックドレイン
    Cleaning = 1,    // リソースクリーンアップ
    Finalizing = 2,  // 最終確認
}

/// シャットダウンコンポーネント登録情報
struct ShutdownComponent {
    name: String,
    phase: ShutdownPhase,
    timeout: Duration,
    handler: Box<dyn FnOnce(CancellationToken) -> tokio::task::JoinHandle<()> + Send>,
}

/// プロダクション級シャットダウンマネージャ
struct ShutdownManager {
    token: CancellationToken,
    components: Arc<Mutex<Vec<ShutdownComponent>>>,
    progress: Arc<Mutex<Vec<String>>>,
}

impl ShutdownManager {
    fn new() -> Self {
        Self {
            token: CancellationToken::new(),
            components: Arc::new(Mutex::new(Vec::new())),
            progress: Arc::new(Mutex::new(Vec::new())),
        }
    }

    fn token(&self) -> CancellationToken {
        self.token.clone()
    }

    fn register<F, Fut>(
        &self,
        name: impl Into<String>,
        phase: ShutdownPhase,
        shutdown_timeout: Duration,
        handler: F,
    ) where
        F: FnOnce(CancellationToken) -> Fut + Send + 'static,
        Fut: std::future::Future<Output = ()> + Send + 'static,
    {
        let token = self.token.clone();
        let handle = handler(token);
        let _ = (handle, shutdown_timeout);
        self.components.lock().unwrap().push(ShutdownComponent {
            name: name.into(),
            phase,
            timeout: shutdown_timeout,
            handler: Box::new(|t| tokio::spawn(async {})),
        });
    }

    async fn shutdown(&self, global_timeout: Duration) -> ShutdownReport {
        let start = Instant::now();
        let mut report = ShutdownReport {
            total_duration: Duration::ZERO,
            phase_results: Vec::new(),
            forced: false,
        };

        println!("🛑 グレースフルシャットダウン開始...");
        self.token.cancel();

        for phase in [ShutdownPhase::Draining, ShutdownPhase::Cleaning, ShutdownPhase::Finalizing] {
            let phase_name = match phase {
                ShutdownPhase::Draining => "トラフィックドレイン",
                ShutdownPhase::Cleaning => "リソースクリーンアップ",
                ShutdownPhase::Finalizing => "最終確認",
            };
            println!("📋 フェーズ: {}", phase_name);

            let components = self.components.lock().unwrap();
            let phase_components: Vec<_> = components.iter()
                .filter(|c| c.phase == phase)
                .collect();

            let mut join_set: JoinSet<(String, Result<(), Duration>)> = JoinSet::new();

            for comp in phase_components {
                let name = comp.name.clone();
                let comp_timeout = comp.timeout;
                join_set.spawn(async move {
                    match timeout(comp_timeout, async {
                        tokio::time::sleep(Duration::from_millis(100)).await;
                    }).await {
                        Ok(()) => (name, Ok(())),
                        Err(_) => (name, Err(comp_timeout)),
                    }
                });
            }

            while let Some(result) = join_set.join_next().await {
                match result {
                    Ok((name, Ok(()))) => {
                        println!("  ✅ {} シャットダウン成功", name);
                        report.phase_results.push(PhaseResult {
                            name,
                            phase,
                            success: true,
                            duration: Duration::from_millis(100),
                        });
                    }
                    Ok((name, Err(t))) => {
                        println!("  ⚠️ {} シャットダウンタイムアウト({:?})", name, t);
                        report.phase_results.push(PhaseResult {
                            name,
                            phase,
                            success: false,
                            duration: t,
                        });
                    }
                    Err(e) => {
                        println!("  ❌ タスクエラー: {}", e);
                    }
                }
            }

            if start.elapsed() > global_timeout {
                println!("⚠️ グローバルタイムアウト、強制終了!");
                report.forced = true;
                break;
            }
        }

        report.total_duration = start.elapsed();
        println!("🛑 シャットダウン完了、所要時間: {:?}", report.total_duration);
        report
    }
}

#[derive(Debug)]
struct PhaseResult {
    name: String,
    phase: ShutdownPhase,
    success: bool,
    duration: Duration,
}

#[derive(Debug)]
struct ShutdownReport {
    total_duration: Duration,
    phase_results: Vec<PhaseResult>,
    forced: bool,
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let manager = ShutdownManager::new();

    manager.register(
        "HTTPサーバー",
        ShutdownPhase::Draining,
        Duration::from_secs(15),
        |token| async move {
            token.cancelled().await;
            tokio::time::sleep(Duration::from_secs(2)).await;
        },
    );

    manager.register(
        "キャッシュフラッシュ",
        ShutdownPhase::Cleaning,
        Duration::from_secs(5),
        |token| async move {
            token.cancelled().await;
            tokio::time::sleep(Duration::from_secs(1)).await;
        },
    );

    manager.register(
        "DB接続プール",
        ShutdownPhase::Finalizing,
        Duration::from_secs(10),
        |token| async move {
            token.cancelled().await;
            tokio::time::sleep(Duration::from_millis(500)).await;
        },
    );

    println!("サービス実行中、3秒後にシャットダウンをシミュレーション...");
    tokio::time::sleep(Duration::from_secs(3)).await;

    let report = manager.shutdown(Duration::from_secs(30)).await;

    println!("\n📊 シャットダウンレポート:");
    println!("  合計所要時間: {:?}", report.total_duration);
    println!("  強制終了: {}", report.forced);
    for result in &report.phase_results {
        let status = if result.success { "✅" } else { "❌" };
        println!("  {} {} - {:?}", status, result.name, result.duration);
    }

    Ok(())
}

重要ポイント

  • フェーズ別シャットダウン:Draining → Cleaning → Finalizing、正しい順序を保証
  • 各コンポーネントは独立タイムアウト、一つのスタックしたコンポーネントで全体がブロックされない
  • ShutdownReportを生成、ログ記録とモニタリングアラートに便利
  • グローバルタイムアウトは最後の防衛線、プロセスが永遠に終了しないことを防止

よくある落とし穴

落とし穴1:selectでcancelledの処理を忘れる

// ❌ 間違い:タスクが永遠に終了しない
tokio::spawn(async move {
    loop {
        do_work().await;
        sleep(Duration::from_secs(1)).await;
    }
});

// ✅ 正しい:selectでキャンセルシグナルをリッスン
let t = token.clone();
tokio::spawn(async move {
    tokio::select! {
        _ = t.cancelled() => {
            println!("キャンセルシグナル受信、ループ終了");
        }
        _ = async {
            loop {
                do_work().await;
                sleep(Duration::from_secs(1)).await;
            }
        } => {}
    }
});

落とし穴2:CancellationTokenをワンショットとして誤用

// ❌ 間違い:cancelled()は一度しかトリガーされない
token.cancelled().await;
token.cancelled().await;

// ✅ 正しい:各タスクが自分のtokenをクローン
let t1 = token.clone();
let t2 = token.clone();
tokio::spawn(async move { t1.cancelled().await; });
tokio::spawn(async move { t2.cancelled().await; });

落とし穴3:JoinHandleの未待機によるタスクリーク

// ❌ 間違い:spawn後放置、タスクがまだ実行中の可能性
tokio::spawn(async { heavy_work().await; });
println!("main終了");

// ✅ 正しい:全タスクの完了を待機
let handle = tokio::spawn(async { heavy_work().await; });
handle.await?;

落とし穴4:シャットダウン時のTCPリスナー処理忘れ

// ❌ 間違い:リスナーが自動クローズされず、新規接続が継続
let listener = TcpListener::bind("0.0.0.0:3000").await?;
loop {
    let (stream, _) = listener.accept().await?;
    tokio::spawn(handle_connection(stream));
}

// ✅ 正しい:selectでシグナルと接続を同時にリッスン
let t = token.clone();
tokio::select! {
    _ = t.cancelled() => {
        println!("新規接続の受け付け停止");
        drop(listener);
    }
    result = listener.accept() => {
        // 新規接続の処理
    }
}

落とし穴5:不当なタイムアウト設定

// ❌ 間違い:タイムアウトが短すぎる、通常リクエストが切断される
timeout(Duration::from_millis(100), server).await;

// ❌ 間違い:タイムアウトが長すぎる、Kubernetesがプロセスを強制終了
timeout(Duration::from_secs(300), server).await;

// ✅ 正しい:KubernetesのterminationGracePeriodSecondsに基づいて設定
timeout(Duration::from_secs(25), server).await;

エラートラブルシューティング表

エラー現象 可能な原因 調査方法 解決策
ctrl+c無反応 シグナル未リッスンまたはselect分岐未トリガー signal::ctrl_c()の呼び出し確認 mainにシグナルリッスンロジックを確保
シャットダウン後プロセスが終了しない 未完了タスクまたは無限ループ tokio-consoleでアクティブタスクを確認 全タスクにcancelled()チェックを追加
"task was cancelled" panic ロック保持中のタスクをabort panicスタックのMutex位置を確認 drop前に全ロックを解放
データ損失 シャットダウン時の書き込み完了待ちなし シャットダウン順序の正確性確認 サービスを先に閉じてからDB
接続リセット graceful_shutdown未使用 axum/hyper設定確認 with_graceful_shutdown()を使用
Kubernetes SIGKILL シャットダウンタイムアウトがterminationGracePeriodSeconds超過 k8s events確認 タイムアウト短縮またはgrace period増加
メモリリーク CancellationToken循環参照 valgrind/heaptrackで確認 タスク完了後にtokenがdropされることを確認
CPU 100% select内の空ループ sleepなしのloopの確認 ループにyield/sleepを追加
"channel closed"エラー シャットダウン後のmpsc送信 シャットダウン後のsender使用確認 送信前にtoken.is_cancelled()を確認
シグナル処理遅延 他のselect分岐がシグナルリッスンをブロック signalが独立タスクでリッスンされているか確認 シグナルリッスンを専用spawnに配置

高度な最適化

  1. ヘルスチェック連動:シャットダウン時に/healthエンドポイントをdraining状態に設定、ロードバランサーが自動的にインスタンスを除外し、新規トラフィックの流入を防止

  2. 進捗可視化:metricsエンドポイントでシャットダウン進捗(active_requests、completed_phasesなど)を公開、運用監視に便利

  3. ホットリスタートサポート:Unix Domain Socketでファイルディスクリプタを渡し、ゼロダウンタイムリスタートを実現—新プロセスがlistenerを引き継いでから旧プロセスをシャットダウン

  4. カスケードキャンセルCancellationToken::child_token()で階層関係を作成—親のキャンセルが子をトリガーするが、逆はしない

  5. シャットダウンフック登録on_shutdown()メソッドでクロージャを登録、atexitと同様に一回限りのクリーンアップロジック(一時ファイル削除、シャットダウン通知送信など)を実行

アプローチ比較

アプローチ 複雑さ 信頼性 適用シナリオ
シンプルctrl+cリッスン 開発/テスト環境
CancellationTokenブロードキャスト ⭐⭐ ⭐⭐⭐ 単一サービスプロダクション
JoinSet + タイムアウト ⭐⭐⭐ ⭐⭐⭐⭐ マルチタスクサービス
GracefulService trait ⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ マルチコンポーネントマイクロサービス
ShutdownManager ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ 大規模プロダクションシステム

まとめ

Rust Tokioグレースフルシャットダウンは「ctrl_cを追加すれば終わり」のような単純な問題ではありません—シグナルキャプチャ、タスク調整、コネクションドレイン、リソース解放を含むシステムエンジニアリングの課題です。CancellationTokenはブロードキャスター、JoinSetはコレクター、タイムアウトはセーフティネット、フェーズ別シャットダウンはコア戦略。 プロダクション環境ではシャットダウンは起動よりも重要です—起動の失敗はリトライできますが、シャットダウンの失敗はデータ損失を招く可能性があります。

オンラインツールおすすめ

  • JSONフォーマッター — シャットダウンレポートJSONをフォーマット、迅速なトラブルシューティング
  • cURL→コード変換 — テストリクエストをRustコードに変換、シャットダウン動作を検証
  • ハッシュ計算 — 設定ファイルのハッシュを計算、シャットダウン設定の一貫性を確保

ブラウザローカルツールを無料で試す →

#Rust Tokio#优雅关闭#异步运行时#Rust#2026#编程语言