必見の記事

SpotInstanceとJMeterを使って400万req/minの負荷試験を行う

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

Apache JMeterのMaster/Slave構成

シナリオを用いた負荷試験といえばJMeterということで、使ったことがある方も多いかと思います。しかし、ほとんどの方は自分のPCを使ってやっている程度ではないでしょうか。最近は、スマホ連動のシステムが多くなってきていますので、1台のPCから負荷を掛けたとしても大した負荷試験になりません。そこで、今回はJMeterをMaster/Slaveのクラスター構成にしてドカーンと同時アクセスを行いたいと思います。

jmeter-000

クラスメソッドの負荷試験の歴史

創業時から業務系のシステム開発が多かったことから、レスポンスは3秒以内でOKとか、ピーク時の同時ユーザは100名といった、緩い条件をクリアすれば良かったことが懐かしく思います。今は、ユーザ数・データ量・トランザクション数・トラフィック等が爆発的に増える可能性のあるプロジェクトも多く、負荷試験は必須項目になりつつあります。また、UXを語る上でアプリのレスポンスタイムは非常に重要であり、どんなに機能が豊富でもサクサク感が無ければユーザが離れてしまいます。通販サイトにおいてはレスポンスタイムが売上げに直結するほどです。以下は、実際のプロジェクトで求められている当社のパフォーマンスの歴史をざっくり表したものです。最近のパフォーマンス要件は未知の領域に来ています。

  • 2010年   : サーバ設計って何?
  • 2011年   : 100万req/年のサーバ設計
  • 2012年   : 100万req/月のサーバ設計
  • 2013年 3月:100万req/日のサーバ設計
  • 2013年 4月:100万req/時のサーバ設計
  • 2013年 5月:100万req/分のサーバ設計(今ココ)
  • 2013年 6月:100万req/秒のサーバ設計(予想)

AWSを用いて負荷試験を簡単に行う

私はAWS中毒のため、AWS環境を用いて負荷試験を行います。まずは、負荷を掛けられる受け側のサーバのセットアップをしたいと思います。今回は直接サーバには向けずにロードバランサーを介して負荷を掛けることで、後にCloudWatchによる統計情報を見て動作を確認したいと思います。

jmeter-001

受け側のサーバ構築

まずは受け側のサーバを構築します。お安くテストするためにSpotInstanceを指定して作成します。SpotInstanceは指値で空いているサーバを安く利用する仕組みです。いつ落ちるか分かりませんが、お試しにはぴったりです。インスタンスが消えるのが怖い方はAMIを作成しておけば後でまた立ち上げ直すことができます。

jmeter-002

続けてWebサーバを立ててみます。今回は簡単な動作確認だけなので、index.phpを見るとPHP情報が表示されるようにします。

> ssh -i key_name.pem ec2-user@セットアップするサーバ名
$ sudo yum update -y
$ sudo yum install httpd-devel php-devel
$ sudo chkconfig httpd on
$ sudo service httpd start
$ sudo vim /var/www/html/index.php
<?php
  phpinfo();
?>

画面にはこんな感じで表示されればOKです。

jmeter-003

続いて、セットアップ済みのサーバを起動したり、オートスケールさせたりできるようにAMIの作成をしておきます。

jmeter-004

ロードバランサーの作成とサーバの登録

負荷を掛けるための入り口となるロードバランサーの作成を行います。ヘルスチェックのURLはアクセス可能なものにしてください。

jmeter-005

無事に登録が完了するとステータスがIn Serviceになります。

jmeter-006-2

負荷側のサーバを構築する

負荷側のサーバはクラスター構成になりますので、まずはじめに共通のセキュリティグループから作成します。1099番ポートはJMeterの待ち受け用で、30000-60000番ポートはJMeterがサーバ間でRMI通信(Remote Method Invocation:Javaでリモートオブジェクトをコールする仕組み)をするために使う作業用ポートです。アクセス制限のソースの指定に自分自身のセキュリティグループを指定しているのがポイントですね。22番ポートはSSHアクセス用にグローバルなIPアドレスを指定しました。

jmeter-007-2.png

受け側サーバと同じように負荷側のサーバもSpotInstanceで作成します。そして、SSHからアクセスしてJMeterのセットアップを行います。サーバ起動時にJMeterがサーバモードで自動起動するようにしておきます。デフォルトで1099番ポートを指定しています。

