HTTPのベンチマークパターンとテストを動的に書けるab-mrubyを作った

abコマンドのベンチマークパターンを書けるab-mrubyを作ったを昨日書いたわけですが、今日はab-mrubyに対して、ベンチマーク後の結果からテストをRubyで書ける機能を追加しました。

ab-mrubygithubのREADMEに大体書き方は書いていますが、ここでも簡単に紹介したいと思います。

ベンチマークの実行とテスト

基本的にはabコマンドにmrubyを組み込む事で、引数のURL毎に動的にベンチマークパターンを決定したり、その後のベンチマーク結果からテストを書けるようにしています。以下のように実行します。

[program lang=’bash’ escaped=’true’]

./ab-mruby -m ab-mruby.conf.rb -M ab-mruby.test.rb http://192.168.12.251/

[/program]

mオプションにベンチマークパターンファイルを、Mオプションにテストケースファイルを指定します。

ベンチマークパターンの書き方

abコマンドのベンチマークパターンを書けるab-mrubyを作ったでも詳しく書いていますが、以下のように書きます。ab-mrubyの引数に渡されるURLから動的にベンチマークパターンを決定するように書くと良いでしょう。

例えば以下のように書きます。

[program lang=’ruby’ escaped=’true’]

#
# Usage: ./ab-mruby -m ab-mruby.conf.rb -M ab-mruby.test.rb[http[s]://]hostname[:port]/path
#
# add_config(
#     "TotalRequests"         => 100,                       # int
#     "Concurrency"           => 10,                        # int max 20000
#     "KeepAlive"             => true,                      # true or false or nil
#     "VerboseLevel"          => 1,                         # int 1 ~ 5
#     "ShowProgress"          => true,                      # true, false or nil
#     "ShowPercentile"        => true,                      # true, false or nil
#     "ShowConfidence"        => true,                      # true, false or nil
#     "WaitSocketError"       => true,                      # true, false or nil
#     "RequestTimeOut"        => 30,                        # int sec
#     "BechmarkTimelimit"     => 50000,                     # int sec
#     "WindowSize"            => nil,                       # int byte
#     "HeadMethodOnly"        => false,                     # true, false or nil
#     "Postfile"              => nil,                       # './post.txt',
#     "Putfile"               => nil,                       # './put.txt',
#     "ContentType"           => nil,                       # 'application/x-www-form-urlencoded',
#     "OutputGnuplotFile"     => nil,                       # './gnu.txt'
#     "OutputCSVFile"         => nil,                       # './csv.txt'
#     "AddCookie"             => nil,                       # 'Apache=1234'
#     "AddHeader"             => 'User-Agent: ab-mruby',    # 'User-Agent: test' 
#     "BasicAuth"             => nil,                       # 'user:pass'
#     "Proxy"                 => nil,                       # 'proxy[:port]'
#     "ProxyAuth"             => nil,                       # 'user:pass'
#     "OutputHtml"            => false,                     # true, false or nil
#     "BindAddress"           => nil,                       # 'matsumoto-r.jp'
#     "SSLCipher"             => 'DHE-RSA-AES128-SHA',      # 'DHE-RSA-AES256-SHA' or get from [openssl ciphers -v]
#     "SSLProtocol"           => 'SSL3',                    # 'SSL2', 'SSL3', 'TLS1', 'TLS1.1', 'TLS1.2' or 'ALL'
# )

# print ab-mruby headers
print <<EOS
======================================================================
This is ab-mruby using ApacheBench Version 2.3 <$Revision: 1430300 $>
Licensed to MATSUMOTO Ryosuke, https://github.com/matsumoto-r/ab-mruby

                          CONFIG PHASE

======================================================================
EOS

# C側からこんなデータが得られるのでこれを元に動的にパターンを記述
p get_config("TargetURL").to_s
p get_config("TargetPort").to_s
p get_config("TargetHost").to_s
p get_config("TargetPath").to_s
p get_config("TargetisSSL").to_s

