This project follows on from my Writing_Django_3 project, which is a simple Hello World in Django 3.
[As might be expected, there were breaking changes between Django 2 and Django 3. So rather than upgrade my Cloud_Django_2 repo, it seemed to be a better idea to create the polls app in Django 3 and proceed from there, this time with Django 3.]
It will use gunicorn which is a web server for Django. Specifically, it is a WSGI server.
One of the new features that Django 3 supports is ASGI, which will be tested here with uvicorn. ASGI is supposed to be faster than WSGI, although this remains to be tested. As is generally usual with these types of claims, it probably depends upon the specific workload.
[For an explanation of why ASGI should be faster than WSGI,
this article
is worth a read. The benchmarking code will be transparent to anyone with recent experience with node.js
(i.e. async/await experience).
While I would personally take issue with the way the benchmarks are run, the results appear conclusive.]
We will run uvicorn
behind gunicorn
:
For production deployments we recommend using gunicorn with the uvicorn worker class.
From: http://www.uvicorn.org/#running-with-gunicorn
The plan of attack is as follows:
- Install and test 'gunicorn'
- Configure 'gunicorn'
- Install and test 'uvicorn'
- Run 'uvicorn' behind 'gunicorn'
To install locally:
$ pip3 install --user gunicorn
Or simply use the requirements.txt
file:
$ pip3 install --user -r requirements.txt
Verify the version:
$ gunicorn --version
gunicorn (version 20.0.4)
$
Lets see if it runs (this needs to be in the same folder as manage.py
):
$ cd polls
$ gunicorn polls.wsgi
[2020-02-19 16:32:10 -0500] [16182] [INFO] Starting gunicorn 20.0.4
[2020-02-19 16:32:10 -0500] [16182] [INFO] Listening at: http://127.0.0.1:8000 (16182)
[2020-02-19 16:32:10 -0500] [16182] [INFO] Using worker: sync
[2020-02-19 16:32:10 -0500] [16185] [INFO] Booting worker with pid: 16185
^C[2020-02-19 16:32:12 -0500] [16182] [INFO] Handling signal: int
[2020-02-19 21:32:12 +0000] [16185] [INFO] Worker exiting (pid: 16185)
[2020-02-19 16:32:12 -0500] [16182] [INFO] Shutting down: Master
$
Now we need to open up gunicorn
with a gunicorn.conf.py
file (for some reason,
this config file needs to be tagged as a Python file). By default, gunicorn
runs
locally and will only accept local connections. We will configure it to run in
promiscuous mode (which is a terrible practice, we should really run it behind
a front-end [nginx is recommended], but we can fix this later).
We will create this file as follows:
import multiprocessing
bind = "0.0.0.0:8000"
workers = multiprocessing.cpu_count() * 2 - 1
Once more, with our config file (the -c gunicorn.conf.py
part is not actually needed):
$ gunicorn -c gunicorn.conf.py polls.wsgi
[2020-02-19 16:35:58 -0500] [16239] [INFO] Starting gunicorn 20.0.4
[2020-02-19 16:35:58 -0500] [16239] [INFO] Listening at: http://0.0.0.0:8000 (16239)
[2020-02-19 16:35:58 -0500] [16239] [INFO] Using worker: sync
[2020-02-19 16:35:58 -0500] [16242] [INFO] Booting worker with pid: 16242
[2020-02-19 16:35:58 -0500] [16243] [INFO] Booting worker with pid: 16243
[2020-02-19 16:35:58 -0500] [16245] [INFO] Booting worker with pid: 16245
[2020-02-19 16:35:58 -0500] [16248] [INFO] Booting worker with pid: 16248
[2020-02-19 16:35:58 -0500] [16249] [INFO] Booting worker with pid: 16249
[2020-02-19 16:35:58 -0500] [16250] [INFO] Booting worker with pid: 16250
[2020-02-19 16:35:58 -0500] [16254] [INFO] Booting worker with pid: 16254
^C[2020-02-19 16:36:06 -0500] [16239] [INFO] Handling signal: int
[2020-02-19 21:36:06 +0000] [16245] [INFO] Worker exiting (pid: 16245)
[2020-02-19 21:36:06 +0000] [16242] [INFO] Worker exiting (pid: 16242)
[2020-02-19 21:36:06 +0000] [16243] [INFO] Worker exiting (pid: 16243)
[2020-02-19 21:36:06 +0000] [16254] [INFO] Worker exiting (pid: 16254)
[2020-02-19 21:36:06 +0000] [16249] [INFO] Worker exiting (pid: 16249)
[2020-02-19 21:36:06 +0000] [16248] [INFO] Worker exiting (pid: 16248)
[2020-02-19 21:36:06 +0000] [16250] [INFO] Worker exiting (pid: 16250)
[2020-02-19 16:36:06 -0500] [16239] [INFO] Shutting down: Master
[2019-11-27 09:57:14 -0500] [9960] [INFO] Shutting down: Master
$
Okay, everything runs.
To install locally:
$ pip3 install --user uvicorn
Or simply use the requirements.txt
file:
$ pip3 install --user -r requirements.txt
Verify the version:
$ uvicorn --version
Running uvicorn 0.11.3 with CPython 3.6.9 on Linux
$
Lets see if it runs (this needs to be in the same folder as manage.py
):
$ cd polls
$ uvicorn polls.asgi:application
INFO: Started server process [7899]
INFO: Waiting for application startup.
INFO: ASGI 'lifespan' protocol appears unsupported.
INFO: Application startup complete.
INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
^CINFO: Shutting down
INFO: Finished server process [7899]
$
[Note that the uvicorn
syntax differs slightly from the gunicorn
syntax.]
Or, to run with the 'lifespan' protocol disabled:
$ uvicorn polls.asgi:application --lifespan off
INFO: Started server process [9590]
INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
^CINFO: Shutting down
INFO: Finished server process [9590]
$
The recommendation is to run uvicorn
behind gunicorn
.
Even if it is not supplied, gunicorn
will read gunicorn.conf.py
(so it needs to be re-named).
This should look as follows (using WSGI):
$ mv gunicorn.conf.py gunicorn.conf
$ gunicorn polls.wsgi -k uvicorn.workers.UvicornWorker
[2020-02-20 11:49:29 -0500] [9237] [INFO] Starting gunicorn 20.0.4
[2020-02-20 11:49:29 -0500] [9237] [INFO] Listening at: http://127.0.0.1:8000 (9237)
[2020-02-20 11:49:29 -0500] [9237] [INFO] Using worker: uvicorn.workers.UvicornWorker
[2020-02-20 11:49:29 -0500] [9240] [INFO] Booting worker with pid: 9240
[2020-02-20 16:49:29 +0000] [9240] [INFO] Started server process [9240]
[2020-02-20 16:49:29 +0000] [9240] [INFO] Waiting for application startup.
[2020-02-20 16:49:29 +0000] [9240] [INFO] ASGI 'lifespan' protocol appears unsupported.
[2020-02-20 16:49:29 +0000] [9240] [INFO] Application startup complete.
^C[2020-02-20 11:49:32 -0500] [9237] [INFO] Handling signal: int
[2020-02-20 16:49:32 +0000] [9240] [INFO] Shutting down
[2020-02-20 16:49:32 +0000] [9240] [INFO] Finished server process [9240]
[2020-02-20 16:49:32 +0000] [9240] [INFO] Worker exiting (pid: 9240)
[2020-02-20 11:49:32 -0500] [9237] [INFO] Shutting down: Master
$
Okay, everything runs.
[The ASGI 'lifespan' protocol appears unsupported.
line is informational, so can be ignored.]
And again, this time with ASGI:
$ gunicorn polls.asgi -k uvicorn.workers.UvicornWorker
[2020-02-20 12:52:37 -0500] [9524] [INFO] Starting gunicorn 20.0.4
[2020-02-20 12:52:37 -0500] [9524] [INFO] Listening at: http://127.0.0.1:8000 (9524)
[2020-02-20 12:52:37 -0500] [9524] [INFO] Using worker: uvicorn.workers.UvicornWorker
[2020-02-20 12:52:37 -0500] [9527] [INFO] Booting worker with pid: 9527
[2020-02-20 17:52:37 +0000] [9527] [INFO] Started server process [9527]
[2020-02-20 17:52:37 +0000] [9527] [INFO] Waiting for application startup.
[2020-02-20 17:52:37 +0000] [9527] [INFO] ASGI 'lifespan' protocol appears unsupported.
[2020-02-20 17:52:37 +0000] [9527] [INFO] Application startup complete.
^C[2020-02-20 12:52:40 -0500] [9524] [INFO] Handling signal: int
[2020-02-20 17:52:40 +0000] [9527] [INFO] Shutting down
[2020-02-20 17:52:41 +0000] [9527] [INFO] Finished server process [9527]
[2020-02-20 17:52:41 +0000] [9527] [INFO] Worker exiting (pid: 9527)
[2020-02-20 12:52:41 -0500] [9524] [INFO] Shutting down: Master
$
Also fine.
[As gunicorn
is a WSGI application, I believe specifying .wsgi
is probably correct.]
The gunicorn.conf.py
config file may be reinstated as follows:
$ mv gunicorn.conf gunicorn.conf.py
$
Some useful references follow.
For more on Coroutines and Tasks: http://docs.python.org/3/library/asyncio-task.html
Sleep: http://docs.python.org/3/library/asyncio-task.html#sleeping
Timeouts: http://docs.python.org/3/library/asyncio-task.html#timeouts
[Note that this is a Python 3 library.]
For a production implementation of Django (WSGI or ASGI), gunicorn
is recommended.
How to use Django with Gunicorn: http://docs.djangoproject.com/en/3.0/howto/deployment/wsgi/gunicorn/
For a production implementation of ASGI, uvicorn
is recommended.
How to use Django with Uvicorn: http://docs.djangoproject.com/en/3.0/howto/deployment/asgi/uvicorn/
Django has a nice utility for checking if the app is ready for deployment: http://docs.djangoproject.com/en/3.0/howto/deployment/checklist/
Uvicorn (even behind Gunicorn) should be faster than Gunicorn by itself:
ASGI should help enable an ecosystem of Python web frameworks that are highly competitive against Node and Go in terms of achieving high throughput in IO-bound contexts. It also provides support for HTTP/2 and WebSockets, which cannot be handled by WSGI.
[The implication here is that Python web frameworks are NOT competitive against Node or Go.]
And:
Uvicorn currently supports HTTP/1.1 and WebSockets. Support for HTTP/2 is planned.
From: http://www.uvicorn.org
Uvicorn settings: http://www.uvicorn.org/settings/
One frustration with running uvicorn
behind gunicorn
is that it is not possible
to specify --lifespan off
(which would prevent the ASGI 'lifespan' protocol appears unsupported.
INFO message).
This is mainly to allow for life-cycle events for the worker (or workers):
The Lifespan ASGI sub-specification outlines how to communicate lifespan events such as startup and shutdown within ASGI.
And:
The lifespan messages allow for an application to initialise and shutdown in the context of a running event loop. An example of this would be creating a connection pool and subsequently closing the connection pool to release the connections.
From:
http://asgi.readthedocs.io/en/latest/specs/lifespan.html
[Within the context of ASGI, the Lifespan Protocol appears to be optional.]
- Django 3.0.3
- gunicorn 20.0.4
- Python 3.6.9
- uvicorn 0.11.3
- Investigate whether specifying WSGI or ASGI to
gunicorn
makes a difference - Benchmark Django 3 ASGI performance against Django 3 WSGI performance
- Add a badge for
Black
formatting style