ユーザー数の大幅な増加に備える Ruby on Rails のスケーリング戦略
本記事では、Ruby on Rails アプリケーションを大規模なユーザーベースまでスケールさせるために活用できるさまざまな戦略について詳しく解説します。
アプリケーションをスケールする最も分かりやすい方法は、とにかくお金をかけることです。実際、これは驚くほど効果的です。サーバーを数台追加し、データベースサーバーをアップグレードすれば、多くのパフォーマンス問題はあっという間に消え去ります。
しかし、サーバーを増やさずともアプリケーションをスケールできるケースは少なくありません。今回はそのようなアプローチについて掘り下げていきます。それでは始めましょう!
まずは AppSignal で Rails アプリケーションを計測する
スケーリングやパフォーマンス最適化の話に入る前に、そもそも最適化が必要なのか、ボトルネックはどこにあるのか、どのリソースがスケール対象になり得るのかを把握することが重要です。
これを簡単に行う方法の一つが、AppSignal の Ruby 向けパフォーマンス監視とメトリクス機能を活用することです。
パフォーマンスダッシュボードを使えば、平均的に遅いコントローラアクションやバックグラウンドジョブを正確に特定できます。
例えば、ActiveRecord のパフォーマンスダッシュボードは次のように表示されます。

この情報は、スケーリングへの取り組みの出発点として非常に有用です。サーバーを追加するにせよ、コードでパフォーマンスを改善するにせよ、まずは現状を正しく把握することから始まります。
それでは、Rails アプリをスケールさせるための最もシンプルなテクニックである「キャッシュ」から見ていきましょう。
Ruby on Rails におけるキャッシング
キャッシュを利用すると、同じ処理を何度も繰り返し計算することを防げます。
例えば、SNS プラットフォームを運営していて、ある投稿がバズって大量に閲覧されている状況を考えてみましょう。キャッシュを使えば、すべてのユーザーのために同じ投稿を描画し続けることで消費していた CPU リソースを一気に取り戻せます。これはキャッシュがもたらす恩恵のほんの一部に過ぎません。
ここで、キャッシュできるリソースをすべて見ていきましょう。
ビューのキャッシュ
ビューのレンダリングは、表示すべきデータ量が多い場合などには負荷の高い処理になることがあります。たとえ処理が軽かったとしても、事前にレンダリング済みのビューを再利用できれば、同じビューを百万回レンダリングし直すのに比べて大きなパフォーマンス向上が期待できます。
Rails は cache ビューヘルパーによって、この機能を標準でサポートしています。例えば、一覧をレンダリングする際に各投稿を個別にキャッシュするには、次のように記述します。
この場合、Rails はテンプレートの HTML コンテンツ、投稿 ID、更新タイムスタンプに基づいたキーで、各投稿を自動的にキャッシュします。
この手法についてさらに詳しく知りたい方は、「Fragment caching in Rails」や「Rails collection caching」の記事を参照してください。
ただし一点注意が必要です。キャッシュキーにはネストされたテンプレートの内容は含まれません。そのため、キャッシュ呼び出しを複数階層にネストすると、古い結果が表示される可能性があります。詳細は「Russian doll caching in Rails(ロシア人形キャッシング)」の記事を参照してください。
レスポンスのキャッシュ
ビューやフラグメントのキャッシュに加えて、GET リクエストのレスポンス全体をキャッシュすることも可能です。これはブラウザが送信する If-None-Match ヘッダーと If-Modified-Since ヘッダーによって実現されます。
リクエストに If-None-Match ヘッダーが含まれている場合、レスポンスに変更がなければ、サーバーはコンテンツなしの 304 Not Modified レスポンスを返せます。この際、サーバー側で計算した Etag がヘッダーの値と比較されます。
同様に、If-None-Match なしで If-Modified-Since ヘッダーだけが送られてきた場合も、その日付以降にレスポンスが変更されていなければ、コンテンツなしの 304 Not Modified レスポンスを返すことができます。
Rails では、コントローラアクション内で簡単にこの仕組みを実装できます。以下のように書くだけです。
Rails はキャッシュに必要なヘッダーを自動的に送信し、受信したヘッダーを処理し、データに変更がなければ 304 を返します。変更がない限り、ビュー全体を再度レンダリングする必要はありません。この戦略の高度な設定については「Client-side caching in Rails: conditional GET requests」を参照してください。
値のキャッシュ
最後に、生の値(キャッシュストアにシリアライズできる任意のオブジェクト)をキャッシュすることもできます。これは主に、リソースを大量に消費する処理や時間のかかる処理の結果を保存し、同じ計算の再実行を避けるために役立ちます。
どの値がキャッシュの恩恵を受けられるかはアプリケーションによって大きく異なりますが、一般的には最も遅いイベントを観察することが正しい方向を見つける手がかりになります。
キャッシュ対象を特定できたら、Rails が提供する API を使えば非常にシンプルに実装できます。
上記のコードでは perform_the_slow_computation は一度しか実行されず、その結果が cache_key_with_version キーのもとでキャッシュされます。同じコードが次に呼ばれたときは、Rails がまずキャッシュ済みの値の有無を確認し、あればそれを利用するため、時間のかかる計算が再実行されることはありません。
このキャッシュ戦略で最も重要なのは、値の計算に使われるすべての入力に依存した、適切なキャッシュキーを設計することです。これにより、古い値を使い続けてしまうことを防げます。
キャッシュストアの選択
何をキャッシュするか、そして Rails が提供する保存方法がわかったところで、次に疑問になるのは「どこにキャッシュするのか」という点です。Rails にはいくつかの組み込みキャッシュストアアダプタが用意されています。本番環境で人気があるのは Redis と Memcached です。その他にもファイルストアやメモリストアといった選択肢があります。各ストアの詳細な比較については「Rails' built-in cache stores: an overview」の記事を参照してください。
ファイルストアやメモリストアは、開発環境ですぐに動かしたい場合には便利ですが、特に複数サーバーで構成される分散環境では本番用途には不向きです。本番環境には Redis または Memcached が適しており、どちらを選ぶかはアプリケーションの特性によります。
Ruby on Rails におけるバックグラウンドワーカー
ほとんどのアプリケーションでは、メール送信、定期的なクリーンアップ、ユーザーがその場にいなくてもよい時間のかかる処理などにバックグラウンドジョブが必要になります。おそらくあなたのプロジェクトでも、すでにバックグラウンドワーカーが導入されていることでしょう。
コントローラアクション内で1秒以上かかるような処理を書いていることに気づいたら、それをバックグラウンドワーカーへ移せないか検討しましょう。対象となるのは、巨大なテーブル内のデータ検索のようなユーザー向け操作から、大量のデータを取り込む API メソッドまで多岐にわたります。
実装例
カスタムジョブを実行するために、Rails は Active Job フレームワークを提供しています。非常に複雑なフィルタリング処理をバックグラウンドジョブへ移す方法を見てみましょう。まず、ジョブを作成します。
このジョブはコントローラから次のように実行できます。
ジョブがデータを計算して結果を返すまでの間、テンプレート側にはローディングインジケーターを表示しておく必要があります。
しかし、ジョブの結果をどうやってビューに届ければよいのでしょうか? Turbo を使えば、これはとても簡単です。例えば、ビュー内で turbo_stream_from を使って、特定の通知チャンネルの turbo-stream イベントを購読できます。
これを使ってテンプレートを書いてみましょう。
初期のコントローラアクションでは data 変数が定義されていないため、この時点ではローディングインジケーターのみが表示されます。続いて、ジョブから結果を配信してみましょう。
ここで重要なのは notify_completed メソッドです。このメソッドは Turbo::StreamsChannel を利用して、ビュー側で購読した [user, :huge_datasets] 通知ストリームに対して replace イベントをブロードキャストします。
以上が、複雑な処理をコントローラからバックグラウンドジョブへ移行するために必要な手順のすべてです。タスクをバックグラウンドへ移す最大の利点は、バックグラウンドワーカーを Web サーバーとは独立してスケールできる点にあります。これにより Web サーバー側のリソースを大きく解放できます。また、ユーザーにとっても即座に応答があり結果が段階的に届くため、インターフェースの反応が非常に良いと感じられます。
補足: バックグラウンドジョブワーカーの選定にお悩みの方は、「Delayed Job vs. Sidekiq: Which Is Better?」を参照してください。
Ruby on Rails アプリケーションのデータベーススケーリング
本記事で取り上げる最後のスケール対象リソースはデータベースです。データベースは多くのアプリケーションの中核を担っています。データ量と、そのデータへアクセスするサーバー数が増えるにつれて、データベースには次第に負荷がかかってきます。
データベースをスケールする最も簡単な方法は、データベースサーバーに CPU やメモリを追加することです。ただし、Web サーバーのスケールアップとは異なり、データベースの場合は特にストレージが大容量だと作業に時間がかかる傾向があります。
もう一つの選択肢は、複数のデータベースを利用したりシャーディングを行うことで水平方向にスケールすることです。詳細は「Multiple Databases with Active Record」を参照してください。
ここでは、PostgreSQL を題材に、データベースのパフォーマンス最適化に焦点を当てていきます。
PostgreSQL で時間のかかるクエリを特定する
まずは、最も実行時間のかかっているクエリを特定する必要があります。そのためには、サーバー上で実行されたすべての SQL ステートメントの統計情報を格納している pg_stat_statements テーブルを照会します。実行時間の上位100件のクエリを取得する方法を見てみましょう。
このクエリは、SQL 文、呼び出し回数、平均実行時間を返します。より速くできそうなクエリを見つけて、なぜ遅かったのかを分析してみましょう。
また、クエリに対して EXPLAIN や EXPLAIN ANALYZE を実行すると、それぞれクエリプランと実際の実行詳細を確認できます。
結果の中で特に注目すべきポイントの一つが Seq Scan(シーケンシャルスキャン)です。これは Postgres がクエリ実行のために全レコードを順番に走査しなければならないことを示しています。これが発生している場合は、フィルタ条件に使っているカラムにインデックスを追加することで、シーケンシャルスキャンを回避できないか試してみましょう。
シーケンシャルスキャンが多いテーブルを見つける
私がよく実行するもう一つの有用なクエリは、テーブルごとのシーケンシャルスキャンの総数を調べるものです。
この結果で、行数の多いテーブルが高カウントを示していたら要注意です。そのテーブルに対するすべてのクエリを確認し、シーケンシャルスキャンを引き起こしているものを特定して、インデックスを追加して解消しましょう。
インデックス使用率
次のクエリを実行すると、インデックスの使用状況に関する統計も確認できます。
このクエリはテーブルごとのインデックス使用率を返します。数値が低い場合は、そのテーブルに不足しているインデックスがあることを意味します。
まとめ
本記事では、キャッシュやバックグラウンドワーカーをはじめとする、Ruby on Rails アプリケーションをスケールさせるためのいくつかの戦略を紹介しました。さらに PostgreSQL データベースのパフォーマンス最適化についても触れました。
Rails には、アプリケーションに複数層のパフォーマンス最適化を容易に追加できる仕組みが備わっています。
スケーラビリティにおいて最も重要なのは、対策を講じる前にアプリケーションのボトルネックを正確に特定することです。優れたパフォーマンス監視ツールがその助けになります。必要であれば、ぜひ AppSignal for Ruby をチェックしてみてください。
それでは、Happy coding!
P.S. Ruby Magic の記事を公開後すぐにお読みになりたい方は、Ruby Magic ニュースレターをご購読ください。最新記事をお見逃しなく!
-
Ruby MRIのソースコードを読んで内部構造を学ぶ方法
Rubyを使い込んでいると、その裏側でどのように動作しているのか気になったことはありませんか? Rubyの内部に深く踏み込む方法の一つが、それを実現しているソースコードを読むことです。C言語がわからなくても、興味深い発見はたくさんあります。 ソースコードはRubyのGitHubリポジトリで公開されています。 理想を言えば、クラス名やメソッド名を簡単に検索できるCodequeryのようなツールを使うのがおすすめです。 コアクラスを探検する 調査のほとんどはルートフォルダで行うことになります。ここには、object.cにあるObjectや、array.cにあるArrayなど、すべてのコアクラスのソ
-
TurbolinksからTurboへの移行完全ガイド:Hotwireで実現する高速なRailsアプリ
Turbolinksは、Webアプリケーションのページ遷移を高速化する優れたツールでしたが、現在では活発な開発が終了しています。その後継として登場したのが、Hotwireファミリーの一部である「Turbo」という新しいフレームワークです。 開発チームは、Turbolinksから抽出されたコンセプトを、フレーム、フォーム送信、ネイティブアプリなど、より高速なWebを実現するためのさまざまな機能に応用できると考えました。 この記事では、Turbolinksの後継であるTurboフレームワークについて解説し、Turbolinksで最も重要かつ頻繁に使われていた機能をTurboでどのように扱うのかを、