初学者がlocust 1.0で負荷試験を行う

Webシステムの負荷試験を行う際、最近はjmeterではなく[locust](https://locust.io/)がオススメと聞いたので、試してみました。

- シナリオをpythonで書けるので複雑なケースに対応できる
- 多重度を上げる際、jmeterだと1ユーザ1スレッドになるが、locustなら少ないリソースでも負荷をかけやすい

結論から言うと、[公式ドキュメント通り](https://docs.locust.io/en/stable/quickstart.html)にやれば動きます。
が、[割と最近に出た1.0](https://github.com/locustio/locust/releases/tag/1.0)で破壊的変更があったようで、少し前の外部記事を元にすると動きません。そしてハマる（実際ハマった）。
この記事はそういった人がエラーメッセージから逆引きするもののつもりで書いています。

なお、いくつか動かない実装として他記事を引用していますが、あくまでlocustの破壊的変更によるものであり、引用元記事が間違っている訳ではないことは注釈しておきます。
細かな仕様や説明は当記事よりそちらの方が詳しいので、ご参考まで。


# 前提環境

- 端末： MacBook Pro Mojave
- 環境: docker for mac 2.0.0.3
- locust: 1.0.2

```
$ docker -v
Docker version 18.09.2, build 6247962
$ docker-compose -v
docker-compose version 1.23.2, build 1110ad01
```

とある事情で、諸々最新ではない。

# シングル構成でlocustを実行する

## コンテナでlocustをインストール

公式のlocustio/locustを使ってもいいのだが、cmdではなくentrypointで`locust`が指定されている。
locustの起動でエラーが起きるとコンテナが即死するだけなので、ログを追いにくくデバッグが辛い。

なのでまずはpythonイメージを元にして試してみる。
Webインターフェースを使えるように8089ポートを繋いでおく。

```
$ docker run -it -p 8089:8089 python:3.6 /bin/bash
```

locustのインストールはpipを叩くだけ。
このとき、古いやり方の`pip install locustio`だと`Locust package has moved from 'locustio' to 'locust'. Please update your reference (or pin your version to 0.14.6 if you dont want to update to 1.0)`と怒られる。

```
$ pip install locust
```

あと、そのままだとエディタがなくて辛いので、vimなどは適宜入れる。

```
$ apt-get update -y
$ apt-get install -y vim
```

## locustfileを作る

最初、[こちらの記事](https://qiita.com/rhirabay/items/bf9c6c2b75412668377b)を参考にしたのだが、これはこのままでは動かなかった。

```py:locustfile.py
from locust import HttpLocust, TaskSet, task, between, constant

class UserBehavior(TaskSet):
    @task(1)
    def profile(self):
        self.client.get("/sample", verify=False)

class WebsiteUser(HttpLocust):
    task_set = UserBehavior
    wait_time = constant(0)
```

実行すると、まず以下のエラーが出る。

```
ImportError: The HttpLocust class has been renamed to HttpUser in version 1.0. For more info see: https://docs.locust.io/en/latest/changelog.html#changelog-1-0
```

エラーメッセージ通り、ver1.0 の仕様変更によるもの。
このエラー自体は、単に`HttpLocust`を`HttpUser`に変えれば良い。
しかしそれ以外にも、この状態で起動すると、あとで実際にリクエストを送るタイミングで以下のエラーが発生する。

```
10b1687a2aee/ERROR/locust.user.task: Cannot choose from an empty sequence
Traceback (most recent call last):
  File "/usr/local/lib/python3.6/site-packages/locust/user/task.py", line 280, in run
    self.schedule_task(self.get_next_task())
  File "/usr/local/lib/python3.6/site-packages/locust/user/task.py", line 408, in get_next_task
    return random.choice(self.user.tasks)
  File "/usr/local/lib/python3.6/random.py", line 260, in choice
    raise IndexError('Cannot choose from an empty sequence') from None
IndexError: Cannot choose from an empty sequence
```

どうやら`TaskSet`周りの実装方法が変わったらしい。
`task_set = UserBehavior`は`tasks = {UserBehavior:1}`と書く必要がある。

結果、完成したファイルは以下の通り。
パス指定などについては[引用元記事](https://qiita.com/rhirabay/items/bf9c6c2b75412668377b)を参照まで。

```py:locustfile.py
from locust import HttpUser, TaskSet, task, between, constant

class UserBehavior(TaskSet):
    @task(1)
    def profile(self):
        self.client.get("/sample", verify=False)

class WebsiteUser(HttpUser):
    tasks = {UserBehavior:1}
    wait_time = constant(0)
```

## locustを起動する

以下を実行する。
（実際にはファイル名がこのままなら`-f`以降は省略できるが、常にこの名前に縛られるのも不便なので、今から明示的に指定）

```
$ locust -f ./locustfile.py
```

このあとでブラウザから `http://localhost:8089/` につなぐと、WebUIが出てくる

![スクリーンショット 2020-05-28 19.15.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/299048/a6a03f0b-9f23-bdeb-b196-900964fe159e.png)

- 最大ユーザ（接続）数
- 接続数の増加率（1秒間に何接続増やすか）
- アクセス先URL

図の例の場合、10ユーザになるまで1秒に2ユーザずつ増やし（1秒で2人、2秒で4人……）、10人に到達したら以降10ユーザでアクセスし続ける（10ユーザになったら止まるわけではない）。

なお、私の場合、アクセス先URLの指定をホスト名のみにしたら(e.g. `http://hoge`)、以下のエラーが発生した。

```
ConnectionError(MaxRetryError("HTTPConnectionPool(host='hoge.huga.com', port=80): Max retries exceeded with url: /sample(Caused by NewConnectionError('<urllib3.connection.HTTPConnection object at 0x....>: Failed to establish a new connection: [Errno -5] No address associated with hostname',))",),)
```

curlではホスト名のみでも正常にアクセスできたが、locustではFQDNにしないと接続できなかった。
詳細は調べていない。

実行した後はWeb上に色々記事があるので、そちらを参照まで。


# docker-compose で並列実行する。

せっかくlocustを使うので、master/slave構成で使いたいと言う欲が出る。

## docker-credential-desktopのエラー回避

docker for macの2.0.0.3のバグ（？）で、docker-composeの初期化で以下のエラーが出た

```
$ docker-compose up -d
Creating network "locust_default" with the default driver
Creating volume "locust_tests" with default driver
Pulling locust-master (locustio/locust:)...
Traceback (most recent call last):
  File "docker-compose", line 6, in <module>
  File "compose/cli/main.py", line 71, in main
  File "compose/cli/main.py", line 127, in perform_command
  File "compose/cli/main.py", line 1080, in up
  File "compose/cli/main.py", line 1076, in up
  File "compose/project.py", line 475, in up
  File "compose/service.py", line 352, in ensure_image_exists
  File "compose/service.py", line 1217, in pull
  File "compose/progress_stream.py", line 101, in get_digest_from_pull
  File "compose/service.py", line 1182, in _do_pull
  File "site-packages/docker/api/image.py", line 381, in pull
  File "site-packages/docker/auth.py", line 48, in get_config_header
  File "site-packages/docker/auth.py", line 96, in resolve_authconfig
  File "site-packages/docker/auth.py", line 127, in _resolve_authconfig_credstore
  File "site-packages/dockerpycreds/store.py", line 25, in __init__
dockerpycreds.errors.InitializationError: docker-credential-desktop not installed or not available in PATH
[60116] Failed to execute script docker-compose
```

確かに、`/Applications/Docker.app/Contents/Resources/bin/`をみても`docker-credential-desktop`なんて存在しない。

https://github.com/docker/for-mac/issues/3785 によると、2.1.0.0では解消されているので、更新すれば良い。
が、何かしらの事情で更新できない場合、`~/.docker/config.json`の`credsStore`を手修正して`desktop`から`osxkeychain`にすれば動くようになる。

```json:config.json
{
  "auths" : {
    "https://index.docker.io/v1/" : {

    }
  },
  "stackOrchestrator" : "swarm",
  "experimental" : "disabled",
  "credsStore" : "desktop <= ここをosxkeychainに変える",
  "credSstore" : "osxkeychain"
}
```

チケットに書いてある通り、確かにどう見てもtypoに見える。。。

## docker-composeファイルを作る

[こちらの記事](https://qiita.com/naoki114/items/7ae5a045bb08c4950b04#%E7%92%B0%E5%A2%83%E6%A7%8B%E7%AF%89)を参考にしたのだが、これもそのままでは動かなかった。

ちなみに、このYAMLはアンカーとエイリアスがあるが、パース後に実際に生成されるオブジェクトが知りたい場合は、[YAMLをjsonにするオンラインパーサがある](http://nodeca.github.io/js-yaml/)ので、活用するとわかりやすいかも。

```yaml:docker-compose.yaml
version: "3.4"

x-common: &common
  image: locustio/locust
  environment: &common-env
    TARGET_URL: http://example.com
    LOCUSTFILE_PATH: /tests/basic.py
  volumes:
    - tests/:/tests

services:
  locust-master:
    <<: *common
    ports:
      - 8089:8089
    environment:
      <<: *common-env
      LOCUST_MODE: master

  locust-slave:
    <<: *common
    environment:
      <<: *common-env
      LOCUST_MODE: slave
      LOCUST_MASTER_HOST: locust-master
```
docker-composeを実行しても、Exited(1)で即死してしまう。
なお、エラーログは`docker logs (コンテナID)`で見れる

```shell
$ docker-compose up -d
Pulling locust-master (locustio/locust:)...
latest: Pulling from locustio/locust
cbdbe7a5bc2a: Pull complete
26ebcd19a4e3: Pull complete
a29d43ca1bb4: Pull complete
979dbbcf63e0: Pull complete
30beed04940c: Pull complete
d4954e7e8c67: Pull complete
45867b33845c: Pull complete
2be134228162: Pull complete
Creating locust_locust-slave_1  ... done
Creating locust_locust-master_1 ... done
$ docker ps -a
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS                     PORTS               NAMES
2fd911a1ee59        locustio/locust     "locust"            5 minutes ago       Exited (1) 5 minutes ago                       locust_locust-master_1
9c35f427c9eb        locustio/locust     "locust"            5 minutes ago       Exited (1) 5 minutes ago                       locust_locust-slave_1
```

[公式](https://docs.locust.io/en/stable/running-locust-docker.html)を見ると、パラメータを環境変数から取らなくなった様子。
`LOCUSTFILE_PATH: /tests/basic.py` と書いても効果はなく、以下のエラーが出る。

```
Could not find any locustfile! Ensure file ends in '.py' and see --help for available options.
```

`command`で`locust`コマンドへの引数を渡せるので、それでコントロールする。
entrypointが定義されたコンテナイメージでのcmdの扱いについては、[こちら](https://qiita.com/uehaj/items/e6dd013e28593c26372d)がわかりやすい。


なお、volumesは `tests/`ではなく`./tests`と書く。
でないとtestsという名前付きVolumeと見なされ、以下のエラーが発生する。

```
ERROR: Named volume "tests:/tests:rw" is used in service "locust-master" but no declaration was found in the volumes section.
```

結果、以下だと動く。

```yaml:docker-compose.yaml
version: "3.4"

x-common: &common
  image: locustio/locust
  volumes:
    - tests/:/tests

services:
  locust-master:
    <<: *common
    ports:
      - 8089:8089
    command: -f /tests/basic.py --master -H http://example.com

  locust-slave:
    <<: *common
    command: -f /tests/basic.py --worker --master-host locust-master
```

なお、workerの数を増やしたい場合、yamlを変更する必要はなく、`docker-compose`を実行する際に`--scale`を指定すれば良い。

```
$ docker-compose up -d --scale locust-slave=3
Creating network "locust_default" with the default driver
Creating locust_locust-slave_1  ... done
Creating locust_locust-slave_2  ... done
Creating locust_locust-slave_3  ... done
Creating locust_locust-master_1 ... done
```

![スクリーンショット 2020-05-29 10.43.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/299048/bb6cebd4-9ced-c2c8-521d-1083fc437d2a.png)

これで気軽に分散負荷試験をできるようになった！

続編として、locustをECSで動かす記事も書いたので、ご参考まで。
https://qiita.com/sekikatsu/items/e6fad26fded58f92fb25