背景
gunicornというWebサーバでは、ワーカーの種類を指定することができる。
- Sync Workers
- 同期処理のワーカー
- Async Workers
- 非同期処理のワーカー(geventなどを利用)
他にも指定可能な種類があるが1、この記事では上記2つのSync/Asyncワーカーの違いをコードで理解しよう。
1. Syncワーカの挙動
問題設定
理解を深めるにあたって、まずはごく簡単な問題を設定してみた。
問い
2人のユーザが、レスポンスに2秒かかる処理を1つのsyncワーカーに投げ続けた場合、
- 最大何RPS(request per second)をさばけるか?
- その時の平均レスポンス時間は何秒か?
予想
2人のユーザをAさん、Bさんとしよう。この時、AさんとBさんが交互にSyncワーカーを利用するのがもっとも効率が良いので、相手の待ち時間2秒を含めて、平均リスポンス時間は**4[秒]**と予想される(2)。また、RPSは4秒で大体2回の処理が行われると考えて2**0.5[rps]**と予想される。
(ま、堅っ苦しく書きましたが、Aさんの処理をA,Bさんの処理をB, 待ち時間を"-"で表現すれば
A-A-A-A...
-B-B-B-...
という感じで処理されるということです。"A-"や"B-"で2+2=4[秒]かかってしまうということ。)
実験手法
概要:
- webアプリケーションのコードを書く
- gunicornでそのアプリケーションサーバを立てる
- locustで負荷をかける。
詳細:
1 . flaskで書いたwebアプリケーションのコード:
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つ持つサーバを立てる。
gunicorn --workers=1 -k sync app_server_1:app
3 . locustで実行する
locust -H http://127.0.0.1:8000
2. Asyncワーカの挙動
次にgunicornからgeventを用いてAsyncワーカーの挙動を調べよう。
問題設定
今度は、非同期処理を取り入れるために上の処理を2つの処理に分割してためしてみる。
問い
2人のユーザが、レスポンスに2秒かかる処理を1つのsyncワーカーに投げ続ける。
ただし、その処理のうち1秒はpython標準ライブラリのtime.sleep処理であり、もう1秒はgeventの(バックエンドで処理を実行することができる)gevent.sleep処理を用いるとする。この時、
- 最大何RPS(request per second)をさばけるか?
- その時の平均レスポンス時間は何秒か?
(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を追加する。
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のワーカーを指定する。
gunicorn --workers=1 -k genv app_server_2:app
locust設定は変わらない。
実験結果
これも予想どおり。Asyncワーカーを用いることで処理が2倍も早くなっていますね。
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/
-
Gunicornのドキュメント: http://docs.gunicorn.org/en/stable/design.html ↩
-
"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 ↩
-
https://github.com/benoitc/gunicorn/blob/master/gunicorn/workers/ggevent.py#L61 ↩