Módulo 4 · Lección 43 de 45
Despliegue
Quién sirve los estáticos, cuántos procesos hacen falta y cuánta memoria comen, y las cuatro cosas que hay que hacer en cada actualización.
Desplegar es la parte del desarrollo web que más veces se explica mal, porque casi siempre se explica como una receta para un alojamiento concreto. Esta lección va por el otro lado: qué piezas hacen falta y qué hace cada una, que es lo que te permite entender la receta de cualquier sitio.
Y cierra la deuda de la lección 20, que dejó anunciado que quien sirve los ficheros estáticos en producción es otra cosa.
Por qué runserver no vale#
Django lo dice en cada arranque, y tiene razones concretas:
WARNING: This is a development server. Do not use it in a production setting.No está probado contra ataques. Está escrito para ser cómodo, no para estar expuesto.
Es de un solo proceso. Una petición lenta bloquea a las demás.
Recarga al cambiar ficheros, que en producción es un desperdicio y un riesgo.
Sirve los estáticos a mano, y solo con DEBUG=True, que es lo que se midió en la lección 20.
Lo que sustituye a runserver no es una cosa, son dos.
Las dos piezas#
Internet → servidor web → servidor de aplicación → Django
(nginx) (Gunicorn)El servidor de aplicación ejecuta tu código Python. Habla WSGI, que es la interfaz que define wsgi.py desde la lección 4. Los habituales son Gunicorn y uWSGI; para ASGI, Uvicorn.
El servidor web está delante y se ocupa de lo que no es tu código: el certificado de HTTPS, los ficheros estáticos, la compresión, los límites de tamaño, aguantar conexiones lentas. El habitual es nginx.
La pregunta razonable es por qué dos. Porque son buenos en cosas distintas: un proceso de Python ocupado mandando una imagen es un proceso que no atiende peticiones, y nginx hace eso con una fracción de los recursos.
La respuesta moderna, en cambio, es que para muchos proyectos con uno basta, porque los estáticos se pueden servir desde dentro y el HTTPS lo pone el alojamiento. Eso es WhiteNoise, y es lo que viene ahora.
Gunicorn#
pip install gunicorn
DJANGO_SETTINGS_MODULE=cfg.ajustes.produccion gunicorn cfg.wsgi:application -b 127.0.0.1:8000 -w 3[INFO] Starting gunicorn 26.2.0
[INFO] Listening at: http://127.0.0.1:8099 (10846)
[INFO] Using worker: sync
[INFO] Booting worker with pid: 10848
[INFO] Booting worker with pid: 10850
[INFO] Booting worker with pid: 10851Un proceso maestro y tres trabajadores. El maestro no atiende peticiones: reparte, y si un trabajador muere, levanta otro. Esa es la diferencia principal con runserver.
Ese cfg.wsgi:application es el módulo y el objeto: el mismo application que startproject dejó escrito y que no habíamos usado para nada.
Los estáticos, por fin#
La lección 20 dejó collectstatic explicado y la pregunta abierta. La respuesta más práctica hoy es WhiteNoise, que sirve los estáticos desde dentro de la aplicación, con las cabeceras correctas:
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'whitenoise.middleware.WhiteNoiseMiddleware', # justo después
# ...
]
STORAGES = {
'default': {'BACKEND': 'django.core.files.storage.FileSystemStorage'},
'staticfiles': {'BACKEND': 'whitenoise.storage.CompressedManifestStaticFilesStorage'},
}El orden importa: va inmediatamente después del middleware de seguridad y antes de todo lo demás, para que una petición de un fichero no atraviese sesiones, autenticación y mensajes para nada.
python manage.py collectstatic --noinput --settings=cfg.ajustes.produccion132 static files copied to '/private/tmp/m2/estaticos_prod', 392 post-processed.Los ciento treinta y dos son casi todos del panel de administración. Los trescientos noventa y dos procesados son porque de cada uno deja varias versiones:
estilo.css
estilo.9dcfc804e470.css
estilo.9dcfc804e470.css.gzY ahora lo que importa, que son las cabeceras con las que se sirven:
con huella: Cache-Control: max-age=315360000, public, immutable
sin huella: Cache-Control: max-age=60, publicDiez años contra un minuto. Esa es toda la idea del ManifestStaticFilesStorage de la lección 20: como la huella cambia cuando cambia el contenido, el fichero se puede guardar para siempre, y al cambiarlo el {% static %} genera otro nombre. El navegador no vuelve a pedirlo nunca.
Y la compresión:
pidiendo gzip: Content-Length: 5504 Content-Encoding: gzip
sin comprimir: Content-Length: 24733Cuatro veces y media menos, y sin comprimir en cada petición, porque el .gz ya estaba hecho desde collectstatic. Eso es lo que hace que esto sea razonable dentro de Python.
Cuántos procesos#
La regla que se repite es 2 × núcleos + 1:
cpu_count: 8 -> 2n+1 = 17Y la regla sola es mala consejera, porque cada proceso es una copia entera de tu aplicación:
PID RSS(KB)
11043 30704 (maestro)
11045 50336
11046 48848Unos 48 MB por trabajador, y eso con un proyecto pequeño. Diecisiete trabajadores serían más de 800 MB, que es más de lo que tienen muchos servidores pequeños.
Lo sensato es empezar por dos o tres, mirar la memoria y el tiempo de respuesta, y subir. Y tener presente de qué depende: si tus vistas se pasan el tiempo esperando a la base de datos —que es lo normal—, más procesos ayudan aunque haya pocos núcleos; si calculan, no.
Dos opciones de Gunicorn que conviene poner desde el principio:
gunicorn cfg.wsgi:application --timeout 30 --max-requests 1000 --max-requests-jitter 50--timeout mata al trabajador que lleve treinta segundos con una petición, que es la defensa contra que una consulta atascada se lleve por delante un proceso para siempre. --max-requests lo recicla cada mil peticiones, que es la manera poco elegante y muy eficaz de que una fuga de memoria no tumbe el servidor; el jitter evita que se reciclen todos a la vez.
Que siga levantado#
Un gunicorn lanzado a mano se muere al cerrar la sesión y no vuelve al reiniciar. Eso lo resuelve el sistema de servicios, que en Linux es systemd:
[Unit]
Description=Agenda
After=network.target
[Service]
User=agenda
WorkingDirectory=/srv/agenda
EnvironmentFile=/srv/agenda/.env
ExecStart=/srv/agenda/.venv/bin/gunicorn cfg.wsgi:application --bind unix:/run/agenda.sock --workers 3
Restart=always
[Install]
WantedBy=multi-user.targetTres líneas que merecen atención. User=agenda es un usuario sin privilegios: la aplicación no corre como root, y así un fallo no compromete la máquina. EnvironmentFile es donde entran los secretos de la lección 40, en un fichero que solo ese usuario puede leer. Y Restart=always es lo que hace que un fallo no deje el sitio caído.
En los alojamientos modernos nada de esto se escribe: se declara el comando y ellos ponen el resto. El concepto es el mismo.
Cada actualización#
Cuatro pasos, en este orden:
export DJANGO_SETTINGS_MODULE=cfg.ajustes.produccion
pip install -r requirements.txt # 1. dependencias
python manage.py migrate # 2. base de datos
python manage.py collectstatic # 3. estáticos
systemctl restart agenda # 4. reiniciarEs un guion de cuatro líneas y hay que escribirlo, porque lo que se hace a mano se olvida. Y los dos olvidos típicos: sin el tercero, la página sale sin estilos con el ManifestStaticFilesStorage quejándose de un fichero que no está en el manifiesto; sin el cuarto, el código sigue siendo el viejo, porque los procesos ya tenían todo cargado en memoria.
La primera línea no es decorativa. La lección 40 dejó manage.py apuntando a los ajustes de desarrollo con un setdefault, precisamente para que en el servidor baste con exportar la variable. Sin ella, estos comandos migran la base de datos de desarrollo: el servicio de systemd tiene su propio entorno y no comparte nada con la sesión desde la que escribes.
Lo de las migraciones tiene un matiz que se descubre tarde. Entre el paso 2 y el 4 hay unos segundos en que la base de datos ya está cambiada y el código todavía no. Si la migración borra una columna que el código viejo lee, esos segundos son errores. Por eso los cambios que quitan cosas se hacen en dos despliegues: primero se deja de usar la columna, y en el siguiente se borra.