AWS移行のため、大規模で複雑な負荷テストをやった話

はじめに

こんにちは、インフラエンジニアの 高山 です。

この記事は 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のテンプレートからスタックを作成すれば、簡単に作成することができます。

導入の説明などは割愛します。 以下を参考にしてください。

テストシナリオ作成

性能的に問題ないことを確認するためには 本番環境と同等の負荷をかける必要があります。 本番環境でのアクセスが多い機能と ログイン/未ログインの割合を調べ、それをテストシナリオにしました。

本番環境の確認

  • ログイン割合

    • 未ログイン状態: 8
    • ログイン状態: 2
      • ログインページへのアクセスは300アクセスに1回程度
  • アクセスの多い機能の割合

    • Web検索: 70
    • Web商品詳細: 90
    • API検索: 35
    • API商品詳細: 35

作成したテストシナリオ

本番環境の確認結果より、1/5の確率でログインするようにしつつ アクセスが多い機能を任意の割合でアクセスするようなテストシナリオを作成しました。

f:id:enigmo7:20211206134416p:plain
作成したテストシナリオ

JMeterの詳細な設定方法などは割愛しますが、ポイントは以下になります。

  • 割合を近似してできるだけ小さい数にした
  • インタリーブコントローラでログインの割合をコントロールするようにした
  • 会員IDリストのcsvファイルを用意してランダムにユーザを変えてログインするようにした
    • (事前に同じパスワードでログインできるように仕込んでおきました)
  • アクセスの割合で特に多い機能のHTTPリクエスサンプラーを割合の数だけ作成
    • さらにそれを5回ループし、ログインを300アクセスに1回程度になるようにした
  • 同じ機能でも、HTTPリクエスサンプラーごとに URLリストファイルを分割して別々のcsvファイルを参照するようにした
    • (同じcsvファイルを使うと、同一スレッドの同じ回で同じURLになってしまったため)

AWS負荷テストソリューションにJMeterの設定ファイルを読み込ませる

f:id:enigmo7:20211206134742p:plain
ファイルアップロード

作成したテストシナリオで外部ファイル(csvファイルやプラグインファイル)を読んでいる場合は、zipにまとめてからアップロードします。

  • ポイント
    • 外部ファイルは相対パスで指定すること
    • テストシナリオの拡張子はjmxとして、複数のjmxファイルは含めないこと
    • ファイルサイズ上限は50MB

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#

分析のための準備

f:id:enigmo7:20211206160828p:plain
Datadog ダッシュボード

AWS負荷テストソリューションのテスト結果レポートを見ても詳細な分析はできないので、詳細な分析をするためには監視ツールで必要なデータを取る必要があります。 今回はDatadogを使い、以下のようなデータを確認できるようにダッシュボードを作成しました。(APMも使っています)

  • 各機能(DBやアプリサーバ、検索サーバ、キャッシュサーバ等)の負荷
  • アプリやLBのbusy/idle worker
  • キャッシュサイズ、キャッシュヒット率、eviction
  • etc

目標値

f:id:enigmo7:20211206153419p:plain
本番環境のある日のスループット/分

負荷テストした結果を分析できても、どの程度の値であればOKと判断できなければ意味がありません。 本番環境のスループットやページごとのレイテンシを調べておき、目標となる値を調査しました。

移行により性能を向上させるというよりは 現状より性能が低下せず移行できることを目標にしているため、 今回は現在の本番環境のスループットやページごとのレイテンシが そのまま目標値となります。

本番環境の全LBサーバのログを合計したところ、ピークタイムのスループットは5万アクセス程度/分でした。 (CDNを利用しているので、オリジンのアクセスのみの計測値です)

負荷テストの流れ

以下のような流れを繰り返し、問題を解決しながら 負荷テストを進めていきました。

  • 同時接続数を少ない数から始め、各サーバの負荷やbusy/idle worker数、スループットを見ながら上げていく
    • workerが枯渇する前に 各機能の負荷やレイテンシが上昇してしまう場合は、その原因を調査して解消
      • (設定のミスや構成的な問題、単純なスペック不足など 低レイヤーから高レイヤーまで様々な問題がでてきました)
  • 問題が解消しworkerが枯渇した状態でも、目標となるスループットに達しない場合は アプリサーバの台数を増設
    • (アプリサーバの台数を増やすと、また別の場所がボトルネックになる場合があるため、徐々に台数を増やします)
  • 目標となるスループットに達しても、各機能の負荷やレイテンシが高くなっていなければOK

