9. Gunicorn#

Gunicorn is another widely used WSGI server.

9.1. Possible Worker models#

Gunicorn offers a wide range of possible worker models. The default is the sync worker model which uses "traditional" multi-threading based on the standard library. Unlike waitress Gunicorn doesn't use an asyncore dispatcher to clear the request queue. Each worker is using select by itself to check for incoming client requests. The official Gunicorn documentation for worker types recommends to put a buffering proxy in front of a default configuration Gunicorn.

Other possible worker types are asynchronous workers based on greenlets, AsyncIO workers and a Tornado worker class. The different worker types and how to choose one suitable for your application is covered in detail in the Gunicorn docs.

9.2. Use gunicorn in our buildout#

Gunicorn has a built-in PasteDeploy entry point, so we don't need a shim package like the one we used for bjoern. On the downside, there is no easy way of passing plone.recipe.zope2instances http-address parameter to gunicorn since the bind directive doesn't seem to work in the ini file. The PasteDeploy entry point is covered in the gunicorn configuration documentation.

We resolve to hard code the socket in the ini template.

From templates/gunicorn.ini.in:

[server:main]
use = egg:gunicorn#main
host = 0.0.0.0
port = 8080
proc_name = plone

[app:zope]
use = egg:Zope#main
...

We use this template in our buildout and add gunicorn to our list of eggs:

[instance]
recipe = plone.recipe.zope2instance
user = admin:admin
zeo-client = on
zeo-address = 8100
shared-blob = on
blob-storage = ${buildout:directory}/var/blobstorage
eggs =
    Plone
    wsgitraining.site
    gunicorn
wsgi-ini-template = ${buildout:directory}/templates/gunicorn.ini.in

9.3. Alternative method for using gunicorn#

An alternative method for using gunicorn with Plone is taken from the Plone Core Development Buildout bypasses plone.recipe.zope2instances wsgi-ini-template option and builds three more parts instead. These parts are working together to create the gunicorn configuration and startup scripts. We do not use an ini template in this case but rather use inline templates to render the gunicorn command line and the WSGI application entry point in two scripts:

[buildout]
extends = base.cfg
parts +=
    gunicorn
    gunicornapp
    gunicorn-instance

[instance]
recipe = plone.recipe.zope2instance
user = admin:admin
zeo-client = on
zeo-address = 8100
shared-blob = on
blob-storage = ${buildout:directory}/var/blobstorage
eggs =
    Plone
    wsgitraining.site

[gunicornapp]
recipe = collective.recipe.template
input = inline:
    from Zope2.Startup.run import make_wsgi_app
    wsgiapp = make_wsgi_app({}, '${buildout:parts-directory}/instance/etc/zope.conf')
    def application(*args, **kwargs):return wsgiapp(*args, **kwargs)
output = ${buildout:bin-directory}/gunicornapp.py

[gunicorn]
recipe = zc.recipe.egg
eggs =
    gunicorn
    ${instance:eggs}
scripts =
    gunicorn

[gunicorn-instance]
recipe = collective.recipe.template
input = inline:
    #!/bin/sh
    ${buildout:directory}/bin/gunicorn -b localhost:8080 --threads 4 gunicornapp:application
output = ${buildout:bin-directory}/gunicorn-instance
mode = 755

Note that in this case we still create the default instance (using waitress). But for starting up Plone with gunicorn we use the new gunicorn-instance script instead, without any parameters:

(wsgitraining) $ bin/gunicorn-instance
[2019-10-01 11:55:41 +0200] [11048] [INFO] Starting gunicorn 19.9.0
[2019-10-01 11:55:41 +0200] [11048] [INFO] Listening at: http://127.0.0.1:8080 (11048)
[2019-10-01 11:55:41 +0200] [11048] [INFO] Using worker: threads
[2019-10-01 11:55:41 +0200] [11051] [INFO] Booting worker with pid: 11051

As a side effect we get rid of the deprecation warning for not starting gunicorn with --paste.

Note

The Zope documentations reports several performance issues with gunicorn, s. https://zope.readthedocs.io/en/latest/operation.html#test-criteria-for-recommendations for details.

9.3.1. Exercise 1#

Modify gunicorn-alt.cfg so it uses the eventlet worker class. Check the number of database connections in the ZMI. What do you notice?

Solution

You need to add eventlet to the list of eggs of the [gunicorn] part and modify the command line for [gunicorn-instance]

...
[gunicorn]
recipe = zc.recipe.egg
eggs =
    gunicorn
    eventlet
    ${instance:eggs}
scripts =
    gunicorn

[gunicorn-instance]
recipe = collective.recipe.template
input = inline:
    #!/bin/sh
    ${buildout:directory}/bin/gunicorn -b localhost:8080 --workers 4 gunicornapp:application --worker-class eventlet
output = ${buildout:bin-directory}/gunicorn-instance
mode = 755
...

After running buildout -c gunicorn-alt.cfg, you can start the instance with gunicorn-instance.

Open the database controlpanel in a browser to check the number of database connection. You will see only one connection despite the 4 workers. ZODB connections are not thread safe so this is not a recommended configuration. The asyncio based gthread worker class (doesn't need additional packages) will show one database connection per worker.