ども、cloudpackかっぱ (@inokara) です。

はじめに

負荷試験ツールで個人的に好きなのは Siege ですが、負荷試験ツールの代名詞 JMeter の影に隠れてイマイチ存在感が薄いようです。やっぱり JMeter 一択なのかなと思っていた矢先に Locust という Python 製の負荷試験ツールがあることを以下のような記事で知りました。

自分も百番煎じ位ですが boot2docker を使ってドカドカ Docker で Locust を試してみたいと思います。尚、オフィシャルサイトやドキュメントは下記の通りです。

ちなみに Locust とはワタリバッタ(いなご)のようです…
Python 製負荷テストツール Locust を Docker コンテナで試す: Locustについて
カロリーって…。

Locust って

前述の通り Python 製の負荷テストツールで以下のような機能があります。

  • テストシナリオを Python で書くことが出来る
  • 複数のノードを利用した分散テストを行うことが出来る
  • Web UI
  • Web サイト以外のテストも行うことが出来ます

また、Locust の重い処理は gevent にお任せすることで、Locust 自身のコアは小さく作らており今後もこれを維持していくとのことです。gevent とは libev を元にした並行ライブラリです。ネットワークや並行プログラミングのためのクリーンな API を提供しているとのことです。

インストール等の準備

Docker コンテナ

Ubuntu コンテナを利用します。

docker run -t -i ubuntu /bin/bash

利用する Ubuntu バージョンは以下のとおり。

root@f5148f9250fa:/tmp# cat /etc/lsb-release
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=14.04
DISTRIB_CODENAME=trusty
DISTRIB_DESCRIPTION="Ubuntu 14.04.1 LTS"

Locust のインストール

pip を利用してインストールしたいと思います。

apt-get update
apt-get install python-pip
apt-get install python-dev
pip install locustio pyzmq

尚、pyzmq は Locust の複数のノードやプロセスを利用したテストを行うのであれば入れておいた方が良いよということなのでインストールしておきます。

トラブル

ocustio と pyzmq をインストールした後で pip を利用出来なく現象に遭遇…。以下のように IncompleteRead がインポート出来ないとのこと。

Traceback (most recent call last):
  File "/usr/bin/pip", line 9, in 
    load_entry_point('pip==1.5.4', 'console_scripts', 'pip')()
  File "/usr/lib/python2.7/dist-packages/pkg_resources.py", line 351, in load_entry_point
    return get_distribution(dist).load_entry_point(group, name)
  File "/usr/lib/python2.7/dist-packages/pkg_resources.py", line 2363, in load_entry_point
    return ep.load()
  File "/usr/lib/python2.7/dist-packages/pkg_resources.py", line 2088, in load
    entry = __import__(self.module_name, globals(),globals(), ['__name__'])
  File "/usr/lib/python2.7/dist-packages/pip/__init__.py", line 11, in 
    from pip.vcs import git, mercurial, subversion, bazaar  # noqa
  File "/usr/lib/python2.7/dist-packages/pip/vcs/mercurial.py", line 9, in 
    from pip.download import path_to_url
  File "/usr/lib/python2.7/dist-packages/pip/download.py", line 25, in 
    from requests.compat import IncompleteRead
ImportError: cannot import name IncompleteRead

対処は以下を参考にして requests をバージョン指定でインストールします。

sudo easy_install requests==2.3.0

焦りました…。

気を取り直して…

初めての Locust です。
ひとまずヘルプを確認します。

root@a858c220e16c:/# locust -h
Usage: locust [options] [LocustClass [LocustClass2 ... ]]

