みなさま、初めまして。
システム開発部のK.Mです。
今回はサーバアプリケーションに対する負荷試験をする時に使用するツールの1つであるLocustを使用手順や使用時に発生した諸々をご紹介していきます。
- なぜLocust?
- 負荷試験とは?
- Locustインストール手順
- Locust実行手順
- Locustのシナリオ書き方例
- Dokerを使ったMaster-Slave設定による沢山の負荷をかける手順
- Locustのアップデート
- まとめ
- 最後に
なぜLocust?
負荷試験で使用するツールとして真っ先に名前が出てくるのは、Apache JMeterになります。
しかし、このJMeterですが、
- テスト用のシナリオを作る時にUI上でポチポチ選択することで時間が思ったより取られる。
- GUIで多機能だが、どの項目が何を表すのかが、分かりにくい点がある。
- テスト用のシナリオがXMLでも用意出来るが、こちらも分かりにくいXMLを読み解いて書いたりするのに時間が取られる。
- リクエストやレスポンス部分の暗号化とかセキュリティ用の機能があると、その機能をOFFにしないとテストがやりにくい。
等々、色々と使う上での難点がありました。
JMeterで難点になっている部分を解消するために、テスト用シナリオをプログラムで書けるツールを探しているところ、pythonで書けるLocustを発見したので使ってみることにしました。
Locustを使う以前は、k6というjsベースでシナリオを書けるツールやvegetaというgoベースでシナリオを書けるツールも使ってみましたが、それぞれ一長一短ありますが現時点ではLocustが上記に挙げたJMeterの難点が無く、使いやすいと個人的には感じています。
負荷試験とは?
負荷試験とは、何?という方向けに簡単な説明から入ります。
Webアプリケーションやアプリ用のAPIサーバは、WebサーバやDBサーバなどの様々なシステムやサーバと連携して動いているので、リリース時に想定しているアクセス(または想定している以上のアクセス)があっても正常に動くことを確認するために行う試験のこととなります。
(どこまで耐えられるかという観点のテストも存在しています。)
一例で上記のような構成があった場合に、どこがネックになってしまうのかを判断する必要があり1つ1つのサーバ単体で確認するのではなく、連携して確認して問題ないかの有無を判断します。
リリース前のサーバに対して負荷試験は必ず2回以上は実施しておく必要があると自分は考えております。
- 1回目・・・問題点を洗い出し、修正。
- 2回目・・・1回目で修正した部分が問題無いか及び修正したことで全体に問題無いかを確認。
2回目に問題があれば3回目、4回目と繰り返すことになりますが、スケジュールの問題で回数が行えないことが多いことがあるので、リリース前のギリギリに負荷試験を実施するのではなく、スケジュールを考えて事前に行ったり、スケジューリングをすることが重要です。
Locustインストール手順
注意点:2021年8月3日でのLocustの最新版はv1.6.0になっておりますが、今回記載の手順はv1.3.1ベースとなります。最後に少しだけ存在するv1.3.1とv1.6.0での差分を記載しております。
macであれば、下記の公式サイト記載の手順でインストールが可能です。
https://docs.locust.io/en/stable/installation.html
- python3.6以上をインストール
pip3 install locust
コマンドでlocustをインストールlocust -V
で下記のようなバージョン情報が表示されればインストール完了
$ locust -V locust 1.3.1
windowsですが、ptyhon3.6以上をインストールしておりpipコマンドが使える状態になっていればインストール可能かと考えられますが、自分の環境では試せておりません。
Locust実行手順
こちらも公式サイト記載の手順でLocustを動かすことが可能です。 https://docs.locust.io/en/stable/quickstart.html
- クイックスタートページに記載されている実行例のpythonコードをコピーしてファイルを作成します。ファイル名は任意の名前でOK。今回はlocustTest.pyとしております。
locust -f locustTest.py
とコマンドを打って、locustを起動させます。
作成したpythonファイルが別ディレクトリにある場合は、フルパスで指定すればOK。- 問題なく起動したら下記のようなログが表示がされます。
$ locust -f locustTest.py [2020-10-23 16:21:05,401] MacBook-Pro.local/INFO/locust.main: Starting web interface at http://0.0.0.0:8089 (accepting connections from all network interfaces) [2020-10-23 16:21:05,413] MacBook-Pro.local/INFO/locust.main: Starting Locust 1.3.1
正常に起動することを確認後、ブラウザを開いて下記の手順を行います。
ブラウザで
http://localhost:8089
へアクセスします。アクセスすると下記のような画面が表示されますので、各種値を入れてStart swarming
ボタンを押す。
・Number of total users to simulate:負荷試験として使用するユーザー数
・Spawn rate:ユーザーを追加していく時間の間隔(単位は秒)
・Host:アクセスするURL下記のような画面に遷移しコードに書いたテストシナリオが動き出します。今回作成しているクイックスタート通りのシナリオだと永遠に動き続けるため、適当なところで右上のSTOPボタンを押すことで、テストが停止します。
クイックスタートのシナリオが適当なURLへのアクセスになっているので全てエラーになりますが、Webブラウザにて結果などが視覚的に見えるのは使い勝手が良いです。
また、「Charts」などのリンクを押すと、結果をグラフで見ることが出来ます。
下記のようなレポートもDL可能です。
また、実行してたlocustコマンドを終了した時にも同じような結果が表示されます。
[2020-10-23 17:28:33,904] MacBook-Pro.local/INFO/locust.main: Running teardowns... [2020-10-23 17:28:33,905] MacBook-Pro.local/INFO/locust.main: Shutting down (exit code 1), bye. [2020-10-23 17:28:33,905] MacBook-Pro.local/INFO/locust.main: Cleaning up runner... Name # reqs # fails | Avg Min Max Median | req/s failures/s -------------------------------------------------------------------------------------------------------------------------------------------- GET /hello 3 3(100.00%) | 20 2 53 3 | 0.11 0.11 GET /item 114 114(100.00%) | 7 3 82 6 | 4.26 4.26 POST /login 5 5(100.00%) | 7 5 8 7 | 0.19 0.19 GET /world 3 3(100.00%) | 3 2 6 3 | 0.11 0.11 -------------------------------------------------------------------------------------------------------------------------------------------- Aggregated 125 125(100.00%) | 8 2 82 6 | 4.67 4.67 Response time percentiles (approximated) Type Name 50% 66% 75% 80% 90% 95% 98% 99% 99.9% 99.99% 100% # reqs --------|------------------------------------------------------------|---------|------|------|------|------|------|------|------|------|------|------|------| GET /hello 3 3 54 54 54 54 54 54 54 54 54 3 GET /item 6 6 7 7 10 24 33 53 82 82 82 114 POST /login 7 8 8 9 9 9 9 9 9 9 9 5 GET /world 3 3 6 6 6 6 6 6 6 6 6 3 --------|------------------------------------------------------------|---------|------|------|------|------|------|------|------|------|------|------|------| None Aggregated 6 6 7 7 10 24 53 54 82 82 82 125 Error report # occurrences Error -------------------------------------------------------------------------------------------------------------------------------------------- 5 POST /login: "HTTPError('404 Client Error: Not Found for url: http://localhost:8080/login')" 3 GET /hello: "HTTPError('404 Client Error: Not Found for url: http://localhost:8080/hello')" 3 GET /world: "HTTPError('404 Client Error: Not Found for url: http://localhost:8080/world')" 114 GET /item: "HTTPError('404 Client Error: Not Found for url: /item')" --------------------------------------------------------------------------------------------------------------------------------------------
Locustのシナリオ書き方例
一例としてサーバとのセッション確立するAPIを呼んでから、ユーザー新規作成のAPIを呼ぶサンプルコード例と下記します。
import time, hashlib, logging, base64, json from locust import HttpUser, task, TaskSet Secret = "" # ヘッダー作成用の処理 def headerCreate(path, id, body): if id == "": headers = { 'Content-Type': 'application/json', 'Accept': 'application/json', 'Accept-Encoding': '' } else: headers = { 'Content-Type': 'application/json', 'Accept': 'application/json', 'Accept-Encoding': '', 'X-Id': id } return headers # セッション作成用 def startSession(self, post): path = "/startSession" res = self.client.post(path, post, headers=headerCreate(path, self.id, post)) return res # ユーザー作成用 def userCreate(self, post): path = "/userCreate" res = self.client.post(path, post, headers=headerCreate(path, self.id, post)) return res # ホーム画面用 def home(self): path = "/home" post = "{}" res = self.client.post(path, post, headers=headerCreate(path, self.id, post)) return res # シナリオ用のクラス class UserBehavior(TaskSet): tasks = {home: 1} def on_start(self): self.id = "" appVersion = "0.0.1" # 1回目の/startSession(新規ユーザー用のIDとセッションIDを渡す) res = startSession(self, json.dumps({'appVersion':appVersion, 'os': 1})) logging.info(res.json()) resJson = res.json() self.id = resJson['id'] self.sessionID = resJson['sessionId'] # 2回目の/startSession(id設定して、サーバ側にセッション保持してもらう res = startSession(self, json.dumps({'appVersion':appVersion, 'id':self.id,'os': 1})) logging.info(res.json()) # サーバ側セッション保持したので、ユーザー作成 res = userCreate(self, json.dumps({'name':'testname', 'family_name': 'myouji', 'given_name':'namae'})) logging.info(res.json()) # 最初に呼ばれるクラス class User(HttpUser): tasks = {UserBehavior: 1} min_wait = 2000 max_wait = 5000
self.client.post
がLocustにてHTTPでPOSTを実施する処理となります。HttpUserを引数にしているクラスから呼ばれる形となります。
- tasksに呼び出すクラスと優先度をリスト型で設定すると、優先度が高いものを優先して動作するようになります。
- 例えば、
tasks = {A: 15, B: 1}
みたいな書き方をするとAが15倍、Bより実施される
- 例えば、
min_wait
がテストを実施する待ち時間の最小、max_wait
が最大(ms指定)上記のサンプルだと2〜5秒間隔で各種テストが実施されることになる。)
- tasksに呼び出すクラスと優先度をリスト型で設定すると、優先度が高いものを優先して動作するようになります。
今回のサンプルだと
UserBehavior
が呼ばれるクラスになるので、そのクラスの引数にTaskSetを設定しておくUserBehavior
にもtasksを設定しておくUserBehavior
クラスのon_start
が最初に呼ばれるのでon_start
内に最初に動く処理を記載する。- tasksに定義したものは
on_start
が動いた後に一定間隔で呼び出される(今回の場合だとhomeにアクセスするAPIがユーザー作成が終わった後に2〜5秒間隔で呼ばれる)
Dokerを使ったMaster-Slave設定による沢山の負荷をかける手順
Dokerfileを作っておき、GKEやEKSなどのKubernetes環境を使ってWebUI用のマスターサーバ1台に、負荷実施を行うワーカーサーバのインスタンスを指定台数分起動して負荷を大量にかけれる環境が作成可能となっています。
(1台のPCでサーバへの負荷をかけるアクセスを行うのには限界があるので、負荷をかけるPCを複数台用意してアクセスする形となります。)
今回の手順では、Googleが用意してくれているDokerfileをベースにGKE環境で動かすまでの流れとなります。
ローカルでdocker動かしてみる
一先ず確認のために、ローカル環境にてdockerを動かしてみます。 (dockerはインストール済み想定とします)
version: "3.4" x-common: &common image: locustio/locust volumes: - ./tests/:/tests services: locust-master: <<: *common ports: - 8089:8089 command: -f /tests/locustTest.py --master -H http://host.docker.internal-:8080 locust-slave: <<: *common command: -f /tests/locustTest.py --worker --master-host locust-master
- docker-compose.yamlと同じ場所にtestsディレクトリ作っておき、そこにシナリオファイル(locustTest.py)を入れます。
docker-compose up -d
でdocker起動します。docker-compose up -d --scale locust-slave=3
とするとmaster1台、slave3台でdocker起動させることが可能となります。- 注意点
GKE環境への構築と実施
Googleが用意してくれている情報があるのですが、内容がLocustのバージョン1.0.0未満のものになっており、1.0.0以降のバージョンでは修正必要な場所が何箇所かあったため、実際に実施した手順を記しておきます。
- GKEのクラスタ作成コマンド 事前にgcloudコマンドでデフォルト設定になっている対象プロジェクトは確認しておくこと。今回は例としてload-test-environmentというプロジェクトを用意して対象プロジェクトとして手順記載しております。
$ gcloud container clusters create gke-load-test \ --zone asia-northeast1-a \ --scopes "https://www.googleapis.com/auth/cloud-platform" \ --enable-autoscaling --min-nodes=2 --max-nodes=5 \ --scopes=logging-write,storage-ro \ --addons HorizontalPodAutoscaling,HttpLoadBalancing \ --preemptible
--preemptible
指定をして、プリエンプティブノードを有効にして、費用を抑える形としています。- コマンドの結果、GCPのGKEコンソールにて下記のような形でクラスタが作成されます。デフォルトのままだとブートデスクが100GBだったので不要であれば
--disk-size
で調整が可能となります。
- 作ったクラスタに接続する
$ gcloud container clusters get-credentials gke-load-test \
--zone asia-northeast1-a \
--project load-test-environment
Fetching cluster endpoint and auth data.
kubeconfig entry generated for gke-load-test.
このコマンドを実施することで、kubectlコマンドでGKEに繋がるようになります。
ここにある、Googleが用意している各種ファイルを取得します。
git clone
をしてもいいのですが、不要なファイルとかがあって消したかったので今回はzipでDL→展開、下記記載の修正をかけたものを使用しています。- sample-webappディレクトリは今回不要なので削除する。(サンプルとして用意されたWebAPI用のソース)
- 展開後の直下にあるDockerfile、flow.groovy、LICENSE、README.mdは不要なので削除
- docker-imageディレクトリのlicensesディレクトリは不要なので削除(ライセンス情報入っているので念のため読んでおきましょう。)
- 残ったファイルを使用するのですがディレクトリ構成上、一つ深い場所になってしまっているのでdocker-imageとkubernetes-configに入っているファイルを一つ上のディレクトリに移動し、docker-imageとkubernetes-configのディレクトリ自体は削除します。
- Dokerfileの中身を修正。
- 20行目をコメントアウトしておく
# ADD licenses /licenses
RUN pip install -r /locust-tasks/requirements.txt
前にRUN pip install --upgrade pip
を追加- locust-tasks/tasks.pyの中身がテストシナリオのコードになるので、事前に作成したものに書き換えておく
- locust-tasks/requirements.txtを修正
- locustioを削除する
- msgpack-pythonを削除する
- locust==1.3.1を追加する
- gevent==20.9.0に変更する
- msgpack==1.0.0に変更する
- greenlet==0.4.17に変更する
- Flask==1.1.2に変更する
- Werkzeug==1.0.1に変更する
- locust-tasks/run.sh内部の
--slave
を--worker
に変更する - 最終的なディレクトリとファイル構成は下記のようになります
gke-docker/ ├─ locust-tasks/ | ├─ requirements.txt | ├─ run.sh | └─ tasks.py ├─ Dockerfile ├─ locust-master-controller.yaml ├─ locust-master-service.yaml └─ locust-worker-controller.yaml
- dockerイメージの作成
- 修正したDockerfileがあるディレクトリで下記のコマンド実行する
$ gcloud builds submit --tag gcr.io/load-test-environment/locust-tasks:latest .
- 問題なければ、下記のように成功ログが出る
ID CREATE_TIME DURATION SOURCE IMAGES STATUS 51c7f5f2-8f79-42bd-891f-8d9e58d1bf19 2020-11-10T06:56:47+00:00 54S gs://load-test-environment_cloudbuild/source/1604991406.41-f50507523a4742d6a362f8ee052644bd.tgz gcr.io/load-test-environment/locust-tasks (+1 more) SUCCESS
- 正常に作成されているか確認
$ gcloud container images list | grep locust-tasks gcr.io/load-test-environment/locust-tasks Only listing images in gcr.io/load-test-environment. Use --repository to list images in other repositories.
Locustのマスター、スレーブのデプロイ
デプロイ時のコマンド
kubectl apply -f locust-master-controller.yaml kubectl apply -f locust-master-service.yaml kubectl apply -f locust-worker-controller.yaml
- Locustのデプロイ確認
$ kubectl get pods -o wide NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES locust-master-6594b6b4c-dgw9c 0/1 ContainerCreating 0 33s <none> gke-gke-load-test-default-pool-f28047d7-jr1d <none> <none> locust-worker-5bc77dc5b6-q5dnc 0/1 ContainerCreating 0 20s <none> gke-gke-load-test-default-pool-f28047d7-jr1d <none> <none> locust-worker-5bc77dc5b6-tvb2f 0/1 ContainerCreating 0 20s <none> gke-gke-load-test-default-pool-f28047d7-cc8c <none> <none> locust-worker-5bc77dc5b6-z9cfg 0/1 ContainerCreating 0 20s <none> gke-gke-load-test-default-pool-f28047d7-zmfn <none> <none>
- サービスの確認(IPはセキュリティの都合上一部マスクしてます。)
$ kubectl get services NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE kubernetes ClusterIP 10.XX.YY.1 <none> 443/TCP 120m locust-master LoadBalancer 10.XX.YY.12 34.XX.YY.ZZ 8089:32725/TCP,5557:32211/TCP,5558:31841/TCP 116s
今回の場合だと、EXTERNAL-IPに設定されている、34.XX.YY.ZZ:8089にアクセスするとLocustの初期画面が表示されるので、テスト実施が可能になる。 (EXTERNAL-IPが未設定(
<pending>
)の場合があるので、その場合は少し時間をおくと設定される。)テスト実施して不要になったらGKEクラスタを削除しておきましょう。1日以上未使用の状態が続くのであれば費用面を考えて削除しておいた方が良いでしょう。
gcloud container clusters delete gke-load-test --zone asia-northeast1-a
Locustのアップデート
執筆当時にインストールしたものがv1.3.1だったため、最新リリースのv1.6.0へ更新する場合は、pip3でのアップデート方法でOKです。
pip3 install -U locust
$ pip3 install -U locust 〜ログ省略〜 Successfully installed Flask-Cors-3.0.10 locust-1.6.0 $ locust -V locust 1.6.0
GKE部分の差分
- locust-tasks/requirements.txtを修正
- locust=1.3.1からlocust==1.6.0に変更する
まとめ
個人的に、下記の点から新規でJMeterを使うならLocustを使った方が良いかと感じました。
- シナリオファイルがptyhonで作成可能な点
- 一度、環境構築の仕方が分かってしまえばGKEなどのKubernate環境にてLocustを使った負荷をかけるのは簡単かつ低費用(サーバ使用料は負荷をどれだけかけるか次第になるが、大した金額にはならない)で可能な点
- しかも構築方法はGoogleが用意してくれているのでGKEだとそこまで苦労せずに環境構築が可能になっている。
ただ一点確認出来てなくて気になっている部分があり、負荷を増加するためにスレーブ(ワーカー)からのアクセス数とかサーバ数を増やすと、GKEのネットワーク周りで何か問題出そうな気がしなくもないので、事前に負荷試験自体の試験はしておいた方が良いかもと感じています。
既にメジャーバージョンアップされた2.0.0が出ているので、これも触ってみて導入手順やシナリオ周りに変更点ありそうであれば、こちらで紹介したいとおもいます。
最後に
リベル・エンタテインメントでは、このような最新技術などの取り組みに興味のある方を募集しています。もしご興味を持たれましたら下記サイトにアクセスしてみてください。
https://liberent.co.jp/recruit/