Webアプリケーションの負荷試験の進め方 ケーススタディー


はじめに

負荷試験ってとっても重要ですが、リリーススケジュール優先でどうしても後回しにされたり、省略される事がありませんでしょうか。
特に近年はクラウドでの動作が前提となっているため、リリース後のスケールアップやスケールアウトが容易であるというということも、事前の負荷試験が軽視されてしまう要因となっているかもしれません。

しかしながら、ある案件で負荷試験を行ってやっぱり重要だなということがわかったので自戒を込めて負荷試験実施からパフォーマンス・チューニングの流れを記載します。
各ツールの詳細な紹介などは自分が参考にしたリンクを随時追記したいと思います。
※以下、数字は例であり、適当に丸めてあります。

負荷試験を軽視することにより発生しうるケース

簡単に思いつくこと

  • サービスの継続に必要なサーバリソースが予算またはサービスの収入を上回った。(ワーストケース)
  • サービスの継続に必要なサーバリソースが膨大であるため管理がめんどくさい。(まだまし)
  • もうこれ以上スケールアップ可能なサーバが存在しない。(あるある)
  • パフォーマンスチューニングしたが、リリース後なのでシステムの刷新にコストがかかる。(あるある)
  • サーバ構成に過不足があって無駄なコストを支払っていた。

以上の内容は簡単に思いつきますので、もし、負荷試験を行わないサービスがあった場合は織り込み済みかと思います。
→ スケール可能な構成でさえあればだいたいお金で解決可能な問題が発生すると考えられます。

上記以外に実際に発生しうること

  • 構成する各サーバのスケールアップをしてもパフォーマンス改善にほとんど結びつかなかった。
  • 構成する各サーバのスケールアウトをしてもパフォーマンス改善にほとんど結びつかなかった。

→ ※実は、スケール出来ないアプリケーションを構築してしまっていた!!

つまり、負荷試験は、システムの限界性能の測定のために行うのではなく、システム全体のチューニングを行ない、システムのスケールが可能な構成であることを担保するという面が重要です。

本編:負荷試験ケーススタディー

対象システム構成

AWS上に以下のシステムを構築し、Jmeterサーバから攻撃することとします。

  • ELB
  • EC2 Webサーバ Apache + PHP
  • EC2 Jmeter攻撃サーバ ※jmeter-serverを起動
  • ElastiCache * 2 (Memcached)
  • RDS (MySQL)

この時、リソースの使用状況はCloudWatchを利用しますが、1分毎の詳細を取りたいため、詳細モニタリングを有効にしておきます。(少し追加料金がかかります)

適当にシナリオを組んでまず負荷をかけてみる前に、、、、。

ほぼ静的なファイルを設置して、そちらのスループットおよび各サーバのリソース使用状況を確認してください。
例として、uniqid();のみを返答する、uniqid.phpを設置してそれを叩くこととします。

Jmeterオプション

オプション
スレッド数 100
ループ回数 100
Ramp-Up 10

実行結果例

label samples Average medium 90%line Min Max Error Throuput KB/sec
uniqid 10000 8 6 10 4 1776 0 164 34
合計 10000 8 6 10 4 1776 0 164 34

リソース使用状況例

台数 CPU使用率
Jmeter c3.xlarge 1 1%
Web c3.xlarge 1 10%
RDS m3.large 1 0%
ElastiCache t1.micro 1 0%

この結果をどう読むか?

全てのサーバのリソースに空きがあるのに、スループット(164req/sec)が明らかに悪いため、この状態ではまともな負荷をかけられていない。
原因として考えられる仮説を挙げて一つづつ潰していく作業をします。

アプリケーションに問題がある可能性について

今回は一番単純なパターンの試験をしているのでこの項目はパスします。

インフラに問題がある可能性について

どうせ、Jmeter攻撃サーバの負荷は低いため、ネットワークの問題を排除するためにJmeter攻撃サーバをWebサーバに建てて、ローカルホストへの攻撃にする。
→ 結果ほとんど改善せず。

Apacheの設定に問題がある可能性について

→ 同時接続数の変更などしてみたがほとんど改善せず

試験シナリオに問題がある可能性について

→ 最初のシナリオにて、「結果をツリーで表示」に、エラー時のみのチェックを入れて利用していたのですが、これを無効化することで全体的にスループットが改善した。
※エラー時のみのチェックを入れることで表示されなかったので負荷にはなっていなかったと判断していたがこれを今回のサーバを構築したシンガポールリージョンから、日本のJmeterクライアントまで転送するための負荷が足を引っ張っていた模様。

実行結果例

label samples Average medium 90%line Min Max Error Throuput KB/sec
uniqid 10000 8 6 10 4 1776 0 1200 250
合計 10000 8 6 10 4 1776 0 1200 250

リソース使用状況例

台数 CPU使用率
Web兼Jmeter攻撃 c3.xlarge 1 98%
RDS m3.large 1 0%
ElastiCache t2.small 2 0%

この結果をどう読むか?

WebサーバにCPUボトルネックが来ているため、このシステムの限界値はWebサーバ一台あたり1200req/secを超えることは出来ない。
この数値にどこまで近づけるかで構築したアプリケーションの性能の評価とする。

