単純なWebアプリケーションの構築なら難しいことはありません。Web開発コミュニティは親切で、Stack Overflowなどのプラットホームでは多くの議論があり、さまざまなWebサイトにはレッスンやチュートリアルがあります。
アプリをローカル環境に構築し、サーバーにデプロイ、友人に公開するだけならほとんどの人ができます。もし、有名になったアプリを実際に作ったことがあれば、この記事は役に立つでしょう。「高トラフィックでもアプリが安定して稼働できることを確認する方法を学びたい」という明確な動機を持っているからです。
Webアプリをブラックボックスだと考えると、リクエストを待ち、処理し、リソース(HTML、JSON、XMLなど)をレスポンスとして返すだけのアプリはシンプルです。「単純なら、アプリのスケーリングも簡単にできる」と思うかもしれませんが、残念ながら世の中はいつもバラ色ではないのです。トラフィックが増えるにつれパフォーマンスの問題が生じます。
スキルやアプリは何度も学習し向上するものですが、そのプロセスを短縮することが本記事の目的です。Siegeでアプリのテストの基本的な考え方(リグレッションテスト、負荷テスト、ストレステスト)を説明します。また、Webアプリのテストでよく用いるコツやワザも紹介します。
テストの種類
1日あたり100万人のユニークユーザー数を獲得することが目標だとします。このトラフィック量でも安定稼働できることを確かめるにはどうすれば良いでしょうか? どのような方法で通常時のトラフィックでもピーク時のトラフィックでも不具合なく動作することを確認するのでしょうか? アプリがどれくらいのトラフィック、つまり「負荷」に耐える必要があるかは決まっていて、耐えられるか確認するテストを「負荷(load)テスト」と呼びます。限界を超えてアプリが停止するまで負荷を増やすテストは「ストレス(stress)テスト」と呼びます。
デプロイしたコードを変更したときに、パフォーマンスにどのような影響を与えるかを把握していますか? トラフィックの多いWebアプリは、単純なアップデートでもパフォーマンスの大幅な悪化または改善につながる可能性があり、実際にアクセスが集中するまでなにが起きるか、なぜ起きるのかを知る方法はありません。アプリのパフォーマンスが変更の前後で変わらないことを確認するテストを「リグレッション(regression)テスト」と呼びます。
リグレッションテストを実施する大きな理由が、インフラストラクチャーの変更です。プロバイダーAからプロバイダーBに移動、またはApacheからNginxへ切り替えたいとき、アプリの通常時における1分間あたりの平均処理量や普段のトラフィック量(解析ツールを確認すれば一目で分かります)は把握できます。プロバイダーBのサーバーにデプロイしたあと、アプリの反応は同じに(またはより良く)なるはずですが、本当でしょうか? リスクはありませんか? 必要なデータは揃っているので、勘に頼らずデプロイ前に新たな環境をテストすれば、安心して眠れます。
アプリに手当たり次第仮想リクエストを送る前に、テストは楽な仕事ではないということ、Siegeなどのテストツールを使って得られる数値は変化を相対的に分析する際の参考値として利用すべきだと認識してください。Siegeとアプリをローカル環境で5分間実行して、アプリが数百、数千リクエストを数秒間で処理できると結論付けるのは感心できません。
テストを成功させるための手順
- 計画
なにをテストしたいのか、どのような結果が期待されるのか考える。トラフィックはどれくらいか、どのURLをテストするのか、そのときのペイロードはなど、アプリに手当たり次第にリクエストを送らず、あらかじめパラメーターを決めておく - 準備
テスト環境は可能なかぎり外部から隔離し、それぞれのテストランが同じテスト環境で実行されるようにする - 分析と考察
数値を考察し、理に適った結論を出すこと。結論に飛びつかず、結果は常に文脈の中で評価されるべき。最低2回はすべてをチェックする
Siegeについて
SiegeはWebアプリをベンチマーク、テストするための優れたツールです。ユーザーによる同時接続をシミュレーションし、指定したURLにリソースをリクエストします。複数のURLも可能で、テストパラメーターは自由にカスタマイズできます。利用可能なオプションはsiege --helpを実行して確認できます。一部のオプションは後ほど説明します。
テストアプリの準備
Siegeはアプリの安定性、パフォーマンス、コード(またはインフラストラクチャー)変更による機能の改善度を測定できます。たとえば、WordPressのWebサイトで人気の出そうな猫の写真を公開したときに、そのWebサイトが予測されるピーク負荷をさばけるか確認したり、VarnishなどのHTTPキャッシュシステムをセットアップしてその効果を評価したりできます。
この記事では、Symfonyのデモアプリケーションを若干修正してフランクフルトにある1つ目のDigital Oceanノードにデプロイし、ニューヨークにある2つ目のDigital OceanノードにSIEGE 4.0.2をインストールしてテストをします。
アプリとテストサーバーを分離するのはとても重要なことです。片方でもローカルマシンで実行すると、パフォーマンスに影響を与えるほかのプロセス(メールクライアント、メッセージツール、デーモン)が動作している可能性があるため同一の環境は保証できません。Homestead Improvedのような高品質な仮想マシンでさえも、リソースの可用性は100%保証できないのです。ただし、負荷テストの段階で費用をかけたくない場合は、分離された環境を作れる仮想マシンを使う選択もありです。
Symfonyのデモアプリケーションは、初期状態ではかなり単純で軽量です。複雑で重いアプリを扱うため、個別記事ページのサイドバーに最新記事10件とコメントが多い人気の記事10件を表示するモジュールを追加しました。アプリはより複雑になり、データベースに最低3回リクエストを発行するようになりました。狙いはできるだけ現実的な状況を作り出すことです。データベースには62,230件のダミー記事と、1,445,505件のダミーコメントを追加しました。
基本を学ぶ
siege SOME-URLコマンドを実行すると、Siegeはデフォルトパラメーターで指定したURLのテストを開始します。最初のメッセージのあと、画面は送信されたリクエストの情報で一杯になります。
** Preparing 25 concurrent users for battle.
The server is now under siege...
リクエストの送信を中止したいときはCTRL + Cを押します。押した時点でテストが終了し結果が出力されます。
次に進む前に、Webアプリのテストやベンチマークで留意して欲しいことがあります。Symfonyデモアプリのブログページ、つまり/en/blog/に送信されるHTTPリクエスト1回分のライフサイクルを考えてみてください。サーバーはステータス200(OK)と、コンテンツと画像やそのほかのアセット(スタイルシート、JavaScriptファイルなど)への参照を持ったHTMLのbodyを組み合せてHTTPレスポンスを生成します。ブラウザーはこれらの参照を処理し、バックグラウンドでWebページを描画するアセットをリクエストします。HTTPリクエストは合計で何回必要でしょうか?
Siegeにテストを1回実行して結果を分析します。siege -c=1 --reps=1 http://sfdemo.loc/en/blog/を実行してターミナルでアクセスログ(tail -f var/logs/access.log)を開きます。Siegeに「回数は1回 (–reps=1)、ユーザー数は1人(-c=1)で、URL http://sfdemo.loc/en/blog/に対しテストを実行せよ」と命令しています。ログとSiegeの出力にリクエストが表示されます。
siege -c=1 --reps=1 http://sfdemo.loc/en/blog/
** Preparing 1 concurrent users for battle.
The server is now under siege...
HTTP/1.1 200 1.85 secs: 22367 bytes ==> GET /en/blog/
HTTP/1.1 200 0.17 secs: 2317 bytes ==> GET /js/main.js
HTTP/1.1 200 0.34 secs: 49248 bytes ==> GET /js/bootstrap-tagsinput.min.js
HTTP/1.1 200 0.25 secs: 37955 bytes ==> GET /js/bootstrap-datetimepicker.min.js
HTTP/1.1 200 0.26 secs: 21546 bytes ==> GET /js/highlight.pack.js
HTTP/1.1 200 0.26 secs: 37045 bytes ==> GET /js/bootstrap-3.3.7.min.js
HTTP/1.1 200 0.44 secs: 170649 bytes ==> GET /js/moment.min.js
HTTP/1.1 200 0.36 secs: 85577 bytes ==> GET /js/jquery-2.2.4.min.js
HTTP/1.1 200 0.16 secs: 6160 bytes ==> GET /css/main.css
HTTP/1.1 200 0.18 secs: 4583 bytes ==> GET /css/bootstrap-tagsinput.css
HTTP/1.1 200 0.17 secs: 1616 bytes ==> GET /css/highlight-solarized-light.css
HTTP/1.1 200 0.17 secs: 7771 bytes ==> GET /css/bootstrap-datetimepicker.min.css
HTTP/1.1 200 0.18 secs: 750 bytes ==> GET /css/font-lato.css
HTTP/1.1 200 0.26 secs: 29142 bytes ==> GET /css/font-awesome-4.6.3.min.css
HTTP/1.1 200 0.44 secs: 127246 bytes ==> GET /css/bootstrap-flatly-3.3.7.min.css
Transactions: 15 hits
Availability: 100.00 %
Elapsed time: 5.83 secs
Data transferred: 0.58 MB
Response time: 0.37 secs
Transaction rate: 2.57 trans/sec
Throughput: 0.10 MB/sec
Concurrency: 0.94
Successful transactions: 15
Failed transactions: 0
Longest transaction: 1.85
Shortest transaction: 0.16
アクセスログは次の通りです。
107.170.85.171 - - [04/May/2017:05:35:15 +0000] "GET /en/blog/ HTTP/1.1" 200 22701 "-" "Mozilla/5.0 (unknown-x86_64-linux-gnu) Siege/4.0.2"
107.170.85.171 - - [04/May/2017:05:35:17 +0000] "GET /js/main.js HTTP/1.1" 200 2602 "-" "Mozilla/5.0 (unknown-x86_64-linux-gnu) Siege/4.0.2"
107.170.85.171 - - [04/May/2017:05:35:17 +0000] "GET /js/bootstrap-tagsinput.min.js HTTP/1.1" 200 49535 "-" "Mozilla/5.0 (unknown-x86_64-linux-gnu) Siege/4.0.2"
107.170.85.171 - - [04/May/2017:05:35:17 +0000] "GET /js/bootstrap-datetimepicker.min.js HTTP/1.1" 200 38242 "-" "Mozilla/5.0 (unknown-x86_64-linux-gnu) Siege/4.0.2"
107.170.85.171 - - [04/May/2017:05:35:18 +0000] "GET /js/highlight.pack.js HTTP/1.1" 200 21833 "-" "Mozilla/5.0 (unknown-x86_64-linux-gnu) Siege/4.0.2"
107.170.85.171 - - [04/May/2017:05:35:18 +0000] "GET /js/bootstrap-3.3.7.min.js HTTP/1.1" 200 37332 "-" "Mozilla/5.0 (unknown-x86_64-linux-gnu) Siege/4.0.2"
107.170.85.171 - - [04/May/2017:05:35:18 +0000] "GET /js/moment.min.js HTTP/1.1" 200 170938 "-" "Mozilla/5.0 (unknown-x86_64-linux-gnu) Siege/4.0.2"
107.170.85.171 - - [04/May/2017:05:35:19 +0000] "GET /js/jquery-2.2.4.min.js HTTP/1.1" 200 85865 "-" "Mozilla/5.0 (unknown-x86_64-linux-gnu) Siege/4.0.2"
107.170.85.171 - - [04/May/2017:05:35:19 +0000] "GET /css/main.css HTTP/1.1" 200 6432 "-" "Mozilla/5.0 (unknown-x86_64-linux-gnu) Siege/4.0.2"
107.170.85.171 - - [04/May/2017:05:35:19 +0000] "GET /css/bootstrap-tagsinput.css HTTP/1.1" 200 4855 "-" "Mozilla/5.0 (unknown-x86_64-linux-gnu) Siege/4.0.2"
107.170.85.171 - - [04/May/2017:05:35:19 +0000] "GET /css/highlight-solarized-light.css HTTP/1.1" 200 1887 "-" "Mozilla/5.0 (unknown-x86_64-linux-gnu) Siege/4.0.2"
107.170.85.171 - - [04/May/2017:05:35:20 +0000] "GET /css/bootstrap-datetimepicker.min.css HTTP/1.1" 200 8043 "-" "Mozilla/5.0 (unknown-x86_64-linux-gnu) Siege/4.0.2"
107.170.85.171 - - [04/May/2017:05:35:20 +0000] "GET /css/font-lato.css HTTP/1.1" 200 1020 "-" "Mozilla/5.0 (unknown-x86_64-linux-gnu) Siege/4.0.2"
107.170.85.171 - - [04/May/2017:05:35:20 +0000] "GET /css/font-awesome-4.6.3.min.css HTTP/1.1" 200 29415 "-" "Mozilla/5.0 (unknown-x86_64-linux-gnu) Siege/4.0.2"
107.170.85.171 - - [04/May/2017:05:35:20 +0000] "GET /css/bootstrap-flatly-3.3.7.min.css HTTP/1.1" 200 127521 "-" "Mozilla/5.0 (unknown-x86_64-linux-gnu) Siege/4.0.2"
URLを1回だけテストするようSiegeに命令したにもかかわらず、トランザクション(リクエスト)は15回実行されました。トランザクションについて知りたい場合は、このGithubのページを確認してください。
トランザクションとは、サーバーがクライアントと通信するためにソケットを開き、リクエストを処理し、回線を経由してデータを送信、完了後にソケットを閉じることを指します。
Siegeは与えられたURLに対してHTTP GETリクエストを1回送るだけではなく、そのURL内のリソースが参照しているすべてのアセットを取得するためのHTTP GETリクエストを生成します。SiegeはJavaScriptを評価しないので、AJAXリクエストは含まれません。また、ブラウザーが静的ファイル(画像、フォント、JavaScriptファイル)をキャッシュできることにも留意してください。
バージョン4.0以降では、~/.siege/siege.confにあるSiegeの設定ファイルをparser = falseと設定することで動作を変更できます。
補足:デフォルトの動作は使用するSiegeのバージョンによって異なります。また、ツールによってもさまざまです。Siege以外のツールを使う場合は、1回のテストがなにを意味するのか(指定したURLへの1回のリクエストなのか、指定したURLへのリクエストとそのリソースすべてを取得するためのサブリクエストも含むのか)を確認してください。
上のテスト出力から、Siegeがリクエスト(トランザクション)を6秒以内に15回生成し、その結果0.58MBのデータが100%の可用性で(トランザクションに15回中15回成功して)転送されたことが分かります。「トランザクションの成功回数とは、サーバーが400未満のコードを返した回数です。従って、リダイレクトもトランザクションの成功とみなされます。」
応答時間(Response time)はすべてのリクエストを完了させ応答を受け取るまでにかかった平均時間です。トランザクションレート(Transaction rate)とスループット(throughput)は一定時間内にアプリがどれだけ多くのトラフィックを処理できるか、アプリのキャパシティを示す指標です。
ユーザーを15人にしてもう一度テストします。
siege --concurrent=15 --reps=1 sfdemo.loc/en/blog/
Transactions: 225 hits
Availability: 100.00 %
Elapsed time: 6.16 secs
Data transferred: 8.64 MB
Response time: 0.37 secs
Transaction rate: 36.53 trans/sec
Throughput: 1.40 MB/sec
Concurrency: 13.41
Successful transactions: 225
Failed transactions: 0
Longest transaction: 1.74
Shortest transaction: 0.16
テストの負荷を増やし、アプリが全力を発揮できるようにします。アプリは15人のユーザーからのブログページへのシングルリクエストを問題なく処理しています。平均応答時間は0.37秒です。デフォルトでは、Siegeはリクエストを1秒から3秒の間でランダムに遅延させます。--delay=Nパラメーターの設定で、リクエスト間の遅延のばらつきを調整(最大遅延を設定)できます。
並行性
並行性(Concurrency)は、テスト結果の中で一番分かりにくい項目です。ドキュメントには次のように書いてあります。
並行性は同時接続数の平均値で、サーバーのパフォーマンスが低下すると上昇します。
FAQには並行性の計算方法が書かれています。
並行性はトランザクションの合計を経過時間の合計で割ったものです。つまり、100トランザクションを10秒間で完了した場合、並行性は10.00です。
公式Webサイトにも並行性についての分かりやすい説明があります。
並行性を説明するための分かりやすい例があります。2つのノードをクラスタさせたWebサイトに対しSiegeを実行します。並行性は6.97でした。次にノードを1つ取り除いて同じページに対し同じテストを実行します。並行性は18.33に上昇しました。このとき、経過時間は65%増加しました。
別の視点で見てみます。とあるレストランのオーナーがビジネスに手を加える前にパフォーマンスを測定したいと考えています。そこで、オープンオーダー(注文済みで料理が届くのを待っている件数。リクエスト)の平均を測定しました。この例では平均オープンオーダー数は7でしたが、厨房の従業員を半分クビにする(つまりノードを1つ取り除く)と並行性は18に上昇します。テストは同一環境で実施する必要があります。客の人数と注文の頻度は同じです。ウェイターは注文を高い頻度で受け付けられますが(Webサーバーができるように)、処理には時間がかかります。そして、厨房(アプリ)は過負荷な状態で、注文をどうにかさばいています。
Siegeによるパフォーマンステスト
アプリのパフォーマンスを把握するために、さまざまな同時接続ユーザー数でSiegeを5分間実行し、結果を比較します。ブログのホームページはデータベースクエリが1つだけの単純なエンドポイントなので、以下ではより重く複雑な個別記事のページをテストします。
siege --concurrent=5 --time=5M http://sfdemo.loc/en/blog/posts/vero-iusto-fugit-sed-totam.`
テスト中、アプリが稼働するサーバーでtopを実行するとサーバーの状況を確認できます。MySQLが高負荷になっています。
%CPU %MEM TIME+ COMMAND
96.3 53.2 1:23.80 mysqld
アプリが個別記事のページを描画するたびに、少なくない負荷がかかるデータベースクエリを複数実行しているため、予想通りです。
- 記事を関連するコメントとともに取り出す
- 公開日時が新しい順に記事をソートし、上位10件を取り出す
- 記事と大きなコメントテーブルを結合し、COUNTを使ってもっとも人気のある記事10件を取得するSELECTクエリを実行
同時接続ユーザー数が5人の、最初のテストが完了しました。数値はいたって普通です。
siege --concurrent=5 --time=5M http://sfdemo.loc/en/blog/posts/vero-iusto-fugit-sed-totam.
Transactions: 1350 hits
Availability: 100.00 %
Elapsed time: 299.54 secs
Data transferred: 51.92 MB
Response time: 1.09 secs
Transaction rate: 4.51 trans/sec
Throughput: 0.17 MB/sec
Concurrency: 4.91
Successful transactions: 1350
Failed transactions: 0
Longest transaction: 15.55
Shortest transaction: 0.16
Siegeは5分間で1350トランザクションを完了できました。1回のページ読み込みにつきトランザクションが15回発生するので、アプリは5分間に90回のページ読み込みを処理できたことが容易に分かります。または、1分間に18回のページ読み込み、1秒間に0.3回のページ読み込みができたともいえます。トランザクションレートを1ページあたりのトランザクション数で割ることでも同様に、4.51/15=0.3が求められます。
そこまで優秀なスループットではありませんでした。しかし、少なくともボトルネックの場所(データベースクエリ)が分かり、アプリを最適化した際の比較対象が得られました。
より負荷のかかった状態でアプリがどのように動作するか確認するためにテストをあと2回実施します。同時接続ユーザー数を10人に設定したところ、HTTP500エラーが多く発生しました。トラフィックを若干増やしたことでアプリが破綻し始めたのです。同時接続ユーザー数が5人、10人、15人の場合でアプリのパフォーマンスがどう異なるか比較します。
siege --concurrent=10 --time=5M http://sfdemo.loc/en/blog/posts/vero-iusto-fugit-sed-totam.
Lifting the server siege...
Transactions: 450 hits
Availability: 73.89 %
Elapsed time: 299.01 secs
Data transferred: 18.23 MB
Response time: 6.17 secs
Transaction rate: 1.50 trans/sec
Throughput: 0.06 MB/sec
Concurrency: 9.29
Successful transactions: 450
Failed transactions: 159
Longest transaction: 32.68
Shortest transaction: 0.16
siege --concurrent=10 --time=5M http://sfdemo.loc/en/blog/posts/vero-iusto-fugit-sed-totam.
Transactions: 0 hits
Availability: 0.00 %
Elapsed time: 299.36 secs
Data transferred: 2.98 MB
Response time: 0.00 secs
Transaction rate: 0.00 trans/sec
Throughput: 0.01 MB/sec
Concurrency: 14.41
Successful transactions: 0
Failed transactions: 388
Longest transaction: 56.85
Shortest transaction: 0.00
アプリのパフォーマンスが低下するにつれて並行性が増加していることが分かります。同時接続ユーザー数が15人の場合、アプリは完全に機能不全に陥りました。つまり、この要塞を攻略するためのユーザー戦力は15人で十分だということです。しかし、私たちはエンジニアです。この困難を嘆くのではなく解決しましょう。
これらのテストは自動化されていて、テストではアプリに負荷をかけます。実際ユーザーは憑りつかれたように再読み込みボタンを押し続けたりしません。表示されたコンテンツを消化する(読む)ので、リクエストの間には待機時間があります。
キャッシュによる救援
アプリの問題点はデータベースクエリの数が多すぎることだと分かりました。毎回人気記事と最新記事の一覧をデータベースからリクエストして取得する必要はあるのでしょうか。答えは不要です。アプリケーションレベルでキャッシュ層を追加(例:Redis)して人気記事と最新記事の一覧をキャッシュします。設定済みのレスポンスキャッシュを個別記事ページに追加します。キャッシュについてはこの記事を見てください。
デモアプリはSymfonyのHTTPキャッシュがすでに有効になっているので、HTTPレスポンスにTTLヘッダーをセットするだけで使えます。
$response->setTtl(60);
同時接続ユーザー数が5人、10人、15人のテストをもう一度実施して、キャッシュの追加がパフォーマンスにどう影響するか確認します。当然ですが、キャッシュを追加したあとはパフォーマンスの向上が期待できます。また、テストの間に1分間待機してキャッシュが期限切れになるのを待ちます。
注記:キャッシュは慎重に実施してください。特に、Webアプリの保護された領域に関するキャッシュは失敗事例を確認してください。キャッシュは計算機科学でもっとも困難な2つの要素の1つであることを常に忘れないようにしてください。
結果:60秒間のキャッシュを追加することで、アプリの安定性とパフォーマンスが劇的に改善しました。結果を以下の表とグラフに示します。
C=5 | C=10 | C=15 | |
---|---|---|---|
トランザクション回数 | 4566回 | 8323回 | 12064回 |
可用性 | 100.00 % | 100.00 % | 100.00 % |
経過時間 | 299.86秒 | 299.06秒 | 299.35秒 |
Data transferred
データ転送量 |
175.62 MB | 320.42 MB | 463.78 MB |
応答時間 | 0.31秒 | 0.34秒 | 0.35秒 |
トランザクションレート | 15.23トランザクション/秒 | 27.83トランザクション/秒 | 40.30トランザクション/秒 |
スループット | 0.59 MB/秒 | 1.07 MB/秒 | 1.55 MB/秒 |
同時性 | 4.74 | 9.51 | 14.31 |
トランザクション成功回数 | 4566 | 8323 | 12064 |
トランザクション失敗回数 | 0 | 0 | 0 |
最長トランザクション | 4.32 | 5.73 | 4.93 |
実際の使用感
アプリに負荷がかかっているときの実際の使用感は、Siegeを実行しながらブラウザーでアプリを使うことで確認できます。Siegeでアプリに負荷をかけると実際のユーザーエクスペリエンスが体験できます。主観的な評価手法とはいえ、多くの開発者にとって目からうろこの経験になると思います。ぜひ試してみてください。
代替ツール
SiegeのほかにもWebアプリの負荷テストやベンチマークが可能なツールがあります。abでアプリをテストします。
Ab
ab(Apache HTTPサーバー・ベンチマークツール)も優れたツールです。ドキュメントが充実していて、さまざまなオプションが用意されています。Siegeとは違い、URLファイルの使用、構文解析、参照しているアセットのリクエスト、ランダムな遅延はサポートしていません。
個別記事ページ(キャッシュなし)でabを実行すると、次の結果になります。
ab -c 5 -t 300 http://sfdemo.loc/en/blog/posts/vero-iusto-fugit-sed-totam.
This is ApacheBench, Version 2.3 <$Revision: 1706008 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/
Benchmarking sfdemo.loc (be patient)
Finished 132 requests
Server Software: Apache/2.4.18
Server Hostname: sfdemo.loc
Server Port: 80
Document Path: /en/blog/posts/vero-iusto-fugit-sed-totam.
Document Length: 23291 bytes
Concurrency Level: 5
Time taken for tests: 300.553 seconds
Complete requests: 132
Failed requests: 0
Total transferred: 3156000 bytes
HTML transferred: 3116985 bytes
Requests per second: 0.44 [#/sec] (mean)
Time per request: 11384.602 [ms] (mean)
Time per request: 2276.920 [ms] (mean, across all concurrent requests)
Transfer rate: 10.25 [Kbytes/sec] received
Connection Times (ms)
min mean[+/-sd] median max
Connect: 81 85 2.0 85 91
Processing: 9376 11038 1085.1 10627 13217
Waiting: 9290 10953 1084.7 10542 13132
Total: 9463 11123 1085.7 10712 13305
Percentage of the requests served within a certain time (ms)
50% 10712
66% 11465
75% 12150
80% 12203
90% 12791
95% 13166
98% 13302
99% 13303
100% 13305 (longest request)
キャッシュを有効にして実行すると、次のようになります。
ab -c 5 -t 300 http://sfdemo.loc/en/blog/posts/vero-iusto-fugit-sed-totam.
This is ApacheBench, Version 2.3 <$Revision: 1706008 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/
Benchmarking sfdemo.loc (be patient)
Completed 5000 requests
Finished 5373 requests
Server Software: Apache/2.4.18
Server Hostname: sfdemo.loc
Server Port: 80
Document Path: /en/blog/posts/vero-iusto-fugit-sed-totam.
Document Length: 23351 bytes
Concurrency Level: 5
Time taken for tests: 300.024 seconds
Complete requests: 5373
Failed requests: 0
Total transferred: 127278409 bytes
HTML transferred: 125479068 bytes
Requests per second: 17.91 [#/sec] (mean)
Time per request: 279.196 [ms] (mean)
Time per request: 55.839 [ms] (mean, across all concurrent requests)
Transfer rate: 414.28 [Kbytes/sec] received
Connection Times (ms)
min mean[+/-sd] median max
Connect: 81 85 2.1 85 106
Processing: 164 194 434.8 174 13716
Waiting: 83 109 434.8 89 13632
Total: 245 279 434.8 259 13803
Percentage of the requests served within a certain time (ms)
50% 259
66% 262
75% 263
80% 265
90% 268
95% 269
98% 272
99% 278
100% 13803 (longest request)
Abのレポートは時間経過を段階的に表示し、テスト結果を見やすく表示します。リクエストの50%が259ミリ秒(キャッシュなしの場合は10,712ミリ秒)で処理され、99%が278ミリ秒(キャッシュなしの場合は13,305ミリ秒)で処理されると一目で分かります。上記は悪くない結果です。繰り返しになりますが、テスト結果は文脈内で、以前の状態との比較で評価されます。
Siegeを使った高度な負荷テスト
基礎的な負荷テストとリグレッションテストは押さえたので、次のステップに進みます。ここまでは、単一のURLに対してリクエストを生成して、レスポンスをキャッシュすることでトラフィック量が増えても余裕を持って処理できることを確認しました。
現実はより複雑です。ユーザーはサイト内をランダムに移動し、各URLにアクセスしコンテンツを消化します。Siegeの特に優れた点はURLファイルに複数のURLを格納し、テスト中にランダムに使用できるところです。
ステップ1:テストの計画
ユーザーがよく訪れるURLはその頻度に見合ったテストを実施する必要があります。また、動的なユーザーの反応(リンクをクリックするまでの時間など)を検討します。
解析ツールやサーバーのアクセスログから得たデータに基づいてURLファイルを作成します。Webサーバーのアクセスログパーサーなどのアクセスログ解析ツールを使ってApacheのアクセスログを解析すれば、人気度順にソートしたURLのリストを作成できます。トップN(20、50、100…)URLを取得してファイルに格納してほかのURLよりも訪問者が多いURL(ランディングページや人気のある記事など)がある場合は、頻度を調整してSiegeがそれらのURLにより多くリクエストを送ります。
N日間のアクセス数が以下の通りだとします。
- ランディングページ/ホームページ:30,000
- 記事A:10,000
- 記事B:2,000
- 記事C:50,000
- About us:3,000
アクセス数を正規化することで以下のリストを得られます。
- ランディングページ/ホームページ:32%(30,000/95,000)
- 記事A:11%(10,000/95,000)
- 記事B:2%(2,000/95,000)
- 記事C:52%(50,000/95,000)
- About us:3%(3,000/95,000)
32個のホームページ、52個の記事C……と組み合わせて100個のURL(行)があるURLファイルを作ります。出来上がったファイルの行をシャッフルしてランダム性を与えてから保存します。
解析ツールから平均セッション時間と1セッションあたりのページ数を取得し、リクエスト間の平均遅延時間を計算してください。平均セッション時間が2分で、ユーザーが1セッションあたり平均で8ページにアクセスした場合は、単純計算で平均遅延時間は15秒(120秒/8ページ=15秒/ページ)になります。
最後に、構文解析とアセットのリクエストを無効にします。本番環境では、別のサーバーを使って静的ファイルをキャッシュし提供します。前述のように、パーサーは~/.siege/siege.confにあるSiegeの設定ファイルでparser = falseと設定すると停止します。
ステップ2:テストの準備と実行
今回のテストはランダム性があるので、より適切な結果を得るためにテスト期間を長くしたほうが賢明です。Siegeを20分間実行し、遅延は最大15秒、同時接続ユーザー数は50人にします。ブログのホームページと記事10件を、それぞれ異なる発生確率でテストします。
このトラフィック量でアプリのキャッシュに空の状態があるとは思えないので、以下のコマンドを使ってテスト前にすべてのURLに最低1回リクエストを発行し、アプリのキャッシュをウォーミングアップしておきます。
siege -b --file=urls.txt -t 30S -c 1
アプリに大きな負荷をかける準備が整いました。--internetスイッチを使うと、SiegeはファイルからURLをランダムに選択します。スイッチを使わない場合は、SiegeはURLを順番に選択します。
siege --file=urls.txt --internet --delay=15 -c 50 -t 30M
Lifting the server siege...
Transactions: 10931 hits
Availability: 98.63 %
Elapsed time: 1799.88 secs
Data transferred: 351.76 MB
Response time: 0.67 secs
Transaction rate: 6.07 trans/sec
Throughput: 0.20 MB/sec
Concurrency: 4.08
Successful transactions: 10931
Failed transactions: 152
Longest transaction: 17.71
Shortest transaction: 0.24
以下は同時接続ユーザー数が60人の場合です。
siege --file=urls.txt --delay=15 -c 60 -t 30M
Transactions: 12949 hits
Availability: 98.10 %
Elapsed time: 1799.20 secs
Data transferred: 418.04 MB
Response time: 0.69 secs
Transaction rate: 7.20 trans/sec
Throughput: 0.23 MB/sec
Concurrency: 4.99
Successful transactions: 12949
Failed transactions: 251
Longest transaction: 15.75
Shortest transaction: 0.21
キャッシュを有効にした修正版のSymfonyデモアプリはテストを良くさばいています。1秒あたり平均7.2リクエストを処理し、平均応答時間は0.7秒でした(RAMがわずか512MBでシングルコアのDigital Ocean Dropletを使っています)。13,200リクエスト中251リクエストが失敗(データベースとの接続に数回失敗)し、可用性が98.10%になりました。
Siegeによるデータの送信
ここまではHTTP GETリクエストを送信してきました。アプリのパフォーマンスを把握するには十分ですが、APIエンドポイントのテストなど負荷テスト時にデータを送信することが理にかなっていることもあります。Siegeを使えばエンドポイントに簡単にデータを送信できます。
siege --reps=1 -c 1 'http://sfdemo.loc POST foo=bar&baz=bash'
JSONフォーマットのデータも送れます。--content-typeパラメーターで、リクエストのコンテンツ種別を指定できます。
siege --reps=1 -c 1 --content-type="application/json" 'http://sfdemo.loc POST {"foo":"bar","baz":"bash"}'
--user-agent="MY USER AGENT"でデフォルトのユーザーエージェントを変更したり、--header="MY HEADER VALUE"で複数のHTTPヘッダーを指定したりできます。
Siegeはファイルからペイロードデータを読み込めます。
cat payload.json
{
"foo":"bar",
"baz":"bash"
}
siege --reps=1 -c 1 --content-type="application/json" 'http://sfdemo.loc POST < payload.json'
--headerオプションを使えばテスト中にクッキーを送信できます。
siege --reps=1 -c 1 --content-type="application/json" --header="Cookie: my_cookie=abc123" 'http://sfdemo.loc POST < payload.json'
最後に
SiegeはWebアプリの負荷テスト、ストレステスト、リグレッションテストを実施するためのとても強力なツールです。テストを実際の環境にできるだけ近づけるための豊富なオプションが用意されています。個人的にはAbなどのツールよりもSiegeのほうが好みです。Siegeが提供するさまざまなオプションを組み合わせ、さらに複数のSiegeプロセスを並行に実行すれば、アプリを徹底的にテストできます。
テストプロセスを自動化(単純なbashスクリプトで十分)したり結果を可視化したりするのはどんな状況においても効果的です。私は普段、Siegeのプロセスを複数並列に実行し、読み取り専用のエンドポイント(GETリクエストだけを送信)は高レート、データ送信(コメントの投稿やキャッシュの無効化など)は低レートでテストして実際の環境と同じ比率になるようにしています。Siegeを1つだけ使ったテストではペイロードを動的に指定できないので、リクエスト間の遅延を大きくし、異なるパラメーターのSiegeコマンドを何度か実行してください。
また、継続的インテグレーションのパイプラインに単純な負荷テストを追加し、アプリのパフォーマンスが重要なエンドポイントの許容レベルを下回らないことを確認する使い方も考えられます。
本記事はIvan Enderlin、Wern Anchetaが査読を担当しています。最高のコンテンツに仕上げるために尽力してくれたSitePointの査読担当者のみなさんに感謝します。
(原文:Web App Performance Testing with Siege – Plan, Test, Learn)
[翻訳:薮田佳佑/編集:Livit]