Ruby
 Computer >> コンピューター >  >> プログラミング >> Ruby

Scientist gemで本番のRuby on Railsコードを安全にリファクタリングする実践ガイド

ソフトウェアエンジニアに本番コードの重要な部分をレビューしてもらえば、必ずと言っていいほど「リファクタリングが必要な箇所」を複数挙げられます。それにもかかわらず、なぜこれほど多くの品質が低いコードや壊れやすいコード、誰にも完全に理解されていないコードが本番環境で動き続けているのでしょうか?

答えはシンプルです。エンジニアがそのコードに触れるのを恐れているからです。リファクタリング作業は課題として認識され、バックログに追加されますが、現在のスプリントに組み込まれることはほとんどありません。

理由はさまざまです。コードが何年も前にチームを去ったエンジニアによって書かれたもので、誰も全体像を把握していない場合もあります。また、その機能がビジネスにとって極めて重要であるケースもあれば、障害や売上損失の責任を負いたくないという心理的な壁もあります。

この記事では、Scientist gemを使って重要なRubyの本番コードを自信を持って移行・リファクタリング・変更する方法を解説します。

しかし、まずこう疑問に思うかもしれません。「テストを使えばコードの問題を発見できるのでは?」と。

Railsのテストはそのためのものでしょ?

答えはイエスでもあり、ノーでもあります。デプロイ前にコード変更への完全な信頼を得るのは、往々にして困難です。ユニットテストもシステムテストも通過した。これでリリースOK、なのでしょうか?

現実には、本番環境というリアルワールドに代わるものは存在しません。データ品質が悪かったり、テストが不足していたらどうなるでしょうか?新しいソフトウェアが本番のスループットに耐えられる性能を持っているか、どうやって確認すればいいのでしょうか?

公開サービスを提供するチームでは、「バグワード互換性(bugwards compatibility)」という問題に直面することもあります。バグが長期間本番に存在していた場合、クライアント側がその一貫した誤動作に依存した実装をしてしまうことがあるのです。顧客はソフトウェアを予想外の使い方で利用することも少なくありません。

ScientistでRuby/Railsの本番変更を観察する

変更への信頼を得る最良の場所が本番環境なら、そこでコードがどう振る舞うかを観察することを考えましょう。最初は恐ろしく感じられるかもしれません。「本番でのテスト」という発想は、古典的なソフトウェアエンジニアリングの慣習に反しているように見えるからです。

しかし朗報があります。RubyとRailsであれば、Scientist gemを使えば簡単かつ安全に実現できます。「Scientist(科学者)」という名前は、仮説を検証するために実験を行う科学的手法に由来しています。ここでの仮説は「新しいコードが正しく機能する」というものです。

このアプローチを安全に取れる理由は、実験中も既存コードの結果がそのまま使われ続ける点にあります。新しいコードは観察と比較の目的でのみ評価され、その評価軸は正確性とパフォーマンスの両方です。実際のデータとパラメータでパフォーマンスを評価することで、前述のテストカバレッジに関する懸念を軽減できます。実験では通常、本番への影響を最小限に抑えるため、リクエストのサンプリング率を選択して評価します。ただし、必要であればすべてのリクエストを評価することも可能です。

それでは、Scientistが抽象化によるブランチ(Branch by Abstraction)方式でどのように機能するのか、具体的に見ていきましょう。

RubyのScientistにおけるBranch by Abstractionパターン

Scientistのアプローチは、Martin Fowlerが「大規模な変更を漸進的に行う方法」として説明したBranch by Abstractionパターンから始まります。

まず、更新対象のコードを隔離するための抽象化レイヤーを導入します。このレイヤーがどちらの実装を使うかを判断するため、実験はシステムの他の部分に対して透過的になります。この手法は、コードパスを切り替えるフィーチャーフラグの使用とも関連しています。

GitHub発のScientist gemは、このパターンを「実験(experiment)」として実装しています。既存のコードはcontrol(対照)、新しい実装はcandidate(候補)と呼ばれます。両方のコードパスがランダムな順序で実行されますが、クライアントに返されるのは常にcontrolの結果のみです。

ScientistでRubyサービスをリファクタリングする

与えられた数値の最大素因数を返すRubyサービスを例に考えてみましょう。候補となる数値の集合を刈り込む最適化手法を特定し、サービスを高速化できたと仮定します。