改めて、本来のシステムに対してJmterのシナリオを流す

実行結果例

label samples Average medium 90%line Min Max Error Throuput KB/sec
page1 10000 584 11 5013 8 16025 0 13 10
page2 10000 431 12 142 9 16071 0 13 24
page3 10000 510 11 170 8 16027 0 13 9
page4 10000 468 11 144 8 11063 0 13 6
page5 10000 556 23 279 17 16042 0 13 6
page6 10000 604 15 5017 12 20047 0 13 7
page7 10000 613 25 5026 19 20060 0 13 5
page8 10000 718 29 5042 21 20100 0 13 10
page9 10000 735 27 5044 20 16058 0 13 7
page10 10000 755 26 5042 18 16079 0 13 9
合計 100000 597 23 5014 8 20100 0 129 94

リソース使用状況例

台数 CPU使用率
Web兼Jmeter攻撃 c3.xlarge 1 20%
RDS m3.large 1 10%
ElastiCache t2.small 2 35%

この結果をどう読むか?

  • どのリソースのCPUもボトルネックになっていないのに全体的なスループットが低すぎる。
  • 特徴的なのは、midiumの応答時間は速いのに、90%lineとMaxが非常に悪い。(memcached接続またはDB接続等の外部リソースの接続待ちの可能性が高い。)
  • ここには記載していないが、CPU負荷以外も特にボトルネックが見当たらなかった。
  • 最適なアプリケーションであれば、Jmeterで負荷をかけ続けているのでどこかにリソースの逼迫が見えるはず。

このままだと、WebサーバやRDSのスケールアップ、スケールアウトなどが一切無意味なシステムとなっている可能性がある。
→ 試験のため、インスタンスタイプを変えて試験したがスループットはほとんど変動せず。

既に、ネットワーク構成の問題およびJmeterのシナリオの問題をクリアしているので、残りは、
アプリケーションに問題があると考えられるので、プロファイリングツールを導入して調査することにする。

xhprofの導入

PHPのプロファイラー「XHProf」の使い方
等、いろいろググりながら適当に導入
可視化のためにgraphvizを導入したがyumでインストール後にsegmentation faultで落ちるようになったので何故かyumで32bit版を入れたら正常に動作するようになど変にハマる。

PDO::connect()とMemcached::connect()がたまにメチャクチャ時間がかかって居ることを突き止める。

MySQL接続の永続化

DB接続に、pconnectを使うように変更、これだけで129req/sec → 170req/sec付近までスループットが向上した。
リソースを見ても、同時接続数がリクエスト数に張り付いた代わりに、新規の接続数が激減。

Memcached接続の永続化

PECL::Memcacheと、PECL::Memcachedの比較で、後者のほうが評判が良かったのでPECL::Memcachedを利用していたのだが、PECL::Memcachedの永続化は、Memcached::connect("接続文字列");の指定で出来るという記事を見つけたが、試したところほとんど変わらない上、私の環境ではsegmentation faultを発生するようになったので断念。
PECL::memcacheをインストールして試してみたところ劇的にスループットが改善された。
170req/sec → 500req/sec付近に

リソースを見ても、同時接続数がリクエスト数に張り付いた代わりに、新規の接続数が激減。

この時のリソースの使用状況例

台数 CPU使用率
Web兼Jmeter攻撃 c3.xlarge 1 80%
RDS m3.large 1 35%
ElastiCache t2.small 2 8%

※スループットは向上したが、新規コネクション数が激減したためにRDSやElastiCacheの負荷は下がっている

この状態だとWebサーバのCPUボトルネックと考えられるので、Webサーバ兼攻撃サーバを増やしてみる。

台数 CPU使用率
Web兼Jmeter攻撃 c3.xlarge 2 80%
RDS m3.large 1 90%
ElastiCache t2.small 2 12%

→ RDSに負荷が移ったが、スループットとしては二倍の1000req/secとなった。
Webサーバの追加でリニアに全体のスループットが改善していることがわかる。
これ以上のスループットを出すには、RDSサーバのスケールアップで対応が可能そうだ。

最後に所感

  • jmeter-server(攻撃サーバを複数立てる方式)を初めて試してみたが、いい感じ。(ここでは書きませんでしたが、jmeter-clientとバージョンを合わせる必要があったり、portを開けたりする必要はあります。)
  • 上記jmeter-serverの利用に関してはクラスメソッドさんのSpotInstanceとJMeterを使って400万req/minの負荷試験を行うの記事が熱いです。参考にさせて頂きました。
  • 負荷試験をやらなかったら、Webサーバ一台あたりで本来の能力の数分の1しかこなせなかったということで、冷やっとしました。
  • xhprof + graphbizでのプロファイリングはなんか眺めているだけで楽しい。
  • 高負荷時には永続的接続は超重要

宣伝

この記事がきっかけで紆余曲折あり、最終的に負荷試験に関するノウハウをまとめた本が出版されるはこびとなりました。
少々お高いですが、特にクラウドにおける設計や、負荷試験、負荷対策を体系的にまとめた入門本となっておりますので興味を持たれた方は是非目を通してみてください。
https://www.amazon.co.jp/dp/4774192627