276,406行のC++コードを捨ててRustへ移行したスタートアップの技術的決断
TL;DR
私たちはデータベースプロジェクトを再構築するために C++ を Rust に置き換えました。この記事では、その理由、直面した課題、そして Rust がプロダクションレベルのプロジェクトに適しているのはどんな時かについて説明します。
RisingWave は、高性能なデータプラットフォームであり、ユーザーが馴染みのある SQL クエリを使って、イベントストリーミングのパイプラインやアプリケーションを数分で構築できるようにします。
RisingWave を作り始めたとき
私たちが RisingWave の構築を始めたのは 2021 年初頭のことでした。当初は C++ で書いていました。創業チームは 10 年以上の経験を持つ熟練した C++ エンジニア数名で構成されていたため、C++ を選ぶのは当然の決定でした。最初の数ヶ月の開発は順調でした。私たちは次世代の素晴らしいデータベースを作るために全力で開発を進め、RisingWave がモダンデータスタックを揺るがす存在になることを夢見ていました。私たちはより高い生産性を追い求めていました。
しかし、エンジニアが増えるにつれて、C++ のいくつかの欠点が私たちを苦しめ始めました。読みづらいコーディングスタイル、メモリリーク、セグメンテーションフォルトなどです。私たちは自問し始めました。「C++ は新しいデータベースシステムを書くために本当に正しい言語なのか?」と。開発を始めてから約 7 ヶ月後、1 ヶ月にわたる議論を経て、最終的に私たちは C++ から Rust への移行という困難な決断を下しました。
この決断が意味するものは何だったのでしょうか?それは、10 人以上の経験豊富なエンジニアからなるチームが、システム全体をゼロから書き直さなければならないということです!そしてそれまでの 7 ヶ月間の努力が無駄になったということでもあります。このような決断は、スタートアップにとっては非常に異常なことです。テックスタートアップの熾烈な競争環境では、時間こそが最大の資産なのです。
この決断を下した後、私たちは約 2 ヶ月をかけて C++ のコードベースを完全に削除し、Rust で書き直しました。合計で 276,406 行のコードを削除しました。
それから 3 年が経過しました。この決断のおかげで、RisingWave は順調に航海を続けています。ソースコード は Apache License 2.0 のもとで誰でもアクセス可能です。170 名以上のコントリビューターが、このクラウドネイティブな ストリーミングデータベース の開発に参加しています。私たちは、RisingWave が書き直しという難関を乗り越えたことを誇りに思っています。そして、RisingWave が GitHub で 7,600 を超えるスターを獲得し、1,000 を超えるデータドリブンな企業にライブデータと履歴データの両方から継続的なインサイトを得るために信頼されていることを嬉しく思います。
Rust コミュニティは急速に成長しており、多くのエンジニアが私たちと同じように、プロジェクトを Rust で(再)実装すべきかどうかを考えていることでしょう。私たちがどのようにこの決断に至ったか、Rust に移行した理由、そして直面した落とし穴について共有したいと思います。
まずは、C++ に何が問題があったのかを振り返ってみましょう。
C++でRisingWaveを実装してみて:良い点、問題点、そして醜い点
C/C++ は、データベースシステムを構築するための最も一般的なプログラミング言語の 1 つであることは間違いありません。よく知られた多くのデータベースシステム、たとえば MySQL、PostgreSQL、Oracle、IBM Db2 は C/C++ で作られています。現在でも実用的で、重要かつ有効な言語です。C++ を選ぶことは、新しいデータベースシステムを構築するうえで決して間違いではありません。しかしそれは、C++ が最適な選択肢であることを意味しません。特に、ゼロから大規模なデータベースシステムを革新しようとする初期段階のスタートアップにとっては、なおさらです。その理由を理解するために、この実戦で鍛えられてきたプログラミング言語の「良い点」「問題点」「そして不足」を見ていきましょう。
良い点
- C++ は、開発者に高性能なプログラムを構築する機会を提供します。自動ガベージコレクションのオーバーヘッドなしに、メモリと計算の両方を細かく制御できます。さらに、C++ コードはアセンブリ言語にコンパイルでき、OS 上で直接実行できます。インタプリタやランタイム環境に依存する必要はありません。
- C++ は、システムプログラミングに適した言語であることが証明されています。実際、多くのデータベースは C/C++ で構築されています。そのため、意思決定者にとって C++ を選ぶことは決して悪い選択肢ではないという安心感があります。
問題点
- C++ はプログラマーに多くの柔軟性を与えますが、それには代償が伴います。バグを埋め込むのが非常に簡単であり、その多くは非常に厄介です。しかし、それ以上に C++ プログラムのデバッグは非常に困難です。特に並行プログラミングにおいてはなおさらです。
- 依存関係の管理が面倒です。たとえば CMake のように、C++ プロジェクトのコンパイルを自動構成するツールはありますが、開発者は依存ライブラリの構成やインストールを手動で行う必要があります。
そして不足
- 標準テンプレートライブラリ(STL)は、たとえばネイティブなコルーチンのサポートなど、モダンプログラミングの一部ツールに対応していません。その結果、開発者は多くのコミュニティプロジェクトに依存せざるを得ず、これらの多くは長期的なサポートがありません。
- 品質保証が難しいです。C++ は非常に多機能な言語であるがゆえに、開発者ごとにまったく異なるコーディングスタイルで C++ を書いてしまう傾向があります。異なるバックグラウンドを持つ開発者がチームに増えると、コードの可読性を維持できなくなりました。さらに、C++ コードのバグは簡単には特定できず、コードレビューが非常に困難になる原因でもありました。
なぜRustをC++の代わりに選んだのか
C++ がデータベースシステム構築に適しているのであれば、なぜ私たちはコードベース全体を書き直すという決断を下したのでしょうか?多くの人が考えるように、「かっこいいから」という理由だったのでしょうか?答えはノーです。私たちは慎重な検討を重ねたうえで Rust への移行を決めました。
ストリーミングデータベースは、ミッションクリティカルで極めて低レイテンシーが求められるタスクで使用されるのが一般的です。したがって、私たちは RisingWave を以下のような言語で構築する必要がありました。
- ゼロコスト抽象(zero-cost abstraction)を保証すること — パフォーマンスの上限を設けないため。
- ランタイムのガベージコレクションを必要としないこと — メモリ管理によるレイテンシースパイクを制御可能にするため。
この 2 点は最先端の性能を目指す上で譲れない必須要件です。
この目標を掲げたとき、私たちは C++ よりも Rust を選びました。どちらの言語もゼロコスト抽象とメモリ管理の完全な制御を開発者に提供しますが、私たちの考えでは Rust の方が開発者の認知負荷を軽減し、大規模で効率的なチーム開発を実現しやすいと判断しました。主な理由は次の 4 点です。
- Rust は安全です。 Rust は、所有権ルールを導入することでコンパイル時にメモリ安全性とスレッド安全性を保証します。これは、C++ でよく使われるメモリ管理機構 RAII を超えた仕組みです。2つの大きな利点があります。1つ目は明白です。Rust コンパイラがプログラムを検証できれば、実行時にセグメンテーションフォルトやデータレースは発生しません。これらは特に非同期かつ並行性の高いコードベースでは、デバッグに何十時間も要するような深刻な問題です。2つ目はより微妙な点です。Rust コンパイラはバグの原因となるコードの相互依存関係を制限することで、複雑に絡み合ったコードの断片の発生を抑制します。決定論的な実行(この点については今後のブログでさらに詳しく述べます)のおかげで、バグの再現性が大幅に向上します。
- Rust は使いやすいです。 C++ は開発者に最大限の自由を与えるという哲学に基づいています。しかし、この自由はときに裏目に出ることがあります。たとえば、C++ のテンプレートはコンパイル時に展開され、その型で利用できない操作があるかどうかを検査します。一方 Rust では、トレイトによってメソッドが呼び出せるかどうかが制約されるため、コンパイラは呼び出し箇所で型の妥当性を検査できます。この違いにより、C++ のテンプレートエラーメッセージは解読困難で、ベテランエンジニアの助けを要することも少なくありません。また、C++ における暗黙的な型変換の乱用も問題です。少ないコード量で済むという利点はありますが、エラーが発生した際にはその原因が「暗黙的」であるため、デバッグが困難になります。Google C++ スタイルガイド でも指摘されている通り、特に大規模なコードベースにおいては、暗黙の型変換は厳密に制限することで混乱よりも恩恵が勝ります。
- Rust は習得しやすいです。 経験豊富な C++ エンジニアにとって、Rust は習得しやすい言語です。Rust 初学者は、最初のうちは所有権(ownership)とライフタイム(lifetime)の概念を理解するのに時間を費やしますが、C++ に慣れたエンジニアであれば、たとえコード上で明示していなくても、それらの概念はすでに頭の中に存在しています。一方で、Rust は初心者には難しいと言われがちですが、当社のインターン生たちはその逆を証明しました。Rust/C++ の経験がなかったにもかかわらず、わずか 1〜2 週間で Rust を習得しました。理由のひとつは、暗黙的な型変換やオーバーロード解決ルールが少なく、覚えるべきことが明確だからです。そして、基本的な Rust コードのレビューは非常に容易です。今では、初心者の Rust コードレビューにかかる時間は C++ よりも大幅に少なくなりました。
- Unsafe Rust も管理可能です。 Rust の静的解析は保守的であるため、一部の高度な機能には unsafe Rust を使わざるを得ない場面があります。典型的なのは自己参照型の作成です。また、パフォーマンス向上のためにビットを直接操作するなど、低レベルなメモリ表現を使いたい場合にも unsafe が必要になります。懐疑的な人からは「これによりコードベースが脆弱になるのでは?」という疑問が出るかもしれません。しかし、RisingWave に関しては、経験的にそうした問題は発生していません。主な unsafe の使用箇所は、LRU キャッシュとビットマップであり、170,000 行のコードのうち 200 行未満です。「まずは Safe Rust で実装し、必要に応じて十分な根拠をもって unsafe に切り替える」という方針が、今では私たちが安眠できる秘訣になっています。
ここにRustの暗黒面がある
Rust は私たちの要求のほとんどを満たしてくれましたが、私たちは同時にその暗黒面も十分に認識しています。
-
非同期エコシステムの分断: 非同期ランタイムに関して初期段階で決断を下さなかったため、
futures-rs
やasync-std
を取り除き、最終的にtokio-rs
に移行するまでに数ヶ月を要しました。 - 扱いにくいエラーハンドリング: エラー発生時にバックトレースを取得するには、手動でエラーを保存・実装する必要があります。
-
AsyncIterator のサポート不足: 安定版ジェネレータやトレイト内の async 関数に対するネイティブサポートがないため、私たちは第三者ライブラリを使って同様の目的を達成しています。しかし、これらのライブラリは、標準実装と比較して追加の
Box
を割り当てるため、最終的にパフォーマンスが低下します。また、これらのライブラリが提供するマクロを使用すると、IDE の動作が妨げられ、開発体験が低下します。 - Generic Associated Type(GAT)の実用上の制約: GAT は多くの既存・将来的な機能(例:トレイト内の静的/動的 async 関数)の基盤となります。しかし、GAT の完全なサポートには複雑な技術課題が伴い、解決までに予想以上の時間を要する可能性があります。それまでは、制限を回避するためにさまざまなトリックを駆使するか、または最適でないソリューションで妥協しなければなりません。
それでも、優秀なエンジニアが多く集まっている当社のチームでは、全体として Rust は生産性とコード品質を大きく向上させており、その負の側面も十分にコントロールできています。
私たちの経験から学ぶこと
このブログ記事は、すべてのデータベース開発チームに「既存のC++コードベースを捨てて、Rustでシステムをゼロから書き直せ」と説得するものではありません。主な目的は、私たちがなぜそのような決断を下したのかを伝えることにあります。コードベース全体の書き直しは、楽しいものではありません。それどころか、スタートアップにとって時間を浪費することは自殺行為にも等しく、極めて過酷なプロセスです。
事実として、Rust がもたらす明らかなメリットがあったとはいえ、以下のような重要な要因がなければ、私たちもこの難しい決断を下さなかったでしょう。
- 私たちはちょうどその時、新しいシステムアーキテクチャに合わせてコードベースをリファクタリングしていたため、(少なくとも一部の)コードを書き直すことが避けられない状況でした。
- 私たちのチームには何人かの Rust エンスージアスト(Rustacean!)がいて、他のエンジニアに Rust の魅力を伝え続けた結果、チーム全体が「Rustでの書き直しは実行可能だ」と確信するようになりました。
- 2021年の夏にかけてエンジニアリングチームが急速に拡大し、新たに参加した多くのエンジニアたちの存在がコードベースの書き直しを大きく加速させました。
Rust はクールなプログラミング言語です。誰もが一度は使ってみるべきだと思います。しかし、「クールだから」という理由だけでプロジェクトを書き直すべきではありません。プロダクションレベルのプロジェクトを Rust で書き直すべきか検討している方は、以下の質問を自分自身に問いかけてみてください:
- 低レベルのプログラミング、パフォーマンス、メモリ安全性、パッケージ管理はあなたのプロジェクトで重要な課題になりますか?
- 潜在的な落とし穴を避けられるような Rust の専門家はチームにいますか?
- このプロジェクトの書き直しにはどれくらいの時間がかかりますか?
- 書き直しによって、重要な期限に間に合わなくなる可能性はありますか?
- 社内で Rust のトレーニングプログラムは用意されていますか?
これらの問いへの答えを慎重に検討した上で、最終的な判断を下すべきです。繰り返しますが、Rust(あるいは他のどんな言語も)がプロジェクトの運命を決めることはありません。しかし、賢い選択をすることで、数百、あるいは数千人月分の工数を節約できる可能性があります。
Discussion