Evil Martiansが贈る「古いRailsアプリを1日1時間✕12日でリフレッシュする方法」(翻訳)
ホリデーシーズンが近づいてきましたので、皆様(および皆様のチーム)に素敵な開発エクスペリエンスをプレゼントしたいと思います!本記事では、私たちが12日間をかけて適用した、Railsをレベルアップするために設計された12のささやかな(しかし強力な)手法を紹介いたします。手法ごとに、解説とともに実践可能な実行計画も添えてあります。個別のタスクは1時間以内で終わるよう一口サイズにとどめています。サンタさんの「ソリ」をレールに乗り換えてもらえるようにするなら急がないといけませんね、何しろサンタさんはお忙しいのですから!
- Day 1: 依存関係を最新に更新する
- Day 2: Railsの起動を高速化する
- Day 3: 自分自身と環境を強化する
- Day 4: 不安定なテストを改善する
- Day 5: システムテストを改善する
- Day 6: テストを高速化する
- Day 7: マイグレーションを改善する
- Day 8: セキュリティを強化する
- Day 9: データベースクエリを高速化する
- Day 10: ドキュメントを改善する
- Day 11: パフォーマンスを強化する
- Day 12: アプリの安定性を強化する
🔗 Day 1: 依存関係を最新に更新する
libyearは、アプリケーションの「新鮮さ」を示すシンプルなメトリクスです。つまり、依存関係がどのぐらい古くなっているかを文字通り「ライブラリ年(libyear)」という単位で示してくれます。
早速、現状のアプリケーションの1つでlibyearレポートを生成してみましょう。
# libyearをインストールする
$ gem install libyear-bundler
# libyearのレポートを生成する
$ libyear-bundler
aasm 5.2.0 2021-05-02 5.5.0 2023-02-05 1.8
actioncable 7.0.8.5 2024-10-15 8.0.0 2024-11-07 0.1
actionmailbox 7.0.8.5 2024-10-15 8.0.0 2024-11-07 0.1
actionmailer 7.0.8.5 2024-10-15 8.0.0 2024-11-07 0.1
actionpack 7.0.8.5 2024-10-15 8.0.0 2024-11-07 0.1
actiontext 7.0.8.5 2024-10-15 8.0.0 2024-11-07 0.1
actionview 7.0.8.5 2024-10-15 8.0.0 2024-11-07 0.1
activejob 7.0.8.5 2024-10-15 8.0.0 2024-11-07 0.1
activemodel 7.0.8.5 2024-10-15 8.0.0 2024-11-07 0.1
activerecord 7.0.8.5 2024-10-15 8.0.0 2024-11-07 0.1
activestorage 7.0.8.5 2024-10-15 8.0.0 2024-11-07 0.1
activesupport 7.0.8.5 2024-10-15 8.0.0 2024-11-07 0.1
acts_as_list 1.1.0 2023-01-31 1.2.4 2024-11-19 1.8
...
System is 663.4 libyears behind
耳寄りなお話: libyearはJavaScriptやPythonなどについても依存関係のトラッキング機能を提供してくれます。
なるほど、663.4ライブラリ年遅れていると出ました。これならそんなに悪くないでしょう。実際、大規模アプリでは数世紀レベルの遅れに達することはざらにあります。しかし慌てる必要はありません。これらはあくまで「ビンテージ」であって時代遅れというわけではありません。
(はい、ビンテージといっても賞味期限がかなり過ぎて味の落ちたエッグノッグ1のような「ビンテージ」ですね。しかしエッグノッグはお祝いムードの演出には欠かせませんし、本記事ではホリデーもテーマのうちなので、そこのところよろしくお願いします🎄)
それはともかく、「ライブラリ年」というメトリクスは単に奇をてらっただけであまり有用そうに見えないかもしれませんが、実はさまざまなサププロジェクトを比較するときやアップグレードの優先順位付けをするときにとても役に立ちます。
さらに、このlibyears gemを定期的に実行するようにすれば、開発者がRailsのような大きく目立って興味を惹かれやすいgemばかりに着目するのではなく、忘れられた小さなgemを更新したり代替gemを探したりするようになります。
libyearでアプリを若返らせるもう1つのメリットは、新しい依存関係は多くの場合バグが修正されていたり無料で速度が向上していたりすることです。必要なのは、あと少しの愛情と注意深さだけです。
Railsをアップグレードするという堅実な判断を下した方に便利なツールを紹介します。RailsDiffは、Railsバージョンごとのデフォルト設定の差分だけをチェックできます。 RailsBumpは、Gemfile.lockの内容を貼り付けることで依存関係の遅れをチェックできます。
🔗 libyearsに追い回されないために
今回のホリデーチャレンジは、ライブラリ年が数光年に達していそうな年代物の古いアプリの煤払いをすることです。
(原注: 上で「数光年(lightyears)」と書いたのは、そのぐらい「古い」という雰囲気を伝えるためです。光年を距離ではなく時間の単位であるかのように使っていますが、一部の読者はきっと察してくれたことでしょう。)
(もっと大事な原注その2: gemで見落としがちな点の1つは、gemのGitHubのカスタムリンクが古くなったり、バグを回避するためにバージョンを固定していた場合です。gemにバグ修正が適用された後で、開発者がgemをアップストリームに戻し忘れることはちょくちょくあります。貴重なアップデートは非常に見落としやすいのでご注意ください。)
それでは、gemをいくつか更新してみましょう。ただし、くれぐれもbundle update
をいきなり実行してはいけません!そんなことをしたらせっかくの休暇が無用なストレスで台無しになってしまいます。楽しい気分を損なわないためにも、gemは数個ずつ様子を見ながら慎重に更新しましょう。
休暇中にダイ・ハードスタイルのスリルを味わいたい方は、Evil Martians謹製のgem_trackerスニペットをテストスイート内でお試しください。使われていないgemを検出してGemfileをキレイにするのがとても簡単になります。
ところで、アップデートするgemはどうやって決めればよいのでしょうか?libyearなどのツールがガイダンスを提供してくれますが、ここでもうひとつサンタの袋から冬休みにふさわしいツールをプレゼントいたします。それがbundle_update_interactiveです。
bundle_update_interactiveは、gemのアップデートを洗練された方法で対話的に進められるだけでなく、gemのChangelogをワンクリックで確認することも可能です。これでもう、無謀なアップグレードパーティに振り回された挙げ句に二日酔い気分で頭を抱えることも(そしておそらく、お祝い気分に最もふさわしいエッグノッグでさらに二日酔いがひどくなることも)なくなるでしょう。
🔗 Day 1のお祭り実行計画
- [ ] libyearのメトリクスを生成し、結果を保存してから改善作業に取り掛かりましょう。
- [ ] バージョンを固定したgemや、GitHubソースへのURLを直接指定しているgemをすべてチェックしましょう。
- [ ] bundle_update_interactiveを実行して、gemの一部を更新しましょう。
- [ ] よい結果を得られたら、もう一度libyearのメトリクスを生成して差分をチェックし、#railsmasハッシュタグで共有しましょう。
本記事を元にチャレンジした方は、結果をRailsコミュニティにも知らせていただけるとありがたいです。最も若いアプリはどれか、最も年季の入ったアプリはどれか、最も派手に若返ったアプリはどれか、見てみましょう。
🔗 Day 2: Railsの起動を高速化する
ほとんどの人は認めないかもしれませんが、ホリデーシーズンでつらいことと言えば、プレゼントを待つことでしょう(一番嬉しいのは?もちろんプレゼントをもらうことです)。「今度は何をもらえるのかな?」プレゼントを待っている間は年がら年中そのことで頭がいっぱいです。
しかしそれよりもっとつらい待ち時間があるのはご存知ですか?そう、Railsアプリが起動するまでの待ち時間です。
そこでDay 2はアプリの起動のスピードアップに当てることにしましょう。これは、開発中のコード再読み込みの高速化や、productionへのデプロイ時間の短縮などで常に役立ちます。
🔗 サンタは「ブート」も改善します
これについて詳しく見ていく前に、個人の起動時間をもう少し詳しく理解してみましょう。自分のアプリケーションの起動時間をさくっと調べてみるために、この新しい CRuby プロファイラーVernierを使います。
Rubyを3.2.1以前にしたまま年越しを迎えてしまった開発者の皆さま、どうかふてくされずに聞いてください。そんなときはrbspyを試してみることをサンタ🎅からおすすめいたします。サンタからの珍しい技術アドバイスでした。
最初に、Vernierをインストールしてブートプロセスをプロファイリングしてみましょう。
# Vernierサンプリングプロファイラをインストールする
$ gem install vernier
# アプリケーションのブートプロセスの完全なプロファイルをキャプチャする
$ DISABLE_SPRING=1 vernier run -- bundle exec rails runner 'puts "Railsmas is here!"'
Railsmas is here!
#<Vernier::Result 2.821947994 seconds, 7 threads, 3943 samples, 3103 unique>
written to /tmp/profile20241126-134618-4btew9.vernier.json.gz
どんなアプリケーションも、(そう、たとえるなら)雪の結晶のように独自の要素を持っているものなので、ブートプロセスの期間に応じて整ったグラフからめちゃめちゃなグラフまで、さまざまなものが生成されます。
そうそう、config/environments/development.rb
でeager loadingを有効にしたレポートも生成するのをお忘れなく。こうすることで、貴重な洞察を得るうえで有用なデータも得られるようになります。
config.eager_load = true
次は、生成されたプロファイルをVernier UIで開きましょう。
起動時間の51%がURI.open
で消費されています(犯人は誰だ!)
コールツリーを操作するときは、各サブツリーのブート中に費やされたレポートの割合を常に記録しておきましょう。
注: レポートで最も大きな部分を最初に調査する方が効果的です。読み込み中にブート実行時間を占める割合が最も大きな初期化期間に注目しましょう。
Vernierのスタックツリーをスキーの大回転競技のようにたどる動画は以下で見られます。
TestProf(3): Rubyテストのプロファイリングを統合・自動化する(翻訳)
読み込み中は、あらゆる初期化コードとクラスレベルのコードが実行されていることを常に意識しておく必要があります。たとえば「巨大なJSONファイルを読み取る場合」「すべてのタイムゾーン情報をeager loadingする場合」「すべての訳文をeager loadingするクラスレベルでの国際化(I18n)を試そうとする場合」などは、この点に注意しておきましょう。
大規模なDSLが起動を遅くしていることもよくあります(Active Admin、Grape、GraphQL、RSwagなど多数)。アプリでこれらがプロファイルの大半を占めているときは、production環境では別のインスタンスで読み込むようにしたり、そうした部分を使わない場合はローカルでスキップするなど工夫しましょう。
このような問題は外部の依存関係にも含まれている可能性があるため、Gemfileを監査することが大事です。今使っているgemは本当にすべて必要ですか?使われていないgemがあると起動時間が増え、アプリも複雑になってしまいます。アプリケーションをシンプルにしてすっきりさせましょう!
Gemfileで怪しいと思ったgemを別のグループに移動して、条件付きで読み込むようにしてもよいでしょう。
私たちのようにgem狩りを愛するエルフの皆さんには、Bumblerをそりのように乗りこなすのが性に合うかもしれません。このツールは、Gemfileにあるどのgemが遅いかを検出してGemfileを整理するのに役立ちます。
🔗 サンタから皆さんへの贈り物
さて、いよいよ本日のメインイベントです。
神妙不可思議な力を駆使する魔法のサンタが、Deviseの設定を1 行変えるだけで起動時間を大幅に短縮できるという素敵なおまけを用意して、お祭り気分をいっそう盛り上げてくれます。
Deviseは、デフォルトではすべてのアプリケーションルーティングを強制的に再読み込みするので、実質的に読み込みが2回行われます。
しかしconfig/initializers/devise.rb
のデフォルトの振る舞いは、以下のコードで簡単に無効にできます。
# この設定をfalseにすると、Deviseはeager loadingでルーティングを再読み込みしなくなる。
# これによりアプリの起動時間を短縮できる。
# ただし、アプリの起動中にDeviseマッピングが必要とされる場合は
# アプリが正しく起動しない可能性がある。
config.reload_routes = false
もちろん、この変更の影響はアプリケーション内のルーティング数にもよりますが、読み込み時間は大幅に改善されるはずです。
Railsコアチームからもビッグな贈り物があるのをご存知ですか?2024/11/07にリリースされたRails 8では、新規アプリでグローバルな遅延ルーティング機能が含まれるようになりました(#52012)
🔗 Day 2のお祭り実行計画
Vernier、またはrbspy(Ruby < 3.2.1の場合)をインストールして、アプリケーションで「eager loadingを有効にした場合」「eager loadingを無効にした場合」のプロファイルをそれぞれ作成しましょう。
プロファイルを生成したら、Vernier UIにアップロードして以下の点を分析しましょう。
- [ ] コールツリーを調べて、最も時間のかかるサブツリーとメソッドを見つけます。
- [ ] ブート中のサブツリーで、占める割合が多いレポートがどれなのかを注意深く調べます。
起動を遅くしている犯人を見つけて最適化しましょう。
- [ ] アプリの大部分を本番環境で別のインスタンスに移動するか、ローカルでスキップすることを検討してください。
- [ ] 使われていないgemをGemfileから削除します。
- [ ] いくつかのgemをGemfileの別のグループに移動して、条件に応じて読み込みます。
- [ ] Deviseのルーティング再読み込みを無効にします(該当する場合)。
変更を加えたらアプリケーションをテストして、アプリケーションの読み込みが正常に機能し続けることを確認できたら、2日目で得られた洞察を例によって#railsmasハッシュタグで共有しましょう。
「ホッホー!サンタブーツの高速の術はいかがだったかの?」
🔗 Day 3: 自分自身と環境を強化する
新年になると気持ちも改まりますね!
アプリにエネルギーを多少注入できたので、Day 3は開発環境の改善に注力してみましょう。
おっと、DHHのようなサンタが最新のLinux推奨ノートパソコンに移行すべきだとささやいてきました。心惹かれる話ですが、そこまですることもないでしょう。
ちなみに、本記事の著者の一人はArch Linuxを使っています。
本日、私たちは、Shopifyが2年前に実際に用意してくれた特別な贈り物の封を切れることを嬉しく思います。ご安心ください、改めて磨き抜かれたこの真の宝とは、すなわちRuby LSPです。
ところで、Ruby-LSPのどんな点が特別なのでしょうか?RubyMineやSolargraphのようなツールでは何年も前からこうした機能を利用できていますが、Ruby用の言語サーバープロトコル(LSP)実装をもう1つ必要とする人はいるのでしょうか?
その答えは、「Rubyで書かれたアドオンシステム」という極めて強力な機能にあります。ruby-lsp
のアドオンシステムには、「定義へのジャンプ」「インラインドキュメント」「コードナビゲーション」といった高度な基本機能が含まれていますが、アドオンシステムはそれをさらに進化させ、機能を誰でも簡単に拡張可能にします。
しかし、Ruby-LSPの本当の力は、これらのアドオンが配布される点にあります。アドオンを手動でインストールする必要すらなく、./lib/ruby_lsp/**/addon.rb
内に配置すれば、(Railstiesのように)自動的に読み込まれます 。つまり、Ruby LSPのすべてのユーザーもgemのすべてのユーザーも、余分な労力をかけずに強化されたエディタ体験を享受できるということです。
IDEを長年使いこなしているユーザーは、コントローラから対応するビューにジャンプしたり、アクションからルーティングを開いたりする組み込み機能を見ても「今さら」と思うかもしれません。しかし、本記事の著者の1人(Arch Linuxを使っていることはお伝えしましたよね?) は、使いやすさのレベルがこれほどまでに高いことに衝撃を受けました。立ち直るには次のホリデーシーズンまでかかるかもしれません。
ここで想像してみて欲しいのです。「設定よりも規約」の背後に潜んでいた魔法がすべて明るみになり、ActiveModelSerializerやCanCanCan、ActiveAdminの背後でうごめいていた闇の妖術師たちが、実は珍妙な衣装を着た人々の集団に過ぎなかったところを。
LSP APIといえば、RuboCopのcopも忘れてはいけませんね。もちろん、Rubyのおかげでアドオンの作成は簡単です。
その気になれば、大晦日の深夜までに、アプリケーションの独自のDSLをハイライト表示するカスタムアプリケーションLSPアドオンを開発することも可能でしょう。
ここで少しばかり奇跡をお目にかけましょう。私たちの愛しいAction Policy gemのプラグインのgistを開いて、DSLの定義への簡単なジャンプサポートを有効にしてみると、それだけでコントローラから関連するポリシーに素早くジャンプしたり戻ったりできるようになります。
素晴らしい!うまくいきました。さらに嬉しい点は、新しいコードをアップストリームgemに提供すれば、すべてのRuby LSPユーザーがこのプラグインを使えるようになることです。
🔗 Day 3のお祭り実行計画
- [ ] 好みのエディタでRuby LSPをセットアップします。
-
[ ] すぐに使える新しい高度な機能を調べてみましょう。
例: 強力な標準の定番機能(Rails の「設定よりも規約」というモットーの範囲内で隠し要素に簡単に移動できる機能など) - [ ] 同僚がコードを操作する時間を節約できるように、アプリケーション(またはお気に入りの gem)用のカスタムDSL用のシンプルなプラグインを作成することを検討してみましょう。
ついでに、Ruby LSPのアイデアを例によって#railsmasハッシュタグで共有しましょう。あなたの提案が他の人のインスピレーションを刺激するかもしれません。
いろいろ書きましたが、休暇中はみんなで集まって楽しむことが肝心です(もちろんプレゼントも)。
🔗 Day 4: 不安定なテストを改善する
毎年、私たちは変わり映えのしないクリスマスソングを耳にしていますが、飽きることがありません。それほど大好きです。
しかし中には、頭の中でずっと鳴りっぱなしになってなかなか止まらない曲もあったりします。Day 4のプレゼントとして、耳にこびりついて離れない音楽のように執拗に繰り返される、壊れやすいテストと戦うための方法が必要です。私たちは、祝日に頭の中で鳴り続ける音楽と同じぐらい、テストの繰り返しが嫌いです。
手始めに、テストを適切に設定することで問題を早期に発見できるようにする必要があります。
テストがランダムな順序で実行されるようになっているかをチェックしましょう。
# RSpecの場合
RSpec.configure do |config|
config.order = :random
Kernel.srand config.seed
end
アプリケーションのテストスイートでは、グローバルなステートを特定の手順でクリーンアップする微妙な方法に知らず知らず依存していることがありますが、テスト実行順序をランダムにすることで、そうした依存が不要になります。
中規模程度のテストスイートでこのオプションがこれまで設定されていなかった場合、このオプションをオンにすると予想外のテスト失敗が大量に発生するかもしれません。最初はつらくても、後でそれに見合ったプレゼントがもらえるのですから、どうか受け入れてください。良薬は口に苦しです。
Minitestユーザーなら、このランダム化オプションはデフォルトでカバーされているので大丈夫なはずです。しかし念のためmy_tests_are_order_dependent!
がコードベースのどこかに含まれていないか確認しておきましょう(さもないと休暇どころではなくなるかもしれません)。
ところで、グローバルステートの漏洩とはどんな問題なのでしょうか?
基本的に、テスト中に変更されたものはすべて、ステートが残留して他のテストコンテキストにリークするリスクがあります。ただしデータベースのやりとりを除きます(トランザクショナルなテストや昔ながらのdatabase_cleaner
ではデフォルトで保護されています)。
この問題には、以下を含むさまざまな原因があります。
- トランザクションの外で作成されたActive Recordモデル
(before(:all)
の内部など) - 環境変数の変更
- コンフィグ(アプリの設定、ルーティング、ロガー、I18nなど)
- グローバル変数、クラス変数、シングルトンレベルの変数
- ファイルシステムの変更
- 追加のデータストア(Redis、Elasticsearch、RabbitMQなど)
(テストでこれらを実際に統合している場合) - キャッシュストアやインメモリストア
- ジョブやメーラーなどのさまざまなキュー
(パフォーマンス上の理由で常にフェイクモデルを優先する) - システムテストでのブラウザストレージ
- データベースの主キーカウンタ
心当たりのありそうなものがリストで見つかりましたか?不安定なテストと戦うためのガイドとそのリンクを使って、混乱を解消しましょう。
グローバルステートの問題は解決できたのに、まだどこかに不安定さが残っていますか?休暇中の飛行機便が遅延したときのようにテストを台無しにしたヤツ、それはTime
です!
テストの内部で時間を扱う場合は、TimeHelpers
で特定の時刻にフリーズさせる必要があります(時間関連のアサーションでは常に停止します)。
ポーラー・エクスプレスで列車旅行するのを避けたい場合は、バトルテストで実績を積んだtimecop gemを使う方法もあります。
もう一つよくある見落としは、不安定で遅い外部のシステムやサービスに依存するテストです。
アプリのリスクを評価すべき場所は以下のとおりです。
- 外部APIへのHTTPリクエスト
(HTTPファサードが隠れているサービスgemも含む) - リモートファイルストレージへのアクセス
- SMTPメール送信
- DNS名前解決
- 生ソケットの利用
試しにテストにWebMockを導入して、外部リクエストを完全に無効にしてみてください。優れたテストは、サンタが主催するワークショップのように、外部の世界に依存せず自己完結しています(免責事項: この伝承は未確認です)。
そして最後は「順序」についてです。スニペットを読めば一目瞭然です。
RSpec.describe 'Delivery order' do
it 'is a naughty test!' do
expect(Gift.all).to eq([gift1, gift2]) # ほっほっ、いかんのう、注文が未定義じゃ!
end
it 'is a nice one!' do
expect(Gift.order(:id)).to eq([gift1, gift2]) #サンタが承認しますぞ!
end
it 'is an extra nice test!' do
expect(Gift.all).to match_array([gift1, gift2]) # サンタの秘密の技じゃ
end
end
このテストを実行してみると、エンドノートがランダムに表示されます。ランダムネスは良いことであるとは限らず、混乱の元になることもあります(たとえばVCR gemのカセットを使うときには、安定した値を使う必要があります)。
🔗 Day 4のお祭り実行計画
🔗 Day 5: システムテストを改善する
ホリデーシーズンといえば、山のようなプレゼントはもちろんのこと、大量のテストパーティを開催する絶好の機会でもありますが、その結果は...必ずといっていいほど失敗に終わります(システムテストが絡んでいるからですね)。しかしご心配なく、助けはそこのクリスマスツリーにぶら下がっています!
テストパーティ救済計画はいくつかのパートに分かれています。
パート1は、避けられないものがあることを受け入れる準備をすることです。
システムテストで行われるアサーションのexpect項目は他のテストよりも多く、アプリケーションの多くの部分をカバーし、実際のブラウザ環境内で実行されるので、当然ながらテスト違反は発生しやすくなります。
すなわち、開発者がテストスイート全体を再実行するために待たされる時間を最小化することが重要です。
この問題にテストの不安定さだけが関連しているとは限りません。無風状態の中で意味もなく空中で動かなくなる雪片のように、システムテストは時たまフリーズしてしまいます。そんなときはsigdumpで問題を診断することを検討してください。
ここで最もシンプルな技は、テスト全体を再実行するのではなく、個別のテストを再実行することです。これに対応するフレームワークとして、rspec-retryやMinitest::Retryがあります。
これらは問題に取り組むための「鋭いナイフ」なので、システムテストだけで用いることをおすすめします。単体テストで実際の問題をうっかり隠蔽してしまわないようご注意ください。
また、リトライ回数に上限を設定することもお忘れなく。さもないと、さほど重要でもないテストのためにテストスイート全体の実行が増えてしまいます。
🔗 翌日
テストスイートを手動で再実行しないで済むようにしたことで多くの時間を節約できるようになったので、パート2に進んで、不安定なシステムテストの修正に取りかかれるようになります。
パート2では、クリスマスの夜になかなか現れないサンタをこっそり覗き見ようとするいたずらっ子たちのように、Railsの自動スクリーンショット撮影機能で失敗をキャプチャしましょう(プロ向けのヒント: CIでも失敗のスクリーンショットを保存することをお忘れなく)。
Rails 7.2以降のrails new
コマンドでは、GitHub Actionsのワークフローファイルが自動生成され、その中にスクリーンショットをアップロードするステップも盛り込まれています。
🔗 1種類のビューポートサイズですべてに対応3
スクリーンショット機能を使うときは、適切なビューポート サイズを設定しておくことが重要です。この設定は、利用するドライバによって異なります。現在人気のCupriteの例についてはこちらの過去記事をご覧ください。スクロールページでの振る舞いが同じになるので、テストがより安定するというボーナスもついてきます。
スピードが遅いのであれば、Capybara.disable_animation = true
を設定することでテスト中のCSSアニメーションをグローバルに無効にできます。
Capybaraのよいマッチャー、悪いマッチャー、不安定なマッチャー4
設定をがっちり固めるだけでは不十分です。テストコードには注意を絶やしてはいけません。Capybaraマッチャーを使うとかっこよく書けますが、セレクタの条件を見落とさないよう十分気をつける必要があります。
Minitestをお使いの方は、Capybara::SlowFinderErrorsツールを使って時折発生する速度低下を防ぎ、テストがサンタのソリより遅くならないようにしましょう。
# 悪い開発者は、人工雪のようにもろくはかないRSpecマッチャーを使います ❄️
# このマッチャーは、サンタのプレゼントリストにエルフが書き込む前にリストを見つけてしまう可能性があります
text_field = find('.santa_list')
expect(text_field['value']).to match /Nice list/
# よい開発者はCapybaraの同期マッチャーを使ってサンタもにっこり 🎅
# このマッチャーはエルフがリストを更新し終わるまでリトライsます
expect(page).to have_field('.santa_list, with: /Nice list/)
🔗 眠れぬ夜にはCapybara Lockstepを
Capybaraは、失敗した一部の操作(要素の期待値など)を再試行できますが、JavaScriptやAJAXについては一切関知しないので、要素の準備が整う前(つまりネットワークリクエストが完了する前)に操作される可能性のある対話的UI操作(モーダルフォームや動的更新など)では、タイミングとマシンのパフォーマンスによっては失敗しやすくなります。
開発者はこういうときに、sleep
をあちこちにばらまいて解決しようとしがちですが、もっといい方法があるのです。それがcapybara-lockstepです。
このgemを設定しておけば、Capybaraは次のマッチャーを実行する前にすべてのJavaScript非同期対話操作が完了するまで待つようになります。そのため、ページの準備が整っていることが保証され、テストステップの間に頼りないsleep
を置く必要はありません。capybara-lockstepは、不要な待機を賢くスキップして非効率的な固定遅延を排除することで、テストのパフォーマンスも向上します。これは非常に重要なエクスペリエンスであるため、すべてのシステムテストスイートにデフォルトで含めるべきだと思います。
このgemが提供するミドルウェアを有効にしておくことをお忘れなく。このミドルウェアは、テストのクリーンアップフック時の荒っぽいリクエストを防ぐなど、レアケースをカバーしてくれます。
🔗 Day 5のお祭り実行計画
- [ ] rspec-retryまたはMinitest::Retryをインストールして、テスト全体ではなく個別のテストをリトライするようにしましょう。
- [ ] システムテストが失敗したときのスクリーンショット撮影を有効にしておきましょう。
- [ ] ブラウザのビューポートサイズを適切に設定して、テストを安定させましょう。
- [ ] システムテストのマッチャーが、ページが未完成になる条件を対象としていないことを確認しましょう。
- [ ] capybara-lockstepをインストールして、頼りにならない
sleep
の明示的な呼び出しを不要にしましょう。
システムテストの失敗に関するその他のヒントや注意事項がありましたら、例によって#railsmasハッシュタグで共有しましょう。
🔗 Day 6: テストを高速化する
ホリデーシーズンが近づけば近づくほど、好き放題に盛り上がる若者たちは素敵なプレゼントを封印する紙を破り開ける興奮を待ちわびて、時間がゆっくりと流れるように感じられます。この待ち時間のぞっとするようなつらさは、開発者がマージボタンを押すために、テストスイートの最後の部分が完了するのをひたすら待っているときのつらさとある意味似ています。
そこで、クリスマスの恐ろしい第四の精霊「焦り」(ディケンズが没にした登場人物)がやってこないようにするために、本日はテストを高速化するための戦略を紐解きましょう。開発プロセスを冬のそよ風のように活発で爽やかにしてみませんか?
まず、テストのボトルネックを特定しましょう。遅いテストを正確に特定することが、より優れた高速なスイートへの第一歩です。まず、プロファイリングを有効にして、遅いspecがどれなのかを明らかにしましょう。
# RSpecの場合:
bin/rails spec --profile
# Minitestファンの場合(Rails 7.1以降):
bin/rails test --profile
これで、テストスイートの中で最も遅いテストが表示されます。しかしこれだけでは、遅い理由まではわかりません(sleep christmas.from_now
を入力するまでは)。TestProfを使ってテストの振る舞いをもっと深く理解しましょう。
TestProfは、テストスイートの奇跡のツールキットのようなもので、特定のテストの問題だけでなく、テストスイート全体の体系的な非効率性も明らかにすることで、大幅に時間を節約できるようになります。
関連記事: 詳しくは、テストスイートをプロファイリングして、ターゲットを絞り込んで最適化するためのさまざまなツールとテクニックを紹介する以下の記事をどうぞ。
テストスイートの速度が低下する原因を特定したら、最適化を行います。微調整と小さな変更を行うだけでテストの実行が大きく変わることが見込める、ある重要な領域について説明します。
最初は、頼りになるユーザーデータ守護天使であるDeviseです。
テストを暗号化することは、ありとあらゆるプレゼントを3重のテープでがっちりと包みたがる親戚に少し似ています。テスト環境では、高速ハッシュ アルゴリズムの利用を検討しましょう。
# config/initializers/devise.rb
Devise.setup do |config|
# テストでは無駄なコストを下げよう
config.stretches = Rails.env.test? ? 1 : 12
end
驚くかもしれませんが、Deviseのアルゴリズムは非常に時間がかかるんですよ。
ログ出力もまた、貴重なテストスイート実行時間を無慈悲に奪う怪物「グリンチ」です。テスト中のログは有用なこともありますが、やりすぎになることもあります。テストを高速化するために、テスト中のログの詳細度を下げるか、完全に止めてしまうことを検討しましょう。
# config/environments/test.rb
unless ENV['WITH_LOGS']
config.logger = Logger.new(nil)
config.log_level = :fatal
end
これは小さな設定変更ですが、あなどってはいけません。場合によってはテストスイートの実行時間を10%削減できることすらあるのですから。
カバレッジツールも同じカテゴリに該当します。ローカルテストが遅くならないよう、特定の環境(おそらくCI)でのみカバレッジツールを実行するようにしましょう。
if ENV['COVERAGE']
require 'simplecov'
end
コールバック内に埋め込まれたロジックも、遅くなる原因のひとつです。
よくある見落としとして、PaperTrailがあります。PaperTrailは変更のトラッキングに最適ですが、テスト中にPaperTrailを動かすのは、あたかも天気予報のために雪片1つ1つを調べて回っているような気分になります。テストが遅くならないよう、PaperTrailをオフにしておきましょう。
後ほど詳しく説明しますが、PaperTrailのような機能を活用してproduction環境をスピードアップしたい場合は、 トラッキングのロジックを低速のコールバックからパフォーマンスの高いデータベースに移動できるLogidze gemをチェックしてみてください。
バックグラウンドジョブもいつの間にか時間を浪費する可能性があります。道草を食わずに高速にテストするために、実際の実行のオーバーヘッドなしでジョブ処理をスキップするSidekiq::Testing.fake!
モードを活用しましょう。
データベーストランザクションに「HTTPリクエスト」「バックグラウンドジョブ」「その他の非ロールバック操作」が絡み合うと、状態の不整合が発生する可能性があります。そんなときサンタさんは、Isolator gemでこうした問題を早期に自動的に検出します。
外部HTTPリクエストも遅くなる原因です。HTTPのやり取りをVCRで記録・再生すれば、ネットワークを待たずにテストをスムーズに実行できます。VCRカセットは、テストを強化して間欠的なテストの失敗を減らすのにも有用です。
🔗 Day 6のお祭り実行計画
- [ ] テストプロファイルを有効にして、最も遅いテストから最適化を試みましょう。
- [ ] TestProfをインストールしてテストスイートのプロファイルを診断し、グローバルな問題から修正に取りかかりましょう。
参考までに、テストで起きがちな問題を以下にまとめておきます。
- Deviseのハッシュ化処理は重い
- ログ出力とカバレッジの処理は重い
- コールバック内のロジック(つまりPaperTrail)は重い
- Sidekiqのfakeモードを無効にする
- 外部リクエストは重い
終わったら、テストを高速化する独自のヒントや発見を例によって#railsmassハッシュタグで共有しましょう。皆さんのスピードショップの結果をぜひ見せてください!
🔗 Day 7: マイグレーションを改善する
Railsmas祭りを繰り返すうちに、いよいよデータベースのツールセットを刷新するときがやって来ました。データベースを整頓せずに放置していると、トランプで作った家がなすすべもなく崩れ落ちるのを見ているときのように(クリスマスっぽく言えば、ジンジャーブレッドハウスが哀れにも崩れ落ちるさまを眺めているときのように)、問題が連鎖的に発生する可能性があります。
とにかく、マイグレーションから手を付けましょう。マイグレーションは慎重に扱わないとリスクを招く可能性があるからです。たとえば、巨大なテーブルでマイグレーションを実行すると、データベースがロックされ、ダウンタイムが発生する可能性があります。
これについては、このstrong_migrations
gemがチームへの贈り物として最高です。
strong_migrations gem は、セーフティネットとして機能し、潜在的に危険なマイグレーションがproduction環境で大混乱をもたらす前にキャッチしてくれます。
bundle add strong_migrations
bin/rails g strong_migrations:install
次は、データベースの一貫性の問題に手を付けましょう。
database_consistency
gemは、休暇のチェックリストのようなもので、データベース制約とモデルのバリデーションが一致しているかどうかをチェックできます。
これによって、バグにつながる可能性のある矛盾の発生を食い止め、アプリケーションをさらに改善してくれます。
bundle add database_consistency --group development --require false
bundle exec database_consistency
NullConstraintChecker fail User code column is required in the database but does not have a validator disallowing nil values
NullConstraintChecker fail User company_id column is required in the database but do not have presence validator for association (company)
...
アプリケーションを長く運用していると、いつしかマイグレーションファイルが大量に積み重なって管理が面倒になってきます。そんなときはsquasher gemの出番です。
squasherは、その名の通り古いマイグレーションを1個のファイルにまとめて、マイグレーションの歴史をシンプルにしてくれます。
gem install squasher
# 2024/01/01以前のマイグレーションをすべてスカッシュする
squasher 2024 -m 8.0 # for Rails 8.0
最後に、データベースをサンタの工房のように整理整頓してくれる「小さなエルフ」を紹介しましょう。このactual_db_schema
gem は、ブランチを切り替えたときのマイグレーションを自動化してくれます。
このgemは、作業中のマイグレーションを自動的にロールバックしてくれるので、ブランチを切り替えたときのデータベーススキーマが一貫していることを保証します。これにより、ブランチを切り替えるときにスキーマの不一致を手動で管理する手間が省けます。
🔗 Day 7のお祭り実行計画
- [ ] 最初に
strong_migrations
をワークフローに導入しておきましょう。 - [ ] データベースのCHECK制約とモデルのバリデーションを
database_consistency
で同期しましょう。 - [ ] Active Recordの古いマイグレーションファイルをsquasherでまとめることを検討しましょう。
- [ ]
actual_db_schema
を導入すると、ローカル環境でのブランチ切り替え作業がシンプルになります。
終わったら、データベースメンテナンスのベストプラクティスを例によって#railsmasハッシュタグで共有しましょう!
🔗 Day 8: セキュリティを強化する
Railsmasの8日目は、アプリを心地よいセキュリティの毛布にくるんであっためてあげましょう。データ漏洩ほどお祭りムードを台無しにするものはありません。それでは、セキュリティのベストプラクティスとツールで「(セキュリティ)ホールを飾り付け」ましょう。
🔗 監査を徹底的に行う方法
アプリケーションの依存関係をbundler-audit
gemとruby_audit
gemでスキャンして、既知の脆弱性があるかどうかをチェックし、安全でないライブラリに依存しないようにしておきましょう。これらのツールはCIで定期的に実行してください。
gem install bundler-audit ruby_audit
bundle-audit check --update
ruby-audit check
(お祭り気分は盛り上がってきましたか?依存関係のチェックと更新はGitHubのDependabotに任せるようにしましょう)
🔗 Brakeman: スキャンしよう、スキャンしよう、スキャンしよう5
Brakeman gemは、Railsアプリの究極セキュリティスキャナーであり、長年にわたって信頼されてきたツールです。コードベースをスキャンして、SQLインジェクションやクロスサイトスクリプティング(XSS)のような一般的な問題を含む脆弱性を検出し、問題が悪化する前に特定して修正するのに役立ちます。
gem install brakeman
brakeman
(なお、Rails 8からはrails new
でアプリを生成すると自動的にBrakemanもCIパイプラインに追加されるようになりました。つまり、新規アプリを立ち上げれば、Brakemanが既にアプリのセキュリティを維持してくれるというわけです)
🔗 古くなった設定を見直す: 過去のRailsの亡霊6
ベストプラクティスも時とともに進化します。古くなった設定がアプリに残っていると、アプリのセキュリティを密かにむしばむ可能性があります。
Railsセキュリティガイドは、古くなった設定や安全でない設定を特定するうえで頼りになるリソースです。アプリの設定を見直す時間を確保してから、アプリが最新のベストプラクティスに沿っていることを確認しましょう。
依存関係の設定ファイルのチェックも忘れてはいけません。
たとえば、認証にDeviseを使っている場合は、config.stretches
設定を最新のデフォルト値と照らし合わせて確認しましょう。この記事の執筆時点では、推奨されるストレッチ数は12です。値がこれより低い場合は、以下のように更新する必要があります。
# config/initializers/devise.rb
config.stretches = Rails.env.test? ? 1 : 12
ただしこの変更は、変更後に生成された新しいパスワードにのみ影響します。既存のユーザーは、パスワードを変更するまで元のストレッチカウントを引き続き使うことになります。
🔗 Day 8のお祭り実行計画
- [ ]
bundler-audit
gemとruby_audit
gemをCIに追加しましょう(Dependabotを使ってもよいでしょう) - [ ] Brakemanを実行してアプリの脆弱性をスキャンしましょう。
続いて、アプリ内の古くなった設定を見直しましょう。
- [ ] アプリがRailsセキュリティガイドに沿っているか見直しをかけましょう。
- [ ] 依存関係(DeviseやLockboxなど)に含まれるセキュリティ関連の設定ファイルをチェックしましょう。
Day 8で得た結果を例によってハッシュタグ#railsmasで忘れずに共有しましょう。ただし、ゼロデイ脆弱性にパッチを当ててからにすること。サンタさんはあなたがいい子にしているかどうかちゃんと見ていますよ。
🔗 Day 9: データベースクエリを高速化する
Railsmasの9日目は、データベースをサンタの移動よりも高速化しましょう(何しろサンタは一晩で地球から太陽までの距離よりも遠くまで移動しなければならないので、非常に高速でなければなりません)。本日は、データベースのパフォーマンスを最適化するツールやヒントを紹介します。これで、アプリは休暇シーズンの帰省ラッシュなどの混雑を汗一つかかずにさばけるようになります。
🔗 PgHero: お馴染みのデータベースツール7
データベースを最適化するときは、最初に設定から手を付けるのがベストです。デフォルトのPostgreSQLは、無難な設定で動くように構成されていることで有名です。サンタの骨董品のようなコンピュータで良い子と悪い子のリストを並べ替えるぐらいなら最適かもしれませんが、現代の速度要求に応えるには十分とは言えません。PgHeroは、そんな私たちにとって必須のツールだと考えています。
PgHeroは、設定の推奨事項、使われていないインデックス、クエリ統計など、データベースの状態に関するさまざまな洞察を得られるダッシュボードを提供します。
ハードコアなDB管理者のような「データベースのみのソリューション」がお好みですか?log_min_duration_statement
設定をオンにして、遅いクエリをログファイルに出力しましょう。
遅いクエリがはっきりわかるようにするために、以下の設定でクエリに自動的にコメントを追加して、クエリを生成したコードがどこにあるのかを追いかけやすくすることも検討してみましょう。
# config/environments/development.rb
config.active_record.query_log_tags_enabled = true
これで、クエリに有用なコンテキストが追加されてデバッグがやりやすくなります。
🔗 N+1問題: パフォーマンスをおびやかすサイレントキラー
N+1クエリは気が付かないうちに忍び寄り、アプリのパフォーマンスに大混乱をもたらします(プレゼントの代わりに悪事を働く、テクノロジーにかぶれたアンチサンタのような存在、つまり本記事の著者の1人が執筆する映画脚本の題材になりそうなサイバーパンクのアンチヒーローのような存在です)。
このいまいましいN+1クエリの問題をProsopite gemでキャッチすることをおすすめします。
Prosopiteは、よく知られたBullet gemとは異なり、ログとスタックトレースを監視してN+1を検出するので、偽陰性や偽陽性の可能性が低くなります。
ただしProsopiteには欠点があります。スタックトレースの収集コストは無料ではないため、production環境でProsopiteをオンにすることはおすすめしません。
# config/initializers/prosopite.rb
unless Rails.env.production?
require 'prosopite/middleware/rack'
Rails.configuration.middleware.use(Prosopite::Middleware::Rack)
end
上のスニペットでは、フォアグラウンドのアプリコードだけがトラッキングされることに注意してください。
バックグラウンドジョブやAction Cableチャネルなどの非同期パーツについては、個別の設定が必要です。統合テスト以外のテストについても同じ考慮事項が適用されます。
🔗 可能なときはデータベースを使おう
実はデータベースを使えばもっと効率的に処理できるタスクを、外部ツールに頼ってしまうことがあります。そうした機能の一部を、その機能が輝ける場所に戻してあげましょう。
PaperTrailをLogidzeに置き換える:
バージョンのトラッキングに使っているPaperTrailをLogidzeに置き換えることを検討してみましょう。
Logidzeはバージョン履歴を直接データベースに保存して管理するので、高速かつスリムになり、すべてを1箇所に集約できます。
モデルのバリデーションをデータベース制約に置き換える:
バリデーションが複雑になると、パフォーマンスに大きな負荷がかかる可能性があります。可能な場合はデータベースの機能を活用した高速化を試してみましょう。DatabaseValidationsは、 Railsのバリデーションだけに頼らずにデータの整合性を確保する、追加のバリデーションヘルパーを提供します。
🔗 Day 9のお祭り実行計画
- [ ] PgHeroをインストールしてデータベース設定をチェックしましょう。
- [ ] クエリログにコメントを追加するRailsの機能を有効にして、遅いクエリを狙い撃ちしましょう。
- [ ] Prosopite gemでN+1クエリ問題を検出しましょう。
データベースにやらせる方がよい場合は、データベースにやらせましょう。
- [ ] PaperTrailをLogidzeに置き換えることを検討しましょう。
- [ ] DatabaseValidationsでバリデーションをデータベースレベルで行いましょう。
クエリを完璧に最適化したら、例によって#railsmasハッシュタグでレシピを共有しましょう。
🔗 Day 10: ドキュメントを改善する
Railsmasの10日目は、ドキュメントに取りかかります。
休暇中にドキュメントのことを考えてみるのは楽しそうだとは思いませんか?信じていただきたいのですが、3月にアプリの設計でおかしな決定を下した理由を後で楽々見つけられるようになったら、あなたのチームも、そして未来のあなたも、みんなあなたに感謝するでしょう。
🔗 READMEの古い記述をチェックする
READMEはプロジェクトへの玄関口です。初心者の目線でREADMEを見直し、すべての指示が現在も正確かどうかを確認しましょう。
注: TODO
、FIXME
、および「後で修正します」という約束をすべてトラッキングする必要があるなら、Railsの標準機能であるbin/rails notes
コマンドを実行すれば、コード内に隠れている注釈のリストがきれいに表示されます。
🔗 APIドキュメントを書く
アプリにAPIがあるなら、休暇明け直後ののんびりとした時期はドキュメント作成を開始する(または、これまで放置していたドキュメントを更新する)のにぴったりです。
皆さんには「ドキュメント優先」のアプローチ、つまりドキュメントを先に書いてからコードを書くことを強くおすすめします。こうすることで、APIが適切にドキュメント化されるだけでなく、実装前に設計を徹底的に検討するようになります。
どれほど見事に仕上がったAPIドキュメントであっても、記述の矛盾やタイプミス、足りないフィールドなどが生じる可能性はつきものです。そんなときはSpectralの出番です。
Spectralは、問題が発生する前に問題を発見するのに役立つAPIドキュメントのlinterです。
npx @stoplight/spectral lint docs/openapi.yml
GraphQLを使っているのであれば、このタイミングでドキュメントの煤払いをしておきましょう。ドキュメントが古くなっていると、混乱やいらつきの元になるので、ドキュメントが正確であることと、スキーマの現在の状態が適切に反映されていることを確認します。RuboCop::GraphQL gemは、統一の取れたドキュメント標準を適用するのに役立ちます。
APIを書くのがつらくなってきたら、いつでもInertia Railsをお使いください。「APIがないのが最高のAPI」ということもあるのです。
🔗 Day 10のお祭り実行計画
- [ ] プロジェクトに初めて参加する人の目線でREADMEを見直しましょう。
- [ ] ドキュメント優先のアプローチを活用して、APIを1つ以上ドキュメント化しましょう(まだやってない場合)。
- [ ] APIドキュメントをSpectralでlintチェックしましょう。
- [ ] GraphQLを使っている場合は、
RuboCop::GraphQL
を追加しましょう。
最後に、今のAPIに心底うんざりしているなら、次のプロジェクトではInertia Railsの採用を検討してみましょう!
APIのドキュメントが足りないつらさと、ドキュメントに関する皆さんのベストプラクティスを例によって#railsmasハッシュタグで共有しましょう。
🔗 Day 11: パフォーマンスを強化する
Railsmasの11日目は、いよいよパフォーマンスチューニングと最適化に取りかかりましょう!「休日の楽しみ」といえば、貴重な起動時間をミリ秒単位で短縮する作業に限ります。
🔗 メモリ管理のマジック
Railsはメモリを「溺愛している」ことを認めましょう。そのためにリソース消費がかさむだけでなく、パフォーマンスも損なわれます。そして残念ながら、メモリ管理はタダというわけにはいかず、しかもRubyの外で管理されています。
ありがたいことに、数年前にメモリアロケーションを最適化する方法がRailsコミュニティによって提案されました。それは、MALLOC_ARENA_MAX
という魔法の環境変数に値を設定するか、システムアロケータをjemallocに完全に置き換えるかのいずれかです。
事前設定された便利なソリューションがご希望の方には、Fullstaq Rubyがおすすめです。Fullstaq Rubyでは、Ruby 3.3より前が対象のmalloc_trim
パッチもDockerイメージにバックポートされています。
これらのチューニングは、特にマルチスレッドアプリで有用です。
ただしこれらの手法は広く知られているものの、デフォルトのRubyには適用されていません。RubyはRailsだけのためにある言語ではなく、メモリアロケーションをカスタマイズすることが必ずしもプログラムにとって有益とは限りません。
🔗 DockerとBootsnapは仲良し
アプリケーション読み込み時の高コストな計算をキャッシュするBootsnapは、開発中の起動時間を短縮するのに使われます。
実はproduction環境のアプリ起動もこのBootsnapで高速化できることをご存知ですか?
production環境の読み込み時間をBootsnapで短縮するには、以下のようにキャッシュをプリコンパイルしておく必要があります。
bundle exec bootsnap precompile --gemfile app/ lib/
コードを適切にキャッシュするには、関連するディレクトリ(config/
、engines/
、packs/
などのカスタムのトップレベルディレクトリ)もすべて上のコマンドで指定しておくことをお忘れなく。
以下のシンプルなinstrumentationで効果を検証できます。
# config/boot.rb
# このファイルの最終行に以下を書く
Bootsnap.instrumentation = ->(event, path) { puts "#{event} #{path}" }
次に、キャッシュをクリアしてからプリコンパイルし、Bootsnapのinstrumentationに'miss'という語が含まれていないことをチェックしましょう。
rm -rf tmp/cache/bootsnap/*
bundle exec bootsnap precompile --gemfile app/ lib/
bundle exec rails r 'puts :ok' | grep 'miss'
(ただしDockerのイメージレイヤにはくれぐれも注意すること。プリコンパイルされたBootsnapキャッシュがtmp/
ディレクトリに残りがちです)
少々だるい作業ですが、1晩の大仕事のために残りの364日をひたすら準備にあてる、それがサンタ流の仕事術です。
🔗 YJITのコンフィグに注意しよう
JITで行われたあらゆる実験を思い出してみると不気味でしたよね?実は、昨年(2023年)からRailsは、特別なフラグを指定しなくてもYJITをデフォルトで有効にするようになりました(訳注: 参考)。
あなたのアプリがRails 7.2とRuby 3.3の組み合わせで動いていれば、おそらく既にYJITが使われているでしょう。ただしご用心!load_defaults
を更新し忘れる開発者はざらにいます。もし心当たりがあれば、以下のコンフィグを追加することでYJITを有効にできます。
# config/application.rb
# development環境やtest環境ではコードの再読み込みやメソッド再定義(モックなど)が頻繁に行われるので、
# YJITを有効にしても一般に高速化されない。
config.yjit = !Rails.env.local?
この方法は、load_defaults
の値を更新するというやんちゃな方法(更新後のコンフィグ値には十分注意が必要)よりも安全です。
YJITの背後にある魔法に興味を惹かれている方は、以下の記事をどうぞ。
Ruby Outperforms C: Breaking the Catch-22 | Rails at Scale
YJITがアプリに顕著な違いをもたらしているかどうかを確認するために、パフォーマンスのメトリクスを見張っておきましょう。また、あらゆる負荷がJITコンパイルの恩恵を受けるわけではないので、コミットする前にテストしておきましょう!
🔗 フロントエンドにViteを使う
Railsアプリを最適化するときは、アセットパイプラインのことも忘れてはいけません。Webpackerベースのアセットパイプラインに今も依存している場合は、そろそろ年代物のソリをターボチャージャー付きのスノーモービル、つまりVite Rubyに交換する時期かもしれません。
Vite Rubyは、Railsに高速ビルド、ライブリロード、ホットモジュール置換をもたらす最新のフロントエンド用ツールチェインです。フロントエンドワークフローにピカピカの新しい贈り物が届けられたようなもので、アセットのコンパイルを永遠に待つ必要はもうありません。
もっと冒険したい気分になってきたら、rolldown-vite
をチェックしてみましょう。これはViteの一時的なforkで、RolldownというRustで書かれた実験的なJavaScriptバンドラーを利用しています。サンタのターボモードのソリよりも高速です。
// package.json
"vite": "npm:rolldown-vite@0.1.0"
まだRuby Viteで必要な設定オプションをすべて網羅したわけではありませんが、ここにはより高速なアセットパイプラインを垣間見る夢があります。サンタがミルクを2パイント飲み干すよりも速くフロントエンドのビルドが終わるかもしれません(なおサンタがミルクを飲むのは相当速いですよ)。
🔗 Day 11のお祭り実行計画
終わったら、Railsスピードアップのコツを例によって#railsmasハッシュタグで共有しましょう。アプリケーションの高速化ほど「ホリデースピリット」にふさわしいものはありません。
🔗 Day 12: アプリの安定性を強化する
Railsmasもいよいよ大詰めの12日目を迎えたので、またとない究極の贈り物を皆さんのアプリに届けたいと思います。そう、アプリの安定性です。
アプリがどれほど高速化しようと、セキュリティが高まろうと、ドキュメントがみっちり書かれていようと、ユーザーのくしゃみ一発でクラッシュするようでは何にもなりません。あったかな毛布にくるまってホットココアを飲みつつ、アプリが生きながらえるための仕上げを行うことにしましょう。
🔗 リクエスト処理にはタイムアウトを設定しよう
特に休暇中の時間は1秒たりとも無駄にしたくありません。アプリに適切なタイムアウトを設定して、時間のかかるリクエストや途中で立ち往生したリクエストによってリソースが食い尽くされてアプリが完全に停止することを防ぎましょう。
タイムアウトを設定しておかないと、応答を停止したサービスによって重要なリソースがいつまでも吸い込まれてしまい、システム規模の速度低下やクラッシュにつながる可能性が生じます。
潜在的な障害点を見逃すのではと心配する必要はありません。以下の「Rubyタイムアウト究極ガイド」では、Rubyアプリケーションでタイムアウトが重要となるあらゆる状況に対応するコードスニペットの包括的なリストが提供されています。
Railsアプリケーションで絶対に必要なタイムアウト設定が少なくとも1つあります。それはデータベースのタイムアウトです。
production:
adapter: postgresql
variables:
statement_timeout: 30s
注意:タイムアウトにどんな値を設定するかが重要なので、アプリケーションのニーズに合わせて適切に調整する必要があります。多くのアプリケーションでは30秒に設定しておけば十分ですが、長時間実行されるクエリが多いアプリケーションでは、正当なクエリが途中で終了しないように、もっと大きな値が必要な場合もあります。
🔗 Sidekiq: ウィンターワンダーランドに繰り出すためのエンジン
Sidekiqはバックグラウンドジョブの有力な製品ですが、有力であっても信頼性を保つにはそれなりのケアが必要です。Sidekiqキューをスムーズに実行するためのヒントをいくつか紹介します。
最もありがちな見落としは、Sidekiqがセグメント違反などの重大なクラッシュ時にジョブを喪失する可能性があることに気付かないことです。SidekiqはジョブエントリをRedisキューから取得しますが、デフォルトでは確認応答(acknowledgement)のメカニズムは用意されていません。
有償版のSidekiq Proに備わっているsuper_fetch
機能ならワーカークラッシュ時のジョブ喪失を防げますが、オープンソースによる別の方法として、GitLabが提供しているsidekiq-reliable-fetchも利用できます。
このgemは、ジョブをより堅牢なプロセスでラップして、処理実行中のワーカーがクラッシュしても、ジョブを別のワーカーが拾い上げて再度キューに登録します。ちょうどサンタの手下であるエルフたちが「良い子リスト」と「悪い子リスト」をダブルチェックするようなもので、おかげでジョブを失わずに済みます。
Sidekiqはかなり柔軟ですが、1つの「貪欲な」クライアントがバックグラウンド処理を片っ端から引き受けてしまい、ジョブが公平に分配されなくなる可能性があります。これを修正するには、ロビンフッド...じゃなくてラウンドロビンスケジューリングアルゴリズムが必要です。
Sidekiq::FairTenant
gemは、リソースを大食いするクライアントからのジョブをスロットリングすることでこの問題を解決します。重み付けキュー機能を追加して、定義されたしきい値を超えたジョブを低優先度キューに再ルーティングします。これにより、全体的なスループットを損なわずに、重いジョブが他のジョブをブロックしないようにできます。
🔗 Action CableからAnyCableへの移行
WebSocketsでAction Cableを使っている場合は、そろそろAnyCableへの乗り換えを検討してもよい時期かもしれません。
AnyCable は WebSocketコネクションの負荷を別のGoサーバーにオフロードするだけでなく、Railsアプリの負荷を軽減してスケーラビリティを向上させます。
AnyCableにはAction Cable Extended protocolという特別なプレゼントもついてきます。これは、コネクションのライフサイクルイベント処理の強化や、より堅牢な再接続戦略のサポートなど、安定性の向上を実現するさまざまな機能を提供してくれます。
🔗 データベースとスレッド数
サーバープロセス数とスレッド数との適切な対応付けは、バランスを取りながら行う作業となります。
ただし、この値は多ければよいというものではありません。たとえば、最近RailsコアチームはPumaのデフォルトスレッド数を5から3に減らしました(#50450)。その理由は、この値にすることで、(長時間かかるクエリ実行やサードパーティ呼び出し同期を行わない)Railsアプリケーションのレスポンス時間やリソース使用量が改善されることが示されたからです。
なお、バックグラウンドジョブにおけるレイテンシ最適化を改善するため、good_jobのissue #1554でRubyのThread Priorityに関する興味深い実験が行われました。
自分たちのアプリケーションに最適なワーカー数やスレッド数を割り出すには、2015年からある定番記事『Scaling Ruby Apps to 1000 Requests per Minute』や、新しいところではRailsガイドのデプロイ用パフォーマンスチューニングガイドをお使いください。
負荷がピークに達したときのエラーを排除したい場合や、リソースが必要以上に割り当てられているアプリのコストを来年から引き下げたい場合は、アプリケーションの設定値が適切であるかどうかをチェックしておきましょう。
アプリの設定を適切に更新したにもかかわらずコネクションでタイムアウトが発生する場合は、以下のRailsガイドを参考に、アプリケーションでデータベースを利用するあらゆる箇所をRailsのExecutor
やReloader
でラップすることを検討してもよいでしょう。
参考: Rails のスレッドとコード実行 - Railsガイド
スレッドメカニズムを活用するgemを利用している場合は、データベースアクセスでタイムアウトエラーが発生するのを防ぐために、それらのgemがRailsのロックメカニズムを利用していることを確認しておきましょう(Karafka、Sidekiq、AnyCableなど)。一部のgem(Sucker Punch、Kicks (旧名Sneakers)など)は、ユーザーがスレッド安全性を手動で管理することが前提になっているため注意が必要です。
🔗 Day 12のお祭り実行計画
- [ ] タイムアウトを設定しましょう(データベースやHTTPなど)。
- [ ] Sidekiqのジョブ復旧をsidekiq-reliable-fetchで強化し、
Sidekiq::FairTenant
でキューイングを公平にしましょう。 - [ ] 接続安定性を強化するためにAction CableからAnyCableへの乗り換えを検討しましょう。
- [ ] ワーカー数やスレッド数を適切にチューニングしてパフォーマンスを最適化しましょう。
- [ ] Railsの
Executor
/Reloader
でデータベースコネクションのタイムアウトを修正しましょう。
アプリの安定性を実現する方法を、毎度おなじみの#railsmasハッシュタグで共有しましょう。ホリデーシーズンの雰囲気を盛り上げてくれるのは、何と言ってもホリデーシーズンのトラフィック急増にも耐えられるほど安定したアプリでしょう。
🔗 以上でおしまいです
さてさて、とうとう12日が過ぎました。新年を迎える頃には、皆さんのアプリのコードベースも祝福にふさわしいものに生まれ変わっていることでしょう。ハッピーニューイヤー、ようこそ2025年!
関連記事
- 訳注: エッグノッグ - Wikipedia ↩
-
訳注: RSpecの
match_array
は、渡す配列の要素の順序にかかわらずマッチします。 ↩ - 訳注: 原文"One Viewport size fits all"は、The Lord of the RingsのOne Ring to rule them all...のもじりです。 ↩
- 訳注: 原文"the good, the bad, and the flaky"は、映画『続・夕陽のガンマン』の英語版タイトル"The Good, the Bad and the Ugly"のもじりです。 ↩
- 訳注: 元記事の見出し"let it scan, let it scan, let it scan"は、Frank Sinatraのクリスマスソング『Let It Snow! Let It Snow! Let It Snow!』のもじりです。 ↩
- 訳注: 原文の"the ghosts of Rails past"は、ディケンズ『クリスマス・キャロル』の「過去のクリスマスの亡霊(Ghost of Christmas past)」のもじりです。 ↩
- 訳注: 原文"your friendly neighborhood database tool"は、スパイダーマンの有名なキャッチフレーズ"Your Friendly Neighborhood Spider-Man"のもじりです。 ↩
概要
元サイトの許諾を得て翻訳・公開いたします。
(技術編集者)
日本語タイトルや見出しは内容に即したものにしました。