しかし、サービスオーナーはバグが混入していないことを確認したいだけでなく、パフォーマンス向上の効果も観察したいと考えています。そこで以下のコードを導入し、クライアント側にこのメソッドを呼び出すよう修正します:

この時点では、use(control)式だけが実行されます。実験を有意義なものにするには、カスタムExperimentクラスを定義して実験を有効化し(以下の例では100%の確率で)、結果を公開します(ここでは単にロギング)。Scientistは優れたデータを生成しますが、デフォルトでは何も行いません。その後の処理は開発者次第です。

実験結果はログに記録され、フィードバックをもとに継続的な改善が可能になります。新しいコードが要件を満たし、信頼度が十分に高まったら、scienceコードを新しい実装への委譲に置き換えるだけで、切り替え(cutover)は完了です。

LabTechでRailsのScientist実験を簡略化

Railsアプリケーションでは、LabTech gemを使うことでScientistの設定と結果の処理を簡単に行えます。

AppSignalを利用しているアプリケーションでは、Appsignal.instrumentカスタム計測ヘルパーを使って、Scientistイベントの完了にかかる時間を追跡できます。各実験コードブロックをこのヘルパーで囲めば、イベントがパフォーマンスタイムラインに表示されるようになります。

さて、LabTechの話に戻りましょう。下記のWebページは、素因数分解したい数値を受け付けるシンプルな画面です。

Scientist gemで本番のRuby on Railsコードを安全にリファクタリングする実践ガイド

コンソールにアクセスできれば、導入はとても簡単です。まずGemfileにLabTech gemを追加し、bundle installを実行します。

実験結果と設定を保存するためのテーブルが必要なので、データベースマイグレーションを実行します。

抽象化レイヤーの構造は同じですが、LabTechモジュールを使用する点が異なります。完全なコードはGitHubで公開されています。

この時点では実験は無効化されているため、コンソールから全ケースで有効にするか、一定の割合でのみ有効にするかを設定します。

これでテストを実行すると、実験が評価されるようになります。結果をテキスト形式で確認するには、Railsコンソールから以下のいずれかのコマンドを実行します。

数回の正常な実行と、意図的に仕込んだ1つのエラーの後の結果サマリー例がこちらです。成功と失敗の概要に加え、パフォーマンス差を示すASCIIチャートも表示されます。

さらにBlazer gemを併用すると、結果を手軽に分析できます。インストールも簡単で、テーブルに対して直接SQLクエリを実行できます。以下のクエリ結果から、candidate実装が元の実装よりも大幅に高速であることがわかります。

Scientist gemで本番のRuby on Railsコードを安全にリファクタリングする実践ガイド

先ほどの素因数分解サービスの例では、改善版の高速化は、考慮すべき因子の一部を排除するヒューリスティックによるものです。大きな数を扱う際、ある素因数が見つかった時点で、その因数で割った値まで探索を進めたら打ち切ることができます。新しいコードパスは、これを実現するためのステートメントを1つ追加するだけです。

LabTechテーブルに対するBlazerクエリでも、実行時間の短縮を確認できます。

Scientist gemで本番のRuby on Railsコードを安全にリファクタリングする実践ガイド

Scientistのユースケースと制限事項

Scientistに最適なユースケースは、検索、計算、副作用のないコードです。一方、トランザクション更新やメール送信などの外部連携を含むコードは、新旧両方の実装が実行される(つまり機能が2回実行される)ため、このモデルにはうまく適合しません。

これは決して軽視できない制限であり、適用できないユースケースも少なくありません。ただし、実験がプロジェクトの成功に不可欠な場合は、いくつかの回避策があります。まず、副作用が本当に問題になるのか、重複が許容できるのかを検討してください。たとえば、評価中に2通のメールが送信されても実害がないケースもあるかもしれません。もう1つの選択肢は、新しいコードに結果の判定だけを行わせ、永続化をスキップすることです。これにより意味のあるパフォーマンス比較はできなくなりますが、正確性の検証は可能になります。

その他の制限は、Scientistが戻り値の比較に焦点を当てていることに起因します。レスポンスにタイムスタンプが含まれていたり、変動する要素があったりと、正当な結果でも時間経過により差異が生じる場合があります。そのようなケースでは、基本的な文字列比較を超えた検証のため、実験内にカスタム比較ロジックを実装できます。

