locust

## はじめに
:v::poultry_leg::santa: 12月がきた！！！！ :santa::cake::v:
負荷テストツール locustを実際に業務で使って大変いい感じだったので紹介します。

### locustとは
locustと調べて一番上に出てくるのはバッタかと思います。 locust jsと調べるとHPにたどり着きます。

https://locust.io/

無料で使えてUIがいい感じの いい感じの負荷テストツールです。
pythonで書きます。 python業務で使ったことなかったですが、すごい楽にテスト記載できました :v:

### どんな結果が得られるのか？

<details><summary>こんな感じで、いい感じに表示してくれます。</summary>

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2952155/93699491-74c8-0e9e-482f-83edd0a0a018.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2952155/4a46eef6-7689-1678-fc8b-681163fbde57.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2952155/ff2ea336-077b-b883-1119-9f1daba2be12.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2952155/55f7d7f1-976b-af0d-3420-510be4f59d0a.png)

</details>

<details><summary>CLIでもいい感じに結果出してくれます</summary>

```
[2023-11-30 22:01:21,743] enoMacBook-Pro.local/INFO/locust.main: Shutting down (exit code 1)
Type     Name                                                                          # reqs      # fails |    Avg     Min     Max    Med |   req/s  failures/s
--------|----------------------------------------------------------------------------|-------|-------------|-------|-------|-------|-------|--------|-----------
GET      /hello                                                                         24468     0(0.00%) |      0       0       3      1 |  736.73        0.00
GET      /world                                                                         24467 24467(100.00%) |      0       0      14      1 |  736.70      736.70
--------|----------------------------------------------------------------------------|-------|-------------|-------|-------|-------|-------|--------|-----------
         Aggregated                                                                     48935 24467(50.00%) |      0       0      14      1 | 1473.43      736.70

Response time percentiles (approximated)
Type     Name                                                                                  50%    66%    75%    80%    90%    95%    98%    99%  99.9% 99.99%   100% # reqs
--------|--------------------------------------------------------------------------------|--------|------|------|------|------|------|------|------|------|------|------|------
GET      /hello                                                                                  1      1      1      1      1      1      1      1      2      2      4  24468
GET      /world                                                                                  1      1      1      1      1      1      1      1      2      3     14  24467
--------|--------------------------------------------------------------------------------|--------|------|------|------|------|------|------|------|------|------|------|------
         Aggregated                                                                              1      1      1      1      1      1      1      1      2      2     14  48935

Error report
# occurrences      Error                                                                                               
------------------|---------------------------------------------------------------------------------------------------------------------------------------------
24467              GET /world: HTTPError('404 Client Error: Not Found for url: /world')                                
------------------|---------------------------------------------------------------------------------------------------------------------------------------------

```

</details>


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2952155/2ce22e52-8acb-fa9a-b2d6-8bf0d77af4d0.png)

結果のダウンロードもできます。 CSVとHTML形式でのダウンロードが可能です。

<details><summary>HTMLはこんな感じで出力されます。</summary>

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2952155/ec46bf65-e6eb-b09c-5abd-1e78c0ae786c.png)

</details>


## はじめかた

インストール手順に従ってインストールしてください。

https://docs.locust.io/en/stable/installation.html#installation

locustfile.pyを作成します。
```python:locustfile.py
from locust import HttpUser, task


class HelloWorldUser(HttpUser):
    @task
    def hello_world(self):
        self.client.get("/hello")
        self.client.get("/world")
```

/hello と /worldに負荷をかける手順です。

locustfile.pyがあるディレクトリで次のコマンドを実行してください。
```
locust
```

こんな感じの起動ログが出たら起動に成功してます。
```
% locust
[2023-11-30 21:33:27,891] enoMacBook-Pro.local/INFO/locust.main: Starting web interface at http://0.0.0.0:8089 (accepting connections from all network interfaces)
[2023-11-30 21:33:27,905] enoMacBook-Pro.local/INFO/locust.main: Starting Locust 2.19.1
```

http://0.0.0.0:8089 にアクセスすると、次のようなページが表示されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2952155/c98783b7-e27b-37c1-7c20-5ff47090b0b3.png)

Number of usersは同時ユーザーのピーク数を表します。例えば50にしたら、ある時点では50ユーザが作成され、アクセスが行われます。

Spawn rateはユーザーを生成するレート (1 秒あたりのユーザー数)です。

Hostは負荷テストを行う先を指定します。

