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」の使い方](http://hamamuratakuo.blog61.fc2.com/blog-entry-466.html)
等、いろいろググりながら適当に導入
可視化のために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の負荷試験を行う](http://dev.classmethod.jp/cloud/apache-jmeter-master-slave-100mil-req-min/)の記事が熱いです。参考にさせて頂きました。
* 負荷試験をやらなかったら、Webサーバ一台あたりで本来の能力の数分の1しかこなせなかったということで、冷やっとしました。
* xhprof + graphbizでのプロファイリングはなんか眺めているだけで楽しい。
* 高負荷時には永続的接続は超重要

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