# defined config pattern
if get_config("TargetHost").to_s == "blog.example.jp"

  add_config(
    "TotalRequests"         => 10,                        # int
    "Concurrency"           => 1,                         # int max 20000
    "KeepAlive"             => true,                      # true or false or nil
    "VerboseLevel"          => 1,                         # int 1 ~ 5
    "ShowProgress"          => true,                      # true, false or nil
    "ShowPercentile"        => true,                      # true, false or nil
    "ShowConfidence"        => true,                      # true, false or nil
    "WaitSocketError"       => true,                      # true, false or nil
    "RequestTimeOut"        => 30,                        # int sec
    "BechmarkTimelimit"     => 50000,                     # int sec
    "WindowSize"            => nil,                       # int byte
    "HeadMethodOnly"        => false,                     # true, false or nil
    "Postfile"              => nil,                       # './post.txt',
    "Putfile"               => nil,                       # './put.txt',
    "ContentType"           => nil,                       # 'application/x-www-form-urlencoded',
    "OutputGnuplotFile"     => nil,                       # './gnu.txt'
    "OutputCSVFile"         => nil,                       # './csv.txt'
    "AddCookie"             => nil,                       # 'Apache=1234'
    "AddHeader"             => 'User-Agent: ab-blog',     # 'User-Agent: test' 
    "BasicAuth"             => nil,                       # 'user:pass'
    "Proxy"                 => nil,                       # 'proxy[:port]'
    "ProxyAuth"             => nil,                       # 'user:pass'
    "OutputHtml"            => false,                     # true, false or nil
    "BindAddress"           => nil,                       # 'matsumoto-r.jp'
    "SSLCipher"             => nil,                       # 'DHE-RSA-AES256-SHA' or get from [openssl ciphers -v]
    "SSLProtocol"           => nil,                       # 'SSL2', 'SSL3', 'TLS1', 'TLS1.1', 'TLS1.2' or 'ALL'
  )

elsif get_config("TargetHost").to_s == "moblog.example.jp"

  add_config(
    "TotalRequests"         => 20,                        # int
    "Concurrency"           => 5,                         # int max 20000
    "KeepAlive"             => false,                     # true or false or nil
    "VerboseLevel"          => 5,                         # int 1 ~ 5
    "ShowProgress"          => true,                      # true, false or nil
    "ShowPercentile"        => true,                      # true, false or nil
    "ShowConfidence"        => true,                      # true, false or nil
    "WaitSocketError"       => true,                      # true, false or nil
    "RequestTimeOut"        => 30,                        # int sec
    "BechmarkTimelimit"     => 50000,                     # int sec
    "WindowSize"            => nil,                       # int byte
    "HeadMethodOnly"        => false,                     # true, false or nil
    "Postfile"              => nil,                       # './post.txt',
    "Putfile"               => nil,                       # './put.txt',
    "ContentType"           => nil,                       # 'application/x-www-form-urlencoded',
    "OutputGnuplotFile"     => nil,                       # './gnu.txt'
    "OutputCSVFile"         => nil,                       # './csv.txt'
    "AddCookie"             => nil,                       # 'Apache=1234'
    "AddHeader"             => 'User-Agent: ab-moblog',   # 'User-Agent: test' 
    "BasicAuth"             => nil,                       # 'user:pass'
    "Proxy"                 => nil,                       # 'proxy[:port]'
    "ProxyAuth"             => nil,                       # 'user:pass'
    "OutputHtml"            => false,                     # true, false or nil
    "BindAddress"           => nil,                       # 'matsumoto-r.jp'
    "SSLCipher"             => nil,                       # 'DHE-RSA-AES256-SHA' or get from [openssl ciphers -v]
    "SSLProtocol"           => nil,                       # 'SSL2', 'SSL3', 'TLS1', 'TLS1.1', 'TLS1.2' or 'ALL'
  )