$ sudo yum update -y
$ wget http://ftp.riken.jp/net/apache//jmeter/binaries/apache-jmeter-2.9.tgz
$ tar zxvf apache-jmeter-2.9.tgz
$ sudo vim /etc/rc.local
/home/ec2-user/apache-jmeter-2.9/bin/jmeter-server &

JMeterを起動後にプロセス表示が以下のようになっていればOKです。

$ ps -ax
...
19717 pts/0    S      0:00 /bin/sh ./jmeter-server
19719 pts/0    S      0:00 /bin/sh ./jmeter -Dserver_port=1099 -s -j jmeter-server.log
19721 pts/0    Sl     0:00 java -server -XX:+HeapDumpOnOutOfMemoryError -Xms512m -Xmx512m ...
...

セットアップ済みのJMeterサーバは、汎用的に使えるものとしてAMIを作成しておきます。そして、作業用のサーバを11台(Master1台+Slave10台)を起動します。SpotInstanceなら怖くないっw。m3.xlargeいってまえー

jmeter-008

SpotInstance手続き中。。。

jmeter-009

すばらしい光景です

jmeter-010

JMeterのMasterサーバをセットアップする

起動した各サーバは、JMeter SlaveとしてMasterからの接続待ち状態になっています。そこで、1台をMasterサーバとして動作するようにセットアップします。ラベル付けしましたのでSSHログインして設定します。JMeterでMasterとして動作させるためには、jmeter.propertiesにSlaveとして動作するリモートサーバ名を列挙する必要があります。

$ cd /home/ec2-user/apache-jmeter-2.9/bin/
$ vim jmeter.properties

remote_hostsの部分を編集してリモートサーバを列挙します。ここで注意点としては、IPやEIPを指定しないでください。JMeterがRMI通信をするときにエラーで落ちてしまいます。解決策はあるのですが設定を最小限にするために今回は省きました。

remote_hosts=ec2-AAA-AAA-AAA-AAA.compute-1.amazonaws.com:1099,ec2-BBB-BBB-BBB-BBB.compute-1.amazonaws.com:1099,ec2-CCC-CCC-CCC-CCC.compute-1.amazonaws.com:1099,......

設定が完了しましたら保存して閉じます。デスクトップPCでは通常JMeterをGUIモードで起動しますが、サーバ内ではnon-GUIモードで起動させます。また、リモートサーバを利用する指定をすると、Master/Slave構成で起動してくれます。で、起動する前にシナリオを指定するのを忘れていましたので、JMeterのシナリオを作成してアップロードしましょう。

JMeterのシナリオを作成してアップロードする

デスクトップPCでJMeterのシナリオを作成して、JMeterのMasterサーバにアップロードします。

スレッドの作成をします。スレッド数100で無限ループとしました。

jmeter-011

HTTPリクエストの作成

jmeter-012

負荷の受け側であるELBアドレスを指定

jmeter-013-2

結果をテキストで出力する設定

jmeter-014

シナリオが完成しましたので保存します。そして、scpコマンドでJMeterのMasterサーバにアップロードします。

> $ scp -i key_name.pem jmeter-senario1.jmx ec2-user@ec2-xxx-xxx-xxx-xxx.compute-1.amazonaws.com:
jmeter-senario1.jmx                           100% 6283     6.1KB/s   00:00

負荷試験を開始してCloudWatchで確認する

全ての準備が整いましたので負荷試験を開始します。JMeterのMasterサーバにログインして以下のコマンドを打ってください。

$ /home/ec2-user/apache-jmeter-2.9/bin/jmeter -n -t jmeter-senario1.jmx -r
Created the tree successfully using jmeter-senario1.jmx
Configuring remote engine for ec2-xxx-xxx-xxx-xxx.compute-1.amazonaws.com:1099
Using remote object: UnicastRef [liveRef: [endpoint:[10.149.12.35:44724](remote),objID:[43e0835:13e79186db6:-7fff, -7005235020197458357]]]
Configuring remote engine for ec2-yyy-yyy-yyy-yyy.compute-1.amazonaws.com:1099
Using remote object: UnicastRef [liveRef: [endpoint:[10.149.12.190:46719](remote),objID:[27311e4:13e79185206:-7fff, 549179843596194467]]]
...
Starting remote engines
Starting the test @ Mon May 06 09:52:35 UTC 2013 (1367833955389)
Waiting for possible shutdown message on port 4445

Waiting forというところまで出たら起動に成功しています!もし、エラーで落ちてしまった場合には、対象サーバを再起動してみてください。私は以下のようなエラーが何回か出ました。また、jmeterを既に起動している場合には落としてください。

