Railsアプリのパフォーマンス・ストレス・負荷テスト徹底解説【JMeterとruby-prof活用】
テストは、保守が悪夢にならず、新機能の追加や既存機能の改善を継続的に行える、健全なRailsアプリケーションに欠かせない要素です。しかし残念なことに、多くのアプリケーションにとって、本番環境で初めて高負荷や大量のトラフィックにさらされるというのが実情です。こうしたテストにはコストがかかるため、後回しにされがちなのは理解できます。
幸い、Railsはユニットテスト、エンドツーエンドテスト、統合テストだけでなく、パフォーマンスや負荷に関するテストもしっかりサポートしています。この記事ではそれらをすべて取り上げ、アプリケーションのパフォーマンスレベルを効率的にテストするためのツールの使い方を実践的な例とともに紹介します。
記事は大きく2つのパートに分かれています。
- 理論編 — テストが必要な理由、実施できるテストの種類、そしてテスト時に重要となる指標について解説します
- 実践編 — 実際のアプリケーションに対してテストを書き、結果を取得してみます
両パートを読み終える頃には、さまざまな種類のテストへの理解が深まり、自分のRailsアプリケーションでそれらを実施できるようになっているはずです。興味がありますか? では、テストに関するちょっとした理論から始めましょう。
テストの理論
テストは、どんな種類のアプリケーション開発においても常に不可欠なプロセスであるべきです。まだその重要性に納得していない方や、一度もテストを書いたことがない方のために、テストを導入すべき理由をいくつか挙げます。
- 壊れる心配なく変更を導入できる — これがテストが必要な最大の理由です。巨大なアプリで作業しているとき、小さな変更であっても、何かを壊していないか確認するためにアプリ全体を手動でクリックして回ることを想像してみてください。テストがあれば、コマンドを1つ実行するだけで検証プロセスが自動的かつ高速に完了します。
- リファクタリングが容易になる — 前述の通り、テストは新機能の追加や変更時に不可欠ですが、テストが整備されていれば既存コードの改善にも自信を持って取り組めます。
- テストはドキュメントの一種になる — よく書かれたテストは、アプリケーションの各機能に対するドキュメントとして機能します。機能が「何」であるかだけでなく、「どのように動作すべきか」まで記述してくれるのです。
- 実装を見直す機会になる — テストを書くとき、実装しようとしているコードの方法が正しく妥当かどうかを再考するきっかけになります。また、コードが期待通りに動作しているかを単純に確認することもできます。
以上の理由から、どんなアプリ開発でもテストを活用すべきだと納得いただけたなら幸いです。コードをテストする「理由」を知ることも重要ですが、さまざまな「種類」のテストについて学ぶことも同じくらい重要です。
テストの主な種類
Railsアプリケーションのパフォーマンスが適切であり、高負荷時にもインフラが正常に動作することを保証するために書けるテストには、主に次の3種類があります。
- 負荷テスト(Load Testing) — 「システムは一定期間、同時に何人のユーザーを処理できるのか」という問いに答えるテストです。人気商品をサイトで発売し、数千人のユーザーが同時に注文しようとする状況を想像してください。適切な負荷テストを行っていなければ、最も重要な瞬間にシステムがクラッシュするリスクを抱えることになります。
- ストレステスト(Stress Testing) — このテストでは、システムが処理できる同時ユーザー数の確認ではなく、「ユーザー数が限界に達したときにシステムがどう振る舞うか」に焦点を当てます。
- パフォーマンステスト(Performance Testing) — これはストレステストと負荷テストの親にあたるテストと言えます。主な目的は、特定の一連の指標を取得し、それを基にアプリケーションコードを改善するアクションにつなげることです。指標については後ほど詳しく説明します。
ここまで理解できれば、理論編の最後のステップである「Railsアプリケーションのパフォーマンステストで重要となる指標」に進む準備ができました。これを知らなければ、テストの出力を正しく解釈し、コードを変更すべきか判断することができません。
重要な指標
取得できる指標の種類は使用するテストツールによって異なりますが、一般的には次のような共通の指標群に分類できます。
- レスポンスタイム(Response Time) — リクエスト送信からレスポンスがブラウザに描画されるまでの時間です。ユーザーが要求した情報を受け取るまでどれだけ待つ必要があるかを示します。「プロセスタイム」と呼ばれることもあります。
- メモリ使用量(Memory Usage) — 特定のリクエストで消費されるメモリ量です。システムがより速く応答し、より少ないリソースで動作するようにコードを改善できる箇所を示してくれる、非常に重要な情報です。
- オブジェクト割り当て(Object Allocation) — メモリ割り当てが多いと、メモリ使用量の増大やレスポンスタイムの長期化を招きます。この指標は、大量のオブジェクトが割り当てられているコード上の正確な場所へと導いてくれるので、すぐに調査を開始できます。
テストで取得できる指標は他にもありますが、この3つが最も重要であり、どんなアプリケーションをテストする場合でも有効です。それでは、実際に手を動かして本物のテストを書いてみましょう。
実践編
テスト対象がなければテストは書けません。そこで実践編の最初のステップとして、テストを書く対象となるシンプルなRailsアプリケーションを作成します。
サンプルRailsアプリケーション
ここではRuby 3.0.1とRails 6.1.3.1を使用しますが、使い慣れたバージョンを自由に使って構いません。RubyとRailsがインストール済みなら、次はアプリケーションのスケルトンを作成します。
rails new simpleapp -d=postgresqlこの記事では、ユーザーのリストとそれぞれのペットの名前を表示するアプリを作成します。この構造にするとN+1クエリを簡単に発生させることができ、パフォーマンステストの際に速度やその他の指標への影響を確認するのが楽になります。
モデルを生成する前に、まずデータベースを作成しましょう。
cd simpleapp/
bin/rails db:create続けてモデルを生成します。
rails g model User name:string
rails g model Animal name:string user:references
bin/rails db:migrateUserモデルとAnimalモデルの関連を反映させるために、Userモデルに小さな修正を1つ加えます。
class User < ApplicationRecord
has_many :animals
end次に、db/seeds.rbファイルにシードデータを追加します。
people = {
'Tim' => ['Pinky', 'Rick'],
'Martha' => ['Rudolph'],
'Mark' => ['Niki', 'Miki', 'Bella'],
'Tina' => ['Tom', 'Luna']
}
people.each_pair do |name, pets|
user = User.create(name: name)
pets.each do |pet_name|
user.animals.create(name: pet_name)
end
endそしてデータをデータベースに読み込みます。
bin/rails db:seed続いて、ユーザー一覧を表示するコントローラを1つ作成し、ビューですべてのユーザーとペットの名前をリスト表示します。意図的にパフォーマンス問題を引き起こすコードを使っているので、後で改善効果を計測できます。
touch app/controllers/home_controller.rb
mkdir app/views/home
touch app/views/home/index.html.erbコントローラはシンプルです。
class HomeController < ApplicationController
def index
@users = User.all
end
endビューも同様にシンプルです。
<h1>List</h1>
<ul>
<% @users.each do |user| %>
<li><%= user.name %> (<%= user.animals.count %>)
<ul>
<% user.animals.each do |animal| %>
<li><%= animal.name %></li>
<% end %>
</ul>
</li>
<% end %>
</ul>最後に、config/routes.rbファイルを更新して、メインURLにアクセスしたときに何を表示するかをRailsに伝えます。
Rails.application.routes.draw do
root to: 'home#index'
endJMeterによる負荷テスト
JMeterはApacheソフトウェア財団が開発したオープンソースソフトウェアで、機能的な振る舞いに対する負荷テスト向けに設計されています。Javaで作られたプログラムなので、どんなOSにもインストールできます。ファイルはこちらからダウンロードできます:https://jmeter.apache.org/download_jmeter.cgi
macOSをお使いなら、Homebrewで簡単にJMeterをインストールできます。
brew install jmeterインストール後、次のコマンドでプログラムを起動します。
jmeterテストの設定
設定プロセスは次のステップで構成されます。
- スレッドグループの追加 — ユーザー数と、各ユーザーがサイトに滞在する時間を指定します
- HTTPリクエストの設定 — JMeterがアクセスすべきエンドポイントを指定します
- 関心のある指標の設定
先ほど作成したシンプルなアプリのメインページへの、単一ユーザーリクエストをシミュレートする簡単なテスト設定を、ステップごとに見ていきましょう。
スレッドグループの追加
「Test Plan」を右クリックして展開されるメニューから、Add → Threads (Users) → Thread Groupを選択します。
ユーザー数などの属性を指定します。
HTTPリクエストの設定
前のステップで作成したスレッドを右クリックし、Add → Sampler → HTTP Requestを選択します。
プロトコル、サーバー名、ポート、リクエストのパスを設定します。
結果表示の指定
HTTPリクエストを右クリックし、Add → Listener → View Results Treeを選択します。
テストの実行
これでテストの設定が完了し、実行できます。緑の再生ボタンをクリックするだけです。
ご覧の通り、アプリケーションはテストに合格しました。ただし、これはリクエストが1件だけだったので当然の結果です。ここからユーザー数やその他の設定オプションを変えて実験し、アプリケーションがどう振る舞うかを確認できます。筆者のテストでは、約200人程度のユーザーが同時にアクセスを始めると、このシンプルなアプリはクラッシュし始めました。
次のステップ
負荷テストを実施すれば、アプリケーションのボトルネック(ペインポイント)が分かります。ユーザー数の限界を把握できたら、次はストレステストを実施して、限界を超えたときにアプリケーションがどう振る舞うかを確認しましょう。
ruby-profによるパフォーマンステスト
パフォーマンステスト機能はRailsバージョン3まで組み込まれていましたが、その後別のgem(https://github.com/rails/rails-perftest)として切り出されました。最新版のRailsで使う際にいくつか問題があったため、この記事では取り上げず、代わりに非常によく動作するruby-profライブラリを使用します。
いつものように、最初のステップはgemをアプリケーションに追加することです。
bundle add ruby-prof設定プロセスの2番目で最後のステップは、config/application.rbを更新してgem用のミドルウェアを使用するよう設定することです。これにより、ライブラリがリクエストを自動的に検査し、それに基づいたレポートを生成できるようになります。
module Simpleapp
class Application < Rails::Application
config.middleware.use Rack::RubyProf, :path => './tmp/profile'
end
endこれでアプリにアクセスできるようになり、リクエストを実行するたびにgemが新しいレポートを生成します。レポートはこんな感じです。
レポートは設定したパス(今回の場合はtmp/profile)以下に保存されます。2つ目のレポートも生成されており、こちらはコールスタックを表示します。Railsアプリケーションのパフォーマンス問題をデバッグする際に非常に役立つ情報です。
重要な注意点として、cache_classesとcache_template_loadingの設定をtrueにしないと、Railsが必要なファイルを都度ロードしようとするため、アプリケーションが遅くなり、計測されるメトリクスが不正確になってしまう点を覚えておきましょう。
まとめ
テストはすべての開発プロセスにおいて不可欠な部分です。コードが期待通りに動作するかを確認することは、ソリューションのパフォーマンスが優れているかを検証することと同じくらい重要です。テストを省略すると、アプリのパフォーマンスとユーザーの信頼に深刻な影響を与える問題につながります。幸い、テスト自体はそれほど難しいものではありません。
この記事では、テストに関する次の重要な側面を扱いました。
- コードをテストすべき理由
- パフォーマンステストのさまざまな種類
- Railsアプリのパフォーマンスをテストする方法
理由と方法の両方が分かった今、テストを書くことの重要性にさらに納得いただけたのではないでしょうか。
ローカル環境だけでなく、本番環境やステージング環境でもアプリのパフォーマンスを監視したい場合は、AppSignalもぜひチェックしてみてください。
P.S. Ruby Magicの投稿が公開されたらすぐに読みたい方は、Ruby Magicニュースレターを購読して、記事を見逃さないようにしましょう!
-
ByebugとRails、Powを使ったリモートデバッグのやり方
Ruby 2.x向けの優れたデバッガー「Byebug」をご存じない方は、ぜひ一度試してみてください。シンプルでありながら多機能で、日々のRuby開発を強力にサポートしてくれるツールです。開発者自身による説明がこちらです。 ByebugはRuby 2向けの、使いやすく機能豊富なデバッガーです。実行制御には新しいTracePoint API、コールスタックのナビゲーションにはDebug Inspector APIを採用しているため、Ruby内部のコアソースに依存しません。C拡張として実装されているため動作が高速で、充実したテストスイートを備えているため信頼性も高いのが特徴です。 基本的な使い方
-
RailsアプリをテストするためのDockerコンテナを設定する
Ruby / Rails開発者として、私たちはテストを書くのが大好きです。テストはソフトウェア開発の不可欠な部分です。優れたテストは、高品質のコードを書くのに役立ちます。これらは開発プロセス全体に付加価値をもたらしますが、テストを適切に管理しないと、速度が低下する可能性があります。不適切に管理されているテストの症状の一部を次に示します。 テストの実行には長い時間がかかります。 テストは信頼性が低く、ランダムに失敗します。 テストはマシンによって動作が異なります。 すべてのテストスイートを実行すると、CIの速度が低下します。 このブログ投稿では、コンテナを使用してテストの信頼性を高める方法