前回は sync worker が uwsgi の置き換えになるかという視点で調べていましたが、次は gthread について調べてみます。 sync worker はシンプルに次のような処理をしていました。
- accept (複数socketをlistenしている時は select->accept)
- リクエストを受け取る
- wsgiアプリを実行する
- レスポンスを返す
- close
gthread は次のような処理になります。
- メインスレッド
- epoll等で listen socket やリクエスト待ちの接続を監視する
- listen socket の場合: accept してスレッドプールに投げる
- リクエスト待ちの接続の場合: スレッドプールに投げる
- スレッドプール
- リクエストを受け取る
- wsgiアプリを実行する
- レスポンスを返す
- keep-alive可能な場合はメインスレッドの監視対象に戻す。それ以外はclose
ポイント
- keep-aliveに対応している。keep-alive中の接続はスレッドを占有しない。
- スレッド数の上限以上にkeep-alive可能。worker_connectionsという設定で接続数の上限を指定し、その接続数に到達したら新規acceptを止める。
nginx などのリバースプロキシの後ろで動かす場合、クライアントとのkeep-aliveはリバースプロキシ側でやっているので、 gthread の worker_connections はスレッド数と同じにするのが良いと思います。でないと余裕があるworkerよりも先にスレッドが埋まってるworkerがacceptしてしまう可能性があるので、負荷の偏りがひどくなり、99%tileのレスポンスタイムが酷く悪化する可能性があります。
freethreadingとの相性
よく知られている通りPythonにはGILがあり複数スレッドが同時に動くことができません。gthreadの場合メインスレッドもGILを必要とするので競合が発生しやすくなります。競合するとオーバーヘッドが発生するので、例えば3スレッドがGILの取りあいをしているとスレッドあたりの性能が3分の1以下に下がることもあり得ます。
もし1workerでそのマシンのCPU利用率を十分に出せるまでスケールするのであれば1workerだけで動かすことも可能で、その場合は上で述べたworker間の負荷分散の問題にも悩まされなくなります。
試しに gthread を 3.14と3.14tで動かしてみます。
# gunicorn_conf.py workers = 1 threads = 2 accesslog = None bind = "127.0.0.1:5000"
# benchmark_app.py def application(environ, start_response): body = b"Hello, World!\n" start_response( "200 OK", [("Content-Type", "text/plain"), ("Content-Length", str(len(body)))], ) return [body]
起動コマンド:
$ .venv/bin/gunicorn -c gunicorn_conf.py benchmark_app:application
ベンチマークコマンド:
$ ./hey -c 2 -z 10s http://127.0.0.1:5000/
結果
# 3.14 Summary: Total: 10.0003 secs Slowest: 0.0046 secs Fastest: 0.0001 secs Average: 0.0002 secs Requests/sec: 10032.0812 Total data: 1404536 bytes Size/request: 14 bytes # 3.14t Summary: Total: 10.0018 secs Slowest: 0.0057 secs Fastest: 0.0001 secs Average: 0.0001 secs Requests/sec: 17708.5014 Total data: 2479638 bytes Size/request: 14 bytes
2倍にまではなりませんでしたが、かなりの性能向上をしているのがわかります。
ちなみにGIL版の性能は前回の記事のsync workerに若干負けています。HTTP keep-aliveを使って再接続がなくなる分よりも、mainスレッドとスレッドプール間でのやりとりが発生する方が重いのでしょう。
まとめ
単純に性能が欲しい時はsync workerが一番良いです。負荷分散が理想的で、freethread版Pythonを使わなくてもGILがほとんど問題にならないのでOSのフェアなスケジューリングにも助けられます。時々遅くなる外部APIを利用していて、その時にworkerが詰まってサービスが停止するのを回避したいという時にはgthreadが選択肢になるでしょう。