はじめまして。プロダクト開発部アプリケーションエンジニアの中村と申します。
現在、「Reader Store(運営:株式会社ソニー・ミュージックエンタテインメント)」のシステム開発を主な業務として日々取り組んでいます。
今回はReader Storeの開発中に行った負荷試験の話をしていきます。
負荷試験実施の背景
Reader StoreではこれまでAmazon Aurora MySQL 1 (MySQL 5.6 互換)を使用しておりました。
しかし、2023 年 2 月 28 日にサポートが終了するため、Amazon Aurora MySQL 3 (MySQL 8.0 互換)へバージョンアップすることにしました。
バージョンアップ後もこれまでと同等の性能で動作することを確認するために負荷試験を行うことにしました。
Apache JMeterについて
Apache JMeter(以後JMeter)は、Apache Software Foundationが開発した、Webアプリケーションの機能動作及びパフォーマンス計測するためのテストツールです。
テストしたい画面のURLやリクエストパラメータやリクエスト数、スレッド数などを定義することで任意の負荷試験を行うことができます。
私はこれまで負荷試験を行ったことはありませんでしたが、本プロジェクトでは過去にJMeterを使用していたことから知見があったため、今回も使用することにしました。
また、JMeterではクラスター構成による複数台のサーバーを利用したテストを行うことができます。
1台のサーバーから負荷をかけようとするとスペックやネットワークの問題で期待している負荷をかけられない場合があります。
そのような場合に複数台のサーバーから負荷をかけることで期待通りのテストを行うことができます。
Masterサーバー1台及びSlaveサーバーを任意の台数用意し、MasterのconfigファイルにSlaveサーバーのプライベートIPアドレスを定義することでクラスター構成となります。
今回は、以前の負荷試験で利用していたWindowsのサーバーをMasterサーバーとし、SlaveとなるLinuxサーバを6台用意してクラスター構成でのテストを行いました。
なお、一部クラスター構成では実施できないテストがあるため、そういう場合はローカル環境から実行してます。
テストシナリオの作成
作成の流れ
JMeterでテストシナリオを作成するためにまずはテスト対象の画面を決めます。
テストは全ての画面に対しては行わず、影響の大きさを鑑みて以下を基準に選定しました。
- リクエスト数が多い画面
- ユーザーと紐付くレコードの多いテーブルにアクセスがある画面
実際の流れは以下になります。
- DBのユーザに紐付く情報を保存しているテーブルのレコード数確認
- 各テーブルの1ユーザに紐付くレコード数を確認し、レコード数の多いテーブルを参照しているURLを確認
- New Relicからリクエスト数の多いURLを確認
- Google Analyticsから該当のURL(レコード数の多いテーブルを参照している or リクエスト数の多い)の分間あたりのリクエスト数を抽出(通常時、セール時(リクエスト増加時)でそれぞれ)
- Top(/)のURLを基準に、対象のURLへの導線を辿るように画面遷移のフローを決定
- 画面遷移フローと分間リクエスト数をもとにJMeterのシナリオを作成
なお、今回作成したシナリオは調べた分間リクエスト数を1時間実行し続けるようにします。
以下は簡単なシナリオのイメージになります。
これはTop画面を表示するだけのシナリオになります。
Thread Groupの各設定値は以下のようになります。
- Number of Threads:テストで使用するスレッド数を指定
- Ramp-up period:スレッド数が生成されるまでの時間(今回は60固定)を指定
- Loop Count:シナリオの回数を指定でき、無限ループの場合はInfiniteにチェック
- Specify Thread lifetime:DurationやStartup delayを指定する場合にチェック
- Duration:シナリオの実行時間(今回は3600秒固定)を指定
- Startup delay:テストを実行してからこのシナリオが動くまでの間隔を指定
画像の通りに設定した状態でシナリオを実行すると、最初の1分間に10スレッドが生成され、1時間リクエストが行われ続けるようになります。
なお、分間リクエスト数についてはConstant Throughput Timerで制御しますが、こちらについては後述で触れさせていただきます。
シナリオ作成時のポイント(苦労したところ)
今回シナリオを作成した際に気をつけたことや苦労したことを紹介させていただきます。
ログインを一度だけ行いたい
ログインは一度行えば、セッションの有効期限が切れるかログアウトを行うまでは継続されるため、ユーザが何度も行うことはありません。
例えばマイページの画面を表示する負荷試験を行いたい場合、マイページを表示するためにログインは必要になります。
しかし、マイページ表示の前に毎回ログインさせるとログイン処理というテストの目的と沿わない負荷がかかってしまいます。
このような場合にOnce Only Controllerを使用することで実現できました。
Once Only Controller配下に定義したリクエストは、そのシナリオが繰り返し実行された場合でも最初の1度だけ行うようになります。
ログインのリクエストをOnce Only Controller配下で行うことによって、そのシナリオ内でログインを一度だけ行うようにできました。
複数のユーザを使用してテストしたい
テストでログインするユーザを同じではなく複数ユーザで分けて使いたいということがあります。
今回、サービスを多く利用しているユーザ、平均的なユーザ、利用が少ないユーザ(=ユーザに紐づくテーブルのレコード数)を想定したユーザを用意してテストを行おうとしました。
このような場合にUser Defined VariablesとUser Parametersを組み合わせることで実現できました。
まずはUser Defined Variablesには変数名(ここではIDとPASSとする)を定義します。
次にUser Parametersにはユーザごとに使用するIDとPASSの値を定義します。
最後に、ログイン処理のリクエストでパラメータの値に${ID}と指定すると、リクエストで変数を参照できるようになります。
そうすると、Thread Groupでスレッド数を2以上にした時、1スレッド目はUser ParametersのUser_1の値、2スレッド目はUser_2の値を使用するようになります。
今回、各条件のユーザのIDとパスワードをそれぞれ定義することでそれぞれのユーザを使用してテストを行うことができました。
ちなみに、User Parametersに定義したユーザ数よりもThread Groupのスレッド数の方が多い場合は、またUser_1の値から使用します。
1ユーザで1度しか行えないテスト(例えば1日1回のログインガチャを引くなど)の負荷試験を行いたい
DBを更新する処理が行われた場合はその後繰り返しテストを行うことはできない場合もあります。
今回あったケースでは、1日1回のログインガチャを引くというテストがあり、同じユーザでは2回目以降はガチャを引けないという問題がありました。
このような場合にPost Processorを使用することで解決できました。
Post Processorをリクエストの配下に定義することで、そのリクエストが完了した後にPost Processorで定義した処理を実行できます。
今回はDBの操作したいため、JDBC PostProcessorを使用しました。
ガチャを引くというリクエストの後にガチャの結果が保存されているテーブルのデータを削除するSQLを実行することで、何度もテストを行うことができました。
実際の定義としては、対象のリクエスト配下にJDBC Connection Configuration(DBの接続情報を定義)とJDBC PostProcessorを定義して使用します。
少々話は逸れますが、クラスター構成で実行する場合は各サーバーで同じテストを行うため、上記のようなケースのテストはうまくテストできない可能性があります。
そのため、今回はそういったケースのテストだけローカルのPC上で実行するという対応をしております。
リクエスト数の制御はConstant Throughput Timerを使用する
時間あたりのリクエスト数を制御するためにはTimerを使用する必要があります。
今回、シナリオを作成し始めた際はプラグインのThroughput Shaping Timerを使用しておりました。
こちらは標準のTimerよりも細かい制御ができる(RPSで指定できる、経過時間によってRPSの値を変更できる等)ため、高性能なTimerとなっております。
しかし、このTimerはスレッド数を増やすとリクエスト数が設定値より少なくなってしまうという問題を発見しました。
試したところ、スレッド数が4以上になると30%ほど低下していました(スレッド数と比例して低下するというわけではなかったです)。
今回は細かいリクエスト数の制御は必要ないため、標準のConstant Throughput Timerを使用することで期待通りのテストを行うことができました。
Throughput Shaping Timerでも適切なチューニングを行うことで期待したリクエスト数を実現できるかは要検証です。
クラスター構成でリクエスト数を制御するには
前述で話題に上がったConstant Throughput Timerですが、こちらはRPMでリクエスト数を指定できます。
例えば100req/分であれば100を指定します。
ただし、クラスター構成で実行する場合はサーバーの台数×RPMとなってしまうため、そのままだとサーバーの数だけリクエスト数が倍増してしまいます。
そのため、クラスター構成で実行する場合はサーバーの台数で割った数を指定する必要があります。
例えば100req/分をサーバー5台で行う場合はConstant Throughput Timerには20を指定します。
負荷試験不可の外部システムがある状態でテストするには
開発しているサービスの中では社内だけではなく社外のサービスと連携している箇所も出てきます。
負荷試験を行う場合には当然外部サービスに対しても負荷がかかってしまうため許可をいただく必要がありますが、場合によっては負荷試験がNGな外部サービスもあります。
このような場合にスタブを用意することで外部への負荷をかけずにテストを行うことができました。
今回はAWSのAPI Gatewayを利用することで簡単にスタブを用意できました。
参考:https://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/how-to-mock-integration.html
失敗談
最後に、負荷試験をしていてハマったことがあったので軽くお話しさせていただきます。
今回、シナリオが全て作成できていざテストを実行してみたところ、テストの実行が途中で止まってしまう事態に遭遇しました。
必ず止まるわけではなく正常に終了することもあり、止まってしまった場合のログにエラーが出ているわけでもなかったため原因がすぐにはわかりませんでした。
最初はローカルのPCから実行していたためスペックの問題なども疑い、クラスター構成にして実行するなど試しましたが結果は変わりませんでした。
そこでログを詳しくみたところ、Slaveのサーバーのログでテストが停止する際にどのサーバーも同じシナリオで止まっていたことがわかりました。
そのシナリオの設定を確認したところ、Thread Groupの設定でテストのリクエストでエラーが返った場合の挙動の設定が他のシナリオと異なっていました。
他はContinue(テストを継続する)になっていたのに、そのシナリオだけは謝ってStop Test Now(テストを中断する)にしてしまっていました。
上記の設定の場合にはJMeterとしては正常動作となりエラーログは当然でないため、ログをしっかりと確認して停止の原因となる情報を見つけることが重要でした。
この設定ミスを発見するために数日もハマってしまったのは悔やまれますが、JMeterへの理解が深まったこととログを確認することの重要性の再認識が出来たことはせめてもの救いでした。
まとめ
初めての負荷試験となりましたが、シナリオを作成する上でのセオリーや注意点など色々なことが学べて良い経験となりました。
ただ、JMeterは画面のボタンを押すなどの操作をしているわけではなく、あくまでその操作をした時の擬似的なリクエストを自分で作成して送信しているに過ぎません。
例えばフェデレーションログインのように外部のサービスの画面を操作したいといった場合にはどうすれば良いかなど、まだわかっていないこともあります。
上記のようなテストケースの手法についてや、JMeter以外の負荷試験ツールなどの理解も今後深めていきたいです。
この記事の内容が、これからJMeterを使用する方の何かしらの助けになれば幸いです。