Volver a la wiki

PostgreSQL 18 + pgbouncer transaction mode

ADR — PostgreSQL 18 + pgbouncer transaction mode

Contexto

CreaRack Pro migró de PostgreSQL 16 a PostgreSQL 18 (via pg_dump/restore, documentado en RELEASE_NOTES.md). Simultáneamente, el servidor de aplicación pasó a Daphne, servidor ASGI que gestiona conexiones HTTP y WebSocket concurrentemente con muchos workers cortos en lugar de procesos persistentes.

Ese modelo de concurrencia hace que Django abra nueva conexión a BD por cada corrutina activa. Con CONN_MAX_AGE > 0, las conexiones se acumulan hasta agotar max_connections; comentario en production.py recoge que el ajuste fue revertido incorrectamente 4 veces. Ante ese patrón, se necesitaba un pooler externo que absorbiera conexiones del cliente y las multiplexara sobre conjunto reducido de conexiones reales al servidor.

Opciones consideradas

A — PG 16 con session pooling (pgpool-II o pgbouncer session): Mantener versión actual e introducir pooler en modo session: cada conexión de cliente ocupa una de servidor durante toda la sesión. Permite SET de sesión y cursores con nombre. Con Daphne el número de clientes es alto y variable; session mode no reduce número de conexiones reales efectivamente. PG 16 alcanzará EOL antes de 2028 y no dispone de mejoras de PG 17/18 en vacuuming y WAL.

B — PG 18 con pgbouncer transaction mode (elegida): Subir a PostgreSQL 18-alpine y colocar pgbouncer delante en transaction: una conexión de servidor se reutiliza en cuanto la transacción termina, independiente de si el cliente sigue conectado. Permite atender 300 conexiones de cliente (MAX_CLIENT_CONN=300) con solo 30 reales al servidor (DEFAULT_POOL_SIZE=30). Coste: no se pueden usar SET de sesión persistentes ni cursores con nombre entre transacciones; CreaRack mitiga lo primero con SET LOCAL dentro de cada transacción (visible en core/middleware/tenant_rls.py).

Decisión

PostgreSQL 18-alpine en producción (compose.prod.yml, imagen postgres:18-alpine) con pgbouncer (edoburu/pgbouncer:latest) configurado en POOL_MODE: transaction, DEFAULT_POOL_SIZE: 30, MAX_CLIENT_CONN: 300, AUTH_TYPE: scram-sha-256. Django apunta al puerto 6432 de pgbouncer, no directamente al 5432 de Postgres. CONN_MAX_AGE=0 en production.py, validado por test de fitness (tests/test_architecture_fitness.py) y por pre-commit hook. En desarrollo (compose.yml) se replica la misma topología con postgres:17-alpine y pool más pequeño (DEFAULT_POOL_SIZE: 20, MAX_CLIENT_CONN: 200), para que tests de integración ejerciten el mismo código de pooling.

Consecuencias

Positivas: Número de conexiones acotado incluso con picos WebSocket. PG 18 aporta mejoras en autovacuum, WAL, estadísticas I/O. scram-sha-256 sustituye md5 como método auth. Archivado WAL (wal_level=replica) habilitado en producción, habilitando futuros replicas o PITR.

Negativas/restricciones: No se puede usar SET de sesión persistente; estado que sobreviva más de una transacción va a BD o a Valkey. Tests acceden directamente a PostgreSQL (5432) para crear/destruir DB de test que pgbouncer no conoce (tests/conftest.py). Imagen pgbouncer latest introduce riesgo de cambio no controlado; candidato a pinear a versión semántica.

Status

Accepted. Implementado Q1 2026 junto con migración PG 16→18.

Véase también

Subir