当サイトは、アフィリエイト広告を利用しています

【Docker × Flask】Locustでflask製のREST APIに負荷をかけてみる

作成日:2024月03月16日
更新日:2024年03月16日

dockerを使ってPythonのフレームワークであるflaskで実装したREST APIに
Pythonで書かれた負荷ツールであるLocustを使って負荷をかける方法をまとめる。

Locustとは?

LocustはwebアプリケーションやAPIの性能テストを行うことができる負荷ツール。
Locustはサーバー上で動作するwebアプリケーションに対してリクエストを送り
負荷をかけるツールであるため、アプリを通してではなく、システムに直接的に負荷を
かけたい場合は「sysbench」を用いた方がいい。
sysbenchに関しては下記記事で紹介しています。

Locustの特徴

Locustは他の負荷ツールに比べて下記のような特徴がある

Pythonでテストスクリプトを書ける

Locustで負荷テストを実行する際のテストケースをPythonプログラムで
書くことができる。

そのため、テスト結果の処理にpythonライブラリを使うことも可能

インストールが簡単

pipで簡単にインストールすることができる

リアルタイムでテスト結果が見れる

LocustにはWEBUIがあり、テストの進捗状況をリアルタイムで確認できる
下記のような感じでブラウザでみることができる
2024-03-10-22-45-52 2024-03-10-22-46-16

Locustで測れる性能は?

Locustを使用してwebアプリケーションに負荷をかけるデータを取得することで
下記のような性能指標を測ることができる

リクエスト数

Locustは特定の時間内に発生したリクエストの総数を計測できる

レスポンス時間

各リクエストのレスポンス時間(サーバーからの応答までの時間)を計測できる
(最小、最大、平均、中央値)で見れる

リクエストあたりのスループット

特定の時間内に処理されたリクエストの数を計測できる
いわゆるリクエストレート(単位時間あたりのリクエスト処理数)

失敗率

リクエストが失敗した割合を計測できる
リクエストがどれだけの頻度で失敗するかを示す。

環境

下記の環境で行う

  • Windows10
  • Docker version 24.0.2(Docker for Windows)
  • VScode
  • Remote Development(VScodeの拡張機能)
  • Flask 3.0.2
  • locust==2.16.1
  • gunicorn==21.2.0
  • Docker for Windows

「Docker for Windows」のインストール方法については下記記事で 紹介しています

実行環境について

Locustでflask製REST APIに負荷をかけるための実行環境は
dockerコンテナを使って作成する

イメージとしては下記のようになる 2024-03-10-23-23-19

Locustコンテナとflaskコンテナを作成して
LocustコンテナからLocustを実行し、flaskコンテナ内の
gunicornで起動しているREST APIに対してリクエストを送って
負荷をかける。

構成

全体的な構成は下記のようになる