else

  add_config(
    "TotalRequests"         => 100,                       # int
    "Concurrency"           => 10,                        # int max 20000
    "KeepAlive"             => false,                     # true or false or nil
    "VerboseLevel"          => 1,                         # int 1 ~ 5
  )

end

if get_config("TargetisSSL")

  add_config(
    "SSLCipher"             => 'DHE-RSA-AES128-SHA',      # 'DHE-RSA-AES256-SHA' or get from [openssl ciphers -v]
    "SSLProtocol"           => 'SSL3',                    # 'SSL2', 'SSL3', 'TLS1', 'TLS1.1', 'TLS1.2' or 'ALL'
  )

end

[/program]

上記の詳しい説明は、abコマンドのベンチマークパターンを書けるab-mrubyを作ったを御覧ください。URLから得られる情報を元にベンチマークパターンを定義しているのがわかると思います。

テストスイートの書き方

次に、abコマンドベースのベンチマークの結果からテストを行いたい場合に、Rubyでテストケースを書いておいて、それをab-mrubyに渡す事ができます。それによって、自動的にベンチマーク結果からテストケースを元にテストを行い、即座に結果を出力してくれます。

テストスイートは例えば以下のように書くことができます。

[program lang=’ruby’ escaped=’true’]

#
# Usage: ./ab-mruby -m ab-mruby.conf.rb -M ab-mruby.text.rb [http[s]://]hostname[:port]/path
#
# TEST PARAMETERS
#
# "TargetURL"
# "TargetHost"
# "TargetPort"
# "TargetPath"
# "TargetisSSL"
# "TargetServerSoftware"
# "TargetServerHost"
# "TargetServerPort"
# "TargetServerSSLInfo"         # if use SSL
# "TargetDocumentPath"
# "TargetDocumentLength"
# "TimeTakenforTests"
# "CompleteRequests"
# "FailedRequests"
# "ConnetcErrors"               # if FailedRequests > 0
# "ReceiveErrors"               # if FailedRequests > 0
# "LengthErrors"                # if FailedRequests > 0
# "ExceptionsErrors"            # if FailedRequests > 0
# "WriteErrors"
# "Non2xxResponses"             # if Non2xxResponse > 0
# "KeepAliveRequests"
# "TotalTransferred"
# "TotalBodySent"               # if body send
# "HTMLTransferred"
# "RequestPerSecond"
# "TimePerConcurrentRequest"
# "TimePerRequest"
# "TransferRate"
#

# print ab-mruby headers
print <<EOS
======================================================================
This is ab-mruby using ApacheBench Version 2.3 <$Revision: 1430300 $>
Licensed to MATSUMOTO Ryosuke, https://github.com/matsumoto-r/ab-mruby

                            TEST PHASE

======================================================================
EOS

module Kernel
  def test_suite &blk
    @@r = get_config
    @@t = blk
  end
  def should_be val
    puts "[TEST CASE] #{self} (#{@@r[self]}) should be #{val}: #{@@r[self] == val}"
  end
  def should_be_over val
    puts "[TEST CASE] #{self} (#{@@r[self]}) should be over #{val}: #{@@r[self] > val}"
  end
  def should_be_under val
    puts "[TEST CASE] #{self} (#{@@r[self]}) should be under #{val}: #{@@r[self] < val}"
  end
  def test_run
    @@t.call
  end
end

# define test suite
test_suite do
  "TargetServerHost".should_be               "192.168.12.251"
  "TargetServerPort".should_be               80
  "TargetDocumentPath".should_be             "/"
  "TargetServerSoftware".should_be           "Apache/2.4.4"
  "FailedRequests".should_be                 0
  "KeepAliveRequests".should_be              0
  "WriteErrors".should_be                    0
  "HTMLTransferred".should_be                600
  "TargetDocumentLength".should_be           6
  "TotalTransferred".should_be               27500
  "CompleteRequests".should_be               100
  "TransferRate".should_be_over              460
  "TimeTakenforTests".should_be_under        0.015
  "RequestPerSecond".should_be_over          1000
  "TimePerRequest".should_be_under           0.5
  "TimePerConcurrentRequest".should_be_under 5
  "TotalBodySent".should_be                  0
  "ConnetcErrors".should_be                  0
  "ReceiveErrors".should_be                  0
  "LengthErrors".should_be                   0
  "ExceptionsErrors".should_be               0
  "Non2xxResponses".should_be                0
