ScaleBench 公開
ScaleBench 公開
どーもみなさま。こんにちは。 amachang と申します。
さて、ようやく ScaleBench というプロダクトが発表されましたね!
で、僕もこれの開発に携わっていたのでちょっと技術的なことについて書いてみたいと思います。
ScaleBench とは
ScaleBench とは、サイボウズ製品向けの負荷テストツールで Grinder というオープンソースの負荷テストツールをベースにしています。
Grinder とは
Java を使った Web の負荷テストツールです。
Jython でシナリオ(ユーザがどう行動するか)を書いてそれを実行します。
またブラウザの操作を記録して、シナリオを自動で生成することもできたりします。
で、僕がこのプロジェクトで担当していたのが
- Grinder の改良、改造
- シナリオ(バーチャルユーザがどのような順で負荷をかけていくかということがかかれたプログラム)から使うライブラリの作成
等の部分です。
という訳で、この記事では負荷テストツール Grinder について紹介したあと、自分がこのプロジェクトでやったことについてちょこっと紹介していきたいと思います。
Grinder のススメ
というわけで、まずは Grinder の説明をしようと思います。
みなさんは Web の負荷テストとかちゃんとやってますか?
やっていないなら Grinder は、かなりオススメです。
この記事を読んだ後、負荷テストのニーズがあったならぜひ使うことを検討してみてください!
というわけで、 Grinder の使い方を順に説明していきたいと思います。
負荷対象のサーバーを用意
まずは、負荷テストをするのでその対象のウェブサーバーが必要ですね。
というわけで、今回は例として ruby で簡単なウェブサーバーを書いてみます。
以下のような感じで、
require 'webrick'
server = WEBrick::HTTPServer.new({ :Port => 10080 })
class Servlet < WEBrick::HTTPServlet::AbstractServlet
def do_GET(req, res)
res['Content-Type'] = 'text/html; charset=UTF-8'
res.body = <<EOS
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Top Page</title>
</head>
<body>
<h1>Top Page</h1>
<ul>
<li><a href="/top">Top Page</a>
<li><a href="/page1">Page 1</a>
<li><a href="/page2">Page 2</a>
</ul>
</body>
</html>
EOS
end
end
server.mount('/top', Servlet);
server.mount('/page1', Servlet);
server.mount('/page2', Servlet);
['INT', 'TERM'].each {|signal|
Signal.trap(signal){ server.shutdown }
}
server.start
ただ HTML を返すだけの簡単なウェブサーバーです。
これを webserver.rb という名前で保存しますa。
以下のように起動することが出来ます。
ruby webserver.rb
Grinder のインストール
対象サーバーの準備が出来たら、 Grinder をインストールしましょう。
以下の手順で Grinder をインストールします。
- http://sourceforge.net/projects/grinder/files/ から最新バージョンの ZIP ファイル(現在は "grinder-3.4.zip" )をダウンロード
- ダウンロードした ZIP ファイルを解凍する
簡単ですね!
解凍したディレクトリは、以下のようになっていると思います。
grinder-3.4/
|-- contrib
| `-- mq
|-- etc
|-- examples
`-- lib
lib ディレクトリに Java のプログラムの本体(jar ファイル)が入っています。
Grinder の中に入っているもの
インストールが完了したら、さっそく Grinder を使ってみたいですね!
でも、ちょっとだけ待ってください><
Grinder には、以下の 3 つの Java アプリケーションが含まれていまして、先にその説明をしたいと思います。
- Agent
- Console
- Proxy
図にすると以下のような感じ
Agent
シナリオを実行し、 Web サーバーに負荷をかけるためのアプリケーションです。
net.grinder.Grinder クラスから起動できます。
java -cp grinder-3.4/lib/grinder.jar:grinder-3.4/lib/jython.jar \
net.grinder.Grinder
このように起動すると grinder.py というファイルがシナリオとして実行されます。
実行するシナリオを切り替えたい場合は、以下のように grinder.script プロパティを設定します。
java -Dgrinder.script=grinder-3.4/examples/helloworld.py -cp \
grinder-3.4/lib/grinder.jar:grinder-3.4/lib/jython.jar \
net.grinder.Grinder
(上の例のように examples ディレクトリには、便利なサンプルシナリオがたくさん入っているのでとても参考になります。)
また、複数のマシンで Agent を走らせることもできます。
そのことについては、次の Console アプリケーションの説明で解説します。
Console
Console は、複数の Agent マシンに対して、シナリオを配布し同時に開始終了したり、 Agent のテスト結果をリアルタイムに監視することができます。
net.grinder.Console クラスから起動できます。
java -cp grinder-3.4/lib/grinder.jar net.grinder.Console
まあ Agent を一つしか立ち上げない場合は、必ずしも Console は必要はありません。
その場合は、以下のように Agent を起動する際に -Dgrinder.useConsole=false を指定して起動することで、 Console からの指示なしに Agent はシナリオを自発的に開始します。
java -Dgrinder.useConsole=false -cp \
grinder-3.4/lib/grinder.jar:grinder-3.4/lib/jython.jar \
net.grinder.Grinder
Proxy
Proxy は、 HTTP リクエストを記録してシナリオを生成するためのアプリケーションです。
プロキシサーバとして立ち上がるので、ブラウザのプロキシとして登録するだけで、ブラウザの操作を自動でシナリオ化してくれます。
java -cp grinder-3.4/lib/grinder.jar net.grinder.TCPProxy \
-http > grinder.py
このようにすると 8001 番ポートに HTTP プロキシーが立ち上がるので、ブラウザのプロキシの設定を変えることでそのブラウザでの操作をスクリプトに記録してくれます。
Grinder を使ってみる!
では、さっき作った Web サーバーを立ち上げて負荷を見てみましょう。さっそく立ち上げます。
ruby webserver.rb &
http://localhost:10080/top にアクセスしてみてください
立ち上がってますね。
つぎに、シナリオを記録するために Proxy を起動します。
java -cp grinder-3.4/lib/grinder.jar net.grinder.TCPProxy -http > grinder.py
そして、ブラウザでプロキシーの設定をします。
このとき localhost が例外になっていないか気をつけましょう。
プロキシーの設定が終わったら、Top -> Page 1 -> Page 2 とアクセスし、プロキシーを落とします。(Ctrl+C などで)
一連の作業が終わったらさっそく、 grinder.py を見てみましょう
# The Grinder 3.4
# HTTP script recorded by TCPProxy at 2010/05/20 3:02:19
from net.grinder.script import Test
from net.grinder.script.Grinder import grinder
from net.grinder.plugin.http import HTTPPluginControl, HTTPRequest
from HTTPClient import NVPair
connectionDefaults = HTTPPluginControl.getConnectionDefaults()
httpUtilities = HTTPPluginControl.getHTTPUtilities()
# To use a proxy server, uncomment the next line and set the host and port.
# connectionDefaults.setProxyServer("localhost", 8001)
# These definitions at the top level of the file are evaluated once,
# when the worker process is started.
connectionDefaults.defaultHeaders = \
[ NVPair('Accept-Language', 'ja,en-US;q=0.8,en;q=0.6'),
NVPair('Accept-Charset', 'Shift_JIS,utf-8;q=0.7,*;q=0.3'),
NVPair('Accept-Encoding', 'gzip,deflate,sdch'),
NVPair('User-Agent', 'Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/533.2 (KHTML, like Gecko) Chrome/5.0.342.7 Safari/533.2'), ]
headers0= \
[ NVPair('Referer', 'http://localhost:10080/page2'),
NVPair('Accept', 'application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5'), ]
headers1= \
[ NVPair('Accept', '*/*'), ]
headers2= \
[ NVPair('Referer', 'http://localhost:10080/top'),
NVPair('Accept', 'application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5'), ]
headers3= \
[ NVPair('Referer', 'http://localhost:10080/page1'),
NVPair('Accept', 'application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5'), ]
url0 = 'http://localhost:10080'
# Create an HTTPRequest for each request, then replace the
# reference to the HTTPRequest with an instrumented version.
# You can access the unadorned instance using request101.__target__.
request101 = HTTPRequest(url=url0, headers=headers0)
request101 = Test(101, 'GET top').wrap(request101)
request102 = HTTPRequest(url=url0, headers=headers1)
request102 = Test(102, 'GET favicon.ico').wrap(request102)
request201 = HTTPRequest(url=url0, headers=headers2)
request201 = Test(201, 'GET page1').wrap(request201)
request202 = HTTPRequest(url=url0, headers=headers1)
request202 = Test(202, 'GET favicon.ico').wrap(request202)
request301 = HTTPRequest(url=url0, headers=headers3)
request301 = Test(301, 'GET page2').wrap(request301)
request302 = HTTPRequest(url=url0, headers=headers1)
request302 = Test(302, 'GET favicon.ico').wrap(request302)
class TestRunner:
"""A TestRunner instance is created for each worker thread."""
# A method for each recorded page.
def page1(self):
"""GET top (requests 101-102)."""
result = request101.GET('/top')
grinder.sleep(27)
request102.GET('/favicon.ico')
return result
def page2(self):
"""GET page1 (requests 201-202)."""
result = request201.GET('/page1')
grinder.sleep(13)
request202.GET('/favicon.ico')
return result
def page3(self):
"""GET page2 (requests 301-302)."""
result = request301.GET('/page2')
grinder.sleep(27)
request302.GET('/favicon.ico')
return result
def __call__(self):
"""This method is called for every run performed by the worker thread."""
self.page1() # GET top (requests 101-102)
grinder.sleep(2932)
self.page2() # GET page1 (requests 201-202)
grinder.sleep(1362)
self.page3() # GET page2 (requests 301-302)
def instrumentMethod(test, method_name, c=TestRunner):
"""Instrument a method with the given Test."""
unadorned = getattr(c, method_name)
import new
method = new.instancemethod(test.wrap(unadorned), None, c)
setattr(c, method_name, method)
# Replace each method with an instrumented version.
# You can call the unadorned method using self.page1.__target__().
instrumentMethod(Test(100, 'Page 1'), 'page1')
instrumentMethod(Test(200, 'Page 2'), 'page2')
instrumentMethod(Test(300, 'Page 3'), 'page3')
おおお、ちゃんとリクエストが記録されているのが分かりますね!これが、シナリオになります。
さっそく、実行させてみましょう!
おっと、その前に、 thread 数が一気に増えると良くないので、ちょっと仕掛けを打ちます。
def __call__(self):
"""This method is called for every run performed by the worker thread."""
# 以下の行を追加!!
if grinder.runNumber == 0:
grinder.sleep(grinder.threadNumber * 5000)
では、改めて実行させてみましょう!
まず、 Console を立ち上げます
java -cp grinder-3.4/lib/grinder.jar net.grinder.Console
つぎに、 Agent を立ち上げます。
java -Dgrinder.runs=0 -Dgrinder.threads=100 -cp \
grinder-3.4/lib/grinder.jar:grinder-3.4/lib/jython.jar \
net.grinder.Grinder
ここでは、シナリオを無限ループ( -Dgrinder.runs=0 )、 100 スレッドにこのシナリオを実行させる( -Dgrinder.threads=100 )という設定で起動しています。
Agent が Console に接続されたら、左上の再生ボタンのようなボタンが押せるようになるので、それを押します。
そうすると以下のようになるので、しばらく待って止めます(無限ループなので、自動では止まりません)
data_XXXXXXXXX.log というファイル名で、詳細な CSV 形式のログが保存されるので Excel なり、データベースに突っ込むなりして分析します。
ね?簡単でしょ?
Grinder の改良
で、今回 ScaleBench を作るにあたって Grinder を改良した点について書きたいと思います。
multipart/form-data への対応
まず、今回一番大変だったのは multipart/form-data の POST に対応するところでした。
もともとの Grinder の Proxy では、 multipart/form-data を POST するというシナリオを記録することができませんでした。
今回、 ScaleBench を作るにあたって Proxy のコードと Agent のコードを改良して、 multipart/form-data を記録することができるようになりました。
この変更は、本家のほうにも取り込まれています。
記録されるシナリオのカスタマイズ
Proxy は、リクエストを内部では XMLBeans のオブジェクトで保持していて、それを XSLT にかまして Jython スクリプトに変換するということをやっています。
ScaleBench ではシナリオとして独自ライブラリを使う Jython コードを吐きたいので、この XSLT に手を入れました。
僕は XSLT にあまり慣れていなかったのでめんどくさく感じましたが、見方によっては XSLT さえ書けばどんな言語も出力できてしまうというのは非常に柔軟性が高いと言えるかもしれません。
独自の XSLT によってシナリオスクリプトを改変したい場合は、以下のように XSLT を指定してやります。
java -cp grinder-3.4/lib/grinder.jar net.grinder.TCPProxy \
-http fooBar.xsl > grinder.py
grinder-3.4/etc/ ディレクトリに Grinder 内部に組み込まれている XSLT のファイルが入っているので、参考にするといいと思います。
スレッド管理
あとは、少しずつスレッドを起動していったり、スレッドごとに決まった比率で別々のシナリオを実行させたりするように改造しました。
この辺は、 Jython のライブラリとして書いています。
その他
Grinder のバグ修正
ログの分析
他に ScaleBench ではログの分析も詳細に行うことができます。
この辺は @hikoma さんがものすごい勢いで作ってくれました!
こんなかっこいいグラフや Excel のシートが生成されます。

というわけで
今回は ScaleBench の紹介を兼ねて、超便利ツール Grinder について紹介しました。
これからも ScaleBench をよろしくお願いします!










コメント