構成
.
|-- api
| |-- Dockerfile
| |-- api.py
| |-- flask_memo.md
| |-- requirements.txt
| `-- wsgi.py
|-- docker-compose.yml
`-- locust
|-- Dockerfile
|-- locustfile.py
`-- requirements.txt

実行環境構築関連ファイル

dockerコンテナを作成するためのファイル

  • docker-compose.yml
  • api/Dockerfile
  • api/requirements.txt
  • locust/Dockerfile
  • locust/requirements.txt

詳細は後述

REST API関連ファイル

flaskのREST APIの実行ファイル

  • api.py
  • wsgi.py

詳細は後述

Locust関連ファイル

Locustのテスト設定ファイル

  • locustfile.py

詳細は後述

実行環境作成(コンテナ作成)

dockerを使って負荷テストを行うための
コンテナを作成していく

実行環境構築関連ファイル

コンテナは

  • flaskコンテナ
  • locustコンテナ

を作成する。

docker-compose.yml

docker-compose.yml
version: "3"
services:
flask:
container_name: "flask"
# Dockerfileをビルド
build:
context: ./api/
dockerfile: Dockerfile
tty: true
# プロジェクトをバインドマウント
volumes:
- ./api:/workspace/api
ports:
- "5000:5000"
# サーバー実行
# ipアドレス ポート番号 パス ファイル名 flaskインスタンス名
command: gunicorn --reload --bind 0.0.0.0:5000 --chdir /workspace/api wsgi:api
networks:
example-network:
ipv4_address: 172.16.238.10
locust:
container_name: "locust"
# Dockerfileをビルド
build:
context: ./locust/
dockerfile: Dockerfile
tty: true
# プロジェクトをバインドマウント
volumes:
- ./locust:/workspace/locust
ports:
- "8089:8089"
networks:
example-network:
ipv4_address: 172.16.238.9
networks:
example-network:
name: example-network
driver: bridge
ipam:
driver: default
config:
- subnet: 172.16.238.0/24
  • 各コンテナはそれぞれdockerfileで作ったイメージを使用する
  • flaskコンテナではgunicornサーバーでapiを起動する
  • コンテナにはそれぞれ固定IPをふっておく。

dockerでコンテナに固定IPをふることに関しては
下記記事でまとめています。

またflaskアプリをgunicornで起動する詳細については下記記事でまとめてます。

api/Dockerfile

api/Dockerfile
# イメージ
FROM python:3.12
# パッケージを最新化
RUN python -m pip install --upgrade pip
# workspaceディレクトリを作成
WORKDIR /workspace
# workspaceにrequirements.txtをコピー
COPY requirements.txt /workspace/
# requirements.txtのパッケージをインストール
RUN pip install -r requirements.txt
  • python:3,12のimageを使用
  • requirements.txtで必要なパッケージをインストールする

api/requirements.txt

api/requirements.txt
Flask==3.0.2
gunicorn==21.2.0
  • REST API実装のためのflaskパッケージをインストール
  • REST APIを起動するサーバーとしてgunicornをインストール

locust/Dockerfile

locust/Dockerfile
# イメージ
FROM python:3.12
# パッケージを最新化
RUN python -m pip install --upgrade pip
# workspaceディレクトリを作成
WORKDIR /workspace
# workspaceにrequirements.txtをコピー
COPY requirements.txt /workspace/
# requirements.txtのパッケージをインストール
RUN pip install -r requirements.txt
  • python:3,12のimageを使用
  • requirements.txtで必要なパッケージをインストールする

locust/requirements.txt

locust/requirements.txt
locust==2.16.1

コンテナ作成

docker composeでコンテナ作成を実行する

コンテナ作成・確認
$ docker compose up -d
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
3fc081b1a740 locust_restapi-prometheus-locust "python3" About a minute ago Up About a minute 0.0.0.0:8089->8089/tcp locust
ed470c1e22ee locust_restapi-prometheus-flask "gunicorn --bind 0.0…" About a minute ago Up About a minute 0.0.0.0:5000->5000/tcp flask

locustコンテナとflaskコンテナが作成されている

この後、VScodeでコンテナにアタッチして操作する

VScodeでコンテナにアタッチ

作成済みのコンテナへのアタッチ方法については
下記で紹介してます。

拡張機能のインストール

コンテナをdocker comoposeで作った場合は
拡張機能がインストールされない。一括インストールする方法を
下記で紹介してます

REST APIについて

flaskコンテナで起動したgunicornサーバー上で動く
flaskで実装したREST APIについて少し解説する。

api/api.py

flaskのREST APIのルーティングと処理を実装

受信したHTTPメソッドに応じて、usersディクショナリを操作するだけの
REST API。

api/api.py
from flask import Flask, request, jsonify
# flaskインスタンス作成
api = Flask(__name__)
# ディクショナリ
users = [
{"user_id": "1", "name": "tujimura", "age": 11},
{"user_id": "2", "name": "mori", "age": 20},
{"user_id": "3", "name": "shimada", "age": 50},
{"user_id": "4", "name": "kyogoku", "age": 70}]
@api.route('/', methods=['GET'])
def get_users():
# GETリクエストを処理
age = request.args.get('age')
if age:
return jsonify(list(filter(lambda user: user['age'] == int(age), users)))
return jsonify(users)
@api.route('/', methods=['POST'])
def post_user():
# POSTリクエストを処理
data = request.get_json()
users.append(data)
return jsonify(users), 201
@api.route('/<user_id>', methods=['PUT'])
def put_user(user_id):
# PUTリクエストを処理
data = request.get_json()
res_users = list(map(lambda user: user.update(data) or user if user['user_id'] == user_id else user, users))
return jsonify(res_users)
@api.route('/<user_id>', methods=['DELETE'])
def delete_user(user_id):
# DELETEリクエストを処理
res_users = list(filter(lambda user: user['user_id'] != user_id, users))
return jsonify(res_users)
  • 関数ベースのルーティングで実装
  • HTTPメソッド「GET,POST,PUT,DELETE」ごとの処理を実装

api/wsgi.py

flaskのREST APIを起動するためのファイル

wsgi.py
# api.pyのflaskインスタンスをimport
from api import api
if __name__ == '__main__':
# gunicornサーバー起動
api.run()

今回はLocustがメインなのでREST APIに関する詳細は割愛する。 REST APIの詳細は下記記事でまとめるので、よければ参照ください

Locustのテストスクリプト

LocustはテストケースをPythonクラスで定義する

locustfile.py

LocustのテストケースをPythonスクリプトで作成

locustfile.py
from locust import HttpUser, TaskSet, task, between
# タスクセットクラス
class UserBehavior(TaskSet):
@task(5)
def get_users(self):
self.client.get("/")
@task(4)
def get_users_urlparam(self):
self.client.get("/", params={"age": 30})
@task(3)
def post_user(self):
self.client.post("/", json={"user_id": "5", "name": "test", "age": 30})
@task(2)
def put_user(self):
self.client.put("/1", json={"name": "updated", "age": 22})
@task(1)
def delete_user(self):
self.client.delete("/3")
# ユーザークラス
class WebsiteUser(HttpUser):
# hostを指定(コマンドラインから実行時に使用)
host = "http://flask:5000"
tasks = [UserBehavior]
# 5秒から15秒の間隔を置いて実行
wait_time = between(5, 15)

LocustのPythonスクリプトでは

  • タスクセットクラス
  • ユーザークラス

の二つを定義する。
※タスクセットクラスは作らず、ユーザークラス内に直接タスクを書くこともできる。

テスト実行するとユーザーごとにタスクセットの中から5つのメソッドのいずれかを
5~15秒ごとに実行するようになる。

taskの横の数字は頻度。具体的にいうと
get_usersタスクが最も頻繁に(5回/10回)、delete_userタスクが最も少なく(1回/10回)実行される

テストの実行のイメージとしては
後述するがLocustでテスト実行時に「大ユーザー数」と「1秒ごとの増加ユーザー数」を設定するので
そのユーザーごとにユーザークラスのユーザーが作成され、タスクセットクラスのタスクを実行する仕組み。

Locustの実行方法について

Locustは実行は

  • webUIを使った実行
  • コマンドラインを使った実行がある。

webUIから実行

webUIからの実行を試してみる

webUIから実行のメリット・デメリット

webUIから実行する場合、下記のメリットがある

  • パラメータの入力とテストの実行が直感的にできる
  • リアルタイムに統計情報が更新され、視覚的に理解しやすい
  • テストデータをCSVとしてダウンロードできる

デメリットとしては下記がある

  • テストの自動化が難しい。毎回手動でパラメータを入力し、テストを開始する必要がある

Locustの起動

locustコンテナにログインし
locustfile.pyがあるディクショナリまで移動し、下記コマンドで起動する

Locust起動
$ locust
[2024-03-12 15:04:35,778] 24771d714839/INFO/locust.main: Starting web interface at http://0.0.0.0:8089 (accepting connections from all network interfaces)
[2024-03-12 15:04:35,793] 24771d714839/INFO/locust.main: Starting Locust 2.16.1

webUIの設定方法

起動後、「http://localhost:8089/」にアクセスするとwebUIが開く
2024-03-13-00-13-19

Number of users (peak concurrency)

最大の多重度。要は最大何ユーザーまで同時実行するかを設定

Spawn rate (users started/second)

1秒ごとに何ユーザーずつ増やすかを設定

Host (http://www.example.com)

負荷をかけるベースのURI

この設定の場合、30ユーザーまで1秒ごとに1ユーザーずつ追加する設定となる

実行

「Start swarming」でテストが開始する
2024-03-13-00-15-41

30ユーザーまで達したら、stopするまでテストが実行される。

Charts

Chartsタグでグラフをみることができる
2024-03-13-00-17-22

Download Data

Download Dataタブでデータをダウンロードできる
2024-03-13-00-18-19

webUIの見方

LocustのWeb UIの各列の意味をまとめておく 2024-03-13-00-15-41

  • Type:HTTPメソッド(GET、POSTなど)
  • Name:リクエストのエンドポイント(URLパス)
  • # Requests:特定のエンドポイントに対するリクエストの総数
  • # Fails:特定のエンドポイントに対する失敗したリクエストの数
  • Median (ms):レスポンス時間の中央値(ミリ秒)
  • 90%ile (ms):レスポンス時間の90パーセンタイル(ミリ秒
  • 99%ile (ms):レスポンス時間の99パーセンタイル(ミリ秒)
  • Average (ms):レスポンス時間の平均値(ミリ秒)
  • Min (ms):レスポンス時間の最小値(ミリ秒)
  • Max (ms):レスポンス時間の最大値(ミリ秒)
  • Average size (bytes):レスポンスの平均サイズ(バイト)
  • Current RPS:特定のエンドポイントに対する現在のリクエストのレート。
  • Current Failures/s:特定のエンドポイントに対する現在の失敗のレート。
  • 「Aggregated」行は、すべてのエンドポイントの統計情報を集約したもの

コマンドラインで実行

コマンドラインで実行から実行する場合、下記のメリットがある

  • テストの自動化が容易です¹。CI/CDパイプラインに組み込むことも可能
  • パラメータをスクリプトに直接書き込むことで、テストの再現性が高まる

デメリットとしては下記がある

  • テスト結果の視覚的な理解が難しい。
  • テストの開始や停止、パラメータの変更などを行うためには、都度コマンドを実行する必要がある

Locustのコマンドラインで実行する

コマンドラインでの実行は下記ようにする

コマンドライン実行テンプレ
locust -f [スクリプトファイル名] --headless -u [ユーザー数] -r [ユーザーの発生率] --run-time [実行時間]
  • -fはLocustのテストスクリプトのパスを指定
  • --headlessはGUIなしで実行することを指定
  • -uはシミュレートするユーザーの数を指定
  • -rはユーザーの発生率(秒あたり)を指定
  • --run-timeはテストの実行時間を指定

locustコンテナにログインし
locustfile.pyがあるディクショナリまで移動し、下記コマンドで起動する

コマンドライン実行
locust -f locustfile.py --headless -u 30 -r 1 --run-time 3m

ホスト名を「locustfile.py」に書いてない場合、下記のように指定することもできる

コマンドライン実行
locust -f locustfile.py --host=http://flask:5000 --headless -u 30 -r 1 --run-time 3m

この設定の場合、30ユーザーまで1秒ごとに1ユーザーずつ追加する設定となる。
テストは3分後に終了する。

csv出力する場合

csv出力する場合は下記のようにする

コマンドライン実行
locust -f locustfile.py --host=http://flask:5000 --headless -u 30 -r 1 --run-time 3m --csv=output

これでlocustfile.pyと同じディクショナリに下記のファイルが作成される

  • output_exceptions.csv
  • output_failures.csv
  • output_stats_history.csv
  • output_stats.csv

まとめ

flaskのREST APIに対して、負荷ツールであるLocustを使って
複数リクエストを送付して負荷をかけてみた。

テストスクリプトをPythonで記述できるのは
かなりのメリットになると思う。

Locustはwebアプリケーションに対して負荷をかけるツールである
システム(もしくはコンテナ)に対して、直接的に負荷をかけたい場合は
Locustではなくsysbench等を使う方法がある。
詳しくは下記で紹介しています。

参考

関連記事

新着記事

目次
タグ別一覧
top