end

test_run

[/program]では、細かく見て行きましょう。

まず、テストスイートを解析するようなメソッドを下記のように定義しておきます。これはあくまでサンプルなので、Ruby力の高い人はもっと良い感じのメソッドを定義すると良いと思います。基本はget_configメソッドにより、ベンチマーク結果のハッシュをC言語側であるab-mrubyからまとめて取得することができます。

[program lang=’ruby’ escaped=’true’]

module Kernel
  def test_suite &blk
    @@r = get_config
    @@t = blk
  end
  def should_be val
    puts "[TEST CASE] #{self} (#{@@r[self]}) should be #{val}: #{@@r[self] == val}"
  end
  def should_be_over val
    puts "[TEST CASE] #{self} (#{@@r[self]}) should be over #{val}: #{@@r[self] > val}"
  end
  def should_be_under val
    puts "[TEST CASE] #{self} (#{@@r[self]}) should be under #{val}: #{@@r[self] < val}"
  end
  def test_run
    @@t.call
  end
end

[/program]

その上で、好きなテストケースを書きます。僕は以下のように書いてみました。

[program lang=’ruby’ escaped=’true’]

test_suite do
  "TargetServerHost".should_be               "192.168.12.251"
  "TargetServerPort".should_be               80
  "TargetDocumentPath".should_be             "/"
  "TargetServerSoftware".should_be           "Apache/2.4.4"
  "FailedRequests".should_be                 0
  "KeepAliveRequests".should_be              0
  "WriteErrors".should_be                    0
  "HTMLTransferred".should_be                600
  "TargetDocumentLength".should_be           6
  "TotalTransferred".should_be               27500
  "CompleteRequests".should_be               100
  "TransferRate".should_be_over              460
  "TimeTakenforTests".should_be_under        0.015
  "RequestPerSecond".should_be_over          1000
  "TimePerRequest".should_be_under           0.5
  "TimePerConcurrentRequest".should_be_under 5
  "TotalBodySent".should_be                  0
  "ConnetcErrors".should_be                  0
  "ReceiveErrors".should_be                  0
  "LengthErrors".should_be                   0
  "ExceptionsErrors".should_be               0
  "Non2xxResponses".should_be                0
end

test_run

[/program]

このように少しだけRSpecをぱくっ…インスパイアした書き方にして、テストケースを書いてみました。should_beメソッドとshould_be_over、underメソッドは、読んで時の如く、引数と同じか数値的に高いか低いを単純にテストするためのメソッドです。

実際にベンチマークとテストを実施してみる

では、上記のベンチマークパターンとテストケースをab-mrubyに渡して、HTTPのベンチマークを行なってみましょう。

以下が実行結果になります。

[program lang=’ruby’ escaped=’true’]

$ ./ab-mruby -m ab-mruby.conf.rb -M ab-mruby.test.rb http://192.168.12.251/
======================================================================
This is ab-mruby using ApacheBench Version 2.3 <$Revision: 1430300 $>
Licensed to MATSUMOTO Ryosuke, https://github.com/matsumoto-r/ab-mruby

                          CONFIG PHASE

======================================================================
"http://192.168.12.251/"
"80"
"192.168.12.251"
"/"
"false"
This is ApacheBench, Version 2.3-mruby <$Revision: 1430300 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking 192.168.12.251 (be patient).....done

Server Software:        Apache/2.4.4
Server Hostname:        192.168.12.251
Server Port:            80

Document Path:          /
Document Length:        6 bytes

