このgistは Cloud Foundry Advent Calendar 2013 の19日目の記事です。またしても大幅に遅れて本当に申し訳ありません。
今日は,「Gorouter と Ruby Router v2 の性能比較」の3回目として,Gatling を使って Gorouterと Ruby Router v2 の負荷テストを行います。
まずは準備から。
(準備1) Gatlingのインストール。面倒なので詳細は省きます。以下のURL等を参考にしました。
- http://blog.udcp.net/2013/10/07/gatling-stress-tool/
- http://clardeur.blogspot.jp/2013/07/getting-started-gatling-alternative-jmeter.html
- https://github.com/excilys/gatling/wiki/Feeders
- https://github.com/excilys/gatling/wiki/Gatling%202
(どうでもいい余談) Gatlingって,gatleみたいな動詞の動名詞形だと思っていましたが,Gatlingという人名に由来するものでした。知らなかった..。
(準備2) massregistrarをさらに修正しました。主な修正点(531b5c0
の箇所)は,
- ダミーURLのドメインをオプションで指定できるようにした
- 生成したダミーURLをファイル出力可能にした
- ダミーDEA数と,1ダミーDEAあたりのダミーインスタンス数の上限を修正した
client.Greet()
を行わないようにした
の4点です。
最初の修正は,Gatlingからの(数千の)ダミーURLへのアクセスが全てrouterのVMに向かうようにするための名前解決の問題への対処を容易にするものです。異なるFQDNへのアクセスを同一のIPアドレスに向かわせるには,ロード・バランサーやラウンドロビンDNSを使うことになるのですが,今回は後者の無料サービスを行っているxip.ioを使いました。そして,これを使いやすくするために,ドメイン名を指定可能にしました。
2番目の修正は,Gatlingのfeedで読み込むアクセス先URLのファイルの作成を容易にするためのものです。
3番目の修正も,Gatlingでの負荷テストを行いやすくするためのもの。
4番目の修正は,前回までの版のバグ修正になります。実はclient.Greet()
を行うと,コード上で指定した時間間隔とは別に,router.registerの発行を全力で行ってしまうことがわかったので,これを行わないようにしました。client.Greet()
の方を修正することも考えたのですが,修正範囲が広がってしまって advent calendar の記事で扱うには面倒なのでやめました。
準備が終わったところで,測定環境のセットアップを行って行きます。
まずはmassregistrarを使ったrouter.registerの発行を開始します。今回想定した環境は,
- DEA数: 100
- 1DEAあたりのインスタンス数: 200
- URL数: 6000
です。本当は,【1DEAあたりのインスタンス数:20】×【DEA数:1000】で総インスタンス数2万としたかったのですが,1000個ものgoroutineを走らせるのが不安だったので,総インスタンス数を変えずにDEA数を減らす方向で調整しました。
実際に実行したコマンドは以下の通り:
bin/massregistrar -domain .192.168.14.111.xip.io -natsAddresses=192.168.14.111:4222 -natsPassword=*** -natsUsername=*** -writeUrl -numDea=100 -insPerDea=200 -numUrl=6000
30秒間隔で2万回の'router.register'が発行され続けるので,natsサーバー(とrouter)はだいたい666件/秒のpublishを処理する計算になります。
次に,routerを起動します。Ruby Router v2 の場合は,Ruby Router v2 とnginxを,Gorouterの場合はGorouterのみを起動することになります。起動方法については前回か前々回で書いた気がするので,省略します。
最後に,Gatlingのシナリオを実行します。今回使用したシナリオはこちら:
package cfrouter
import io.gatling.core.Predef._
import io.gatling.http.Predef._
import io.gatling.jdbc.Predef._
import io.gatling.http.Headers.Names._
import scala.concurrent.duration._
import bootstrap._
import assertions._
class RandomUrlSimulation extends Simulation {
val httpProtocol =
http
.baseURL("")
val urls = csv("urls.csv").random
val scn =
scenario("Test")
.repeat(10) {
feed(urls).exec(
http("test")
.get("${url}")
.check(status.is(200)))
}
setUp(scn.inject(ramp(1000 users) over (10 seconds)))
.protocols(httpProtocol)
}
26行目のusers
の左の数字(ユーザー数)をいろいろ変えていく感じでテストを実施しました。
#当初は19行目のrepeat()
の括弧の中の数字も試行錯誤して変えていましたが,最終的に10に落ち着きました。
上の例だと,10秒間に,1人あたり10のURLへのアクセスを,1000人のユーザーが順次発行するという流れになるので,10秒間にトータル10000アクセス,平均1000件/秒のアクセスということになります。
では,測定結果を見て行きましょう。
性能比較,といっても何を基準にするかは迷ったんですが,Ruby Router v2 で何度か試験していると,ユーザー数の増加に伴い200以外のレスポンス(以下,この比率を「エラー率」と呼ぶ)が増えていくことがわかったので,エラー率が5%を超える直前のユーザー数で比較することにしました。
まず,Ruby Router v2 (以下「RRv2」と略)から。エラー率5%前後のユーザー数は,前:110,後:120でした。
- ユーザー数:110
---- Global Information --------------------------------------------------------
> numberOfRequests 1100 (OK=1059 KO=41 )
> minResponseTime 0 (OK=0 KO=480 )
> maxResponseTime 27620 (OK=800 KO=27620 )
> meanResponseTime 863 (OK=440 KO=11780 )
> stdDeviation 3277 (OK=147 KO=12799 )
> percentiles1 570 (OK=530 KO=27590 )
> percentiles2 26760 (OK=580 KO=27620 )
> meanNumberOfRequestsPerSecond 13 (OK=12 KO=0 )
---- Response Time Distribution ------------------------------------------------
> t < 800 ms 1057 ( 96%)
> 800 ms < t < 1200 ms 2 ( 0%)
> t > 1200 ms 0 ( 0%)
> failed 41 ( 3%)
- ユーザー数:120
---- Global Information --------------------------------------------------------
> numberOfRequests 1200 (OK=1123 KO=77 )
> minResponseTime 10 (OK=10 KO=360 )
> maxResponseTime 26580 (OK=2220 KO=26580 )
> meanResponseTime 1108 (OK=473 KO=10365 )
> stdDeviation 3884 (OK=210 KO=11952 )
> percentiles1 1410 (OK=570 KO=26540 )
> percentiles2 26410 (OK=1410 KO=26570 )
> meanNumberOfRequestsPerSecond 13 (OK=12 KO=1 )
---- Response Time Distribution ------------------------------------------------
> t < 800 ms 1096 ( 91%)
> 800 ms < t < 1200 ms 9 ( 0%)
> t > 1200 ms 18 ( 1%)
> failed 77 ( 6%)
これよりさらにユーザー数が多くなると,たとえば150ではエラー率は20%に,200では26%に達し,500では46%と急激に上昇することがわかりました。なお,ログを見る限り,エラー・レスポンスのレスポンス・コードは(全てまたはほとんど)404でした。
以上より,RRvv2 のテスト結果としては,ユーザー数110が上限ということになりました。
次にGorouterの結果を。こちらはエラー率5%前後のユーザー数は,前:330,後:340でした。
- ユーザー数:330
---- Global Information --------------------------------------------------------
> numberOfRequests 3300 (OK=3141 KO=159 )
> minResponseTime 0 (OK=0 KO=60000 )
> maxResponseTime 63720 (OK=63720 KO=60540 )
> meanResponseTime 6219 (OK=3474 KO=60454 )
> stdDeviation 0 (OK=2305 KO=0 )
> percentiles1 59720 (OK=31530 KO=60500 )
> percentiles2 60490 (OK=56670 KO=60510 )
> meanNumberOfRequestsPerSecond 9 (OK=8 KO=0 )
---- Response Time Distribution ------------------------------------------------
> t < 800 ms 2795 ( 84%)
> 800 ms < t < 1200 ms 2 ( 0%)
> t > 1200 ms 344 ( 10%)
> failed 159 ( 4%)
- ユーザー数:340
---- Global Information --------------------------------------------------------
> numberOfRequests 3400 (OK=3212 KO=188 )
> minResponseTime 0 (OK=0 KO=60000 )
> maxResponseTime 63710 (OK=63710 KO=60530 )
> meanResponseTime 6690 (OK=3545 KO=60432 )
> stdDeviation 0 (OK=2425 KO=0 )
> percentiles1 60180 (OK=31530 KO=60510 )
> percentiles2 60500 (OK=56690 KO=60520 )
> meanNumberOfRequestsPerSecond 9 (OK=8 KO=0 )
---- Response Time Distribution ------------------------------------------------
> t < 800 ms 2832 ( 83%)
> 800 ms < t < 1200 ms 2 ( 0%)
> t > 1200 ms 378 ( 11%)
> failed 188 ( 5%)
Gorouterのユーザー数上限は,330でした。Gorouterの場合,エラー率の上昇が非常に緩やかで,ユーザー数400でも7%程度です。このことがユーザー数上限でRRv2を大きく上回った要因でしょう。
ただ,ユーザー数の上限だけで見るとGorouterの圧勝のなのですが,結果を詳細に見ると,妙なことに気がつきます。meanResponseTimeやmeanNumberOfRequestsPerSecondでは,Ruby Router v2 の数字の方が良いのです。
- RRv2/ユーザー数:110
> meanResponseTime 863 (OK=440 KO=11780 )
> meanNumberOfRequestsPerSecond 13 (OK=12 KO=0 )
- RRv2/ユーザー数:120
> meanResponseTime 1108 (OK=473 KO=10365 )
> meanNumberOfRequestsPerSecond 13 (OK=12 KO=1 )
- Gorouter/ユーザー数:330
> meanResponseTime 6219 (OK=3474 KO=60454 )
> meanNumberOfRequestsPerSecond 9 (OK=8 KO=0 )
- Gorouter/ユーザー数:340
> meanResponseTime 6690 (OK=3545 KO=60432 )
> meanNumberOfRequestsPerSecond 9 (OK=8 KO=0 )
これはおそらく,以下のような理屈なのではないかと考えています。
- Gorouterは,(Gatlingの)タイムアウトぎりぎりまでレスポンスを返そうと努力する
- その結果,レスポンスの成功数は増えるが,応答時間は平均的に長くなる
- 一方RRv2は,(おそらくnginxが)短い時間でレスポンスを返すのをあきらめる
- その結果,応答時間の平均は短くなるが,レスポンスの失敗数が増える
これ,どちらがいいかは微妙なところですね..。
なお,Gorouterのテスト中,エラーレスポンスがで始めると,Gorouterのコンソールに
2013/12/xx xx:xx:xx http: Accept error: accept tcp [::]:80: too many open files; retrying in 1s
のような出力の頻出が観察されたので,(GorouterのREADME.md(今見たら最新版には記述がないので,今回ベースにしている一つ前の版を参照)に従い)fd数の上限を増やして,もう一度テストしてみることにしました。
現状のfd数の上限は1024:
ulimit -a
core file size (blocks, -c) 0
data seg size (kbytes, -d) unlimited
scheduling priority (-e) 0
file size (blocks, -f) unlimited
pending signals (-i) 31486
max locked memory (kbytes, -l) 64
max memory size (kbytes, -m) unlimited
open files (-n) 1024
pipe size (512 bytes, -p) 8
POSIX message queues (bytes, -q) 819200
real-time priority (-r) 0
stack size (kbytes, -s) 8192
cpu time (seconds, -t) unlimited
max user processes (-u) 31486
virtual memory (kbytes, -v) unlimited
file locks (-x) unlimited
これを2048に増やします。
ulimit -n 2048
ulimit -a
core file size (blocks, -c) 0
data seg size (kbytes, -d) unlimited
scheduling priority (-e) 0
file size (blocks, -f) unlimited
pending signals (-i) 31486
max locked memory (kbytes, -l) 64
max memory size (kbytes, -m) unlimited
open files (-n) 2048
pipe size (512 bytes, -p) 8
POSIX message queues (bytes, -q) 819200
real-time priority (-r) 0
stack size (kbytes, -s) 8192
cpu time (seconds, -t) unlimited
max user processes (-u) 31486
virtual memory (kbytes, -v) unlimited
file locks (-x) unlimited
この状態で,再びRRv2から負荷テストを実行していきました。すると今度は,RRv2が全くエラー・レスポンスを返さなくなりました。処理時間はかかるのですが,100%成功レスポンス(200)が返ってくるのです。この調子で5%まで行こうとすると,さすがに時間がかかり過ぎると感じたので,キリのいいところで1000ユーザーの時の性能を比較することにしました。
まずはRRv2の結果から。
---- Global Information --------------------------------------------------------
> numberOfRequests 10000 (OK=10000 KO=0 )
> minResponseTime 0 (OK=0 KO=- )
> maxResponseTime 5660 (OK=5660 KO=- )
> meanResponseTime 407 (OK=407 KO=- )
> stdDeviation 184 (OK=184 KO=- )
> percentiles1 520 (OK=520 KO=- )
> percentiles2 560 (OK=560 KO=- )
> meanNumberOfRequestsPerSecond 24 (OK=24 KO=- )
---- Response Time Distribution ------------------------------------------------
> t < 800 ms 9998 ( 99%)
> 800 ms < t < 1200 ms 0 ( 0%)
> t > 1200 ms 2 ( 0%)
> failed 0 ( 0%)
完走しました。meanResponseTimeは407msでユーザー数110の時(863ms)より向上し(OKレスポンスの値だけ見るとあまり変わらない:ユーザー数110の時440ms),meanNumberOfRequestsPerSecondもユーザー数110の時(13)のほぼ倍近くになりました。
一方,Gorouterの結果はどうなったかというと,
---- Global Information --------------------------------------------------------
> numberOfRequests 10000 (OK=10000 KO=0 )
> minResponseTime 0 (OK=0 KO=- )
> maxResponseTime 55170 (OK=55170 KO=- )
> meanResponseTime 3008 (OK=3008 KO=- )
> stdDeviation 7573 (OK=7573 KO=- )
> percentiles1 27650 (OK=27650 KO=- )
> percentiles2 32360 (OK=32360 KO=- )
> meanNumberOfRequestsPerSecond 16 (OK=16 KO=- )
---- Response Time Distribution ------------------------------------------------
> t < 800 ms 8684 ( 86%)
> 800 ms < t < 1200 ms 5 ( 0%)
> t > 1200 ms 1311 ( 13%)
> failed 0 ( 0%)
同じく完走したのですが,性能指標的には, meanResponseTime:3008ms, meanNumberOfRequestsPerSecond:16 と,RRv2に完敗しました。Gorouterのコンソールには,時々先に述べたtoo many open files
が出ていたので,その影響もありそうです。RRv2とは出力が異なるので安易な推測は危険ですが,GorouterはRRv2(というよりnginx)に比べて,fd数上限の影響を受けやすい傾向にあるようです。
ということで,3回に亘って書いてきた「Gorouter と Ruby Router v2 の性能比較」ですが,結果はまさかのRRv2(というよりおそらくnginx)強し,というものでした。特定の一環境の,しかも1回の測定だけの結果なので,これだけで断定するのは危険ですが,RRv2,というよりnginxのパフォーマンスには,やはり一日の長があるといった感じでしょうか。ただ造り的にはかなりわかりやすくなった(RRv2だとRuby,nginxのconfig,luaの3種類の文法を扱わなければならなかったのが,GorouterではGoだけになった)こともあり,やはり今後はGorouterかと思うのですが,(ulimit系も含めた)パフォーマンス・チューニングはもうちょっと詰めていく必要がありそうです。
では,長々とお付き合いありがとうございました。