LocustでRailsアプリに負荷試験やる
唐突にやりたくなった。 Locust は Python スクリプトで記述する負荷試験ツール。 Locust には簡単なチュートリアルがある。
チュートリアルの中でログインしてるが、だいぶ昔から必須のCSRFの取り回しの記載が無い。チュートリアル後に、仕事で作ってるRailsアプリの負荷試験やってみようとすると、いきなり躓く。ログインできねえじゃねえかと。ちょっとフォローを交えた記事を書いてみる。
Locust入れる
Locust と lxml (CSRFトークン切り出しのため) を入れる。lxmlでなくともreでどうとでもなりそうな気がする。
pip3 install locust lxml
locust -V
この記事は Locust 1.2.3 で動作確認しています。
Redmineを立てる
rails new してたら連休が終わる。Rails製でログイン機構があり、docker compose でパッと立つものとしてRedmineを使うことにした。 https://hub.docker.com/_/redmine から拾ってきた redmine の yml で起動する。このとき Redmine のコンテナの環境変数に LOG_LEVEL=DEBUG
を追加して、状態が見えるようにしておく。
# LOG_LEVEL: DEBUG を追加してる
version: '3.1'
services:
redmine:
image: redmine
restart: always
ports:
- 8080:3000
environment:
REDMINE_DB_MYSQL: db
REDMINE_DB_PASSWORD: example
REDMINE_SECRET_KEY_BASE: supersecretkey
LOG_LEVEL: DEBUG
db:
image: mysql:5.7
restart: always
environment:
MYSQL_ROOT_PASSWORD: example
MYSQL_DATABASE: redmine
立てる。ブラウザ操作がログで見えるようにしておく。
docker-compose up -d
docker-compose logs -f redmine
http://localhost:8080/login をブラウザで開き、 admin admin で初回ログインする。パスワードは hogehoge
に設定する。ログアウトして、間違ったログイン情報でログイン失敗するときのログも観察しておこう。
locustfileを書く
Locustの負荷試験シナリオは locustfile
に書く。 locust コマンドも指定がなければカレンドディレクトリの locustfile.py
を探して実行する。 Locust が用意している HttpUser
を継承したクラスを書く。クラス名は何でも構わない。
今回はログイン後の状態で負荷をかけまくりたいので on_start
にログイン操作を書く。いきなり /login
にPOSTしてもログインできない。RailsではCSRFのセキュリティトークンは /login
を GET したときのHTMLに埋め込まれているので、正規表現なりXPathなりで抜き出してくる。
あとはプラプラ巡回するコードを書く。
import time
from locust import HttpUser, task, between
from lxml import html
class QuickstartUser(HttpUser):
wait_time = between(1, 2)
def on_start(self):
response = self.client.get("/login")
tree = html.fromstring(response.text)
auth_token = tree.xpath('//form/input[@name="authenticity_token"]/@value')[0]
self.client.post(
"/login",
json={
"username": "admin",
"password": "hogehoge",
"authenticity_token": auth_token
}
)
@task(1)
def index_page(self):
self.client.get("/")
@task(3)
def admin_page(self):
self.client.get("/admin")
@task(3)
def any_page(self):
self.client.get("/projects")
self.client.get("/my/page")
Locust で Redmine にログインできているか確認する
これ端折ると、ログインできてない状態で空振りな負荷をかけてることが、後になって発覚したりする。
並列数(--user
)を1、実行時間(--run-time
)を短くして、 docker-compose で流れてるログを眺める。ブラウザ操作でログインできたとき、失敗したときのログとも見比べて、ログインできてるか確認する。
locust -f ./locustfile.py --headless --host http://localhost:8080 --user 1 --run-time 3s
self.client.post()
で渡すべきJSONの構造はウェブアプリ毎に千差万別のため、都度で調べる必要があるだろう。
Locust でフルボッコにする
きちんとログインできることを確認できたら、並列数と実行時間を増やして、負荷試験にとりかかる。
locust -f ./locustfile.py --headless --host http://localhost:8080 --user 10 --run-time 300s
実行中も進捗が表示される。 --run-time
が経過すると終了し、レスポンスタイムの成績を集計したレポートが出る。
[2020-09-22 01:17:45,143] sasasin/INFO/locust.main: Cleaning up runner...
Name # reqs # fails | Avg Min Max Median | req/s failures/s
--------------------------------------------------------------------------------------------------------------------------------------------
GET / 2 0(0.00%) | 21 16 27 16 | 0.07 0.00
GET /admin 12 0(0.00%) | 19 14 21 20 | 0.40 0.00
GET /login 1 0(0.00%) | 15 15 15 15 | 0.03 0.00
POST /login 1 0(0.00%) | 35 35 35 35 | 0.03 0.00
GET /my/page 6 0(0.00%) | 27 23 36 26 | 0.20 0.00
GET /projects 6 0(0.00%) | 23 21 27 23 | 0.20 0.00
--------------------------------------------------------------------------------------------------------------------------------------------
Aggregated 28 0(0.00%) | 22 14 36 22 | 0.94 0.00
Response time percentiles (approximated)
Type Name 50% 66% 75% 80% 90% 95% 98% 99% 99.9% 99.99% 100% # reqs
--------|------------------------------------------------------------|---------|------|------|------|------|------|------|------|------|------|------|------|
GET / 27 27 27 27 27 27 27 27 27 27 27 2
GET /admin 21 21 21 21 22 22 22 22 22 22 22 12
GET /login 15 15 15 15 15 15 15 15 15 15 15 1
POST /login 35 35 35 35 35 35 35 35 35 35 35 1
GET /my/page 26 26 27 27 37 37 37 37 37 37 37 6
GET /projects 24 24 25 25 27 27 27 27 27 27 27 6
--------|------------------------------------------------------------|---------|------|------|------|------|------|------|------|------|------|------|------|
None Aggregated 22 24 26 26 27 35 37 37 37 37 37 28
devise で認証機構を組んでいる場合
HTTP POSTしてる内容が若干異なるため、self.client.post()
のJSONの作りも違う。たとえば devise のサンプルアプリ https://github.com/imhta/rails_6_devise_example を立てて、sign upして、sign inしてみる。そのときのChromeデベロッパーコンソールで見えたPOST情報と、railsのログから、LocustからPOSTすべきJSONの構造は、これが正解。
self.client.post(
"/users/sign_in",
json={
"user": {
"email": "admin",
"password": "hogehoge",
},
"authenticity_token": auth_token
}
)
他のgemで認証機構を組んだ場合も、微妙に異なっていると思われる。
Discussion