本記事は、当社オウンドメディア「Doors」に移転しました。
約5秒後に自動的にリダイレクトします。
こんにちは、DE本部アナリティクスアプリケーション部の中島です。
継続的デリバリーは本番環境へのデプロイ前にアプリケーションの問題を見つけるために重要です。継続的デリバリーでは、テストコードでの単体テストのほか、UIテストや負荷テストも行われることがあります。今回は、この負荷テストを行うにあたって短期間での簡易的な性能試験(スモークテスト)を行いましたので、その際に得られた知見について紹介します。
負荷テストにおけるスモークテストの目的
負荷テストは、一般に同時ユーザのリクエスト数に対する「システムの応答性」や「システムのスケール特性」を確認するために行われます。また、負荷テストはシステムの性能改善を行うためにも重要です。
今回は負荷テストを実施するにあたってスモークテストを行いました。スモークテストの目的は主に以下の2つです。
- 負荷テストを行うにあたって、テストスクリプトに問題がないこと。
- 少ない負荷で適切なレスポンスを返すこと。
上記が確認できない場合、そもそも負荷テストが行うことができません。
アプリケーションの構成とテスト環境
テスト環境およびアプリケーションの概略構成を以下に示します。アプリケーションはk8s上に認証サーバーを含む形で構成されています。
私が本テストを実施することになったときには、上記システムがほぼ完成している状態でテストを行うことになりましたが、システムが複雑であればあるほど問題があった場合にボトルネックの特定が難しくなります。本テスト環境においても、リクエストを送ってからWebシステムにたどり着くまでにApplication Gateway や認証サーバーを経由するため、適切なレスポンスが返ってこなかった場合にどこが原因かを判断しにくいという課題がありました。
そのため、システム完成後のアプリケーション総合試験の中の項目として入りやすい負荷テストですが、可能であればシステム構成が比較的単純な状態から負荷テストの準備や最低限のスモークテストを行っておくことが重要だと考えます。
また、認証サーバーを用いる場合、認証サーバー自体でさまざまな制約をかけられます。認証サーバーも含めたシステム全体の性能を確認する場合には、実際のユースケースに合わせた設定で問題ないですが、アプリケーション側の性能を確認したい場合などは一時的に認証サーバーの設定を変えるなども検討する必要があります。
テストシナリオ
一般に、負荷テストはユーザーのユースケースに合わせて負荷テストのシナリオを作成します。ユースケースにあった負荷テストを実施しないと、負荷テスト上問題がなくても実際に使ってみると想定よりレスポンスが悪いといったことが起こる可能性があるからです。
今回はスモークテストということで厳密なユースケースにあわせたシナリオを設定したわけではありませんでしたが、テストスクリプトの確認を行うためにも負荷テストでシナリオに入る可能性が高いアクセス数・アクセス頻度の高いページや、DBの参照を含むページ、DBの更新を含む処理、CSVアップロードなどの負荷が高い処理をテストシナリオに含めました。
また、シナリオではDBに登録する値をランダムに変化させるといったことも必要です。DBのキャッシュ機能が働いた場合、適切に負荷がかけられない可能性があるためです。
例として、一般的なアプリケーションにおいては、以下のようなシナリオが考えられます。
- アプリケーションへのログイン(目的:画面表示、アプリケーションサーバー、認証サーバーの確認)
- 一覧画面(Top画面)の表示(目的:画面表示、アプリケーションサーバー、DBのSelect クエリの確認)
- 詳細画面の表示(目的:画面表示、アプリケーションサーバー、DBのSelect クエリの確認)
- 更新処理(目的:画面表示、アプリケーションサーバー、DBのUpdate クエリの確認)
- アプリケーションからのログアウト(目的:画面表示、アプリケーションサーバー、認証サーバーの確認)
テストツール
負荷テストの負荷をかけるツールとしてlocust を用いました。locust はpythonのコードで簡単に負荷テストのシナリオを書くことができます。以下では、スクリプト実行時に on_start でログインして、 index と profile のページをランダムに表示し、スクリプトが終了する際にon_stopで ログアウトするという一連のシナリオを記載しております。
# locustfile.py from locust import TaskSet, task class UserBehavior(TaskSet): def on_start(self): self.login() def on_stop(self): self.logout() def login(self): self.client.post("/login", {"username":"ellen_key", "password":"education"}) def logout(self): self.client.post("/logout", {"username":"ellen_key", "password":"education"}) @task def index(self): self.client.get("/") @task def profile(self): self.client.get("/profile")
locust はGUIを用いたテスト実行もできますが、CUIを用いて行うことも可能です。私は実行条件と実行結果を対応させて保存したかったため、configファイルを読み込んでCUIで実行する方法を取りました。以下のようにconfig ファイルを記載することが可能です。
# locust.conf locustfile = locustfile.py # テストシナリオのファイルを指定 headless = true # CUIでの実行 host = http://target-system # アプリケーションのURL users = 2 # 同時接続ユーザー数 spawn-rate = 1 # 1秒当たりの増加ユーザー数 run-time = 300s # 実行時間
CUIで実行する際に、実行引数に--csv を指定することで、example_stats.csv, example_failures.csv, example_history.csv (--csv=example の場合) の3つのファイルが作成されます。
$ locust --config=locust.conf --csv example
example_stats.csv, example_failures.csvのファイルには、テスト実行全体の統計情報と失敗例が含まれます。example_history.csv は、テストの間の10秒単位での統計情報が表示されます。
なお特に接続ユーザー数が多い場合には、負荷をかけた直後の値は正確な値でないため、一定時間計測した後の値を見るようにしたり、中央値の値をとるといったことが必要になります。
モニタリングツール
本アプリケーションはAzure上に構成されたシステムのため、負荷の状況等はAzureMonitorを用いて確認することができます。そのため、はじめはAzureMonitorを用いてメモリ使用量やCPU使用量をみていましたが、AzureMonitorの値は一定時間の平均値をとっており、負荷のピークの値が測定できないことに気が付いたため、以下のパフォーマンス値を一定間隔で取得してファイルに書き出すスクリプトを作成しました。
- 設定した Namespace に所属する Pod の /sys/fs/cgroup/cpu/cpuacct.usage
- 設定した Namespace に所属する Pod の /sys/fs/cgroup/memory/memory.usage_in_bytes
- 設定した Namespace に対する `kubectl get hpa` 結果
- 設定した Namespace に対する `kubectl get pods` 結果
- kubectl get nodes 結果
また、攻撃サーバー側の状況を確認するために、必要に応じて以下のlinux コマンドを使いました。
- dstat: 攻撃サーバーのcpu, memory 情報
- iftop: 攻撃サーバーのnetwork 情報
なお、攻撃サーバーの状況も確認する理由としては、アプリケーション側より先に攻撃サーバー側のメモリやCPUを使い切ってしまった場合に、十分にアプリケーションに負荷がかけられないことがあるためです。
実施成果
テストの結果、作成したテストスクリプトでシナリオに沿ってリクエストを送れ、正しくレスポンスを返すことが確認できました。
一方で、アプリケーションからのレスポンスタイムは最高でも100μs くらいを想定していましたが、レスポンスタイムの中央値が500μs以上とかなり高い値を示しており、アプリケーションのAPIの速度に課題があることを確認できました。
まとめ
今後のアクションとしては、ボトルネックの特定を行います。今回のケースでは、ある程度すでに複雑な構成となっているため、アプリケーションに問題があるのか、認証サーバーに問題があるのか、DBに問題があるのかなど、様々な原因のパターンが考えられます。そのため、なるべくシステム構成が単純なうちから負荷テストを実施できるようにし、定期的に負荷テストを実施する必要があると感じました。
また、今回の実施により負荷テストを行うための事前確認ができたので、より具体的なユースケースやレスポンスタイムの目標値などを定めた負荷テストを行っていきたいと思っています。
ブレインパッドでは新卒採用・中途採用共にまだまだ仲間を募集しています。
ご興味のある方は、是非採用サイトをご覧ください!
www.brainpad.co.jp