« KVM にシリアルコンソールで Ubuntu をインストールする | メイン | MACアドレスからIPを自動設定する »

2010年5月20日 (木)

ScaleBench 公開

ScaleBench 公開

どーもみなさま。こんにちは。 amachang と申します。

さて、ようやく ScaleBench というプロダクトが発表されましたね!

ScaleBench のご紹介

で、僕もこれの開発に携わっていたのでちょっと技術的なことについて書いてみたいと思います。

ScaleBench とは

ScaleBench とは、サイボウズ製品向けの負荷テストツールで Grinder というオープンソースの負荷テストツールをベースにしています。

Grinder とは

Java を使った Web の負荷テストツールです。

Jython でシナリオ(ユーザがどう行動するか)を書いてそれを実行します。

またブラウザの操作を記録して、シナリオを自動で生成することもできたりします。

Grinder

で、僕がこのプロジェクトで担当していたのが

  • 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 をインストールします。

  1. http://sourceforge.net/projects/grinder/files/ から最新バージョンの ZIP ファイル(現在は "grinder-3.4.zip" )をダウンロード
  2. ダウンロードした ZIP ファイルを解凍する

簡単ですね!

解凍したディレクトリは、以下のようになっていると思います。

grinder-3.4/
|-- contrib
|   `-- mq
|-- etc
|-- examples
`-- lib

lib ディレクトリに Java のプログラムの本体(jar ファイル)が入っています。

Grinder の中に入っているもの

インストールが完了したら、さっそく Grinder を使ってみたいですね!

でも、ちょっとだけ待ってください><

Grinder には、以下の 3 つの Java アプリケーションが含まれていまして、先にその説明をしたいと思います。

  • Agent
  • Console
  • Proxy

図にすると以下のような感じ

Untitleddrawing_2

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

Screenshotthe_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 にアクセスしてみてください

Screenshottop_page_google_chrome_3

立ち上がってますね。

つぎに、シナリオを記録するために Proxy を起動します。

java -cp grinder-3.4/lib/grinder.jar net.grinder.TCPProxy -http > grinder.py

そして、ブラウザでプロキシーの設定をします。

Screenshot

このとき localhost が例外になっていないか気をつけましょう。

Screenshot1

プロキシーの設定が終わったら、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 

Screenshotthe_grinder_console_2

つぎに、 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 に接続されたら、左上の再生ボタンのようなボタンが押せるようになるので、それを押します。

Screenshotthe_grinder_console1

そうすると以下のようになるので、しばらく待って止めます(無限ループなので、自動では止まりません)

Screenshotthe_grinder_console2

data_XXXXXXXXX.log というファイル名で、詳細な CSV 形式のログが保存されるので Excel なり、データベースに突っ込むなりして分析します。

Screenshot_1_openofficeorg_calc

ね?簡単でしょ?

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 をよろしくお願いします!

トラックバック

このページのトラックバックURL:
http://bb.lekumo.jp/t/trackback/404050/24192712

ScaleBench 公開 を参照しているブログ:

コメント

コメントを投稿

コメントは記事の投稿者が承認するまで表示されません。