Rust6ヶ月本番運用して気づいた、ネットの嘘と本当

Rustは本当に完璧な言語なのか?6ヶ月の実装経験から見えた、借用チェッカーの苦しみ、パフォーマンスの現実、チーム開発の課題を正直に言語化しました。

最初は「Rustなら全部解決」と思ってた

先日、うちのチームで新しいマイクロサービスをRustで書くことになった。当初は、メモリ安全性が得られるしパフォーマンスも最高らしいし、これは神言語だと思い込んでた。ただ、実際に6ヶ月本番運用してわかったのは、ネット上の「Rustは最高」という声の半分以上は、都合の良い部分だけ切り取った話だったということだ。

最初のコミットから本番デプロイまで、本当に山あり谷ありだった。借用チェッカーでハマったとか、非同期ランタイム選びで失敗したとか、そういう小さな話ではなく、プロジェクトの進め方全体に影響する課題ばかりだった。

今日は、その実装記録を正直に共有する。成功した部分もあるし、「こんなはずじゃなかった」という部分も結構ある。本当のところを知りたい人向けの記事だと思ってくれればいい。

借用チェッカーとの3ヶ月戦争

実装開始から最初の3週間は、本当に地獄だった。単純なデータ構造を作ろうとしても、借用チェッカーに怒られる。エラーメッセージは親切だと言われてるけど、僕たちのチームは全員Rust初心者だったから、その親切さがまったく理解できなかった。

最初のハマりどころは、複数のスレッドから同じデータにアクセスする処理だ。JavaやPythonなら、同期プリミティブ(ミューテックスとか)をちょっと使えば済む。でもRustの場合、所有権と借用の概念を完全に理解した上で、Arc<Mutex<T>>みたいな構造を設計しないといけない。

use std::sync::{Arc, Mutex};
use tokio::task;

#[tokio::main]
async fn main() {
    let counter = Arc::new(Mutex::new(0));
    let mut handles = vec![];

    for i in 0..5 {
        let counter_clone = Arc::clone(&counter);
        let handle = task::spawn(async move {
            for _ in 0..100 {
                let mut num = counter_clone.lock().unwrap();
                *num += 1;
            }
        });
        handles.push(handle);
    }

    for handle in handles {
        handle.await.unwrap();
    }

    println!("Result: {}", *counter.lock().unwrap());
}

このコード、JavaやPythonのエンジニアなら3分で理解できるかもしれない。でも僕たちは最初、Arc::cloneって何?cloneじゃダメなの?という超基本的なところでハマってた。2週間くらい悶々としてたから、地味に痛い。

借用チェッカーが学習曲線になるのは本当だ。ただ、その曲線が思ってた以上に急だった。チームに「Rustで書きます」と提案したとき、「難しいと思ったら切り替えます」という前提を作っておいてよかった。実際、最初の1ヶ月は、メンバーのモチベーションが結構下がってた。

ただ、ここが重要なんだけど、3ヶ月経つと急に「あ、これこういう仕組みなんだ」という感覚が見えてくる。完全に理解できたわけじゃなくても、コンパイラエラーの読み方がわかると、自分で問題を解決できるようになる。そこまで行くと、Rustで書く速度がぐんと上がる。

非同期ランタイム選びの落とし穴

Rust本番運用で2番目の大きな課題は、非同期ランタイムの選択だ。うちはtokioを選んだんだけど、その過程で結構迷った。

当初の計画では、Rustなら何でも速いだろうという楽観的な想定で、ランタイムの詳細な比較をしなかった。tokioが標準的だという認識だけで決めてた。でも本当は、プロジェクトの要件に応じて、async-std、smol、embassy(マイクロコントローラ向け)とか、選択肢があるんだ。

tokioを使い始めてから気づいたのが、tokioはマルチスレッド前提だということ。シングルスレッドで十分なケースもあるし、その場合はメモリ効率やCPU使用率が変わる。うちのサービスは、スループットよりレイテンシを重視する設計だったから、もっと軽量なランタイムでよかったかもしれない。

// tokio (マルチスレッド)
#[tokio::main]
async fn main() {
    // 複数のスレッドプールで実行される
}

// tokio (シングルスレッド)
#[tokio::main(flavor = "current_thread")]
async fn main() {
    // シングルスレッドで実行
}

この選択だけで、メモリ使用量が10~15%変わることに、実装半年目で気づいた。本来は開始時点で比較検討しておくべき内容だ。正直、この失敗は結構もったいなかった。

もう一つ痛感したのが、非同期処理とエラーハンドリングの複雑さ。RustのResult<T, E>型とasync/awaitを組み合わせると、エラーチェーンがすごく深くなる。カスタムエラー型を設計するのも、けっこう手間だ。

