Locustでお手軽負荷検証
LocustというPython製の負荷検証ツールをご存知でしょうか。
Webアプリケーションの負荷検証がお手軽に行えるフレームワークで、Pythonを使ってテストを定義できるうえ 便利なWebUIも付いてきます。
もともと筆者はJavaのJMeterを使っていました。
特殊な認証処理を自動化するため、リクエストごとに内容を動的に書き換えられるようなプラグインの実装を検討していたのですが、
JMeter自体年季が入っていることもあり開発体験が体に合わず断念してしまいました。
というわけで、Locustの使い方について紹介します。
Quickstart
フレームワークに乗っかった使い方で、次のような手順です。
Pythonのクラスとしてテストケースを定義
Locustサーバの起動
WebUIからパラメータ入力&テスト実行
テストケースだけをカスタマイズして、リクエストの実行はLocustフレームワークがパラメータに応じて良しなに実行してくれます。
公式のquickstartをトレースした内容ですが、かいつまんで説明します。
0.準備
Python3が動いていれば、pip3コマンドで次のようにインストール&確認ができます。
pip3 install locust
locust -V
また、動作確認のため GET /hello_world に応答できるダミーのWebサーバを起動しておきます。
echo 'hello world!' > hello_world
python3 -m http.server 3000
1.テストケースの定義
HttpUser を継承したクラスを定義して、locustfile.py として保存します。
試しに次のコードを locustfile.py として保存してください。
from locust import HttpUser, task
class HelloWorldUser(HttpUser):
@task
def hello_world(self):
self.client.get("/hello_world")
見たままですが、上のコードは GET /hello_world を繰り返すテストケースになっています。
2.Locustサーバの起動
次のコマンドでLocustサーバを起動し、テストケースを読み込ませます。
locust
# locustfile.py 以外の名前で保存した場合は -f で直接指定する
# locust -f hoge.py
起動に成功すると
[2022-12-13 09:58:46,215] .../INFO/locust.main: Starting web interface at http://*:8089
[2022-12-13 09:58:46,285] .../INFO/locust.main: Starting Locust 2.13.2
こんなメッセージが流れ、http://localhost:8089 で待ち受けが開始します。
3.WebUIからテスト実行
ブラウザから http://localhost:8089 にアクセスすると、次のような画面が現れてパラメータの入力を促されます。
各パラメータの意味は次のとおりです。
Number of Users
最大の多重度
Spawn rate
0多重から「Number of Users」多重まで少しずつ増やしていく際、1秒間に何多重ずつ増やしていくか
Number of Users と同じにしておけば、始めから負荷が最大に近づく
Host
負荷をかける先のベースURL
Hostに http://localhost:3000 を指定して「Start swarming」を押下すると、次の画面に遷移してダミーのWebサーバに対して負荷がかかり始めます。
Number of Users の分だけ並行して同期的にリクエスト送出・レスポンス受信を休みなく繰り返し、rpm やパスごとの統計情報がリアルタイムに更新されていきます。
Charts のタブをクリックすると、グラフによるrpmの可視化に切り替わります。
Download Data のタブからは、Chartsのデータをcsvとしてダウンロードできます。
止めたくなったら、STOPボタンを押してテストを終了してください。
カスタマイズ
こちらのページにある通り、locustfile.py を書き換えることで所望の負荷テストが実施できます。
以下、いくつかの機能を抜粋します。
リクエストの内容
既出のサンプルコードの self.client というオブジェクトについて、
from locust import HttpUser, task
class HelloWorldUser(HttpUser):
@task
def hello_world(self):
self.client.get("/hello_world")
これはHttpSessionというクラスのインスタンスで、
有名なrequestsパッケージのSessionクラスを独自に拡張したものになっています。
そのため、requestsパッケージのお作法に沿って Httpリクエストをカスタマイズすることができます。
class HelloWorldUser(HttpUser):
@task
def hello_world(self):
self.client.request(
method="GET",
url="/hello_world",
params={"a":"b"},
headers={"c":"d"}
)
リクエストとタスク
Locustにおけるテストの粒度は「タスク」という単位で、@task アノテーションが付いたメソッドが対応しています。
HttpUser の実装クラスでは、複数のタスクをもつことができます。
from locust import HttpUser, task
class HelloWorldUser(HttpUser):
@task
def hoge(self):
self.client.get("/hoge")
@task
def fuga(self):
self.client.get("/fuga")
新しく GET /hoge と GET /fuga ができるようにしたうえで
echo 'hoge' > hoge
echo 'fuga' > fuga
python3 -m http.server 3000
テストを実行すると、それぞれのパスについて結果の集計が行われていることが判ります。
1タスクの中で複数回リクエストを実行することが出来ます
from locust import HttpUser, task
class HelloWorldUser(HttpUser):
@task
def hoge(self):
self.client.get("/hoge")
@task
def fuga(self):
for i in range(0, 10):
self.client.get("/fuga?q=" + str(i))
タスク同士の重み付けを設定することが出来ます。
from locust import HttpUser, task
class HelloWorldUser(HttpUser):
@task(1)
def hoge(self):
self.client.get("/hoge")
@task(3)
def fuga(self):
self.client.get("/fuga")
/hoge、/fuga に対して 1:3 でリクエストが送出されていることが判ります。
タスク間隔
デフォルトの動作として、Usersに指定した数だけ多重度が増え、多重度1つごとに同期的かつ待ち時間0でタスクを繰り返し実行します。
HttpUser に wait_time というフィールドがあり、このパラメータを指定することでタスク同士の間隔を調整できます。
constant
wait_time = constant(sec) にすると、タスク完了後に指定した秒数だけ待機します。
from locust import HttpUser, task, constant
class HelloWorldUser(HttpUser):
wait_time = constant(1.0)
@task
def hello_world(self):
self.client.get("/hello_world")
Javaでいうところの ScheduledThreadPoolExecutor#scheduleWithFixedDelay に相当します。
constant(0) にすると、多重度をおおよそ Number of Users の値に固定できます。
ちなみに、constant(0) が wait_time のデフォルト値です。
constant_pacing
wait_time = constant_pacing(sec) にすると、等間隔でタスクを実行します。
from locust import HttpUser, task, constant_pacing
class HelloWorldUser(HttpUser):
wait_time = constant_pacing(1.0)
@task
def hello_world(self):
self.client.get("/hello_world")
Javaでいうところの ScheduledThreadPoolExecutor#scheduleAtFixedRate に相当...しない点に気をつけてください。
constant_pacing によって調整されるのはタスクの待機時間(図の白い部分)だけです。
タスクの処理時間が constant_pacing の値を上回る場合、constant(0)と同じ結果になります。
constant_pacing の逆数に相当する constant_throuput という値もありますが、タスクの待機時間しか調整されないのは同様です。
スループットを固定したい場合は、constant_pacing の値がタスク単発の処理時間よりも大きくなるようにしつつ、Number of Users と組み合わせて
スループット = ( Number of Usersの値 / constant_pacingの秒数 ) rpm
として調整すると良いと思います。
between
wait_time = between(arg1, arg2) にすると、リクエスト完了後に指定した範囲でランダムに待機します。
from locust import HttpUser, task, between
class HelloWorldUser(HttpUser):
wait_time = between(0, 1)
@task
def hello_world(self):
self.client.get("/hello_world")
master/worker構成
Python(CPython) は GIL(Global Interpreter Lock) の制限により実質1スレッドしか動作しません。
高い負荷をかけようにも、Python製のツールであるLocust側は1コア分しかリソースを使えないということになります。
そこでLocustではグリーンスレッド(実装はGreenlet。コルーチン、軽量スレッド、ファイバーとも)によって大量のリクエストを捌いています。
グリーンスレッドは OSに依存することなくユーザ空間でマルチスレッド環境を実現する技術のことです。C10K問題への対応と同様、グリーンスレッド中のI/Oは基本的に非同期処理で、I/Oのタイミングでコンテキストスイッチしながら1スレッドのリソースを無駄なく使ってマルチスレッドを捌きます。
このグリーンスレッドによって1スレッドでも相当の負荷を書けることが出来ますが、 マルチプロセス化=master/worker構成によって複数コアのリソースも動員して負荷試験を行うことが出来ます。
locust --worker &
locust --worker &
locust --master
このようなオプション付きで複数プロセスを起動し、次のメッセージが出現すれば正しくmaster/worker構成で動いています。
[2022-12-13 15:39:00,060] .../INFO/locust.main: Starting web interface at http://0.0.0.0:8089 (accepting connections from all network interfaces)
[2022-12-13 15:39:00,064] .../INFO/locust.main: Starting Locust 2.13.2
[2022-12-13 15:39:02,209] .../INFO/locust.runners: Worker xxx. (index 0) reported as ready. 1 workers connected.
[2022-12-13 15:39:04,309] .../INFO/locust.runners: Worker xxx. (index 1) reported as ready. 2 workers connected.
この後はこれまで同様 WebUI からテストを実行できます。
Usersで設定した多重度は各workerで分散され、worker から master へリクエストの実行結果が逐次送信&master側で集計されます。
ちなみに master のホスト、ポートを指定すればネットワーク越しに worker を起動できるので、別筐体も稼働してさらに大きな負荷をかけることができます。
# masterのデフォルトポートは5557
locust --worker --master-host 127.0.0.1 --master-port 5557
Locustをライブラリとして使う
設定ファイルを色々用意して動的に条件を変えたい、
ブラウザのdevtoolから取得した.harファイルからHTTPリクエストをトレースする等、
locustfile.py の枠内で実現するには厳しい場合があり、そのようなときには Locust をライブラリとして利用できます(こちらのページを参照)。
import gevent
from locust.env import Environment
from locust import HttpUser, task
class HelloWorldUser(HttpUser):
host = "http://localhost:3000" # WebUIから入力していた Host
@task
def hello_world(self):
self.client.get("/hello_world")
env = Environment(user_classes=[HelloWorldUser])
env.create_web_ui(host="127.0.0.1", port=8089) # WebUIのホスト・ポート
env.create_local_runner()
env.runner.start(10, spawn_rate=10) # WebUIから入力していた Users, Spawn rate
gevent.spawn_later(60, lambda: env.runner.quit()) # テストの継続時間(秒)
env.runner.greenlet.join()
上記のコードをtest.pyとして保存して
python3 test.py
を実行後 テストが開始します。
http://localhost:8089 にアクセスすると、パラメータの入力画面はスキップされて直接 Statisticsのページ に移り、テストがすでに動いていることを確認できます。
CUIですべて完結させたい場合は、このようなコードで WebUIを出すことなく結果をcsvとして出力することも可能です。
import gevent
from locust.env import Environment
from locust.stats import stats_printer, stats_history, StatsCSVFileWriter
from locust import HttpUser, task
class HelloWorldUser(HttpUser):
host = "http://localhost:3000" # WebUIから入力していた Host
@task
def hello_world(self):
self.client.get("/hello_world")
env = Environment(user_classes=[HelloWorldUser])
csv_writer = StatsCSVFileWriter(
environment=env,
base_filepath="./output",
full_history=True,
percentiles_to_report=[0.0, 100.0]
)
gevent.spawn(csv_writer)
env.create_local_runner()
env.runner.start(10, spawn_rate=10) # WebUIから入力していた Users, Spawn rate
gevent.spawn_later(60, lambda: env.runner.quit()) # テストの継続時間(秒)
env.runner.greenlet.join()
まとめ
Python製のLocustという負荷検証ツールについて紹介しました。
きれいなWebUIを備え、カスタマイズも容易です。
Python製ですがCPUリソースをきっちり使って負荷をかけられます。
結果をcsvとして出力し、作業をCUIで完結させることも出来ます。
幅広い用途に対応する負荷検証ツールなので、機会があれば使ってみてください。