最後に、LabTechの制限として、執筆時点ではRails 7へポーティングされていない点が挙げられます。

Railsで効果的なScientist実験を行うためのベストプラクティス

実験を実装する際は、以下のポイントを考慮してください:

  • Railsプロジェクトでは、Scientistをイニシャライザで直接設定するか、LabTech gemのようなラッパーを利用します。ほとんどのRailsアプリケーションはすでにデータベースを持っているため、LabTechはActiveRecordを活用して結果を保存します。
  • 開発およびテスト環境の速度低下を避けるため、実験はステージング環境と本番環境でのみ有効にします。
  • 本番への潜在的な影響を最小限に抑えるため、実験はリクエストの一定割合でのみ実行します。LabTechは実験の有効化時にオプションパラメータとしてこの機能を標準搭載しています(初期状態では無効)。純粋なScientistを使う場合も、実験のenabled?メソッドにこのロジックを簡単に実装できます。
  • リソース消費が大きいロジックもあるため、まず低いサンプリング率から始めるのがおすすめです。結果への信頼が高まるにつれて、評価対象のリクエスト割合を段階的に引き上げましょう。
  • 結果を最大限に活用するため、コンテキスト属性を追加できます。実験コンテキストにはSymbolをキーとするHashを設定でき、公開される結果の中で参照可能になります。例:

まとめ:ScientistでRubyアプリを観察・監視する

この記事では、Scientist gemを使って本番環境のRubyコードを変更・移行・リファクタリングする方法を解説しました。

まずScientistの基盤であるBranch by Abstractionパターンを確認し、次にリファクタリングの具体的な流れを見ました。さらに、結果の収集とScientist設定の管理に役立つLabTechの活用方法も紹介しました。

その後、Scientistのいくつかの制限事項に触れ、最後に実践的なベストプラクティスを整理しました。

システム内で何が起こっているかを観察し、監視することが何よりも重要です。Scientistを開発プロセスに統合すれば、重要なRubyコードの変更をより高い信頼性をもって実施できるようになります。

Happy coding!

P.S. Ruby Magicの記事を公開と同時に読みたい方は、Ruby Magicニュースレターを購読すれば、記事を見逃すことはありません!

Scientist gemで本番のRuby on Railsコードを安全にリファクタリングする実践ガイド

Darren Broemmer

Darrenは文章を通じて人々を鼓舞し、複雑なものをわかりやすく伝えることを得意としています。興味の対象は科学と物理学、そしてその両方を理解するのに十分な数学です。高品質なコンテンツとテクノロジーソリューションを制作しており、SNSでもときどき発信しています。

  1. Ruby on Rails でポリモーフィズムをマスターする:初心者にもわかる実践ガイド

    オブジェクト指向プログラミング(OOP)でアプリケーションを開発したことがあるなら、ポリモーフィズムを実際に使ったことがあるか、少なくともその用語は耳にしたことがあるでしょう。 科学やコンピュータサイエンスの教科書に登場しそうな言葉です。調べてみたことはあっても、概念をきちんと理解しないまま実装してしまった、という経験がある方もいるかもしれません。 この記事では、特にRuby on Railsにおけるポリモーフィズムへの理解を深めていきます。取り上げるトピックは以下の通りです。 現実世界におけるポリモーフィズム OOPにおけるプログラミングのポリモーフィズム Railsアプリケーションに組み

  2. Railsのセキュリティ脅威に立ち向かう:認証と機密データ露出への対策

    本シリーズの第1回では、インジェクション攻撃について解説しました。 OWASP Top 10 Webアプリケーションセキュリティリスクをテーマにした本シリーズの第2回となる今回は、認証の不備と機密データの露出という脅威の世界を深く掘り下げていきます。 具体的には、ハッカーがどれほど簡単にあなたが書いたコードを欺き、ユーザーのデータを盗み出すことができるのか、以下のような攻撃手法を見ていきます。 ユーザー列挙(User Enumeration):ログインページを悪用し、候補となるユーザー名のリストを総当たりで試して、データベース内にそのユーザーが存在するかどうかを確認する攻撃。 弱いパスワード