Concurrency Level:      10
Time taken for tests:   0.015 seconds
Complete requests:      100
Failed requests:        0
Write errors:           0
Total transferred:      27500 bytes
HTML transferred:       600 bytes
Requests per second:    6839.48 [#/sec] (mean)
Time per request:       1.462 [ms] (mean)
Time per request:       0.146 [ms] (mean, across all concurrent requests)
Transfer rate:          1836.77 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.1      0       0
Processing:     1    1   0.3      1       2
Waiting:        1    1   0.3      1       2
Total:          1    1   0.3      1       3

Percentage of the requests served within a certain time (ms)
  50%      1
  66%      2
  75%      2
  80%      2
  90%      2
  95%      2
  98%      2
  99%      3
 100%      3 (longest request)
======================================================================
This is ab-mruby using ApacheBench Version 2.3 <$Revision: 1430300 $>
Licensed to MATSUMOTO Ryosuke, https://github.com/matsumoto-r/ab-mruby

                            TEST PHASE

======================================================================
[TEST CASE] TargetServerHost (192.168.12.251) should be 192.168.12.251: true
[TEST CASE] TargetServerPort (80) should be 80: true
[TEST CASE] TargetDocumentPath (/) should be /: true
[TEST CASE] TargetServerSoftware (Apache/2.4.4) should be Apache/2.4.4: true
[TEST CASE] FailedRequests (0) should be 0: true
[TEST CASE] KeepAliveRequests (0) should be 0: true
[TEST CASE] WriteErrors (0) should be 0: true
[TEST CASE] HTMLTransferred (600) should be 600: true
[TEST CASE] TargetDocumentLength (6) should be 6: true
[TEST CASE] TotalTransferred (27500) should be 27500: true
[TEST CASE] CompleteRequests (100) should be 100: true
[TEST CASE] TransferRate (1836.7737329) should be over 460: true
[TEST CASE] TimeTakenforTests (0.014621) should be under 0.015: true
[TEST CASE] RequestPerSecond (6839.4774639) should be over 1000: true
[TEST CASE] TimePerRequest (0.14621) should be under 0.5: true
[TEST CASE] TimePerConcurrentRequest (1.4621) should be under 5: true
[TEST CASE] TotalBodySent (0) should be 0: true
[TEST CASE] ConnetcErrors (0) should be 0: true
[TEST CASE] ReceiveErrors (0) should be 0: true
[TEST CASE] LengthErrors (0) should be 0: true
[TEST CASE] ExceptionsErrors (0) should be 0: true
[TEST CASE] Non2xxResponses (0) should be 0: true

[/program]今回は、上記のベンチマークパターンにおいては、URLがhttp://192.168.12.251/なので、ベンチマークパターンは以下になりますね。

[program lang=’ruby’ escaped=’true’]

else

  add_config(
    "TotalRequests"         => 100,                       # int
    "Concurrency"           => 10,                        # int max 20000
    "KeepAlive"             => false,                     # true or false or nil
    "VerboseLevel"          => 1,                         # int 1 ~ 5
  )

end

[/program]

ちゃんと上記の通り、ベンチマークをしていることがベンチマーク結果から分かります。さらにその結果を用いて、テストを行なっている事がわかります。今回は全てtrueになって、テストが全て通ったことになりますね!

また、今回のテストは一番シンプルなものにしているので、get_configで得られるパラメータやベンチマーク結果の値から、URLやパス情報を元に動的にテストケースを書いておくことも可能なので、テストの書き方にも夢が広がると思います。

最後に

このように、HTTPのベンチマークにおいて、汎用的にパターンとテストを記述できるツールを探していいたのですが、あまりシンプルで良いものがなかったので自分で作ってみました。

また、abコマンドをベースにしていることから、abでベンチマークをするような状況においては、同様にab-mrubyを使えるので、今後HTTPサーバのテストも捗るのではないでしょうか。

とりあえず、既存のabよりは大分使いやすく、ベンチマークパターンやテストの可読性向上、ベンチマークパターンとベンチマークとテストの連携が楽になったので、abを使うような場面ではab-mrubyを使っていこうと思います。