gunicornのSync/Asyncワーカーの挙動を調べる

# 背景
gunicornというWebサーバでは、ワーカーの種類を指定することができる。

+ Sync Workers
    + 同期処理のワーカー
+ Async Workers
    + 非同期処理のワーカー（geventなどを利用）

他にも指定可能な種類があるが[^1]、この記事では上記2つのSync/Asyncワーカーの違いをコードで理解しよう。


# 1. Syncワーカの挙動
**問題設定**
理解を深めるにあたって、まずはごく簡単な問題を設定してみた。
>問い
2人のユーザが、レスポンスに2秒かかる処理を1つのsyncワーカーに投げ続けた場合、
1. 最大何RPS（request per second）をさばけるか？
2. その時の平均レスポンス時間は何秒か？


**予想**
2人のユーザをAさん、Bさんとしよう。この時、AさんとBさんが交互にSyncワーカーを利用するのがもっとも効率が良いので、相手の待ち時間2秒を含めて、平均リスポンス時間は**4[秒]**と予想される（２）。また、RPSは4秒で大体2回の処理が行われると考えて[^2]**0.5[rps]**と予想される。


(ま、堅っ苦しく書きましたが、Aさんの処理をA,Bさんの処理をB, 待ち時間を"-"で表現すれば
> 
A-A-A-A...
-B-B-B-...

という感じで処理されるということです。"A-"や"B-"で2+2=4[秒]かかってしまうということ。)

**実験手法**
概要：
1. webアプリケーションのコードを書く
2. gunicornでそのアプリケーションサーバを立てる
3. locustで負荷をかける。

詳細：
1 . flaskで書いたwebアプリケーションのコード:

```python:app_server_1.py
import flask, time

app = flask.Flask(__name__)

@app.route('/', methods=['GET'])
def test():
    message='test'
    time.sleep(2) # 2秒待つ
    return flask.Response(response=message, status=200, mimetype='application/json')
```

2 . gunicornでsyncワーカーを1つ持つサーバを立てる。

```shell

gunicorn --workers=1 -k sync  app_server_1:app

```

3 . locustで実行する

```
locust -H http://127.0.0.1:8000
```


**結果**
これは、ま、予想通り。
<img width="986" alt="スクリーンショット 2018-02-25 20.47.02.png" src="https://qiita-image-store.s3.amazonaws.com/0/237873/ff1948d0-390a-47d6-fce4-cec461e94589.png">


# 2. Asyncワーカの挙動
次にgunicornからgeventを用いてAsyncワーカーの挙動を調べよう。

**問題設定**
今度は、非同期処理を取り入れるために上の処理を2つの処理に分割してためしてみる。
>問い
2人のユーザが、レスポンスに2秒かかる処理を1つのsyncワーカーに投げ続ける。
ただし、その処理のうち1秒はpython標準ライブラリのtime.sleep処理であり、もう1秒はgeventの（バックエンドで処理を実行することができる）gevent.sleep処理を用いるとする。この時、
1. 最大何RPS（request per second）をさばけるか？
2. その時の平均レスポンス時間は何秒か？

（gevent.sleepがIO待ちの状態を模擬し、time.timeがCPUを使う挙動を模擬していると考えてください）

**予想**
time.sleepの場合はgeventはバックエンドで処理をしないから、gevent.sleepの1秒間を利用してAさんBさんが交互に処理を実行していくこととなる。つまり、AさんBさんそれぞれで前半の処理を1, 後半の処理を2とすると、
>A1A2A1A2A1...
 --B1B2B1B2...

というように処理をしていくのがベストである。なので、平均レスポンス時間は**2[秒]**と予想され、rpsは2秒間でだいたい2人を捌くので[^2]、**1[rps]**と予想される。


**実験手法**
上と同じだが、アプリケーションサーバのコードは以下のようにgevent.timeを追加する。

```python:app_server_2.py
import gevent
import flask, time

app = flask.Flask(__name__)

@app.route('/', methods=['GET'])
def test():
    message='test'
    time.sleep(1) # 1秒待つ
    gevent.sleep(1) #1秒待つ（バックエンドで処理走る）
    return flask.Response(response=message, status=200, mimetype='application/json')
```
また、gunnicornでgenvのワーカーを指定する。

```shell

gunicorn --workers=1 -k genv  app_server_2:app

```

locust設定は変わらない。

**実験結果**
これも予想どおり。Asyncワーカーを用いることで処理が**2倍**も早くなっていますね。
<img width="980" alt="スクリーンショット 2018-02-25 21.08.48.png" src="https://qiita-image-store.s3.amazonaws.com/0/237873/56b1b3ad-fa7d-c124-6668-ef3cd167c968.png">


# 3. Asyncワーカの調査（続き）
で、上のように実験していたのですが、実はgunicorn.timeをtime.sleepに変更しても挙動がかわらなくてびっくりしました。だってtime.sleepはpythonが止まるだけなのでバックグラウンドで処理とかできないはず...??と思っていたからです。[^3]

なぜじゃーと思っていたら、どうやらgunicornからgeventワーカーを呼び出したときには、初期化の際にmonkey patchが当てられてtime.sleepがgeventで処理できるsleepに変更されているみたいです[^4]。なるほどねー。


# まとめ
+ IO待ちが多いプログラムだったら、Asyncワーカーを検討しよう。

# 参考なったサイト
+ はむかず！
    + 「bottleとgeventによる高速軽量非同期ウェブアプリ」
    +  http://hamukazu.com/2016/01/04/bottle_and_gevent/
    
[^1]: Gunicornのドキュメント: http://docs.gunicorn.org/en/stable/design.html
[^2]: ここ厳密性がないですが無限大の時間をとってその間のレスポンス回数を考えれば同じと分かる（たぶん）
[^3]: "On the example you gave, time.sleep(2) will put the process to sleep inside the operating system, so gevent's scheduler won't run and won't be able to switch to another greenlet."  https://stackoverflow.com/questions/12040880/how-to-avoid-blocking-code-in-python-with-gevent
[^4]: https://github.com/benoitc/gunicorn/blob/master/gunicorn/workers/ggevent.py#L61