LoginSignup
9

More than 5 years have passed since last update.

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

Last updated at Posted at 2018-02-25

背景

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[秒]と予想される(2)。また、RPSは4秒で大体2回の処理が行われると考えて20.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アプリケーションのコード:

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つ持つサーバを立てる。


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

3 . locustで実行する

locust -H http://127.0.0.1:8000

結果
これは、ま、予想通り。
スクリーンショット 2018-02-25 20.47.02.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人を捌くので21[rps]と予想される。

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

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のワーカーを指定する。


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

locust設定は変わらない。

実験結果
これも予想どおり。Asyncワーカーを用いることで処理が2倍も早くなっていますね。
スクリーンショット 2018-02-25 21.08.48.png

3. Asyncワーカの調査(続き)

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

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

まとめ

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

参考なったサイト


  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 

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9