個人で運営している賃貸物件の検索サービス Comfy のバックエンドを Rust でリプレースしました。この記事では、そのリプレースの背景と詳細をご紹介します。
まずは結果から
- 技術構成: Rust + Cloud Run1 へ移行 (Python + GCE2 から)
- 性能向上: 約 1.5 倍
- 開発期間: 1 ヶ月間
- コード行数: 約 40 %
- インフラ費用: かなり減少 (多分3)
短い期間・少ないコードでかなり高速化できちゃった上に、開発体験もとてもよい Rust は本当に素晴らしいです…!!
サービス概要
Comfy は 日本全国の賃貸物件を超高速に検索できる Web サービス です。
技術構成等の概要は、以前書いた記事 「【個人開発】爆速な賃貸物件の検索サービスを作った」 をご覧頂ければと思います。
もしよかったらこちらからぜひ試して頂き、さらによくなった性能を体感してみて下さい…!!4
背景
リプレース前の構成
Python3 と FastAPI を使い、Google Compute Engine 上で動かしていました。バックエンドの性能はサービスの根幹となっており、高速化の工夫をいろいろ行いました。
ひとつの工夫としてC++ による拡張モジュールの実装があります。
主要な API はサーバ側でのゴリゴリの計算が必要となり CPU バウンドな処理です。それらは素の Python では遅すぎるため、 C++ を使った拡張モジュールを実装して Python に組み込んでいました。
リプレースの理由
最も大きな理由は、上述の C++ の拡張モジュールを世話するのが面倒となってきたことです5。
古くなってビンテージ化したソフトウェアをいやいやメンテするより、新しくいちからピカピカのものをつくって磨き上げて、面倒を見るのが楽しい状態にして今後の機能追加に弾みをつけようとなった感じです。
Rust であれば実行速度・開発体験ともに申し分なく、今回の役割にピッタリという理由から Rust を選定しました。
リプレース詳細
リプレースにあたっては、リプレース前と完全に同等の機能を開発しました6。
実際に受信したログから、旧環境と全く同様のレスポンスが返ることも入念にテストしました7。
構成
フレームワークとして Actix Web を採用しました。シンプルなフレームワークで十分で、できればより高速化したいと思ったため、現時点では Actix Web がベストと判断しました。
インフラ構成に関しては、当初はリプレース前と同様 Compute Engine 上で動かす想定でした。
しかしログ出力先である Cloud Logging のライブラリが Rust には提供されておらず、自前実装しかけたのですが若干面倒だったため、標準出力を logging できる Cloud Run への移行を決めました8
リプレースでの変化
性能
API が複数ありそれぞれの処理の性質も異なるため具体的な数字は出しにくいのですが、ベンチマークの結果では全ての API で性能向上が確認できました。特に CPU バウンドとなっていた主要な API では大きな改善がみられ、およそ 1.4 倍から 1.6 倍の性能の向上となりました(処理時間がおおよそ 60%〜70%)9。その他の API に関しても 1.5 倍から 10 倍程度までの改善がみられました(ただし、数ミリ秒から数十ミリ秒の性能向上のため、正直体感的にはほぼ変わりません10…w)。
開発期間
Actix Web の Getting Started から開発をはじめ、その日からちょうど 1 ヶ月でリリースしました11。なお、旧バックエンドの開発期間は、コミットログを確認したところおよそ 3,4 ヶ月といった感じでした12。
コードの量
コードの行数は下記でした13。
- 旧バックエンド: 3437行 (Python 及び C++)
- 新バックエンド: 1450行 (Rust14 のみ)
約 40 % 程度の規模となりました。
コード行数が減った大きな理由は、プリミティブ型を多用している箇所を Rust ではとてもきれいに実装できたためです。
C++でもできる限り Template と仮想関数を用いた抽象化をしていたのですが、型による条件分岐が若干煩雑になっていました。Rust ではいわゆる代数的データ型により、型による振る舞いの違いを、とてもシンプルに美しく抽象化することができ、それらを担うモジュールのみの行数では 3 分の 1 以下となりました15。
なお、フロントエンドの TS・TSX ファイルの行数は 8000 行を超えていました…
インフラ費用
旧バックエンドは Compute Engine のインスタンスをオートスケールさせる構成としており Load Balancer も含める必要があったため、1ヶ月あたり数千円程度のコストがかかっていました。
リプレース後は現時点の数字としてはかなり抑えられていますが、まだはっきりとした費用感は把握できていないため 一ヶ月問題なく運用できたらまた 𝕏 あたりで報告したいと思います。
Rust 化の大変だったこと
あまり思い浮かばないくらい Rust が最高なのですが、上述の通り Cloud Logging の公式なライブラリがなかったことでちょっと思い悩みました16。
他の主な言語では提供されている公式のライブラリなどが、現時点ではまだ Rust には提供されていないことはそれなりにあるかもしれません。
また Python や TypeScript だとものの数行で書けるような処理が、トレイトや所有権の制約で実装量がとても多くなるようなケースもありました17。
ただ、そこら辺を加味しても今回は圧倒的に Rust を採用するメリットが上回っていると考えています(少なくとも大きな問題が起きていない現時点では…)。
まとめ
とにかく Rust は素晴らしいです…! 長々書きましたがその一点に尽きます。
あとは…今から 1 ヶ月経っても 「Rust 最高!! Cloud Run 最高!!」 と言ってられるか (クラッシュやサービス停止・クラウド破産などでげっそりしているかもしれません) 、もしよければ 𝕏 の僕のアカウント をウォッチしてみて下さい(なにかあったらさりげなくツイートするので優しく見守って下さい…)。
また、いいね等でもし多くのリアクションを頂ければ、より詳細な内容や今後の変化に関しても改めて記事を書いていきたいと思いますので、もしよければぜひぜひいいねを…!!お願いします…!!!。
長文ながら最後まで読んで頂き、本当にありがとうございました。
-
AWS の Fargate 相当(App Runner 相当かも。AWS よく知りません)。Container ベースの実行環境。 ↩
-
AWS の EC2 相当。古きよき仮想マシンベースの実行環境。 ↩
-
まだ旧環境も並行稼働させていたり(なにかあったらいつでも切り戻せるように…) Cloud Run 上で色々実験したりで、はっきりとした費用は把握しずらく、一ヶ月問題なく運用できたら報告したいと思います ↩
-
といっても以前から速すぎたため、体感的にはほぼ変わらないはずです… ↩
-
サービスの主要言語の Python / TypeScript に比べると C++ の部分をさわる機会は圧倒的に少なく、正直手をつけたくないという潜在意識が強くなってきました(ミスってクラッシュしてサービス停止とか怖すぎるし)。バックエンドで追加したい機能も増えてきており、そろそろなんとかせねばという機運が高まり、去年から趣味でふれていた Rust へのリプレースを検討しました。 ↩
-
といってもリプレースにあたり API を見直したり削ぎ落とした部分もいくつかあり、旧環境でまずは変更を実装しつつ、新 API を追随させるといったかたちで開発を進めました。 ↩
-
それでも最後に API の向き先を新環境に変更する際はとてもドキドキしました。最後は、「多分動くと思うからリリースしようぜ」(参照) の字面そのままの気持ちでえいやでリリースしました。 ↩
-
この判断をするまではかなり思い悩みました。手っ取り早くリプレースしたかったので、現時点では Cloud Run の検証などに時間をかけたくなかったためです。しかしリリースしてみた今では、かなりよい判断に踏み切れたと思っています。 ↩
-
大きな要因は、C++ では Virtual Function を使って抽象化していたため動的ディスパッチとなっており、Rust では Enum を使った静的ディスパッチの実装に変更できたためです。Rust でも最初はトレイトオブジェクトを使用した動的ディスパッチを実装したのですが、その時点では 1.1 倍程度の性能向上となっており、静的ディスパッチの実装にしたところ大きくパフォーマンスが上がった、という感じです。おそらく C++ でも動的ディスパッチにならないように実装していれば、そこまでの差異はでなかったと考えられます。
動的ディスパッチ・静的ディスパッチに関しては、この記事及び書籍『コンセプトから理解するRust』が大変参考になりました。 ↩ -
むしろ、検索以外の地図データのロードなどが相対的に遅くなってしまい、もっさり感が出てきてしまっているため、その辺りの体験の向上が喫緊の課題です。 ↩
-
個人開発専業マンではないので、別の本業がありつつの開発でした。(旧バックエンドの開発期間も別の本業ありつつ) ↩
-
初期開発でもあり、リリース後に機能追加もしてきたので、正確にはどれくらい時間をかけたかはわかりません… ↩
-
基本的にどれも、API サーバのレポジトリの src ディレクトリ以下にあるコードを拡張子で find して wc コマンドで雑にカウントしたものです。前処理のロジック等もあるのですが、それらは含まれません。また、テストコードやスクリプト類も除外されています。空行やコメント行は含まれています。
コードの行数は個人の実装スタイルによっても色々変わりうるし、それこそ異なる言語間で比較する意味は大きくないとも思いますが、個人的にはコードはシンプルで少ないほど、必要となる認知資源が少なくなって開発体験が良いと思っています(もちろんそれ以上に適切なモジュールの分割等は大事だと思います)。また、コードは資産ではなく負債であるという考えにも大方同意するため、管理すべきコードはできる限り少なくした方がよいと近年は思っています。 ↩ -
デフォルト設定の Rustfmt で format された上での行数です。 ↩
-
C++ でもきれいに実装する方法があるのかもしれませんが、知りうる限り、また調べた限りでは、うまくできませんでした… ↩
-
Actix Web の Middleware の実装を頑張ってみたのですが、Python の FastAPI では 7,8 行で書ける内容が Rust では 100 行ほどになってしまう上になかなかうまくいかず、結果Cloud Run への移行に踏み切るといったことになりました。 ↩
-
改めてスクリプト言語の変幻自在な柔軟性にも気づきました。 ↩