Options:
  -h, --help            show this help message and exit
  -H HOST, --host=HOST  Host to load test in the following format:
                        http://10.21.32.33
  --web-host=WEB_HOST   Host to bind the web interface to. Defaults to '' (all
                        interfaces)
  -P PORT, --port=PORT, --web-port=PORT
                        Port on which to run web host
  -f LOCUSTFILE, --locustfile=LOCUSTFILE
                        Python module file to import, e.g. '../other.py'.
                        Default: locustfile
  --master              Set locust to run in distributed mode with this
                        process as master
  --slave               Set locust to run in distributed mode with this
                        process as slave
  --master-host=MASTER_HOST
                        Host or IP address of locust master for distributed
                        load testing. Only used when running with --slave.
                        Defaults to 127.0.0.1.
  --master-port=MASTER_PORT
                        The port to connect to that is used by the locust
                        master for distributed load testing. Only used when
                        running with --slave. Defaults to 5557. Note that
                        slaves will also connect to the master node on this
                        port + 1.
  --master-bind-host=MASTER_BIND_HOST
                        Interfaces (hostname, ip) that locust master should
                        bind to. Only used when running with --master.
                        Defaults to * (all available interfaces).
  --master-bind-port=MASTER_BIND_PORT
                        Port that locust master should bind to. Only used when
                        running with --master. Defaults to 5557. Note that
                        Locust will also use this port + 1, so by default the
                        master node will bind to 5557 and 5558.
  --no-web              Disable the web interface, and instead start running
                        the test immediately. Requires -c and -r to be
                        specified.
  -c NUM_CLIENTS, --clients=NUM_CLIENTS
                        Number of concurrent clients. Only used together with
                        --no-web
  -r HATCH_RATE, --hatch-rate=HATCH_RATE
                        The rate per second in which clients are spawned. Only
                        used together with --no-web
  -n NUM_REQUESTS, --num-request=NUM_REQUESTS
                        Number of requests to perform. Only used together with
                        --no-web
  -L LOGLEVEL, --loglevel=LOGLEVEL
                        Choose between DEBUG/INFO/WARNING/ERROR/CRITICAL.
                        Default is INFO.
  --logfile=LOGFILE     Path to log file. If not set, log will go to
                        stdout/stderr
  --print-stats         Print stats in the console
  --only-summary        Only print the summary stats
  -l, --list            Show list of possible locust classes and exit
  --show-task-ratio     print table of the locust classes' task execution
                        ratio
  --show-task-ratio-json
                        print json data of the locust classes' task execution
                        ratio
  -V, --version         show program's version number and exit

--master--slave オプションがありますな。また、分散テストを実施する際にはマスターノードとスレーブノード間では 55575558 番ポートを利用するので開けておく必要があるようです。

初めての Locust

シナリオを Python で…

残念ながら Python も初心者枠なのでドキュメントを見よう見まねで以下のようにテストシナリオを書いてみました。

from locust import HttpLocust, TaskSet, task

class MyTaskSet(TaskSet):
    @task(2)
    def index(self):
        self.client.get("/")

    @task(1)
    def about(self):
        self.client.get("/about/about.html")

class MyLocust(HttpLocust):
    task_set = MyTaskSet
    min_wait = 5000
    max_wait = 15000

せっかくなので自分なりに上記のコードを読み解いていきます。

  • MyTaskSetTaskSet クラス を利用してタスクを定義
  • HttpLocustclient インスタンスメソッドで HTTP リクエストを任意の名前の関数オブジェクトにて定義(例えば self.client.get("/") は HTTP の GET メソッドで / にアクセス )
  • @task はタスクの実行率を指定するために使用できるオプションで重み付けの引数を設定(例では @task(2)@task(1) の倍実行される)
  • MyLocust(HttpLocust)HTTPLocust を利用して定義したタスクを利用して HTTP アクセスを定義

はじめてのテスト

作ったシナリオファイルを以下のように指定して Locust を起動したいと思いますが、事前にテスト対象となる Web サーバーコンテナを起動しておきます。

locust -f test.py -H http://172.17.0.7

上記の各オプションの意味は以下の通りです。

オプション パラメータ 意味
-f test.py シナリオファイルを指定
-H http://172.17.0.7 テスト対象のホストを指定(今回は別で起動している Docker コンテナ)

起動すると以下のように表示されます。

# locust -f test.py -H http://172.17.0.7
[2015-01-17 03:26:22,849] f5718236c996/INFO/locust.main: Starting web monitor at *:8089
[2015-01-17 03:26:22,850] f5718236c996/INFO/locust.main: Starting Locust 0.7.2

Locust の Web UI が 8089 ポート待機しているようですのでブラウザからアクセスします。今回は boot2docker を利用しているので事前に VirtualBox の設定で 8089 ポートにアクセス出来るようにしておきましょう。

アクセスしてみると以下のような画面が表示されます。
Python 製負荷テストツール Locust を Docker コンテナで試す: 動作確認(1)
おお。

