Locustの気持ち

[ただの集団 Advent Calendar 2018](https://adventar.org/calendars/3447)の6日目

## はじめに
今回はLocustの中身の話をします。

当初は、別の記事を書くためにPythonの負荷テストツールのLocustが必要だったので、Locustの環境を準備して作業を進めていました。ただ、使っていくうちにLocustの実装が気になり、コードジャンプで深追いしていたらAdvent Calendarの担当の日ギリギリになっていました。。。

新卒エンジニアの僕に、経験あるエンジニアの方がもつような小ネタ用の引き出しがあるわけもなく、他の記事を書くのは到底間に合わない。コードもせっかく追ったのにもったいない。もうLocustでいくか、という流れでLocustの記事を書くことにしました。

ただ、気になって調べたことのすべてが理解できたわけではありません。そこで、この記事では、公式ドキュメントに載っているクイックスタートの実装をもとに、知っておくとコードが追いやすい英単語、クイックスタートをクイックに理解するための注意点、そして自分が気になったLocustの中身の順で書いていきます。


## ドキュメントとコードを追うときに意味を知っておきたい英単語
ドキュメントとコードを読む上で、下記の4単語だけ知っていれば問題ないです。
特にswarmとspawnは押さえておきましょう！

ただ僕は昆虫が苦手なので、Locustをイナゴと脳内で訳せるようになった瞬間から、とても嫌な気持ちになりながらLocustの中身を調べるはめになりました。

| 英単語 | 意味 |
|:-----------|:------------|
| locust       | イナゴ        |
| swarm     | 群れ      | 
| spawn       | 発生させる, 引き起こす        | 
| hatch         | 孵化する          |


## クイックスタートを始めるときの知っておくと良い関数

[公式ドキュメントのクイックスタート](https://docs.locust.io/en/stable/quickstart.html)から必要なコードだけ抜粋

```python
from locust import HttpLocust, TaskSet, task

class UserBehavior(TaskSet):
    @task(2)
    def index(self):
        self.client.get("/")

    @task(1)
    def profile(self):
        self.client.get("/profile")

class WebsiteUser(HttpLocust):
    task_set = UserBehavior
```

### 1. taskデコレーター
サンプルコードを初めて見たとき、task(1)、task(2)となっていたので、この1、２はリクエストを送る順番かと思っていたら違いました。

早く動かしたい気持ちでいっぱいであまり気にしなかったことを反省。

locust/core.pyのtask関数の実装を見ると明らかで、引数に渡していたのはweightでした。

```python
def task(weight=1):
    def decorator_func(func):
        func.locust_task_weight = weight
        return func
    
    if callable(weight):
        func = weight
        weight = 1
        return decorator_func(func)
    else:
        return decorator_func
```

このweightを使うことによって、対象となる関数を使って、どれくらいの割合でリクエスト送るかということを明示的に宣言することが出来ます。
実際にprofile関数のtaskデコレーターの引数を1にしたときと5にしたときの結果を表示してみると...


```python
# weight=1のとき
@task(1)
def index(self):
    ...

@task(1)
def profile(self):
    ...

"""tasksの中身
[<function UserBehavior.profile at 0x10b76b048>,
 <function UserBehavior.index at 0x10b76b0d0>]
"""
```

```python
# weight=5のとき
@task(1)
def index(self):
    ...

@task(5)
def profile(self):
    ...

"""tasksの中身
[<function UserBehavior.profile at 0x10b76b048>,
 <function UserBehavior.profile at 0x10b76b048>, 
 <function UserBehavior.profile at 0x10b76b048>,
 <function UserBehavior.profile at 0x10b76b048>,
 <function UserBehavior.profile at 0x10b76b048>,
 <function UserBehavior.index at 0x10b76b0d0>]
"""
```

結果を見て分かるように、生成されるリクエストの数が違うことが分かります。
(locust/core.py/Tasksetあたりの実装をみると分かると思います。)

この生成されたリクエスト群がtasksリストで管理され、locustで実際にリクエストを送る際には、このリストの中からランダムでリクエストが送られます。

> If tasks is a list, the task to be performed will be picked randomly. 
(locust/core.pyの256行目から引用)

```python
class TaskSet(object):
    ...
    def get_next_task(self):
        return random.choice(self.tasks)
```

じゃあ、順番通りにリクエストを送りたいときはどうすればよいのだろう...
これが次の話になります。

### 2. seq_taskデコレーター
Locustは負荷テストツールですね。負荷テストをする際は、シナリオ通りに負荷をかけていきたいときもありますよね。このようなときは、taskを定義する際にseq_taskデコレーターを使いましょう。

```python
# 実装例
from locust import HttpLocust, TaskSet, task, seq_task

class UserBehavior(TaskSet):
    @seq_task(1)
    @task(1)
    def index(self):
        self.client.get("/")

    @seq_task(2)
    @task(1)
    def profile(self):
        self.client.get("/profile")
```
たったこれだけです！！！

## Locustの中身
最後はlocust関数のコードの中身を追っていったので、登場人物、登場人物が加わることによって生まれた特徴、そしてmainコードの処理手順のまとめの順番で書いていきます。

### 1. 登場人物
|登場人物名 | 役割 | 公式ドキュメントリンク |
|:-----------|:------------|:------------|
| greenlet       | 疑似スレッド。<br> 実際にはコルーチンである。        |[greenlet](https://learn-gevent-socketio.readthedocs.io/en/latest/greenlets.html)|
| gevent     | Pythonのネットワーキングライブラリ。 <br> イベントループ上に同期的APIを提供するために、内部でgreenletを使う。|[gevent](https://learn-gevent-socketio.readthedocs.io/en/latest/gevent.html)|



### 2. 特徴
greenletというスレッドでプログラムを実行するが、greenletはあくまで疑似スレッドで、geventのeventsをもとにeventを切り替えながら処理が行われるイベントバウンドな非同期的かつ協調的マルチタスクとなっています。このイベントバウンドなシステムのおかげで、１つのマシーンで1000以上のユーザーでもリクエストを生成することが可能です。


### 3. 処理の流れ
locustのmain関数の中でメインとなる処理だけを抜粋しました。

(1) 起動時のオプションコマンドを取得する

```python
parser, options, arguments = parse_options()
```

(2) オプションコマンドからlocustfileを呼び出す

```python
locustfile = find_locustfile(options.locustfile)
```

(3) locustfileの情報からHTTPLocustクラスを継承した子クラスを取り出し、この子クラスからlocustインスタンスを生成する

```python
docstring, locusts = load_locustfile(locustfile)
# print(locusts)
# {'WebsiteUser': <class 'locustfile.WebsiteUser'>}
```

(4) locusts_dictから子クラスオブジェクトのリスト取得しlocust_classesに保存する

```python
locust_classes = list(locusts.values())
# print(locust_classes)
# [<class 'locustfile.WebsiteUser'>]
```

(5) geventのspawn関数の引数にflaskのコードが書かれているweb.pyの中のstart関数とlocust_classes、optionsを渡し、geventを通して、負荷をかけるアプリにコネクトする準備をする。
  
```python
main_greenlet = gevent.spawn(web.start, locust_classes, options)
```

(7) Locustを動かすもととなるRunnerクラスに必要な情報を記載する。
  
```python
runners.locust_runner = LocalLocustRunner(locust_classes, options)
```

(8) ここまでに用意したgreenlet(コルーチン)をrunする。

```python
main_greenlet.join()
```

gevent.spawn(...)とmain_greenlet.join()は[geventのチュートリアル](http://sdiehl.github.io/gevent-tutorial/)の流れがそのまま実装されていますね！

コードをさらに追っていくと、gevent/Semaphoreクラスのacquire関数とrelease関数がロックの管理をしているのも分かります。
コードジャンプ機能があるとどこまでも終えてしまいきりがないので一旦これで終わりにします！

## 終わりに
ApacheBeamをPythonで実装する記事を書くつもりでしたが、まさか昆虫に関する記事がエンジニア記事初投稿になるとは...

ただ、コードの中身を追っていくのはすごく楽しいなと思いました。コードジャンプがあるIDEは最高です！おかげで、関数デコレーターの復習もでき、Ellipsisというオブジェクトにも出会え、ずっと気になっていた同期/非同期・並行/並列・ブロッキング/ノンブロッキングの理解を深めるための一歩を歩み始めることが出来ました。

次回の投稿こそは、Beamの記事をかけるといいな！

## 参照
> 
1. [bottleとgeventによる高速軽量非同期ウェブアプリ](http://hamukazu.com/2016/01/04/bottle_and_gevent/)
2. [イケてるエンジニアになろうシリーズ 〜メモリとプロセスとスレッド編〜](https://moro-archive.hatenablog.com/entry/2014/09/11/013520)
3. [2015年Webサーバアーキテクチャ序論](https://blog.yuuk.io/entry/2015-webserver-architecture#%E3%82%A4%E3%83%99%E3%83%B3%E3%83%88%E9%A7%86%E5%8B%95%E3%83%A2%E3%83%87%E3%83%AB)
4. [ノンブロッキングI/Oと非同期I/Oの違いを理解する](https://blog.takanabe.tokyo/2015/03/26/240/)
5. [非同期とノンブロッキングとあと何か](https://keens.github.io/blog/2017/05/19/hidoukitononburokkingutoatonanika/)
6. [コンビニでわかるノンブロッキングIO](http://takezoe.hatenablog.com/entry/2014/10/12/000000)
7. [Locustで動的にタスクを切り替え](http://honeplus.blog50.fc2.com/blog-entry-189.html)
8. [Google Container EngineとLocustを使って負荷テスト環境をさくっと作る](http://www.mpon.me/entry/2017/11/07/005053)
9. [Implementing dynamic allocationof user load in a distributed load testing framework](http://uu.diva-portal.org/smash/get/diva2:685934/FULLTEXT01.pdf)
10. [JMeter vs. Locust - Which One Should You Choose?](https://www.blazemeter.com/blog/jmeter-vs-locust-which-one-should-you-choose)
11. [Concurrency vs Event Loop vs Event Loop + Concurrency](https://medium.com/@tigranbs/concurrency-vs-event-loop-vs-event-loop-concurrency-eb542ad4067b)
12. [Why do I need an the event loop in gevent?](https://stackoverflow.com/questions/51410030/why-do-i-need-an-the-event-loop-in-gevent)
13. [What are the differences between event-driven and thread-based server system?](https://stackoverflow.com/questions/25280207/what-are-the-differences-between-event-driven-and-thread-based-server-system)
14. [CPU usage vs Number of threads](https://stackoverflow.com/questions/12892941/cpu-usage-vs-number-of-threads)
15. [Greenlet Vs. Threads](https://stackoverflow.com/questions/15556718/greenlet-vs-threads)
16. [gunicornのSync/Asyncワーカーの挙動を調べる](https://qiita.com/iceman-k/items/86d54b07defa47fd35a9)
17. [ZeroMQ覚書](https://qiita.com/gwappa/items/9677e1ea4adcf2d457f4)
18. [Python: 組み込みのソケットサーバをマルチスレッド化する](https://blog.amedama.jp/entry/2017/09/23/172252)
19. [Python: ソケットプログラミングのアーキテクチャパターン](https://blog.amedama.jp/entry/2017/03/29/080000)
20. [MultiprocessingとMultithreading?](https://www.quark.kj.yamagata-u.ac.jp/~hiroki/python/?id=15)
21. [Pythonでselect, poll, epoll, kqueue試してみた](https://blog.freedom-man.com/select-poll-epoll/)
22. [プロセススケジューラ](https://ja.osdn.net/projects/linux-kernel-docs/wiki/1.5%E3%80%80%E3%83%97%E3%83%AD%E3%82%BB%E3%82%B9%E3%82%B9%E3%82%B1%E3%82%B8%E3%83%A5%E3%83%BC%E3%83%A9)
23. [非同期処理と同期処理の実装パターンと特徴](http://ossforum.jp/node/753)
24. [Javaの並行処理を理解する（入門編）](https://qiita.com/bremen/items/b87bff49565fc9abc91b)
25. [ブロッキングとかノンブロッキングを理解したい](https://qiita.com/kimullaa/items/c083757078406da17825)