Dos cabeceras donde Django ponía seis, el número de trabajadores que no es el de siempre, y el arranque que se ejecuta una vez por proceso.
⏱ 16 min de lectura
La lección 43 del módulo 4 desplegó una web con Django. Aquí se despliega una API con FastAPI, y las diferencias importan más de lo que parece, porque todo lo que Django hacía por defecto hay que ponerlo.
Se ve con una medida sencilla: las cabeceras de una respuesta.
Uvicorn solo basta si quien orquesta es otro: Kubernetes, un sistema de contenedores o el systemd de la lección 43 del módulo 4. Reiniciar un proceso caído es trabajo de quien está por encima.
Gunicorn como director es lo que conviene en una máquina normal, porque hace lo que Uvicorn no: reinicia trabajadores que se cuelgan, recarga sin cortar el servicio, y sabe apagarse esperando a que terminen las peticiones en curso.
Y los complementos importan:
bash
pip install "uvicorn[standard]"
Salida en pantalla
uvloop 0.22.1 | httptools: instalado
uvloop sustituye el bucle de eventos de Python por uno más rápido y httptools hace lo mismo con el análisis de HTTP. Los dos entran con el extra, y a diferencia del [standard] de FastAPI de la lección 23, este sí merece la pena: son dos piezas de rendimiento, no un cliente de un servicio comercial.
La regla de 2n+1 es para servidores síncronos, donde cada trabajador atiende una petición cada vez y casi siempre está esperando a la base de datos. Ahí sobresuscribir tiene sentido.
Con trabajadores asíncronos, no. Cada uno atiende cientos de peticiones a la vez por su cuenta, así que lo que limita es el procesador, y poner más procesos que núcleos solo añade cambios de contexto y memoria.
La regla práctica es un trabajador por núcleo, y bajarlo si la máquina es pequeña, porque cada uno mantiene su propio depósito de conexiones a la base de datos. Cuatro trabajadores con veinte conexiones cada uno son ochenta conexiones, y PostgreSQL por defecto acepta cien.
Ese cálculo hay que hacerlo:
Salida en pantalla
trabajadores × pool_size ≤ conexiones que aguanta la base de datos
Con más, el síntoma es un error de "demasiadas conexiones" que aparece solo con carga y desaparece al reiniciar.
from contextlib import asynccontextmanager
@asynccontextmanager
asyncdefciclo_de_vida(app: FastAPI):# al arrancarasyncwith motor.connect()as c:await c.execute(text('select 1'))yield# al pararawait motor.dispose()
app = FastAPI(lifespan=ciclo_de_vida)
Lo de antes del yield se ejecuta una vez al levantar el proceso, lo de después al apagarlo.
Sirve para lo que debe crearse una vez: el cliente HTTP compartido —que es la sesión de la lección 5, y crear uno por petición era el error de allí—, la conexión a Redis, precargar algo en memoria.
Y la comprobación de la base de datos al arrancar merece la pena: es mejor que el proceso no levante a que levante y falle en cada petición. Con un despliegue gradual, un proceso que no arranca detiene el despliegue y deja la versión anterior sirviendo; uno que arranca roto sustituye a la buena.
Lo de después del yield es lo que permite un apagado limpio, y depende de que quien orqueste mande la señal correcta y espere. Es el graceful timeout de Gunicorn y el terminationGracePeriodSeconds de Kubernetes.
La de salud dice si el proceso vive y no debe tocar nada externo. Si la base de datos se cae y esta ruta falla, quien orqueste reiniciará los procesos, que es justo lo que no ayuda y lo que multiplica el problema.
La de preparación dice si puede atender peticiones. Esa sí comprueba las dependencias, y su fallo debe hacer que el repartidor de carga deje de mandarle tráfico sin reiniciar nada.
Confundirlas es el error clásico, y el síntoma es una caída de base de datos que además reinicia todos los procesos en bucle.
sin configurar:
Access-Control-Allow-Origin: None
OPTIONS previo -> 405
Que es lo correcto: un navegador no dejará que una página de otro dominio llame a tu API.
Con el intermediario puesto:
python
from fastapi.middleware.cors import CORSMiddleware
app.add_middleware(
CORSMiddleware,
allow_origins=['https://mi-web.es'],
allow_credentials=True,
allow_methods=['*'],
allow_headers=['*'],)
Salida en pantalla
desde https://mi-web.es -> allow-origin: https://mi-web.es
desde https://otro.es -> allow-origin: None
La lista blanca funciona. Y hay una combinación que hay que evitar:
python
app.add_middleware(
CORSMiddleware,
allow_origins=['*'],# MAL: el comodín
allow_credentials=True,# junto con las credenciales)
Es contradictoria y el propio navegador la rechaza. Lo que suele pasar después es que alguien, para «arreglarlo», pone la lista de orígenes leyendo la cabecera que le manda el cliente, y eso es abrir la API a cualquier página de internet con las credenciales del usuario incluidas.
CORS protege al usuario del navegador, no a tu API: un programa que no sea un navegador se salta CORS entero. No es un mecanismo de autorización, y confundirlo con uno es lo que lleva a dejar puntos de acceso sin permisos «porque solo los llama nuestra web».