Number of users to simulateHatch rate にそれぞれ数値を入れて Start swarming をクリックします。尚、Number of users to simulateHatch rate の意味は以下のとおりです。

オプション パラメータ例 意味
Number of users to simulate 300 想定する(シュミレート)するユーザー数
Hatch rate 10 毎秒で生成するユーザー数

かなり訳が怪しい感じですが、上記例の場合には最大 300 ユーザーを想定し、毎秒 10 ユーザーを生成するという設定になります。このパラメータを設定した状態でテストを開始してみると以下のように表示されました。
Python 製負荷テストツール Locust を Docker コンテナで試す: 動作確認(2)

おお、ちゃんとテストが行われているようですね。また、シナリオで定義したように //about/about.html へのアクセスが 2:1 になっていることも解ります。尚、ストップボタンをクリックして結果を csv で出力することも可能です。今回は細かいテスト結果の見方については割愛しますが、結果はリクエスト数、レスポンスタイムの最小、最大、平均、中央値が出力されています。

最初に Locust を起動したコンソールで Ctrl+c を押してフォアグランドで起動している Locust を停止すると以下のように CUI ベースの結果が出力されます。

 Name                                                          # reqs      # fails     Avg     Min     Max  |  Median   req/s
--------------------------------------------------------------------------------------------------------------------------------------------
 GET /                                                           5848     0(0.00%)      16       2     283  |      14   20.70
 GET /about/about.html                                           2842     0(0.00%)      13       2     102  |      11    9.70
--------------------------------------------------------------------------------------------------------------------------------------------
 Total                                                           8690     0(0.00%)                                      30.40

Percentage of the requests completed within given times
 Name                                                           # reqs    50%    66%    75%    80%    90%    95%    98%    99%   100%
--------------------------------------------------------------------------------------------------------------------------------------------
 GET /                                                            5848     14     17     19     21     27     33     42     50    283
 GET /about/about.html                                            2842     11     14     16     18     24     31     39     45    102
--------------------------------------------------------------------------------------------------------------------------------------------

上記のように CUI での結果出力も可能ということは…そうですね、Web UI を動かさないモードもあるようです。起動時に --no-web で Locust を起動することで CUI モードでの起動が可能となります。

分散テストを試す

Locust の醍醐味(かも)

Locust はシナリオが Python で書けたり Web UI があったりと魅力十分ですが、もうひとつの魅力として分散テストが行えるということが挙げられるかと思いますので試してみたいと思います。

Master / Slave

Locust の Master / Slave の通信は 55575558 ポートを利用して行われるようですので、それぞれのポートを開放した状態のコンテナを用意した上で試してみたいと思います。(事前に Locust をセットアップしたコンテナを inokappa/locust という名前で作っておきます)

docker run -t -i -p 8089:8089 -p 5557 -p 5558 --name locust-master inokappa/locust /bin/bash
docker run -t -i -p 5557 -p 5558 --name locust-slave01 inokappa/locust /bin/bash

それぞれコンテナが起動したら、以下のように Locust を起動します。

# Master Locust
locust -f test.py -H http://172.17.0.7 --master
# Slave Locust(マスターの IP は 172.17.0.8 となる)
locust -f test.py --slave --master-host=172.17.0.8

上記の各オプションの意味は以下の通りです。

オプション パラメータ 意味
–master なし Locust を Master モードで起動します
–slave なし Locust を Slave モードで起動します
–master-host 172.17.0.8 マスターとなるホストを指定

それぞれの Locust が起動した状態でマスターの Locust にブラウザでアクセスしてみます。
Python 製負荷テストツール Locust を Docker コンテナで試す: 動作確認(3)
おお。Slave が追加されているのが解ります。あとはシングルノードの時と同じように負荷試験を行うことが可能です。

今日の知見

以下のような知見を得ました。

  • Locust という Python 製の負荷試験ツールがある
  • シナリオが Python で書けるのでちょっと嬉しい(JMeter の XML との格闘から少し開放される)
  • 意外に簡単に分散テスト環境を構築することが出来るので Docker との相性も良さそうな気がするけど…どうだろう

今回は超簡単なシナリオで試してみましたが、引き続き、シナリオの書き方を勉強しつつ弄っていきたいと思います。

元記事はこちらです。
Python 製負荷テストツール Locust を Docker コンテナで試す