他にもたくさんのオプションがあります。
参考： https://docs.locust.io/en/stable/configuration.html#all-available-configuration-options

### 実行してみよう

#### テストのためにローカルでサーバを起動します。 

http://localhost:8080/hello を作ります。
お好きな方法で起動してもらって大丈夫です。 

※次の手順をする時、もしnodeをインストールしていなければインストールからお願いします。
```
npm init -y
npm install --save-dev connect 
```

server.jsを作成します。
```js:server.js
const connect = require('connect');

const app = connect();

app.use('/hello', function(req, res) {
  res.end('Hello!');
});

app.listen(8080, function() {
  console.log('Hello World is running on port 8080...');
});
```

起動します。
```
node server.js
```

起動に成功するとこんな感じで表示されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2952155/2bbb5c56-a16c-8679-7281-d9708efeaae3.png)

### 負荷をかけてみよう
Hostに起動したローカルサーバのホスト( http://localhost:8080 )を指定します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2952155/51555adf-0111-5507-a304-473855bff2e1.png)

Start swarmingをクリックします。

リアルタイムに数値が更新されていく様子が確認できましたでしょうか。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2952155/6ae2629f-3364-f594-9095-07b73103881b.png)

今回はhelloだけ用意したので、helloは全て成功してworldは全て失敗していますね。
実際の負荷試験では、右上のFAILURESの値を見て、数値が上がってきたら失敗の原因を確認するために、FaiuresやExceptionsを確認したり、サーバーのログを見たり、一度負荷試験を止めて負荷を下げた試験をやったりします。

### いろんなことができます
豊富なevents handler :santa:
```py
from locust import HttpUser, events, task

@events.init.add_listener
def request_handler(request_type, name, response_time, response_length, response, context, exception, start_time, url, **kwargs):
    print('init')
```
initのタイミングで処理を挟めたりします。
token取得処理をinitでして global変数につめたり...

#### おまけ： こんな感じで使ってました
```:tree
.
├── docker-compose.yml
├── locust.conf
├── locustfile.py
├── sequential_task
│   ├── hoge.py
│   └── fuga.py
└── test_data
    ├── aaa.py
    └── bbb.xlsx
```

```py:test_data/aaa.py
def nanka_data() -> dict:
    return {
        "x": "なんかデータ", "y":"なんかでーた2"
    }
    
def nanka_morota_data(ukewatasi: str) -> dict:
    return {
        "x": "なんかもろたデータ", "y": ukewatasi
    }
```

```py:sequential_task/hoge.py
import test_data.aaa as t
from locust import task, SequentialTaskSet


class Hoge(SequentialTaskSet):
    # 1回目に実行されて欲しいtask
    @task
    def first(self):
        response = self.client.post("/aaa", json=t.nanka_data())
        self.ukewatasi = response.json()["XXX"]["YYY"]

    # 2回目に実行されて欲しいtask
    @task
    def second(self):
        self.client.post(
            "/bbb",
            json=t.nanka_morota_data(self.ukewatasi),
        )
```
SequentialTaskSetは宣言されているtaskの順序でtaskを実行してくれます。
ukewatasi のように selfに対して値を設定してtaskを跨いだ値の受け渡しが可能です。

1回だけlogin処理をしてtokenを受け渡すようなこともできます。
```js:locustfile.py
import requests
from sequential_task.hoge import Hoge

from locust import HttpUser, between, events


@events.init.add_listener
def locust_init(environment, **kwargs):
    global token
    token = requests.post(environment.host + "/login", {"id": "nankaId", "password": "nankaPassword"}).text


class Hoge(HttpUser):
    def on_start(self):
        self.client.headers = {"Authorization": "Bearer " + token }

    wait_time = between(60, 180)

    tasks = [Hoge]

```

classは実行したい手順(シナリオ)単位で用意したら便利でした。

```:locust.conf
host = http://localhost:8080
class-picker = true
```
locust.confにclass-pickerを指定することで1class(1シナリオ)ずつ負荷をかけることも可能になります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2952155/38ecdb84-dd77-c4a8-b817-cd06ab135c10.png)


##### 普段はdocker-compose upで起動してます
```yml
version: '3.5'

services:
  locust:
    image: locustio/locust
    ports:
     - "8089:8089"
    volumes:
      - ./:/mnt/locust
    working_dir: /mnt/locust
    command: --config=/mnt/locust/locust.conf -f /mnt/locust/locustfile.py
    extra_hosts:
      - "localhost:host-gateway"

```