use thiserror::Error;

#[derive(Error, Debug)]
pub enum MyError {
    #[error("Database error: {0}")]
    Database(String),
    #[error("Timeout error")]
    Timeout,
    #[error("Invalid request: {0}")]
    InvalidRequest(String),
}

async fn process_request() -> Result<String, MyError> {
    // 処理
    Ok("success".to_string())
}

thiserrorクレート使うと楽になるけど、そういう便利な知識は、本当に必要になるまで調べない傾向がある。つまり、無駄な時間が発生するんだ。

パフォーマンスが期待値と違った

これは、かなり驚いた。Rustはメモリ安全で高速という触れ込みだから、Go や Python と比べて圧倒的に速いと思い込んでた。

実際に書いてみると、確かに実行速度は速い。でも、その差はプロダクション環境では「ほぼ無視できる」レベルだった。うちのサービスのボトルネックは、言語の性能ではなく、データベースクエリとネットワーク I/O だったんだ。

うちのシステムのレイテンシ分布はこんな感じだ:

xychart-beta
    title Latency breakdown (%)
    x-axis [DB Query, Network I/O, Rust Logic, JSON Parsing]
    y-axis "Percentage" 0 --> 100
    line [65, 25, 1, 9]

Rust Logicは全体の1%程度。つまり、言語を何に変えても劇的には改善されない。当然っちゃ当然だけど、僕たちはこの現実に直面するまで、Rustの高速性に過度な期待をしてた。

CPU集約的な処理(機械学習の推論とか、複雑な計算)なら、Rustの価値は別だろう。でも CRUD操作が中心なら、言語の選択よりデータベース設計の方がはるかに重要だ。これを理解するのに、本番環境での測定が必要だったのは、ちょっともったいなかった。

メモリ安全性は本当に得られた

ここまで課題ばかり書いてるけど、Rustで実現できたメリットも当然ある。一番大きいのは、メモリ関連のバグがほぼゼロになったこと。

JavaやPythonでもメモリリークは発生するし、C/C++なら use-after-free とか、本当に怖いバグがある。Rustなら、そのクラスのバグはコンパイル時に検出される。これは開発効率的には、本当に大きい。

本番運用6ヶ月で、メモリ関連で本番障害になったことは一度もない。これは、Go や Python では考えられない。定期的にメモリプロファイリングして、リークを追跡する手間が完全になくなった。正直なところ、この部分だけでもRust導入の価値があると感じてる。

もう一つ実感したのが、リファクタリング時の安心感。借用チェッカーのおかげで、大規模なコード変更をしても、コンパイルが通れば本番バグになるケースがかなり少ない。これは、チーム開発での心理的負担を大きく減らしてくれた。個人的には、この心理的な安心感が、長期メンテナンスでの生産性向上に繋がると思ってる。

開発生産性は想定より低かった

ただし、この安全性と引き換えに、開発速度は落ちた。初期段階では特に顕著だ。

うちは、最初のマイルストーンを「2ヶ月で基本的なAPI実装完了」と見積もってた。実際には4ヶ月かかった。借用チェッカーとの戦いで2ヶ月ロストしてる。正直、この遅れは予想してなかった。

2ヶ月目以降は速くなったけど、それでもPythonやGoと比べると、同じ仕様の実装には1.5~2倍の時間がかかった。これは「Rustは学習コストが高い」という理由じゃなく、単純に書く量が多いということ。型システムが厳密だから、自動型推論が効きにくい。エラーハンドリングも明示的に書かないといけない。

開発生産性という観点では、Rustは「長期運用前提のプロジェクト」に向いてると思う。初速が遅い代わりに、メンテナンスフェーズの生産性が高いし、バグが少ない。短期間で作って捨てる系なら、Pythonの方が合理的だ。これは経験してみて初めて実感できる。

実装して見えた、Rustが活躍する場面

ネガティブな話が多くなっちゃってるけど、実装を進める中で「あ、これはRustが活躍する」という場面も結構あった。

一つは、マルチスレッド処理が必要な場合。Rustの所有権システムがあるおかげで、スレッド間のデータ競合をコンパイル時に検出できる。JavaやPythonなら、実行時に予期しない挙動をする可能性がある。

use std::sync::Arc;
use std::thread;

fn main() {
    let data = Arc::new(vec![1, 2, 3]);
    let mut handles = vec![];

    for i in 0..3 {
        let data_clone = Arc::clone(&data);
        let handle = thread::spawn(move || {
            println!("Thread {}: {:?}", i, *data_clone);
        });
        handles.push(handle);
    }

    for handle in handles {
        handle.join().unwrap();
    }
}

