アプリケーション全体をRustで書き直すのではなく、DiscordのStaff EngineerであるLily Maraは、リソースを最も消費する関数を、Pythonのような動的言語からRustへ段階的に移行し、同一プロセス内で両方の実装を接続する方法を提案しています。Maraは、Discordユーザーに毎日数百億件の通知を送信する分散システムの構築経験と、著書Refactoring to Rustを基に、この方法論をInfoQで公開された講演で紹介しました。
基本的な考え方は、Rustのほうが速いという理由だけで一方の言語を他方に置き換えることではなく、改善した際に最大の効果が得られるシステムの狭い部分を特定することです。このアプローチにより、変更範囲を縮小し、新しい挙動を古い挙動と比較してテストし、結果の一致を保証することが難しい場合には元の実装を保持できます。
なぜ全面的な書き換えを出発点にしないのか?
Maraは、古いシステム全体をより新しい言語で再構築したいという熱意は理解できるものの、実務上の大きなリスクを伴うと考えています。書き換えプロジェクトは納期を超過したり、予想以上に複雑になったり、古いシステムがすでに解決していた問題を再び持ち込んだりする可能性があります。また、古いコードが複雑なのは、必ずしも古いからとは限りません。実際の利用状況から蓄積された制約や詳細、そして新しいプロジェクトへ移すのが難しい組織的な知識を反映している場合があります。
講演では、パフォーマンスの問題をプログラミング言語だけに還元することについても警告しています。遅さの原因は、データベースのスキーマ、クエリやキャッシュのパターン、サービス設計にある可能性があり、コード行の実行コストにあるとは限りません。したがって、Rustを利用しても、アーキテクチャ上のボトルネックが自動的に解消されるわけではありません。
FFIによるリファクタリングとは何か?
この方法論では、MaraがFFI refactoringと呼ぶ、関数または関数の小さな部分を別の言語で書き直し、言語間連携インターフェースを通じて既存のアプリケーションに接続する手法を用います。紹介された例では、FlaskアプリケーションがHTTPリクエストの受信とJSONのデコードを引き続き担当し、その後、Rustで記述された統計関数へデータを渡し、結果をPythonに戻してレスポンスを送信します。
接続には、多くのシステムや言語の間で実質的な共通言語として機能するCインターフェースを使用します。Pythonの場合、PyO3プロジェクトがPythonからインポート可能なモジュールを作成するためのツールを提供し、Maturinをそのモジュールのビルドに使用できます。pyfunctionやpyclassなどの属性により、Rustの関数やデータ構造をPythonから呼び出せるインターフェースへ変換できます。
適切な関数の選定と効果の測定
この経験では、呼び出し回数が多い、または1回あたりのコストが高い関数を探すよう勧めています。関数は1回ごとのコストが比較的低くても、APIハンドラーの前段にある検証ロジックのように、すべてのリクエストで実行される場合があります。一方で、まれにしか実行されなくても非常に高コストな処理もあります。基準となるのは、特定の関数が遅そうだという印象ではなく、CPU時間への累積的な影響です。
統計の例では、Pythonを介して同じ関数を測定した場合、Rustの実装は100倍弱高速でした。ただし、Flask、JSONのシリアライズとデシリアライズを含むHTTPハンドラー全体を測定すると、改善は約15%にとどまりました。元の関数の実行時間は約86マイクロ秒でした。単独では短い時間ですが、大規模に繰り返されると重要なコストになり得ます。
部分的な測定と全体的な測定の差は、講演における最も重要な教訓の一つです。関数の高速化を記録するだけでは不十分で、実際の利用経路を表し、必要に応じてネットワーク、Webフレームワーク、データのシリアライズを含む全体テストを実施する必要があります。
機能互換性、テスト、制約
例を再実装すると、Pythonの統計ライブラリとRustのライブラリの結果に違いが生じました。一方のライブラリは四分位数を正確に計算したのに対し、もう一方は巨大なデータセットに適した推定値を使用しており、小数の丸めにもわずかな差が現れました。したがって、ライブラリを置き換えれば自動的に同じ挙動が維持されると想定すべきではありません。
一致が必要な場合は、別のライブラリを探す、元のアルゴリズムをRustで再実装する、または重要な部分をPythonに残すといった対応が可能です。関数レベルで移行する利点は、コンポーネントを独立したサービスに分離してネットワーク通信や新たな運用コストを追加するのではなく、同一プロセス内で両言語を組み合わせられることにあります。
テストには、Rustの直接的なテスト、Flaskハンドラー向けの既存のPythonテスト、両方の実装の結果を比較するテスト、さらに適切な場合にはプロパティベースのランダムテストが含まれます。ただし、ネイティブコードを追加すると運用上の複雑さが生じます。開発環境にRustコンパイラー、またはOSとアーキテクチャに対応したバイナリが必要となり、デプロイが複雑になり、新たなエラーが発生する可能性もあります。
実務上、この方法論は既存システムの選択した部分を比較的低リスクで改善する道筋を提供しますが、アーキテクチャ分析、互換性テスト、全体的な測定の必要性をなくすものではありません。本当の節約が証明されるのは、孤立した関数のベンチマークではなく、サービス全体の経路に改善が反映された場合だけです。
ニュースの出典
InfoQ - Architecture Articles
原文を開く ↗