Sock Shopの負荷テストをlocustでやってみた

# 更新
最終的な`locustfile.py`に`self.client.get("/basket.html")`と`self.client.post("/orders")`を入れ忘れていたので追記しました．
# やること
マイクロサービスのデモアプリである[Sock Shop](https://microservices-demo.github.io/)について，locustを用いて負荷テストをしてみます．



# 作業
Sock Shopでは開発者が用意した[load test](https://github.com/microservices-demo/load-test)がありますが，こちらをreadme通りに試してみたところ上手くいきませんでした．
```
Default Locust file: locustfile.py
Will run locustfile.py against http://hikida-v6:30001. Spawning 8089 clients and 10 total requests.
Traceback (most recent call last):
  File "/home/hikida/.local/bin/locust", line 8, in <module>
    sys.exit(main())
  File "/home/hikida/.local/lib/python3.8/site-packages/locust/main.py", line 68, in main
    docstring, _user_classes, shape_class = load_locustfile(_locustfile)
  File "/home/hikida/.local/lib/python3.8/site-packages/locust/util/load_locustfile.py", line 58, in load_locustfile
    imported = source.load_module()
  File "<frozen importlib._bootstrap_external>", line 522, in _check_name_wrapper
  File "<frozen importlib._bootstrap_external>", line 1027, in load_module
  File "<frozen importlib._bootstrap_external>", line 852, in load_module
  File "<frozen importlib._bootstrap>", line 265, in _load_module_shim
  File "<frozen importlib._bootstrap>", line 702, in _load
  File "<frozen importlib._bootstrap>", line 671, in _load_unlocked
  File "<frozen importlib._bootstrap_external>", line 848, in exec_module
  File "<frozen importlib._bootstrap>", line 219, in _call_with_frames_removed
  File "/home/hikida/load-test/locustfile.py", line 27, in <module>
    class Web(HttpLocust):
  File "/home/hikida/.local/lib/python3.8/site-packages/locust/util/deprecation.py", line 27, in __new__
    raise ImportError(deprecation_message)
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
done
```
恐らくlocustのversion違いが原因かなと

先程のgitには`locustfile.py`があります．
```locustfile.py
import base64

from locust import HttpLocust, TaskSet, task
from random import randint, choice


class WebTasks(TaskSet):

    @task
    def load(self):
        base64string = base64.encodestring('%s:%s' % ('user', 'password')).replace('\n', '')

        catalogue = self.client.get("/catalogue").json()
        category_item = choice(catalogue)
        item_id = category_item["id"]

        self.client.get("/")
        self.client.get("/login", headers={"Authorization":"Basic %s" % base64string})
        self.client.get("/category.html")
        self.client.get("/detail.html?id={}".format(item_id))
        self.client.delete("/cart")
        self.client.post("/cart", json={"id": item_id, "quantity": 1})
        self.client.get("/basket.html")
        self.client.post("/orders")


class Web(HttpLocust):
    task_set = WebTasks
    min_wait = 0
    max_wait = 0
```
こちらをこのまま`locust -f locustfile.py`で実行したところ
```
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
```
やはりversionの問題のようです．
また，`base64`の書き方もよくなさそうなので書き直します．

`locust -f locustfile.py`で実行してみます．
```
[2022-12-22 06:12:27,320] hikida-db/INFO/locust.main: Shutting down (exit code 1)
Type     Name                                  # reqs      # fails |    Avg     Min     Max    Med |   req/s  failures/s
--------|------------------------------------|-------|-------------|-------|-------|-------|-------|--------|-----------
GET      /                                        151     0(0.00%) |     43       1      49     44 |    4.98        0.00
GET      /basket.html                             150     0(0.00%) |     44      41      49     44 |    4.95        0.00
DELETE   /cart                                    151     0(0.00%) |      8       5      88      7 |    4.98        0.00
POST     /cart                                    151     0(0.00%) |     19      11     135     14 |    4.98        0.00
GET      /catalogue                               151     0(0.00%) |      7       4      26      6 |    4.98        0.00
GET      /category.html                           151     0(0.00%) |      3       1      45      2 |    4.98        0.00
GET      /detail.html?id=03fef6ac-1896-4ce8-bd69-b798f85c6e0b      22     0(0.00%) |      3       1      43      2 |    0.73        0.00
GET      /detail.html?id=3395a43e-2d88-40de-b95f-e00e1502085b      14     0(0.00%) |      4       1      47      2 |    0.46        0.00
GET      /detail.html?id=510a0d7e-8e83-4193-b483-e27e09ddc34d      21     0(0.00%) |      8       1      49      2 |    0.69        0.00
GET      /detail.html?id=808a2de1-1aaa-4c25-a9b9-6612e8f29a38       9     0(0.00%) |      6       1      47      2 |    0.30        0.00
GET      /detail.html?id=819e1fbf-8b7e-4f6d-811f-693534916a8b      19     0(0.00%) |      6       1      50      2 |    0.63        0.00
GET      /detail.html?id=837ab141-399e-4c1f-9abc-bace40296bac      16     0(0.00%) |      4       1      43      2 |    0.53        0.00
GET      /detail.html?id=a0a4f044-b040-410d-8ead-4de0446aec7e      19     0(0.00%) |      3       1      44      2 |    0.63        0.00
GET      /detail.html?id=d3588630-ad8e-49df-bbd7-3167f7efb246      17     0(0.00%) |      9       1      49      2 |    0.56        0.00
GET      /detail.html?id=zzz4f044-b040-410d-8ead-4de0446aec7e      14     0(0.00%) |     15       1      51      2 |    0.46        0.00
GET      /login                                   151     0(0.00%) |     15      10      96     13 |    4.98        0.00
POST     /orders                                  150   22(14.67%) |     50      24     188     47 |    4.95        0.73
--------|------------------------------------|-------|-------------|-------|-------|-------|-------|--------|-----------
         Aggregated                              1357    22(1.62%) |     22       1     188     13 |   44.74        0.73
```
エラーが出ずに動作しましたが，`/orders`のfailが目立ちます．なぜだ．．．

考えていたところ，fail数22と同じ数のリクエストが送られている商品があることに気が付きます．

`GET      /detail.html?id=03fef6ac-1896-4ce8-bd69-b798f85c6e0b      22`

どうやらSock Shopでは100$以上の注文をするとこのようにエラーが発生するようです．
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2747697/028b4dd2-99ee-ad54-1420-63599844292a.png)
この画像の`Holy`という商品がカートに入れられてしまうとTotalが100$を超えてしまいます．

`locustfile.py`ではカタログにある商品をランダムで選ぶように設定されているので`Holy`が選ばれないようにする必要がありそうです．

Sock Shopを編集して値段や上限の変更が出来れば良いのですが難易度が高そうなので，`locustfile.py`を編集する方向でいきます．

最終的な`locustfile.py`がこちら
```
import base64
from locust import HttpUser, task
from random import randint, choice


class WebTasks(HttpUser):

    @task
    def load(self):
        base64string = base64.b64encode(('%s:%s' % ('user', 'password')).encode()).decode()

        catalogue = self.client.get("/catalogue").json()

        ## delete Holy
        count = 0
        for dic in catalogue:
            if dic['name'] == 'Holy':
                catalogue.pop(count)
            count += 1

        category_item = choice(catalogue)
        item_id = category_item["id"]

        self.client.get("/")
        self.client.get("/login", headers={"Authorization":"Basic %s" % base64string})
        self.client.get("/category.html")
        self.client.get("/detail.html?id={}".format(item_id))
        self.client.delete("/cart")
        self.client.post("/cart", json={"id": item_id, "quantity": 1})
        self.client.get("/basket.html")
        self.client.post("/orders")
```
`catalogue = self.client.get("/catalogue").json()`の中身は辞書型となっていて`Holy`は先頭に記述されていました．順番が変わることを考慮して辞書内から`Holy`を探して削除しています．

無事正常に動作しました！
```
[2022-12-22 07:06:47,049] hikida-db/INFO/locust.main: Shutting down (exit code 0)
Type     Name                                  # reqs      # fails |    Avg     Min     Max    Med |   req/s  failures/s
--------|------------------------------------|-------|-------------|-------|-------|-------|-------|--------|-----------
~~~
POST     /orders                                  147     0(0.00%) |     42      28     125     39 |    5.40        0.00
--------|------------------------------------|-------|-------------|-------|-------|-------|-------|--------|-----------
         Aggregated                              1328     0(0.00%) |     20       1     125     12 |   48.75        0.00
```

# おわりに
私は大学でマイクロサービスについて研究をしています．今回はSock Shopを用いて実験している中で負荷テストが必要となったので，locustを用いてやってみました．研究する上で今回のように，既に用意されているファイルの書き方が古く上手くいかないケースが何度かあり大変でした．同じく悩んでいる方にこの記事が役立てば幸いです．

何かあれば気軽にコメントお願いします！