このコードは安全に実行される。もし誰かがArcの代わりに普通の参照を使おうとすると、コンパイラが怒ってくれる。これが本当に心強い。

もう一つは、大規模なデータ処理。メモリ効率が重要な場合、Rustの細かい制御は本当に役立つ。ヒープアロケーションを最小化したり、スタックメモリを活用したりするのが、言語レベルで容易だ。

あと、マイクロサービスアーキテクチャでの相互連携。うちは複数のRustサービス同士で通信してるんだけど、シリアライゼーション/デシリアライゼーションのオーバーヘッドが小さい。gRPCとかprotobufとの相性も良い。地味に便利な点だ。

2026年時点でのRust採用判断

じゃあ、Rustを採用すべきか。それは、プロジェクトの特性による。実装して6ヶ月経った今、もう一度冷静に考えると、こんな感じだ。

項目Rustが合理的Rustが過剰
プロジェクト期間3年以上1~2年の短期
メモリ効率の重要度高い(エッジ、高トラフィック)低い(一般的なAPI)
マルチスレッド中核機能軽微
チーム適性学習に投資できる短期成果が優先
開発速度の優先度低い高い

Rustが合理的な場合:

  • 長期メンテナンスが必要なシステム(3年以上)
  • メモリ効率が重要(エッジデバイス、高トラフィック環境)
  • マルチスレッド処理が中核機能
  • チーム全員が学習に投資できる時間がある

Rustが過剰な場合:

  • MVP(最小実行可能製品)開発
  • CRUD操作が中心
  • 開発速度が最優先
  • 1~2年の短期プロジェクト

前に書いた記事で、Clean Architecture実践ガイド2026|Go・Rustで学ぶレイヤー設計を共有したことあるけど、その時点では「どちらも長期プロジェクト向き」という論調だった。6ヶ月運用した今は、その認識を少し修正したい。RustはGo以上に学習コストが高いから、チームの習熟度を見込んだスケジュール設定が超重要だ。

バッチ処理設計|スケーラブルなシステム構築ガイドでも書いたけど、バックエンド開発ではバッチ処理の設計が全体的な成功を左右する。Rustなら、そのバッチ処理部分でメモリ効率を極限まで高められる。ただし、そこまでのメリットが必要ないなら、学習コストに見合わない。

今のところの結論

Rustは本当に良い言語だ。ただし、ネット上での評判の半分は誇大広告である。実装してみるまで、その良さと課題の両方を認識できない。

うちのチームは、新規プロジェクト開始時に「Rustで書きたい」という気持ちより、「このプロジェクトには何の言語が合理的か」という議論を優先すべきだったと思う。結果的には、Rustで正解だった。でも、それは事後的な評価に過ぎない。

今、Rust導入を考えてるなら、以下の点を確認することをお勧めする:

  1. 学習期間の確保:3ヶ月は覚悟する。これを短縮することはほぼ不可能だ
  2. メリットの明確化:メモリ安全性やパフォーマンスが本当に必要か、本番環境で測定してから判断する
  3. チームの適性:細かい型システムに向き合える気持ちがあるか、最初のハマり期を乗り越える根気があるか
  4. 長期コミット:短期で成果を求めないこと。焦ると失敗する

実装を進める中で困った時は、Rust公式ドキュメントか、イベント駆動アーキテクチャ実装ガイド|Kafka・マイクロサービス対応みたいな実装系の記事を参考にするといい。理論より実装が大事だ。

まとめ

  • 借用チェッカーは3ヶ月で慣れる:最初は地獄だけど、学習曲線の先には理解がある。焦らない
  • パフォーマンスの期待値は下げておく:言語の速さより、アーキテクチャとI/Oの方が重要。ボトルネック測定は必須
  • メモリ安全性は本当に得られた:バグ減少とリファクタリング時の安心感は実感できた。これが長期メリット
  • 開発生産性は落ちる:特に初期段階。長期プロジェクト前提で計画しよう。焦りは禁物
  • 採用は「本当に必要か」から判断する:誇大広告に踊らされず、プロジェクト特性で判断すべき

次のアクション:Rustの採用を検討してるなら、まずはスモールスケールでパイロットプロジェクトを立ち上げることをお勧めする。チーム全体が借用チェッカーと向き合う時間を作ることで、本当の適性が見えてくる。焦らず、確認してから本格導入に進もう。

U

Untanbaby

ソフトウェアエンジニア|AWS / クラウドアーキテクチャ / DevOps

10年以上のIT実務経験をもとに、現場で使える技術情報を発信しています。 記事の誤りや改善点があればお問い合わせからお気軽にご連絡ください。

関連記事