Error in NonGUIDriver java.lang.IllegalArgumentException: The following remote engines could not be configured:[ec2-zzz-zzz-zzz-zzz.compute-1.amazonaws.com:1099]
Error in NonGUIDriver java.lang.IllegalStateException: Engine is busy - please try later

または以下のコマンドが有効かもしれません。

$ /home/ec2-user/apache-jmeter-2.9/bin/shutdown

CloudWatchでCPU負荷とリクエスト数を確認する

実際にどんな負荷が掛かっているかCloudWatchを使って受け側のサーバ状況を確認します。今回は受け側を1台構成でm1.smallインスタンスにしました。結果、CPU負荷が100%でレスポンスの遅延が発生してしまっています。

m1.smallのCPU使用率:100%張り付いています。。。

jmeter-020

m1.smallの遅延:遅延が大きいですね。。。

jmeter-021

m1.smallのリクエスト対応数

jmeter-022

サーバをパワーアップして負荷試験する

m1.smallでは弱すぎましたね。AWSの中の人から怒られてしまいそうです。そこで、m3.xlargeにして試してみましょう。起動したインスタンスはDetailedMonitoringにしておきましょう。起動したサーバはELBに追加登録します。登録完了後、m1.smallのインスタンスはELBから外します。

m3.xlargeのCPU使用率:だいぶ下がりました!

jmeter-024

m3.xlargeの遅延:遅延も下がってサクサク動きます!

jmeter-025

m3.xlargeのリクエスト対応数:対応できるリクエスト数も10倍程度にドーンと伸びています!

jmeter-023

m3.xlargeの本気を見せてもらおう

それでは、負荷を掛ける側のサーバをどんどん増やしてみましょう。インスタンスを立てまくります。

jmeter-026

そして負荷を掛けました!、、、、しかし、なぜか思ったようにリクエスト数が伸びません。

jmeter-027

負荷試験の際のチューニングポイント

負荷側のサーバ台数をいくら増やしても直に結果に繋がりませんでした。そこで、あれこれと試す中で出てきた答えが以下の通りです。

  • 受け側のサーバのCPU使用率を低くするためにサーバ台数を増やす。今回は1台から9台にしました。
  • 受け側からレスポンスするデータサイズをできるだけ小さくする。今回は50KBから1KBにしました。
  • JMeterのスレッドグループを増やす。スレッドを増やすよりもスレッドグループを増やしたほうが良さそうです。

これらの施策の結果、以下のようになりました!

チューニング後のCPU使用率

jmeter-030

チューニング後の遅延

jmeter-031

チューニング後のリクエスト対応数:脅威の400万req/min越え

jmeter-032

受け側のサーバに負荷が掛かっていますので、安定稼働をさせるためには台数をもっと増やす必要がありそうですね。また、負荷側のチューニングをさらに行うことでもっと効率良く大量のリクエストを飛ばすことができる気がしています。今回はとりあえずここまでとします。

まとめ

jmeter-040

今回は、受け側のサーバでPHPを実行する簡単な構成でしたので、データベースアクセスやディスクアクセスの無い、ボトルネックがほとんど無い中でテストを行いました。実際のシステムでは、データの圧縮、キャッシュ、CloudFrontを使ったコンテンツ配信、Expiresヘッダを付けたリクエスト数の削減等々、様々な知恵を絞って負荷に備えます。それにしても、ELBの性能は凄まじいですね。負荷が上がった場合、ロードバランサーはボトルネックになったり、単一障害点になるかなと思ったのですが、安定して動作していました。世の中のほとんどのWebシステムはELB経由で良いのではと思いました。また、今回はSpotInstanceを上限いっぱいまで確保して数時間使いましたが、米国バージニア価格ですと、m3.xlargeのSpotInstanceは、m1.smallのオンデマンド価格の3割引程度でしたので、かなりお安く負荷試験を行うことができました。当初は100万req/minを目指していたのですが、最終的には400万req/minまで上げることができました。これは、66000req/secになります。1時間あたり2億4千万req、1日あたり57億6千万reqです!?。もし、このようなアクセスの下でトランザクションを扱うのであれば、DynamoDBを使ってみてはいかがでしょうか。クラスター化されたサーバ群のセッション管理であればElastiCacheでも良いですね。ということで、ますますAWSにどっぷりな感じになりそうです。

参考資料

JMeter Distributed Testing Step-by-step

Client/Server Jmeter