はじめに
こんにちは、インフラエンジニアの 高山 です。
この記事は Enigmo Advent Calendar 2021 の 9 日目の記事です。
現在、BUYMAをオンプレからAWSへ移行するプロジェクトを進めています。 テスト環境の移行は完了し、本番環境の移行をしようというところです。
本番環境の移行をする前に 性能的に問題ないことを確認するため、本番環境と同程度のスペックで検証環境を構築し負荷テストを実施しました。 まだ終わっていませんが、今の時点で得た知見を記事にしようと思います。
負荷テストツール選定
詳細は割愛しますが、
以下のような要件からAWSの分散負荷テストのソリューション(正式名称はDistributed Load Testing on AWS
以下、AWS負荷テストソリューションと呼ぶ)を使うこととしました。
- 大規模な負荷テストができること
- 複雑なテストシナリオが作成できること
- 情報が多いこと
- 学習コストや構築運用コストが低いこと
- 費用が安価であること
- テストシナリオをコードで管理できること
AWS負荷テストソリューションは それ自体の情報は多くないものの JMeterの設定ファイルを読み込むことができるためテストシナリオ作成の情報は多く、テストシナリオをコードで管理できること
以外は要件を満たしています。
(ruby-jmeterを使えばコード管理できそうですが、手は出しませんでした。)
AWS負荷テストソリューションの概要
AWS負荷テストソリューションは AWSのマネージドサービスを組み合わせたAWSのソリューションの1つで、AWSが提供しているCloudFormationのテンプレートからスタックを作成すれば、簡単に作成することができます。
導入の説明などは割愛します。 以下を参考にしてください。
- 参考
- AWS ソリューション
- AWS ソリューションのライブラリ
- AWS 分散負荷テスト ソリューション
- ここにCloudFormationのテンプレートが置いてあります
- 大規模な負荷テストを実行可能。「Distributed Load Testing on AWS」 を試してみる
- AWS公式のやってみた記事
- 日本語のドキュメント
- 日本語のドキュメント 例によって、ちょっと古い
- 英語のドキュメント
- 英語のドキュメント こちらは最新
- AWS ソリューション
テストシナリオ作成
性能的に問題ないことを確認するためには 本番環境と同等の負荷をかける必要があります。 本番環境でのアクセスが多い機能と ログイン/未ログインの割合を調べ、それをテストシナリオにしました。
本番環境の確認
ログイン割合
- 未ログイン状態: 8
- ログイン状態: 2
- ログインページへのアクセスは300アクセスに1回程度
アクセスの多い機能の割合
作成したテストシナリオ
本番環境の確認結果より、1/5の確率でログインするようにしつつ アクセスが多い機能を任意の割合でアクセスするようなテストシナリオを作成しました。
JMeterの詳細な設定方法などは割愛しますが、ポイントは以下になります。
- 割合を近似してできるだけ小さい数にした
- インタリーブコントローラでログインの割合をコントロールするようにした
- 会員IDリストのcsvファイルを用意してランダムにユーザを変えてログインするようにした
- (事前に同じパスワードでログインできるように仕込んでおきました)
- アクセスの割合で特に多い機能のHTTPリクエストサンプラーを割合の数だけ作成
- さらにそれを5回ループし、ログインを300アクセスに1回程度になるようにした
- 同じ機能でも、HTTPリクエストサンプラーごとに URLリストファイルを分割して別々のcsvファイルを参照するようにした
- (同じcsvファイルを使うと、同一スレッドの同じ回で同じURLになってしまったため)
AWS負荷テストソリューションにJMeterの設定ファイルを読み込ませる
作成したテストシナリオで外部ファイル(csvファイルやプラグインファイル)を読んでいる場合は、zipにまとめてからアップロードします。
AWS負荷テストソリューションのイメージとスクリプト
使用されているコンテナのイメージと実行されるスクリプトを確認してみましょう。
Dockerfileを見ると、コンテナイメージはtaurusを元にしています。 ENTRYPOINTに指定されているスクリプトの中でアップロードしたファイルを読み込んでいる箇所を見てみます。
# download JMeter jmx file if [ "$TEST_TYPE" != "simple" ]; then # Copy *.jar to JMeter library path. See the Taurus JMeter path: https://gettaurus.org/docs/JMeter/ JMETER_LIB_PATH=`find ~/.bzt/jmeter-taurus -type d -name "lib"` echo "cp $PWD/*.jar $JMETER_LIB_PATH" cp $PWD/*.jar $JMETER_LIB_PATH if [ "$FILE_TYPE" != "zip" ]; then aws s3 cp s3://$S3_BUCKET/public/test-scenarios/$TEST_TYPE/$TEST_ID.jmx ./ else aws s3 cp s3://$S3_BUCKET/public/test-scenarios/$TEST_TYPE/$TEST_ID.zip ./ unzip $TEST_ID.zip # only looks for the first jmx file. JMETER_SCRIPT=`find . -name "*.jmx" | head -n 1` if [ -z "$JMETER_SCRIPT" ]; then echo "There is no JMeter script in the zip file." exit 1 fi sed -i -e "s|$TEST_ID.jmx|$JMETER_SCRIPT|g" test.json fi fi
スクリプトから以下のことが わかりました。
- jmxファイルをfindで探しているようなので、zip内のjmxファイルのパスは気にしなくても良さそうですが、含めるjmxファイルは1つだけにする必要がある
- プラグイン用のjarファイルをJMETER_LIB_PATH配下へコピーしていますが、zipを解凍する前に コピーしているので プラグインは追加できない模様
- (今回のテストシナリオで使用した追加プラグインは
Random CSV Data Set Config
のみなのですが、zipに含めなくても使えました 謎です)
- (今回のテストシナリオで使用した追加プラグインは
$ docker run -it --rm --entrypoint "bash" public.ecr.aws/aws-solutions/distributed-load-testing-on-aws-load-tester:v2.0.0 root@1328dc7cdfdd:/bzt-configs# ls -l total 1296 -rwxr-xr-x 1 root root 1210 Sep 30 04:16 ecscontroller.py -rwxr-xr-x 1 root root 1360 Sep 30 04:16 ecslistener.py -rw-r--r-- 1 root root 16542 Sep 30 04:19 jetty-alpn-client-9.4.34.v20201102.jar -rw-r--r-- 1 root root 19600 Sep 30 04:19 jetty-alpn-openjdk8-client-9.4.34.v20201102.jar -rw-r--r-- 1 root root 320564 Sep 30 04:19 jetty-client-9.4.34.v20201102.jar -rw-r--r-- 1 root root 214251 Sep 30 04:19 jetty-http-9.4.34.v20201102.jar -rw-r--r-- 1 root root 164646 Sep 30 04:19 jetty-io-9.4.34.v20201102.jar -rw-r--r-- 1 root root 565135 Sep 30 04:19 jetty-util-9.4.34.v20201102.jar -rwxr-xr-x 1 root root 2998 Sep 30 04:16 load-test.sh root@1328dc7cdfdd:/bzt-configs# find ~/.bzt/jmeter-taurus -type d -name "lib" /root/.bzt/jmeter-taurus/5.2.1/lib root@1328dc7cdfdd:/bzt-configs# find /root/.bzt/jmeter-taurus/5.2.1/lib -type f -name "*jar" -ls | wc -l 109 root@1328dc7cdfdd:/bzt-configs# find /root/.bzt/jmeter-taurus/5.2.1/lib -type f -name "*jar" -ls | egrep -i "csv" root@1328dc7cdfdd:/bzt-configs#
- 参考
分析のための準備
AWS負荷テストソリューションのテスト結果レポートを見ても詳細な分析はできないので、詳細な分析をするためには監視ツールで必要なデータを取る必要があります。 今回はDatadogを使い、以下のようなデータを確認できるようにダッシュボードを作成しました。(APMも使っています)
- 各機能(DBやアプリサーバ、検索サーバ、キャッシュサーバ等)の負荷
- アプリやLBのbusy/idle worker
- キャッシュサイズ、キャッシュヒット率、eviction
- etc
目標値
負荷テストした結果を分析できても、どの程度の値であればOKと判断できなければ意味がありません。 本番環境のスループットやページごとのレイテンシを調べておき、目標となる値を調査しました。
移行により性能を向上させるというよりは 現状より性能が低下せず移行できることを目標にしているため、 今回は現在の本番環境のスループットやページごとのレイテンシが そのまま目標値となります。
本番環境の全LBサーバのログを合計したところ、ピークタイムのスループットは5万アクセス程度/分でした。 (CDNを利用しているので、オリジンのアクセスのみの計測値です)
負荷テストの流れ
以下のような流れを繰り返し、問題を解決しながら 負荷テストを進めていきました。
- 同時接続数を少ない数から始め、各サーバの負荷やbusy/idle worker数、スループットを見ながら上げていく
- workerが枯渇する前に 各機能の負荷やレイテンシが上昇してしまう場合は、その原因を調査して解消
- (設定のミスや構成的な問題、単純なスペック不足など 低レイヤーから高レイヤーまで様々な問題がでてきました)
- workerが枯渇する前に 各機能の負荷やレイテンシが上昇してしまう場合は、その原因を調査して解消
- 問題が解消しworkerが枯渇した状態でも、目標となるスループットに達しない場合は アプリサーバの台数を増設
- (アプリサーバの台数を増やすと、また別の場所がボトルネックになる場合があるため、徐々に台数を増やします)
- 目標となるスループットに達しても、各機能の負荷やレイテンシが高くなっていなければOK
AWS負荷テストソリューションの設定値
Concurrencyは どのように設定すれば良いのか?
Task Count
: <タスク数(コンテナ数)>Concurrency
: <タスク毎の同時接続数(ユーザー数)>
合計の同時接続数(合計ユーザー数)はTask Count
x Concurrency
になります。
Task Count=1でConcurrency
を増やせば安上がりなのですが、負荷をかける側にも負荷がかかるので そうはいきません。
Concurrency
の推奨制限は200になっていますが、ECRの負荷を見ながら調節する必要があります。
これはドキュメントのユーザー数の決定
の項目が詳しかったので、ドキュメントを参照してください。
Ramp Upって必要?
Ramp Up
は負荷テスト開始時の暖気運転のためだと思っていて、ずっと0に設定して負荷テストしていたのですが
暖気運転以外でもRamp Up
を設定した方が良いケースに遭遇しました。
BUYMAではアプリケーションサーバとしてPHPとRuby on Railsを使用しています。 PHPで処理している機能は少ないのですが、ログイン処理はPHPを使用しています。
ログイン処理は300アクセスに1回程度ですが、Ramp Up
を設定しないと負荷テストのすべてのスレッドが同じタイミングでログイン処理をしようとするため、定期的に負荷が上がるような不可解なグラフになったと考えられます。
(時間経過とともにスレッドごとのタイミングがずれていくため、徐々に解消されていきます)
Ramp Up
を設定したところ、定期的な負荷上昇はなくなりました。
どれくらいの時間、負荷テストするべきか?(Hold for)
あたりまえですが、テストをする環境や どんな負荷テストをしたいかにより、どれくらいの時間 負荷をかけるべきか変わってきます。
例えば 徐々にキャッシュがたまり、キャッシュヒット率が上がるにつれてDBへの問い合わせが減っていく様子を確認するため 長時間の負荷テストを実施しました(上記のグラフです)。 キャッシュされた状態でのテストをしたい場合はURLリストを少なくして負荷テストしました。
何をテストしたいかによって 設定を変更したり、負荷テストの時間を調節する必要があります。
AWS負荷テストソリューションの問題
Failed to parse the results.
長時間負荷テストを実施したり、何回もテストを作成すると AWS負荷テストソリューションのテスト結果レポートが表示されず、Failed to parse the results.
になることがあります。
その場合は 負荷テストはできいて、レポート作成処理に失敗しているだけのようです。
ERROR finalResults function error ValidationException: Item size to update has exceeded the maximum allowed size
CloudWatch Logsで確認したところ 原因はDynamoDBの制限超過エラーのようなのですが、サポートへ問い合わせたところ 仕様だそうです。
分析は主にDatadogを使用しているため、あまり支障はありませんでした。
ダッシュボードからテストシナリオが消えていく
何個もテストケースを作成して負荷テストしていくとなぜか、ダッシュボードからテストシナリオが消えていくことがあります。 テストシナリオの数ではなく、テストの回数か何かに制限があるようです。
消えたテストシナリオでもタブが残っていれば/URLを覚えていれば、設定が残っていて、テスト実行も可能でした。 こちらはそんなに困らなかったので、サポートへ問い合わせはしていません。
サポートに項目がない
明らかにDynamoDBの問題の場合はサポートに問い合わせできたのですが、AWS負荷テストソリューション自体の問題の場合は サポートのサービスに項目がありませんでした。
AWS負荷テストソリューション自体の問題は SAの方に聞いてみましょう。
AWS負荷テストソリューションのAPI
30分おきにTask Count
を変更して負荷をかけていて APIあったらいいなと思っていたのですが、
今回 ドキュメントを見返していたら、APIありました。 見逃してました。
設定を変えてながら連続して負荷テストするような場合はAPIを使いましょう。
最後に
今回は大きなサービスの移行のための負荷テストで、テスト環境では発生しなかった問題が次々と発生するなど いろいろと大変でした。 負荷テストツールにはあまりコストをかけたくなかったので、AWS負荷テストソリューションを使うことで だいぶラクができたと思います。
これから、バッチサーバなどが動いてDBに負荷をかけている状態でも 性能的に問題ないかなどを確認し、自信を持って本番環境の移行に臨めるようにしていこうと思っています。
明日の記事の担当は カスタマーマーケティング事業本部の 杉山 さんです。お楽しみに。
株式会社エニグモ 正社員の求人一覧