これは、なにをしたくて書いたもの?
先日、WSGIサーバーとして、GunicornやuWSGIを動かしてみました。
この時に、これらのサーバーには起動時にプロセス数やスレッド数を与えることができるとわかりました。
ところで、複数プロセスや複数スレッドにした時に、アプリケーションから見たらどういうことを気にした方が
よさそうなんでしょうかね?
WSGIから見る
まずは、仕様であるWSGIから見るのが筋でしょう。
幸い、日本語訳もあるようですので。
PEP 333: Python Web Server Gateway Interface v1.0 — knzm.readthedocs.org 2012-12-31 documentation
ちなみに、あまり重厚な仕様でもなさそうなので、割と読める感じなのではないかと。
ここで、「スレッドサポート」について見てみます。
Thread support, or lack thereof, is also server-dependent. Servers that can run multiple requests in parallel, should also provide the option of running an application in a single-threaded fashion, so that applications or frameworks that are not thread-safe may still be used with that server.
スレッドサポート、あるいはその欠如もまた、サーバ依存である。複数の要求 を並列に実行することができるサーバは、スレッド・セーフでないアプリケー ションまたはフレームワークを依然としてそのサーバと共に使用できるように、 シングルスレッド方式でアプリケーションを実行するオプションも提供すべき である (should)。
…はい。
というわけで、複数スレッドで実行するような場合はアプリケーション側も意識すべきだというように見えます。だいぶ
ふわっとした感じですね。
ちょっといくつか気になるパターンを、WSGIを実装したサーバーであるGunicornとuWSGIで試してみることにしましょう。
環境とインストール
今回の環境は、こちら。
$ python3 -V Python 3.6.7
GunicornとPythonをインストール。
$ pip3 install gunicorn $ pip3 install uwsgi
バージョン。
$ pip3 freeze ... gunicorn==19.9.0 uWSGI==2.0.18 ...
プロセス数やスレッド数を変更してみる
まずは、プロセス数やスレッド数を調整しながら変化を見ていこうと思います。
サンプルアプリケーションは、こちら。
wsgi-app.py
import os import threading def application(environ, start_response): start_response('200 OK', [('Content-Type', 'text/plain')]) pid = os.getpid() thread_name = threading.current_thread().getName() return ['get response / pid = {}, thread-name = {}'.format(pid, thread_name).encode()]
このスクリプトを実行するプロセスのIDや、スレッド名を返すようにしています。
こちらを使って、変化を見ていきましょう。
Gunicorn
最初は、Gunicorn。とりあえず、オプションなしでGunicornを起動します。
$ gunicorn wsgi-app:application [2019-04-16 23:56:02 +0900] [26240] [INFO] Starting gunicorn 19.9.0 [2019-04-16 23:56:02 +0900] [26240] [INFO] Listening at: http://127.0.0.1:8000 (26240) [2019-04-16 23:56:02 +0900] [26240] [INFO] Using worker: sync [2019-04-16 23:56:02 +0900] [26245] [INFO] Booting worker with pid: 26245
シングルプロセス、1スレッドです。
アクセス。
$ curl localhost:8000 get response / pid = 26245, thread-name = MainThread
複数回アクセスしても、スレッドは切り替わりません。
$ for i in `seq 300`; do curl -s localhost:8000; echo ; done | sort -u get response / pid = 26245, thread-name = MainThread
プロセス数4、スレッド数2にしてみます。
$ gunicorn --workers 4 --threads 2 wsgi-app:application [2019-04-17 00:23:22 +0900] [753] [INFO] Starting gunicorn 19.9.0 [2019-04-17 00:23:22 +0900] [753] [INFO] Listening at: http://127.0.0.1:8000 (753) [2019-04-17 00:23:22 +0900] [753] [INFO] Using worker: threads [2019-04-17 00:23:22 +0900] [756] [INFO] Booting worker with pid: 756 [2019-04-17 00:23:22 +0900] [757] [INFO] Booting worker with pid: 757 [2019-04-17 00:23:22 +0900] [758] [INFO] Booting worker with pid: 758 [2019-04-17 00:23:22 +0900] [760] [INFO] Booting worker with pid: 760
スレッド名が「MainThread」ではなくなりました。
$ curl localhost:8000 get response / pid = 760, thread-name = ThreadPoolExecutor-0_0
複数回アクセスすると、プロセスが4つ、スレッドが2つ、確認することができます。プロセス間でスレッド名が同じなので
スレッドが分かれているかどうかわかりにくいですね…。
$ for i in `seq 300`; do curl -s localhost:8000; echo ; done | sort -u get response / pid = 756, thread-name = ThreadPoolExecutor-0_0 get response / pid = 756, thread-name = ThreadPoolExecutor-0_1 get response / pid = 757, thread-name = ThreadPoolExecutor-0_0 get response / pid = 758, thread-name = ThreadPoolExecutor-0_0 get response / pid = 758, thread-name = ThreadPoolExecutor-0_1 get response / pid = 760, thread-name = ThreadPoolExecutor-0_0
psコマンドで確認してみましょう。
$ ps aux -L | grep gunicorn xxxxx 753 753 0.3 1 0.1 75024 23832 pts/2 S+ 00:23 0:00 /path/to/venv/bin/gunicorn --workers 4 --threads 2 wsgi-app:application xxxxx 756 756 0.1 3 0.1 230944 21444 pts/2 Sl+ 00:23 0:00 /path/to/venv/bin/gunicorn --workers 4 --threads 2 wsgi-app:application xxxxx 756 845 0.0 3 0.1 230944 21444 pts/2 Sl+ 00:23 0:00 /path/to/venv/bin/gunicorn --workers 4 --threads 2 wsgi-app:application xxxxx 756 846 0.0 3 0.1 230944 21444 pts/2 Sl+ 00:23 0:00 /path/to/venv/bin/gunicorn --workers 4 --threads 2 wsgi-app:application xxxxx 757 757 0.2 3 0.1 230944 21452 pts/2 Sl+ 00:23 0:00 /path/to/venv/bin/gunicorn --workers 4 --threads 2 wsgi-app:application xxxxx 757 793 0.2 3 0.1 230944 21452 pts/2 Sl+ 00:23 0:00 /path/to/venv/bin/gunicorn --workers 4 --threads 2 wsgi-app:application xxxxx 757 794 0.0 3 0.1 230944 21452 pts/2 Sl+ 00:23 0:00 /path/to/venv/bin/gunicorn --workers 4 --threads 2 wsgi-app:application xxxxx 758 758 0.2 3 0.1 230944 21456 pts/2 Sl+ 00:23 0:00 /path/to/venv/bin/gunicorn --workers 4 --threads 2 wsgi-app:application xxxxx 758 799 0.0 3 0.1 230944 21456 pts/2 Sl+ 00:23 0:00 /path/to/venv/bin/gunicorn --workers 4 --threads 2 wsgi-app:application xxxxx 758 800 0.2 3 0.1 230944 21456 pts/2 Sl+ 00:23 0:00 /path/to/venv/bin/gunicorn --workers 4 --threads 2 wsgi-app:application xxxxx 760 760 0.2 3 0.1 230944 21472 pts/2 Sl+ 00:23 0:00 /path/to/venv/bin/gunicorn --workers 4 --threads 2 wsgi-app:application xxxxx 760 777 0.1 3 0.1 230944 21472 pts/2 Sl+ 00:23 0:00 /path/to/venv/bin/gunicorn --workers 4 --threads 2 wsgi-app:application xxxxx 760 778 0.0 3 0.1 230944 21472 pts/2 Sl+ 00:23 0:00 /path/to/venv/bin/gunicorn --workers 4 --threads 2 wsgi-app:application
むしろ、1プロセスあたり、もっとたくさんスレッドがいるようにも見えます…どうなってるんでしょう…。
今度は、4プロセス、4スレッドにしてみます。
$ gunicorn --workers 4 --threads 4 wsgi-app:application [2019-04-17 00:26:39 +0900] [1567] [INFO] Starting gunicorn 19.9.0 [2019-04-17 00:26:39 +0900] [1567] [INFO] Listening at: http://127.0.0.1:8000 (1567) [2019-04-17 00:26:39 +0900] [1567] [INFO] Using worker: threads [2019-04-17 00:26:39 +0900] [1570] [INFO] Booting worker with pid: 1570 [2019-04-17 00:26:39 +0900] [1571] [INFO] Booting worker with pid: 1571 [2019-04-17 00:26:39 +0900] [1572] [INFO] Booting worker with pid: 1572 [2019-04-17 00:26:39 +0900] [1573] [INFO] Booting worker with pid: 1573
確認。
$ curl localhost:8000 get response / pid = 27520, thread-name = ThreadPoolExecutor-0_0
複数回アクセス。
$ for i in `seq 300`; do curl -s localhost:8000; echo ; done | sort -u get response / pid = 1570, thread-name = ThreadPoolExecutor-0_0 get response / pid = 1570, thread-name = ThreadPoolExecutor-0_2 get response / pid = 1570, thread-name = ThreadPoolExecutor-0_3 get response / pid = 1571, thread-name = ThreadPoolExecutor-0_0 get response / pid = 1571, thread-name = ThreadPoolExecutor-0_1 get response / pid = 1571, thread-name = ThreadPoolExecutor-0_2 get response / pid = 1572, thread-name = ThreadPoolExecutor-0_0 get response / pid = 1572, thread-name = ThreadPoolExecutor-0_1 get response / pid = 1572, thread-name = ThreadPoolExecutor-0_2 get response / pid = 1572, thread-name = ThreadPoolExecutor-0_3 get response / pid = 1573, thread-name = ThreadPoolExecutor-0_0 get response / pid = 1573, thread-name = ThreadPoolExecutor-0_1 get response / pid = 1573, thread-name = ThreadPoolExecutor-0_2
psコマンドで見ると、やっぱりたくさんいます…。
$ ps aux -L | grep gunicorn xxxxx 1567 1567 0.1 1 0.1 75024 23832 pts/2 S+ 00:26 0:00 /path/to/venv/bin/python3 /path/to/venv/bin/gunicorn --workers 4 --threads 4 wsgi-app:application xxxxx 1570 1570 0.0 5 0.1 378408 21448 pts/2 Sl+ 00:26 0:00 /path/to/venv/bin/python3 /path/to/venv/bin/gunicorn --workers 4 --threads 4 wsgi-app:application xxxxx 1570 1635 0.0 5 0.1 378408 21448 pts/2 Sl+ 00:26 0:00 /path/to/venv/bin/python3 /path/to/venv/bin/gunicorn --workers 4 --threads 4 wsgi-app:application xxxxx 1570 1636 0.0 5 0.1 378408 21448 pts/2 Sl+ 00:26 0:00 /path/to/venv/bin/python3 /path/to/venv/bin/gunicorn --workers 4 --threads 4 wsgi-app:application xxxxx 1570 1687 0.0 5 0.1 378408 21448 pts/2 Sl+ 00:26 0:00 /path/to/venv/bin/python3 /path/to/venv/bin/gunicorn --workers 4 --threads 4 wsgi-app:application xxxxx 1570 1691 0.0 5 0.1 378408 21448 pts/2 Sl+ 00:26 0:00 /path/to/venv/bin/python3 /path/to/venv/bin/gunicorn --workers 4 --threads 4 wsgi-app:application xxxxx 1571 1571 0.1 5 0.1 378408 21456 pts/2 Sl+ 00:26 0:00 /path/to/venv/bin/python3 /path/to/venv/bin/gunicorn --workers 4 --threads 4 wsgi-app:application xxxxx 1571 1602 0.0 5 0.1 378408 21456 pts/2 Sl+ 00:26 0:00 /path/to/venv/bin/python3 /path/to/venv/bin/gunicorn --workers 4 --threads 4 wsgi-app:application xxxxx 1571 1603 0.0 5 0.1 378408 21456 pts/2 Sl+ 00:26 0:00 /path/to/venv/bin/python3 /path/to/venv/bin/gunicorn --workers 4 --threads 4 wsgi-app:application xxxxx 1571 1625 0.0 5 0.1 378408 21456 pts/2 Sl+ 00:26 0:00 /path/to/venv/bin/python3 /path/to/venv/bin/gunicorn --workers 4 --threads 4 wsgi-app:application xxxxx 1571 1626 0.0 5 0.1 378408 21456 pts/2 Sl+ 00:26 0:00 /path/to/venv/bin/python3 /path/to/venv/bin/gunicorn --workers 4 --threads 4 wsgi-app:application xxxxx 1572 1572 0.1 5 0.1 378408 21460 pts/2 Sl+ 00:26 0:00 /path/to/venv/bin/python3 /path/to/venv/bin/gunicorn --workers 4 --threads 4 wsgi-app:application xxxxx 1572 1607 0.0 5 0.1 378408 21460 pts/2 Sl+ 00:26 0:00 /path/to/venv/bin/python3 /path/to/venv/bin/gunicorn --workers 4 --threads 4 wsgi-app:application xxxxx 1572 1608 0.0 5 0.1 378408 21460 pts/2 Sl+ 00:26 0:00 /path/to/venv/bin/python3 /path/to/venv/bin/gunicorn --workers 4 --threads 4 wsgi-app:application xxxxx 1572 1611 0.0 5 0.1 378408 21460 pts/2 Sl+ 00:26 0:00 /path/to/venv/bin/python3 /path/to/venv/bin/gunicorn --workers 4 --threads 4 wsgi-app:application xxxxx 1572 1612 0.0 5 0.1 378408 21460 pts/2 Sl+ 00:26 0:00 /path/to/venv/bin/python3 /path/to/venv/bin/gunicorn --workers 4 --threads 4 wsgi-app:application xxxxx 1573 1573 0.1 5 0.1 378408 21476 pts/2 Sl+ 00:26 0:00 /path/to/venv/bin/python3 /path/to/venv/bin/gunicorn --workers 4 --threads 4 wsgi-app:application xxxxx 1573 1585 0.0 5 0.1 378408 21476 pts/2 Sl+ 00:26 0:00 /path/to/venv/bin/python3 /path/to/venv/bin/gunicorn --workers 4 --threads 4 wsgi-app:application xxxxx 1573 1586 0.0 5 0.1 378408 21476 pts/2 Sl+ 00:26 0:00 /path/to/venv/bin/python3 /path/to/venv/bin/gunicorn --workers 4 --threads 4 wsgi-app:application xxxxx 1573 1598 0.0 5 0.1 378408 21476 pts/2 Sl+ 00:26 0:00 /path/to/venv/bin/python3 /path/to/venv/bin/gunicorn --workers 4 --threads 4 wsgi-app:application xxxxx 1573 1599 0.0 5 0.1 378408 21476 pts/2 Sl+ 00:26 0:00 /path/to/venv/bin/python3 /path/to/venv/bin/gunicorn --workers 4 --threads 4 wsgi-app:application
これ、ちゃんと確認した方がよさそうですね…。
https://github.com/benoitc/gunicorn/blob/19.9.0/gunicorn/workers/gthread.py#L101
uWSGI
続いて、uWSGIを見てみます。
デフォルトで起動。
$ uwsgi --http :8000 --wsgi-file wsgi-app.py ... spawned uWSGI worker 1 (and the only) (pid: 28159, cores: 1)
1プロセス、1ワーカーです。
確認。
$ curl localhost:8000 get response / pid = 28159, thread-name = uWSGIWorker1Core0
独自のスレッドですね。
複数回アクセス。
$ for i in `seq 300`; do curl -s localhost:8000; echo ; done | sort -u get response / pid = 28159, thread-name = uWSGIWorker1Core0
続いて、プロセス数4、スレッド数2にしてみます。
$ uwsgi --http :8000 --master --processes 4 --threads 2 --wsgi-file wsgi-app.py ... spawned uWSGI master process (pid: 28788) spawned uWSGI worker 1 (pid: 28789, cores: 2) spawned uWSGI worker 2 (pid: 28790, cores: 2) spawned uWSGI worker 3 (pid: 28791, cores: 2) spawned uWSGI worker 4 (pid: 28792, cores: 2) spawned uWSGI http 1 (pid: 28793)
1回のリクエストで確認。
$ curl localhost:8000 get response / pid = 28792, thread-name = uWSGIWorker4Core0
複数アクセスすると、1プロセスあたり、スレッドが2つありそうなことが確認できます。
$ for i in `seq 300`; do curl -s localhost:8000; echo ; done | sort -u get response / pid = 28789, thread-name = uWSGIWorker1Core0 get response / pid = 28789, thread-name = uWSGIWorker1Core1 get response / pid = 28790, thread-name = uWSGIWorker2Core0 get response / pid = 28790, thread-name = uWSGIWorker2Core1 get response / pid = 28791, thread-name = uWSGIWorker3Core0 get response / pid = 28791, thread-name = uWSGIWorker3Core1 get response / pid = 28792, thread-name = uWSGIWorker4Core0 get response / pid = 28792, thread-name = uWSGIWorker4Core1
実際、psコマンドで見ても、ワーカープロセスあたり、2スレッドが子として作られていました。
プロセス数、スレッド数もともに4にしてみましょう。
$ uwsgi --http :8000 --master --processes 4 --threads 4 --wsgi-file wsgi-app.py ... spawned uWSGI master process (pid: 29460) spawned uWSGI worker 1 (pid: 29461, cores: 4) spawned uWSGI worker 2 (pid: 29462, cores: 4) spawned uWSGI worker 3 (pid: 29463, cores: 4) spawned uWSGI worker 4 (pid: 29464, cores: 4) spawned uWSGI http 1 (pid: 29465)
1リクエストで確認。
$ curl localhost:8000 get response / pid = 29461, thread-name = uWSGIWorker1Core0
複数回のリクエストで確認。
$ for i in `seq 300`; do curl -s localhost:8000; echo ; done | sort -u get response / pid = 29461, thread-name = uWSGIWorker1Core0 get response / pid = 29461, thread-name = uWSGIWorker1Core1 get response / pid = 29461, thread-name = uWSGIWorker1Core2 get response / pid = 29461, thread-name = uWSGIWorker1Core3 get response / pid = 29462, thread-name = uWSGIWorker2Core0 get response / pid = 29462, thread-name = uWSGIWorker2Core1 get response / pid = 29462, thread-name = uWSGIWorker2Core2 get response / pid = 29462, thread-name = uWSGIWorker2Core3 get response / pid = 29463, thread-name = uWSGIWorker3Core0 get response / pid = 29463, thread-name = uWSGIWorker3Core1 get response / pid = 29463, thread-name = uWSGIWorker3Core2 get response / pid = 29463, thread-name = uWSGIWorker3Core3 get response / pid = 29464, thread-name = uWSGIWorker4Core0 get response / pid = 29464, thread-name = uWSGIWorker4Core1 get response / pid = 29464, thread-name = uWSGIWorker4Core2 get response / pid = 29464, thread-name = uWSGIWorker4Core3
やはり、4対4です。これはわかりやすいですね。
アプリケーションの初期化処理をどう考える?
ところで、WSGIの仕様はアプリケーションのエントリポイントが定義されているだけです。
Djangoのようなアプリケーションフレームワークでは、起動時に初期化処理とかあったのですが、どうなっているのでしょう?
DjangoのWSGI向けのファイルを見てみます。
xxproject/xxproject/wsgi.py
""" WSGI config for mysite project. It exposes the WSGI callable as a module-level variable named ``application``. For more information on this file, see https://docs.djangoproject.com/en/2.2/howto/deployment/wsgi/ """ import os from django.core.wsgi import get_wsgi_application os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'xxproject.settings') application = get_wsgi_application()
「application」に関数の戻り値を格納しておしまいみたいです。
この中身を見てみます。
https://github.com/django/django/blob/2.2/django/core/wsgi.py
def get_wsgi_application(): """ The public interface to Django's WSGI support. Return a WSGI callable. Avoids making django.core.handlers.WSGIHandler a public API, in case the internal WSGI implementation changes or moves in the future. """ django.setup(set_prefix=False) return WSGIHandler()
セットアップをして、別のクラスのインスタンスを返しているようです。
セットアップの中身は、こちら。
https://github.com/django/django/blob/2.2/django/__init__.py
実際のWSGIのエントリポイントとしては、こちらが機能します、と。
https://github.com/django/django/blob/2.2/django/core/handlers/wsgi.py#L130
つまり、委譲先の関数(として機能するもの)を返す前に、初期化処理を入れ込んでいるというわけですね。
これをマネて、こんなソースコードを用意しました。
wsgi-app-with-startup.py
import os import threading def startup(): print('application startup!!') return Handler() class Handler(): def __call__(self, environ, start_response): start_response('200 OK', [('Content-Type', 'text/plain')]) pid = os.getpid() thread_name = threading.current_thread().getName() return ['get response / pid = {}, thread-name = {}'.format(pid, thread_name).encode()] application = startup()
実際の処理は、Handlerというクラスが行いますが、Handlerのインスタンスを返す前に初期化処理としてメッセージ出力を
行います。
こちらを、起動するプロセス数を変えたりしながら動作を確認してみましょう。
Gunicorn
まずは、Gunicorn。
$ gunicorn wsgi-app-with-startup:application [2019-04-17 00:09:44 +0900] [30839] [INFO] Starting gunicorn 19.9.0 [2019-04-17 00:09:44 +0900] [30839] [INFO] Listening at: http://127.0.0.1:8000 (30839) [2019-04-17 00:09:44 +0900] [30839] [INFO] Using worker: sync [2019-04-17 00:09:44 +0900] [30842] [INFO] Booting worker with pid: 30842 application startup!!
初期化処理が呼ばれました。
アクセスしてみても、初期化処理はもう呼ばれません。
$ curl localhost:8000 get response / pid = 30842, thread-name = MainThread
次に、プロセス数を4、スレッド数を2にしてみます。
$ gunicorn --workers 4 --threads 2 wsgi-app-with-startup:application [2019-04-17 00:10:09 +0900] [31224] [INFO] Starting gunicorn 19.9.0 [2019-04-17 00:10:09 +0900] [31224] [INFO] Listening at: http://127.0.0.1:8000 (31224) [2019-04-17 00:10:09 +0900] [31224] [INFO] Using worker: threads [2019-04-17 00:10:09 +0900] [31227] [INFO] Booting worker with pid: 31227 application startup!! [2019-04-17 00:10:09 +0900] [31228] [INFO] Booting worker with pid: 31228 application startup!! [2019-04-17 00:10:09 +0900] [31229] [INFO] Booting worker with pid: 31229 application startup!! [2019-04-17 00:10:09 +0900] [31230] [INFO] Booting worker with pid: 31230 application startup!!
初期化処理が4回呼ばれました…。
アクセスしてみても、ここから初期化処理が再度呼ばれることはないのですが…。
$ curl localhost:8000 get response / pid = 31227, thread-name = ThreadPoolExecutor-0_0
uWSGI
続いて、uWSGI。
デフォルト状態で起動してみます。
$ uwsgi --http :8000 --wsgi-file wsgi-app-with-startup.py ... *** Operational MODE: single process *** application startup!! WSGI app 0 (mountpoint='') ready in 0 seconds on interpreter 0x557b141ec140 pid: 31884 (default app) *** uWSGI is running in multiple interpreter mode *** spawned uWSGI worker 1 (and the only) (pid: 31884, cores: 1)
初期化処理は呼び出されましたね。
アクセスしても、初期化処理はもう呼び出されません。
$ curl localhost:8000 get response / pid = 31884, thread-name = uWSGIWorker1Core0
では、プロセス数を4、スレッド数を2にしてみます。
$ uwsgi --http :8000 --master --processes 4 --threads 2 --wsgi-file wsgi-app-with-startup.py ... *** Operational MODE: preforking+threaded *** application startup!! WSGI app 0 (mountpoint='') ready in 0 seconds on interpreter 0x5612b14c5270 pid: 32534 (default app) *** uWSGI is running in multiple interpreter mode *** spawned uWSGI master process (pid: 32534) spawned uWSGI worker 1 (pid: 32535, cores: 2) spawned uWSGI worker 2 (pid: 32536, cores: 2) spawned uWSGI worker 3 (pid: 32537, cores: 2) spawned uWSGI worker 4 (pid: 32538, cores: 2) spawned uWSGI http 1 (pid: 32539)
すると、uWSGIの場合は初期化処理が1回しか呼び出されませんでした。
アクセスしても、初期化処理は呼ばれたりしません。
$ curl localhost:8000 get response / pid = 32535, thread-name = uWSGIWorker1Core0
GunicornとuWSGIというWSGIサーバーでも、挙動の面でいろいろ違いがありそうですね。
こうなると、ひとつのアプリケーション内でインスタンスの数が制限することを前提にしているものとか、
アプリケーション内で同じインスタンスを見ていることが前提になっているものとかがあったら、
どうなるんだろうとかいろいろ気になります。
使う時には、気になるところをちゃんと確認した方が良さそうですね…。