Volver al blog

ProCat Solutions

Arquitectura SaaS multi-tenant en la práctica

Modelos de aislamiento de inquilinos, row-level security, configuración por inquilino en base de datos, migraciones, vecino ruidoso, facturación y backups.

ProCat Solutions saasmulti-tenantpostgresqlarquitectura
Arquitectura SaaS multi-tenant en la práctica

El SaaS multi-tenant es ese tipo de sistema en el que una única base de código y una única infraestructura dan servicio a muchos clientes independientes entre sí. Los problemas de carga que mencionábamos en la entrada anterior se amplían aquí con una capa más: no basta con que el sistema sea rápido y seguro, además hay que garantizar que ningún inquilino vea ni ralentice a los demás. A continuación recogemos las decisiones que hemos tomado en los proyectos SaaS del último año.

Tres modelos de aislamiento

Para separar los datos de los inquilinos existen tres modelos básicos, y cada uno tiene su lugar.

Esquema compartido, con columna tenant_id. Todas las tablas contienen un identificador de inquilino y todas las consultas filtran por él. Es lo más barato de operar, las migraciones son simples y el backup afecta a una sola base de datos. El riesgo: un único WHERE tenant_id = ... olvidado y ya hay fuga de datos.

Un esquema por inquilino. Dentro de una misma base de datos PostgreSQL, cada inquilino recibe su propio esquema. El aislamiento es más fuerte y, configurando el search_path, las consultas no cambian. A cambio, la migración hay que ejecutarla por inquilino y, por encima de unos pocos cientos de esquemas, las operaciones sobre el catálogo se ralentizan de forma apreciable.

Una base de datos por inquilino. Aislamiento total, backup y restauración por inquilino, incluso en servidores separados. Es la opción más cara y solo está justificada cuando hay un motivo contractual o regulatorio.

La mayoría de nuestros proyectos elige el modelo de esquema compartido, y el riesgo del filtro olvidado no lo gestiona con disciplina, sino con una herramienta a nivel de base de datos.

Row-level security como red de seguridad

El row-level security (RLS) de PostgreSQL permite que el filtrado no lo imponga la aplicación, sino la base de datos. El patrón que usamos:

  • en todas las tablas asociadas a un inquilino, ENABLE ROW LEVEL SECURITY y una policy que filtra por el valor de current_setting('app.tenant_id'),
  • la aplicación fija el inquilino al principio de la petición, dentro de la transacción, con SET LOCAL app.tenant_id = ...,
  • el usuario de base de datos de la aplicación no es el propietario de la tabla, de modo que la policy también le aplica a él.

Con esto, una condición WHERE que falta no provoca una fuga de datos, sino un conjunto de resultados vacío, que se nota mucho antes y duele mucho menos. El precio es una complicación mínima de los planes de consulta y el hecho de que, con PgBouncer en modo transacción, SET LOCAL es la única forma segura.

Configuración por inquilino en la base de datos

Una lección que hemos pagado varias veces: los ajustes específicos de cada inquilino (límites, funcionalidades activadas, claves de integración, elementos de marca) no los guardamos en variables de entorno ni en ficheros de configuración, sino en la base de datos, en una tabla versionada del estilo de tenant_settings. Los motivos son varios:

  • dar de alta un inquilino nuevo no requiere un despliegue,
  • la modificación de los ajustes es auditable y reversible,
  • la interfaz de administración ve exactamente los mismos datos que la aplicación.

Los valores secretos (claves de API, contraseñas) se almacenan cifrados y la clave se lee del entorno de la aplicación. Los ajustes se cachean en Redis con un TTL corto, porque aparecen en todas las peticiones.

Migraciones y el vecino ruidoso

La gran ventaja del modelo de esquema compartido es que la migración se ejecuta una sola vez. Al mismo tiempo, modificar una tabla grande afecta a todos los inquilinos a la vez, así que respetamos algunas reglas: solo cambios aditivos en horario de producción, borrado de columnas en dos pasos (primero el código, después el esquema) y CREATE INDEX CONCURRENTLY para todos los índices.

Contra el fenómeno del “vecino ruidoso” (la carga de un inquilino ralentiza a los demás) nos defendemos en varios niveles:

  • rate limit por inquilino en Redis, no solo global,
  • las operaciones pesadas (exportación, importación masiva, informes) van a una cola en segundo plano, con un límite de concurrencia por inquilino,
  • las consultas tienen configurado un statement_timeout, para que una sola consulta mala no ocupe una conexión durante minutos,
  • el identificador de inquilino aparece como etiqueta en todas las métricas, de modo que el inquilino problemático se ve al instante.

Hooks de facturación y backups

La facturación en un SaaS no es un añadido posterior, sino una cuestión arquitectónica. Hay dos cosas que incorporamos desde el primer día:

Eventos de uso. Cada operación facturable (llamada a la API, mensaje enviado, registro almacenado) escribe un evento en una tabla append-only, con identificador de inquilino y marca de tiempo. Al final del periodo de facturación, de ahí sale el resumen, y en caso de disputa es de donde se puede reconstruir lo ocurrido.

Hooks de transición de estado. El ciclo de vida del inquilino (prueba, activo, impago, suspendido, eliminado) es una máquina de estados explícita. En cada transición se ejecutan los hooks correspondientes: notificación, limitación de funcionalidades, arranque del temporizador de retención de datos. Así, la pregunta de “qué pasa si no paga” tiene una respuesta documentada.

En cuanto al backup, la desventaja del modelo de esquema compartido es que restaurar un único inquilino no es trivial. Lo salvamos generando, además del backup completo diario, un export lógico por inquilino (pg_dump con datos filtrados o un export a nivel de aplicación), y probando el proceso de restauración con regularidad, no solo sobre el papel.

En resumen: la arquitectura multi-tenant no es una única decisión, sino una docena de decisiones menores relacionadas entre sí. El esquema compartido con RLS, con la configuración de inquilinos en base de datos y con límites por inquilino, da en la mayoría de los casos un buen equilibrio entre simplicidad operativa y seguridad. Si un inquilino pide aislamiento total por motivos contractuales, lo resolvemos con una base de datos separada, pero lo tratamos como excepción, no como regla.

Széchenyi Terv Plusz kedvezményezetti infoblokk
QR Code