AWS負荷テストソリューションの設定値

f:id:enigmo7:20211206161621p:plain
設定項目

Concurrencyは どのように設定すれば良いのか?

  • Task Count : <タスク数(コンテナ数)>
  • Concurrency : <タスク毎の同時接続数(ユーザー数)>

合計の同時接続数(合計ユーザー数)はTask Count x Concurrencyになります。 Task Count=1でConcurrencyを増やせば安上がりなのですが、負荷をかける側にも負荷がかかるので そうはいきません。 Concurrencyの推奨制限は200になっていますが、ECRの負荷を見ながら調節する必要があります。

これはドキュメントユーザー数の決定の項目が詳しかったので、ドキュメントを参照してください。

Ramp Upって必要?

Ramp Upは負荷テスト開始時の暖気運転のためだと思っていて、ずっと0に設定して負荷テストしていたのですが 暖気運転以外でもRamp Upを設定した方が良いケースに遭遇しました。

f:id:enigmo7:20211209094154p:plain
Ramp Upの設定で解消した定期的な負荷上昇

BUYMAではアプリケーションサーバとしてPHPRuby on Railsを使用しています。 PHPで処理している機能は少ないのですが、ログイン処理はPHPを使用しています。

ログイン処理は300アクセスに1回程度ですが、Ramp Upを設定しないと負荷テストのすべてのスレッドが同じタイミングでログイン処理をしようとするため、定期的に負荷が上がるような不可解なグラフになったと考えられます。 (時間経過とともにスレッドごとのタイミングがずれていくため、徐々に解消されていきます)

Ramp Upを設定したところ、定期的な負荷上昇はなくなりました。

どれくらいの時間、負荷テストするべきか?(Hold for)

f:id:enigmo7:20211207135714p:plain
長時間負荷テスト

あたりまえですが、テストをする環境や どんな負荷テストをしたいかにより、どれくらいの時間 負荷をかけるべきか変わってきます。

例えば 徐々にキャッシュがたまり、キャッシュヒット率が上がるにつれてDBへの問い合わせが減っていく様子を確認するため 長時間の負荷テストを実施しました(上記のグラフです)。 キャッシュされた状態でのテストをしたい場合はURLリストを少なくして負荷テストしました。

何をテストしたいかによって 設定を変更したり、負荷テストの時間を調節する必要があります。

AWS負荷テストソリューションの問題

Failed to parse the results.

f:id:enigmo7:20211207203019p:plain
Test Failed

長時間負荷テストを実施したり、何回もテストを作成すると 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を使用しているため、あまり支障はありませんでした。

ダッシュボードからテストシナリオが消えていく

f:id:enigmo7:20211207202649p:plain
test-400,test-700,test-800が消えたダッシュボード

何個もテストケースを作成して負荷テストしていくとなぜか、ダッシュボードからテストシナリオが消えていくことがあります。 テストシナリオの数ではなく、テストの回数か何かに制限があるようです。

消えたテストシナリオでもタブが残っていれば/URLを覚えていれば、設定が残っていて、テスト実行も可能でした。 こちらはそんなに困らなかったので、サポートへ問い合わせはしていません。

サポートに項目がない

f:id:enigmo7:20211209090705p:plain
サポート その1

f:id:enigmo7:20211209090759p:plain
サポート その2

明らかにDynamoDBの問題の場合はサポートに問い合わせできたのですが、AWS負荷テストソリューション自体の問題の場合は サポートのサービスに項目がありませんでした。

AWS負荷テストソリューション自体の問題は SAの方に聞いてみましょう。

AWS負荷テストソリューションのAPI

30分おきにTask Countを変更して負荷をかけていて APIあったらいいなと思っていたのですが、 今回 ドキュメントを見返していたら、APIありました。 見逃してました。

設定を変えてながら連続して負荷テストするような場合はAPIを使いましょう。

最後に

今回は大きなサービスの移行のための負荷テストで、テスト環境では発生しなかった問題が次々と発生するなど いろいろと大変でした。 負荷テストツールにはあまりコストをかけたくなかったので、AWS負荷テストソリューションを使うことで だいぶラクができたと思います。

これから、バッチサーバなどが動いてDBに負荷をかけている状態でも 性能的に問題ないかなどを確認し、自信を持って本番環境の移行に臨めるようにしていこうと思っています。

明日の記事の担当は カスタマーマーケティング事業本部の 杉山 さんです。お楽しみに。


株式会社エニグモ 正社員の求人一覧

hrmos.co