El alojamiento multiinquilino es donde el optimismo va a que lo auditen. Vendes “sitios ilimitados”, un cliente instala un plugin que convierte cada visita de página en un escaneo de tabla completa y, de repente, tu cola de soporte parece un informe de denegación de servicio.
El problema no es que MariaDB o PostgreSQL sean “lentos”. El problema es que las bases de datos compartidas son un bien común, y un inquilino puede, sin duda, dejarlas en la ruina: CPU, E/S, bloqueos, conexiones, espacio temporal, autovacuum/historial de undo, lo que sea. Tu trabajo es asegurarte de que el radio de impacto del comportamiento dañino sea pequeño, diagnosticable y aburrido.
Qué realmente deja a todos fuera en bases de datos multiinquilino
Un “incidente” multiinquilino rara vez es un único bug. Normalmente es un inquilino que activa un límite compartido y entonces el sistema colapsa educadamente para todos los demás.
Los sospechosos habituales (en orden de producción)
- Agotamiento de conexiones: la app escala “abriendo otra conexión”, y tu base de datos escala “hundirse”.
- CPU desbocada: predicados sin índices, joins patológicos, filtrado de JSON sin GIN/índices funcionales y crímenes generados por ORM.
- Saturación de E/S: grandes ordenaciones en disco, tablas temporales, tormentas de checkpoints y “exportemos todo el inquilino cada hora”.
- Contención de bloqueos: un inquilino haciendo grandes migraciones a mediodía; todos esperan, reintentan y se amontonan.
- Deuda de mantenimiento: autovacuum de PostgreSQL quedando atrás, o la longitud del historial de InnoDB creciendo hasta que todo empieza a pagar intereses.
- Eventos de disco lleno: archivos temporales, binlogs/WAL, crecimiento descontrolado de tablas—nada hace a las bases de datos más honestas que un sistema de archivos al 100%.
En el hosting multiinquilino, el patrón “un sitio mata a todos” no es misterioso. Es predecible. Lo cual es genial, porque puedes diseñar contra ello.
Broma #1: La nube es solo la computadora de otra persona, y el alojamiento multiinquilino es solo la computadora de otra persona con tu pager conectado.
MariaDB vs PostgreSQL: la decisión que importa
La gente compara MariaDB y PostgreSQL como si fuera una lista de características. En alojamiento multiinquilino, la pregunta real es: ¿qué sistema te da más palancas efectivas para aislar inquilinos y diagnosticar al vecino ruidoso rápidamente?
Resumen opinado
- Si puedes estandarizar en un pooler y aplicar timeouts, PostgreSQL suele ser la plataforma multiinquilino más limpia. Tiene mejores primitivas para gobernanza de consultas y control por rol, y su historia de introspección es excelente.
- Si operas a la escala del “hosting LAMP clásico” con muchos inquilinos pequeños y una fuerte memoria operativa MySQL/MariaDB, MariaDB puede ser perfectamente sólida—si tomas los límites de recursos en serio y dejas de fingir que “max_connections=5000” es un plan.
- Si no estás dispuesto a operacionalizar salvaguardas, ninguna de las dos bases de datos te salvará. La base de datos no es una estrategia de crianza.
Dónde tiende a ganar PostgreSQL para contención de inquilinos
- Modelo de roles de primera clase para apuntar privilegios; fácil mapear “rol de inquilino” a políticas.
- Row Level Security (RLS) permite fila-por-inquilino de forma aplicable en la capa de base de datos, no “juramos que la app siempre filtra tenant_id”.
- Observabilidad poderosa: pg_stat_statements, pg_locks, pg_stat_activity, auto_explain y, en general, menos momentos de “¿qué está haciendo esto?”.
- statement timeouts y lock timeouts son directos y pueden ser por rol o por base de datos.
Dónde tiende a ganar MariaDB (o al menos duele menos)
- Familiaridad operativa en entornos de hosting: muchos equipos ya tienen herramientas alrededor.
- Límites por usuario existen y son simples de aplicar (MAX_QUERIES_PER_HOUR, MAX_USER_CONNECTIONS, etc.). Son toscos, pero a veces las herramientas toscas son las únicas que se usan.
- Ecosistema de replicación puede ser sencillo para escalar lecturas en patrones comunes de hosting (con advertencias sobre expectativas de consistencia).
Qué no hacer
No elijas una base de datos porque alguien te dijo que es “más rápida”. En el alojamiento multiinquilino no estás afinando para un benchmark. Estás afinando para el peor cliente en el peor momento.
Modelos de aislamiento: base de datos por inquilino, esquema por inquilino, fila por inquilino
Antes de discutir MariaDB vs PostgreSQL, decide qué vas a aislar: datos, rendimiento, radio operativo o límites de cumplimiento. No obtienes los cuatro gratis.
Modelo A: base de datos por inquilino (aislamiento fuerte, operaciones pesadas)
Este es el clásico “cada cliente obtiene su propia base de datos”. Es excelente para contención: la hinchazón, los bloqueos y las migraciones de un inquilino son menos propensos a afectar a otros.
- Pros: Radio de impacto claro, backups/restores por inquilino más fáciles, mover inquilino a otro host más sencillo, borrado y retención para GDPR más simple.
- Contras: Más objetos, más conexiones, más trabajo en segundo plano, más superficie de monitorización. Necesitas automatización o te ahogarás.
- PostgreSQL: Funciona bien, pero vigila el recuento global de conexiones y la sobrecarga de autovacuum en muchas bases de datos.
- MariaDB: También funciona bien; esquema/base de datos están estrechamente relacionados. Vigila la caché de tablas y límites de archivos abiertos.
Modelo B: esquema por inquilino (punto medio, todavía desordenado)
Una base de datos, muchos esquemas (o bases de datos de MariaDB como espacios de nombres). Obtienes cierta separación organizativa, pero el aislamiento de rendimiento es débil a menos que añadas salvaguardas.
- Pros: Menos objetos a nivel servidor que DB-por-inquilino, informes cross-tenant a veces más simples.
- Contras: Pool de búfer compartido, WAL/binlog compartidos, espacio temporal compartido. DDL puede seguir causando dolor.
- Uso ideal: Recuento moderado de inquilinos con cargas similares y una propiedad SRE estricta.
Modelo C: fila por inquilino (densidad máxima, indulgencia mínima)
Un esquema, tablas compartidas, tenant_id en cada fila. Es eficiente. También es donde un único índice faltante se convierte en una pesadilla para el hosting compartido.
- Pros: Simplifica desplegar cambios de esquema; menos objetos; excelente para SaaS con modelo consistente.
- Contras: El riesgo de vecino ruidoso es el más alto. Backups/restores por inquilino son más difíciles. Necesitas disciplina estricta en las consultas.
- PostgreSQL: RLS puede hacer esto relativamente seguro desde el punto de vista de acceso a datos.
- MariaDB: Confías en la corrección de la aplicación para los filtros de inquilino a menos que implementes patrones elaborados de vistas/procedimientos (que la mayoría de los equipos no mantienen correctos con el tiempo).
Chequeo de la realidad del aislamiento
“Aislamiento de inquilinos” no son solo permisos. Es controlar recursos compartidos: tiempo de CPU, E/S, memoria, bloqueos y conexiones. Si tu modelo no te permite limitar eso, no tienes aislamiento: tienes esperanza.
Límites duros y salvaguardas (la capa de “eso no puedes hacerlo”)
Las salvaguardas son lo que pones antes de necesitarlas. Después de que un inquilino derrita el clúster, las añadirás igualmente—solo que bajo estrés y con clientes gritando. Hazlo ahora.
Salvaguardas de PostgreSQL que realmente funcionan
- statement_timeout por rol o por base de datos: mata consultas desbocadas.
- lock_timeout: evita montones que esperan “para siempre”.
- idle_in_transaction_session_timeout: detiene conexiones que mantienen bloqueos sin hacer nada.
- work_mem por rol: evita que la ordenación de un inquilino se coma la RAM (o forzar grandes volcados a disco).
- configuraciones por rol via ALTER ROLE … SET: hace que la política sea aplicable.
Salvaguardas de MariaDB que están subestimadas
- Límites por usuario (MAX_USER_CONNECTIONS, MAX_QUERIES_PER_HOUR): toscos, efectivos.
- max_execution_time (para SELECT): puede matar lecturas largas si se usa con cuidado.
- Configuraciones de InnoDB que reducen el daño por ráfagas (por ejemplo, tamaños sensatos de log file, dimensionamiento del buffer pool, comportamiento de flushing).
Qué ninguna de las dos bases de datos te da de forma nativa
Ninguna de las dos hará cumplir perfectamente “el inquilino obtiene 10% de CPU y 50 IOPS” a nivel SQL. Si necesitas ese nivel de multi-tenancy duro, acabarás usando:
- Instancias separadas por inquilino (o por nivel de inquilino).
- Controles a nivel de SO (cgroups) para limitar recursos del proceso de la base de datos—útiles, pero toscos.
- Encolamiento en la app o pooler: no puedes ejecutar lo que no aceptas.
Tormentas de conexiones: pool, topes y encolamiento
La manera más rápida para que un solo inquilino deje fuera a todos es el reflujo de conexiones. PHP-FPM escala, workers de Node se forkean, cron jobs se amontonan y de repente la base de datos se pasa la vida montando conexiones y cambiando contexto.
PostgreSQL: casi siempre quieres un pooler
PostgreSQL maneja concurrencia bien, pero miles de procesos backend activos no son gratis. En hosting multiinquilino con tráfico en picos, normalmente quieres PgBouncer (transaction pooling para la mayoría de apps web), con topes por inquilino si puedes mapear usuarios/roles limpiamente.
MariaDB: existe pooling, pero sé honesto sobre dónde vive
MariaDB puede aceptar muchas conexiones, pero no confundas “acepta conexiones TCP” con “rinde bien”. A menudo la estrategia práctica es:
- Limitar conexiones por usuario.
- Usar pooling a nivel de aplicación (dependiendo del stack).
- Mantener max_connections realista y diseñar para gestionar la presión hacia atrás.
Broma #2: Si tu estrategia de escalado es “aumentar max_connections”, no tienes una base de datos; tienes una cola muy cara.
La fea verdad sobre la equidad
Sin controles explícitos de equidad, el inquilino más ruidoso gana. La base de datos no sabe qué es “un cliente”. Sabe sesiones. Necesitas un mapeo: tenant → rol/usuario → límites. Si no puedes mapearlo, no puedes aplicarlo.
E/S y almacenamiento: evita que el disco sea la tragedia compartida
En sistemas multiinquilino, los discos fallan de una manera muy específica: no muriendo, sino atareándose. Tus CPUs están bien, tu RAM está bien y la base de datos “está lenta” porque espera al almacenamiento. Entonces todos los inquilinos hacen timeout y reintentan, y obtienes una tormenta de reintentos que hace que el problema original parezca casi anecdótico.
Patrones de contención que funcionan
- Separar WAL/binlog del dato cuando puedas. No es magia, pero reduce la contención y hace los eventos de disco lleno menos catastróficos.
- Datasets/volúmenes por inquilino (cuando sea factible): si haces base de datos por inquilino, colocar cada base de datos de inquilino en su propio dataset puede permitir aplicar cuotas y detectar crecimiento.
- Monitoreo de espacio temporal: ordenaciones y tablas temporales son los comedores silenciosos de disco.
- Tuning de checkpoint y flushing: tanto PostgreSQL como InnoDB pueden generar ráfagas de escritura; en multiinquilino, las ráfagas se correlacionan con picos de carga de páginas, que es como obtienes “todos los sitios están lentos”.
Toma de ingeniería de almacenamiento: no compartas sin telemetría
Si no puedes atribuir E/S a un inquilino (o al menos a una base de datos/usuario), lucharás contra fantasmas. Como mínimo, atribuye a consultas (pg_stat_statements / Performance Schema) y a bases de datos.
Observabilidad: prueba quién es ruidoso, rápido
En hosting, “¿qué inquilino lo hizo?” no es una cuestión filosófica. Es una habilidad de on-call. Necesitas:
- Consultas top por tiempo total, tiempo medio, llamadas y filas.
- Gráficas de bloqueos: quién bloquea a quién.
- Conteos de conexiones por mapeo de inquilino (usuario/rol/db).
- Crecimiento de disco/WAL/binlog en tendencias.
- Retrasos de mantenimiento: autovacuum retrasado, longitud de lista de InnoDB, etc.
Una cita para una nota adhesiva. De John Allspaw: “No puedes mejorar lo que no mides”.
Datos interesantes y contexto histórico (porque moldean las compensaciones actuales)
- PostgreSQL desciende del proyecto POSTGRES en UC Berkeley, diseñado con extensibilidad mucho antes de que “extensiones” fuera de moda.
- MySQL se convirtió en el defecto del hosting web temprano porque era fácil de desplegar y suficientemente rápido para las cargas de la época—muchas lecturas, esquemas simples, datos más pequeños.
- MariaDB se bifurcó de MySQL después de que Oracle adquiriera Sun Microsystems; la bifurcación fue tanto por gobernanza como por tecnología.
- El modelo MVCC de PostgreSQL hace lecturas no bloqueantes por diseño, pero crea un requisito de mantenimiento: vacuum de tuplas muertas no es opcional.
- InnoDB (el motor dominante en MySQL/MariaDB) trajo transacciones y bloqueo a nivel de fila al ecosistema MySQL y cambió la fiabilidad del hosting en los 2000.
- PgBouncer se convirtió en estándar de facto en entornos Postgres de alta concurrencia porque el proceso-por-conexión tiene overhead real a escala.
- Row Level Security en PostgreSQL se volvió una característica mainstream para multiinquilino solo después de que los equipos se cansaron de “juramos que la app siempre añade tenant_id”.
- Los límites por usuario de MySQL/MariaDB han existido durante mucho tiempo, pero muchas pilas de hosting los ignoraron porque “podría romper a un cliente”, hasta que los clientes rompieron a todos.
Tareas prácticas: comandos, salidas, decisiones (12+)
Estos son los tipos de comprobaciones que ejecutas cuando intentas mantener estable el hosting multiinquilino. Cada tarea incluye: comando, salida realista, qué significa y qué decisión tomas.
Tarea 1: Encontrar las principales consultas de PostgreSQL por tiempo total (pg_stat_statements)
cr0x@server:~$ sudo -u postgres psql -d postgres -c "SELECT queryid, calls, round(total_exec_time)::int AS total_ms, round(mean_exec_time,2) AS mean_ms, left(query,120) AS sample FROM pg_stat_statements ORDER BY total_exec_time DESC LIMIT 5;"
queryid | calls | total_ms | mean_ms | sample
----------+-------+----------+---------+----------------------------------------------------------
99120311 | 2140 | 987654 | 461.52 | SELECT * FROM orders WHERE tenant_id = $1 ORDER BY created_at DESC LIMIT $2
77112002 | 120 | 501223 | 4176.86 | SELECT count(*) FROM events WHERE tenant_id = $1 AND payload::text ILIKE $2
55110019 | 98000 | 210112 | 2.14 | SELECT id FROM sessions WHERE expires_at < now()
44110070 | 42 | 180990 | 4309.29 | SELECT * FROM invoices WHERE status IN (...) AND tenant_id=$1 ORDER BY id
99120355 | 300 | 150333 | 501.11 | UPDATE products SET stock = stock - 1 WHERE tenant_id=$1 AND id=$2
(5 rows)
Significado: La segunda consulta es un clásico que mata a inquilinos (ILIKE en payload text). Tiempo medio alto, menos llamadas, gran impacto.
Decisión: Identificar al inquilino vía logs de la app/ parámetros de consulta, añadir un índice o rediseñar (por ejemplo, GIN/trigram, columna extraída) y establecer statement_timeout por rol para limitar el daño.
Tarea 2: Ver sesiones de PostgreSQL y quién está esperando
cr0x@server:~$ sudo -u postgres psql -c "SELECT pid, usename, datname, state, wait_event_type, wait_event, now()-query_start AS age, left(query,80) AS q FROM pg_stat_activity WHERE state <> 'idle' ORDER BY age DESC LIMIT 10;"
pid | usename | datname | state | wait_event_type | wait_event | age | q
------+------------+-----------+--------+-----------------+------------+----------+--------------------------------------------------------------------------------
8123 | tenant_442 | appdb | active | Lock | relation | 00:03:14 | ALTER TABLE posts ADD COLUMN foo text
8201 | tenant_101 | appdb | active | | | 00:02:59 | SELECT * FROM orders WHERE tenant_id=$1 ORDER BY created_at DESC LIMIT $2
8210 | tenant_333 | appdb | active | Lock | tuple | 00:02:10 | UPDATE posts SET status='x' WHERE tenant_id=$1 AND id=$2
(3 rows)
Significado: Un inquilino está ejecutando DDL y manteniendo bloqueos; otros inquilinos esperan bloqueos de relación/tuple.
Decisión: Matar o posponer el DDL, aplicar lock_timeout para roles de inquilino e implementar una política de “ventana de mantenimiento” para migraciones.
Tarea 3: Detectar bloqueadores de bloqueo de PostgreSQL
cr0x@server:~$ sudo -u postgres psql -c "SELECT blocked.pid AS blocked_pid, blocked.usename AS blocked_user, blocker.pid AS blocker_pid, blocker.usename AS blocker_user, left(blocker.query,80) AS blocker_query FROM pg_locks bl JOIN pg_stat_activity blocked ON blocked.pid=bl.pid JOIN pg_locks kl ON bl.locktype=kl.locktype AND bl.database IS NOT DISTINCT FROM kl.database AND bl.relation IS NOT DISTINCT FROM kl.relation AND bl.page IS NOT DISTINCT FROM kl.page AND bl.tuple IS NOT DISTINCT FROM kl.tuple AND bl.virtualxid IS NOT DISTINCT FROM kl.virtualxid AND bl.transactionid IS NOT DISTINCT FROM kl.transactionid AND bl.classid IS NOT DISTINCT FROM kl.classid AND bl.objid IS NOT DISTINCT FROM kl.objid AND bl.objsubid IS NOT DISTINCT FROM kl.objsubid AND kl.granted JOIN pg_stat_activity blocker ON blocker.pid=kl.pid WHERE NOT bl.granted LIMIT 5;"
blocked_pid | blocked_user | blocker_pid | blocker_user | blocker_query
-------------+--------------+-------------+--------------+---------------------------------------------------------------
8210 | tenant_333 | 8123 | tenant_442 | ALTER TABLE posts ADD COLUMN foo text
(1 row)
Significado: Tienes un bloqueador claro. Sin conjeturas, sin intuiciones.
Decisión: Terminar la sesión bloqueadora si viola la política; añadir controles de DDL en la pipeline de despliegue.
Tarea 4: Verificar timeouts de PostgreSQL para roles de inquilino
cr0x@server:~$ sudo -u postgres psql -c "SELECT rolname, rolconfig FROM pg_roles WHERE rolname IN ('tenant_333','tenant_442');"
rolname | rolconfig
------------+----------------------------------------------------------
tenant_333 | {statement_timeout=30000,lock_timeout=2000,work_mem=16MB}
tenant_442 | {statement_timeout=30000,lock_timeout=2000,work_mem=16MB}
(2 rows)
Significado: La política está codificada en el rol de base de datos, no en una wiki interna que nadie lee.
Decisión: Si falta, añadir valores por defecto al rol; si es demasiado estricto, crear tiers (pequeño/medio/enterprise).
Tarea 5: Comprobar la salud del autovacuum de PostgreSQL
cr0x@server:~$ sudo -u postgres psql -c "SELECT relname, n_dead_tup, last_autovacuum, last_vacuum FROM pg_stat_user_tables ORDER BY n_dead_tup DESC LIMIT 5;"
relname | n_dead_tup | last_autovacuum | last_vacuum
-----------+------------+-------------------------------+-------------------------------
events | 5021132 | |
sessions | 901223 | 2025-12-29 08:11:02.12345+00 |
orders | 200111 | 2025-12-29 08:09:10.99431+00 |
invoices | 150991 | 2025-12-29 07:59:34.33121+00 |
posts | 90112 | 2025-12-29 08:00:20.11200+00 |
(5 rows)
Significado: La tabla events tiene millones de tuplas muertas y no tiene autovacuum reciente. Eso es una mina de rendimiento.
Decisión: Investigar por qué no corre autovacuum (umbrales, peligro de wraparound, transacciones largas). Considerar tuning de autovacuum por tabla o particionado por inquilino/tiempo.
Tarea 6: MariaDB: identificar las consultas principales (digest de Performance Schema)
cr0x@server:~$ sudo mariadb -e "SELECT DIGEST_TEXT, COUNT_STAR, ROUND(SUM_TIMER_WAIT/1000000000000,2) AS total_s FROM performance_schema.events_statements_summary_by_digest ORDER BY SUM_TIMER_WAIT DESC LIMIT 3\G"
*************************** 1. row ***************************
DIGEST_TEXT: SELECT COUNT ( * ) FROM events WHERE tenant_id = ? AND payload LIKE ?
COUNT_STAR: 118
total_s: 632.11
*************************** 2. row ***************************
DIGEST_TEXT: SELECT * FROM orders WHERE tenant_id = ? ORDER BY created_at DESC LIMIT ?
COUNT_STAR: 9801
total_s: 201.44
*************************** 3. row ***************************
DIGEST_TEXT: UPDATE products SET stock = stock - ? WHERE tenant_id = ? AND id = ?
COUNT_STAR: 3002
total_s: 95.12
Significado: Puedes ver “formas de consulta” aunque los literales difieran. El COUNT con LIKE es el matón.
Decisión: Añadir índices adecuados, limitar tiempo de ejecución o reescribir la característica. Si el inquilino insiste, muévelo a su propia instancia.
Tarea 7: MariaDB: encontrar hilos activos y quién acapara
cr0x@server:~$ sudo mariadb -e "SHOW FULL PROCESSLIST;"
Id User Host db Command Time State Info
9112 tenant442 app1:44210 appdb Query 210 Waiting for table metadata lock ALTER TABLE posts ADD COLUMN foo TEXT
9201 tenant101 app2:51012 appdb Query 45 Sending data SELECT * FROM orders WHERE tenant_id=101 ORDER BY created_at DESC LIMIT 50
9303 tenant333 app3:39910 appdb Query 39 Locked UPDATE posts SET status='x' WHERE tenant_id=333 AND id=9
Significado: La misma historia que en Postgres: DDL causando esperas por metadata lock y amontonamientos.
Decisión: Matar el DDL, aplicar herramientas/políticas de cambios de esquema en línea y programar migraciones.
Tarea 8: MariaDB: aplicar límites de conexión por usuario
cr0x@server:~$ sudo mariadb -e "ALTER USER 'tenant442'@'%' WITH MAX_USER_CONNECTIONS 20;"
cr0x@server:~$ sudo mariadb -e "SHOW GRANTS FOR 'tenant442'@'%';"
Grants for tenant442@%
GRANT USAGE ON *.* TO `tenant442`@`%` WITH MAX_USER_CONNECTIONS 20
Significado: El inquilino 442 ya no puede abrir 500 conexiones. Harán cola en la app, no en tu base de datos.
Decisión: Establecer límites por niveles; monitorizar errores de “demasiadas conexiones” y ofrecer guía de pooling.
Tarea 9: PostgreSQL: ver consumo de conexiones por base de datos
cr0x@server:~$ sudo -u postgres psql -c "SELECT datname, count(*) AS conns FROM pg_stat_activity GROUP BY datname ORDER BY conns DESC;"
datname | conns
---------+-------
appdb | 312
postgres| 3
(2 rows)
Significado: La mayor presión está en appdb. No sorprende, pero ahora lo sabes.
Decisión: Si appdb es multiinquilino, considerar mover inquilinos pesados a bases de datos separadas o clusters separados.
Tarea 10: Comprobar iowait del sistema y principales ofensores
cr0x@server:~$ iostat -xz 1 3
Linux 6.8.0 (db01) 12/29/2025 _x86_64_ (16 CPU)
avg-cpu: %user %nice %system %iowait %steal %idle
12.10 0.00 5.20 32.40 0.00 50.30
Device r/s rkB/s rrqm/s %rrqm r_await rareq-sz w/s wkB/s wrqm/s %wrqm w_await wareq-sz aqu-sz %util
nvme0n1 220.0 18432.0 2.0 0.90 8.20 83.78 600.0 51200.0 120.0 16.67 25.10 85.33 17.3 99.20
Significado: 32% iowait y el NVMe está cerca del 100% de utilización con alta latencia de escritura. Es un cuello de botella de almacenamiento ahora mismo.
Decisión: Identificar por qué las escrituras están disparadas (checkpoint, vacuum, binlog/WAL, volcados temporales). Considerar ajustar checkpoint/flush y asegurar suficiente margen de IOPS.
Tarea 11: PostgreSQL: comprobar síntomas de tasa de generación de WAL
cr0x@server:~$ sudo -u postgres psql -c "SELECT now(), pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(),'0/0')) AS wal_bytes_since_start;"
now | wal_bytes_since_start
------------------------------+------------------------
2025-12-29 08:22:10.1122+00 | 842 GB
(1 row)
Significado: Eso no es “malo” por sí solo, pero si WAL crece rápido y las réplicas se retrasan, vas hacia presión de disco.
Decisión: Comprobar lag de replicación y archivado; si un inquilino está escribiendo en masa, limitarlo o aislarlo.
Tarea 12: MariaDB: comprobar estado del motor InnoDB por contención e historial
cr0x@server:~$ sudo mariadb -e "SHOW ENGINE INNODB STATUS\G" | sed -n '1,70p'
*************************** 1. row ***************************
Type: InnoDB
Name:
Status:
=====================================
2025-12-29 08:23:01 0x7f3c6c1fe700 INNODB MONITOR OUTPUT
=====================================
...
History list length 412398
...
Trx id counter 928331120
Purge done for trx's n:o < 928300000 undo n:o < 0 state: running
...
Significado: Una gran history list length puede indicar retraso en purge, frecuentemente debido a transacciones de larga duración. Eso puede degradar el rendimiento entre inquilinos.
Decisión: Encontrar y terminar transacciones largas; ajustar comportamiento de la app; asegurar que el purge puede avanzar.
Tarea 13: PostgreSQL: encontrar transacciones de larga duración (bloqueadores de vacuum)
cr0x@server:~$ sudo -u postgres psql -c "SELECT pid, usename, now()-xact_start AS xact_age, state, left(query,80) AS q FROM pg_stat_activity WHERE xact_start IS NOT NULL ORDER BY xact_age DESC LIMIT 5;"
pid | usename | xact_age | state | q
------+------------+------------+---------------------+--------------------------------------------------------------------------------
9001 | tenant_777 | 02:14:09 | idle in transaction | SELECT * FROM reports WHERE tenant_id=$1
(1 row)
Significado: “Idle in transaction” mata el vacuum y es un riesgo de bloqueo. En multiinquilino, es un impuesto compartido.
Decisión: Aplicar idle_in_transaction_session_timeout para roles de inquilino; arreglar la ruta de la app que olvida commit/rollback.
Tarea 14: Verificar uso de disco y localizar áreas de crecimiento rápido
cr0x@server:~$ df -h /var/lib/postgresql /var/lib/mysql
Filesystem Size Used Avail Use% Mounted on
/dev/nvme0n1p2 900G 812G 43G 95% /var/lib/postgresql
/dev/nvme1n1p1 900G 620G 235G 73% /var/lib/mysql
Significado: El volumen de Postgres está al 95%. Estás a una ráfaga de WAL o archivos temporales de un incidente muy educativo.
Decisión: Limpieza de emergencia (revisión de retención/archivado de WAL, búsqueda de archivos temporales), luego cuotas/alertas permanentes. También: deja de permitir que un clúster corra al 95%.
Guion de diagnóstico rápido
Esta es la secuencia de “alguien te ha paginado y todos los inquilinos están gritando”. La meta no es elegancia. La meta es encontrar el cuello de botella en minutos.
Primero: decide si es CPU, E/S, bloqueos o conexiones
- ¿Saturación de CPU? Comprueba carga del sistema y CPU user/system.
- Si la CPU está alta y iowait baja: sospecha consultas malas, índices faltantes, ordenaciones/joins caros.
- ¿Alta espera de E/S? Si iowait es alto y %util disco cerca del 100%: sospecha tormentas de checkpoint/flush, volcados temporales, churn de WAL/binlog, retraso de vacuum/purge.
- ¿Amontonamiento de bloqueos? Busca sesiones esperando bloqueos (pg_stat_activity wait_event_type=Lock; MariaDB processlist “Locked” / metadata locks).
- ¿Agotamiento de conexiones? Comprueba conexiones activas vs max configurado; busca errores de “too many connections” y explosión de threads/procesos.
Segundo: identifica al principal atacante y el radio de impacto
- Consultas top (pg_stat_statements / Performance Schema digests): encuentra la forma de consulta que consume más tiempo total.
- Mapear al inquilino: mediante rol/usuario, base de datos, etiquetas de la app o correlación de logs.
- Confirmar impacto: ¿está causando bloqueos, churn de E/S o consumo de CPU?
Tercero: aplica la mitigación menos mala
- Mata la consulta/sesión si está dañando activamente a otros y viola la política.
- Tasa en el borde: limita conexiones por inquilino; reduce concurrencia en el pooler; rate limit en endpoints pesados.
- Temporalmente aumenta la equidad: baja statement timeouts para el rol ruidoso, no globalmente.
- Estabiliza el almacenamiento: si el disco es el cuello de botella, reduce la presión de escritura (posponer vacuum full/reindex, ajustar checkpoints con cuidado, parar jobs masivos).
Si haces estos pasos consistentemente, “colapso multiinquilino” se vuelve un diagnóstico repetible en vez de una sesión de espiritismo.
Errores comunes: síntoma → causa raíz → solución
1) Síntoma: “Todos los sitios están lentos” y la CPU de la base de datos está bien
Causa raíz: Saturación de almacenamiento (volcados temporales, ráfagas de checkpoint, acumulación de WAL/binlog).
Solución: Revisa iowait y %util de disco; limita work_mem/tamaños de sort; ajusta checkpoints; separa WAL/binlog; añade margen de IOPS.
2) Síntoma: Timeouts aleatorios, muchos “waiting for lock”
Causa raíz: DDL de inquilino o transacciones largas que causan colas de bloqueo.
Solución: Aplica lock_timeout e idle-in-transaction timeout; exige herramientas de schema change online; programa migraciones; mata bloqueadores rápido.
3) Síntoma: Periódico “demasiadas conexiones” y luego recuperación
Causa raíz: Tormentas de conexiones por escalado de la app o picos de cron; sin pooling ni presión hacia atrás.
Solución: Desplegar PgBouncer (Postgres) o pooling en la app; limitar conexiones por inquilino (límites de MariaDB, límites basados en roles en Postgres); reducir conteo de workers.
4) Síntoma: Consultas de Postgres se vuelven más lentas en días y luego “vacuum no da abasto”
Causa raíz: Autovacuum quedando atrás por transacciones largas, umbrales pobres o tablas de churn intenso (a menudo tablas compartidas multiinquilino).
Solución: Encontrar transacciones largas; ajustar autovacuum por tabla; particionar tablas de alto churn; asegurar recursos para vacuum; reducir motores de bloat.
5) Síntoma: Rendimiento de MariaDB degrada con muchas escrituras; problemas de undo/purge
Causa raíz: Transacciones de larga duración y retraso en purge (history list length crece).
Solución: Identificar transacciones largas; acortar ámbitos de transacción; matar ofensores; asegurar que el purge pueda avanzar; revisar niveles de aislamiento y tamaños de lote.
6) Síntoma: “Solo es el reporte de un inquilino” pero destroza a todos
Causa raíz: Consulta analítica pesada en primaria, sin segregación de recursos, sin réplica de lectura, sin timeout.
Solución: Enrutar reporting a réplicas/warehouse; aplicar statement_timeout; crear un nivel de inquilino que incluya recursos dedicados.
7) Síntoma: Disco lleno, la base de datos se cae o pasa a modo solo-lectura, pánico
Causa raíz: Crecimiento de WAL/binlog, archivos temporales descontrolados o datos de inquilino sin límites.
Solución: Aplicar cuotas (cuando sea posible), monitorizar crecimiento, mantener política de espacio libre (no opcional) y limitar retenciones. Trata disco lleno como fallo de diseño, no como sorpresa.
Tres mini-historias corporativas desde el terreno
Mini-historia 1: El incidente causado por una suposición errónea
Mantenían un clúster PostgreSQL compartido para una flota de sitios de clientes pequeños. Cada inquilino tenía su propio usuario de base de datos, pero todos compartían la misma base de datos y esquema. El equipo de producto insistía en que la aplicación siempre filtraba por tenant_id. El equipo SRE les creyó, porque los logs parecían limpios y todos querían que el sprint terminara.
Un cliente nuevo se incorporó con un “plugin de analítica” que añadió una función de búsqueda en eventos históricos. El plugin usaba un constructor dinámico de consultas y, en un camino de código, olvidó incluir tenant_id en el WHERE. No siempre. Solo en ciertas combinaciones de filtros. Eso lo hizo difícil de captar en staging; staging nunca tenía suficientes datos para que doliera.
En producción, la consulta pasó de “escanear unos miles de filas para un inquilino” a “escanear cientos de millones de filas para todos los inquilinos”. PostgreSQL hizo lo que le dijeron. La CPU subió, luego la E/S se disparó porque el plan empezó a volcar ordenaciones a disco. Mientras tanto, las peticiones de otros inquilinos se apilaron y expiraron, y su lógica de reintento aumentó la carga. El plugin no solo supuso un riesgo de fuga de datos: provocó un incidente de rendimiento en todo el host.
La solución no fue heroica. Implementaron Row Level Security, forzaron que todas las consultas de inquilinos pasaran por una función con SECURITY DEFINER con contexto de inquilino y añadieron statement_timeout para roles de inquilino. El cambio más grande fue cultural: “la app siempre filtra” dejó de ser aceptado como decisión arquitectónica. Se convirtió en un input no confiable.
Mini-historia 2: La optimización que salió mal
Una plataforma de hosting estandarizó en MariaDB. Un trimestre, alguien decidió “optimizar” aumentando mucho max_connections porque los clientes ocasionalmente veían errores de conexión durante picos. El razonamiento fue simple: menos errores = clientes más felices.
Durante unos dos días, pareció una victoria. Luego una campaña de marketing afectó el sitio de un inquilino. Sus workers de PHP se ramp-up. Cada worker abrió su propia conexión, ejecutó una consulta lenta y se quedó esperando E/S. Más conexiones significaron más trabajo lento concurrente. El churn del buffer pool aumentó. InnoDB empezó a hacer flushing más agresivo. La latencia subió en todo el sistema. Otros inquilinos reintentaron, lo que creó más conexiones, lo que incrementó el flushing. Tormenta autoinfligida clásica.
Cuando finalmente redujeron max_connections, el síntoma mejoró, pero el equipo aprendió la lección equivocada: “max_connections debe ajustarse”. La lección real fue “necesitas backpressure y pooling”. Añadieron topes por usuario, arreglaron la consulta afectada con un índice e implementaron un pequeño pool de conexiones en la capa de aplicación para su stack gestionado.
La conclusión del postmortem fue seca pero acertada: permitir más concurrencia no reduce la demanda; aumenta la capacidad del sistema para soportar más demanda a la vez.
Mini-historia 3: La práctica aburrida pero correcta que salvó el día
Otra compañía usaba PostgreSQL para SaaS multiinquilino. No eran sofisticados. Simplemente tenían políticas: timeouts por rol, PgBouncer y ventanas estrictas de migración. Cada rol de inquilino tenía un statement_timeout y lock_timeout por defecto, y las migraciones en producción se ejecutaban por un rol “deploy” separado con límites elevados y una ventana de cambio.
Un viernes, un inquilino lanzó una nueva versión de su integración que por error realizó un gran UPDATE en una sola transacción, tocando una tabla caliente. La consulta era válida. También fue desastrosa en esa hora del día. El statement_timeout la mató automáticamente. La app reintentó unas veces y luego retrocedió porque la integración tenía lógica de reintento sensata. La base de datos siguió viva. Otros inquilinos apenas lo notaron.
El on-call vio los errores, identificó rápidamente al inquilino por logs basados en roles y trabajó con soporte para coordinar una solución. Nadie tuvo que entrar en una reunión de emergencia. Nadie “escaló heroicamente” el clúster. El sistema se comportó como un sistema con salvaguardas.
No fue glamuroso. Ese es el punto. El mejor trabajo de SRE parece que nada pasó.
Listas de verificación / plan paso a paso
Paso a paso: fortalecer una base de datos compartida contra inquilinos ruidosos
- Elige el modelo de aislamiento
- Si los inquilinos pueden ejecutar cargas impredecibles (plugins, SQL personalizado, migraciones): preferir base de datos por inquilino o instancia por tier.
- Si controlas todas las consultas y quieres densidad: fila-por-inquilino con RLS de PostgreSQL es viable.
- Aplica el mapeo de identidad
- Crea un rol/usuario de BD distinto por inquilino (o por tier) para poder aplicar límites y atribuir comportamiento.
- Establece timeouts de consulta por defecto
- PostgreSQL: statement_timeout, lock_timeout, idle_in_transaction_session_timeout.
- MariaDB: usa límites por usuario; considera max_execution_time para SELECT según corresponda.
- Implementa control de conexiones
- PostgreSQL: PgBouncer, más dimensionado de pool; evita miles de backends.
- MariaDB: aplica MAX_USER_CONNECTIONS y mantén max_connections realista.
- Establece una regla de “no DDL al mediodía”
- Exigir migraciones dentro de una ventana; preferir patrones de cambio de esquema en línea.
- Haz que el almacenamiento sea aburrido
- Alertar sobre uso de disco temprano (p. ej., 70/80/90%).
- Separar WAL/binlog si es posible; monitorizar uso temporal.
- Barrera mínima de observabilidad
- Formas de consulta top, esperas por bloqueos, conteos de sesiones por inquilino y salud de mantenimiento (vacuum/purge).
- Crea rutas de escalado
- Cuando un inquilino causa incidentes repetidos, muévelo a un tier superior con recursos dedicados—o aplica topes más estrictos. No negocies con la física.
Lista de verificación: al añadir un nuevo inquilino
- Crea rol/usuario de inquilino con valores por defecto apropiados (timeouts, work_mem, etc.).
- Configura tope de conexiones y confirma comportamiento de pooling.
- Confirma proceso de backup/restore para ese modelo de inquilino.
- Activa logging/atribución de consultas para identificar al inquilino rápidamente durante incidentes.
- Valida que endpoints peligrosos (exports, reporting, updates masivos) estén limitados o externalizados.
Lista de verificación: cuando un inquilino quiere “un gran reporte”
- ¿Puede ejecutarse en una réplica o en una tienda analítica separada?
- ¿Existe una excepción de timeout solo para el rol de reporting?
- ¿La consulta está indexada y probada con volúmenes similares a producción?
- ¿Se limita la concurrencia (un reporte a la vez por inquilino)?
Preguntas frecuentes
1) ¿Debo usar una base de datos para todos los inquilinos o una por inquilino?
Si los inquilinos pueden ejecutar cargas impredecibles (plugins, reporting personalizado, migraciones), base de datos por inquilino reduce el radio de impacto. Si controlas las consultas estrictamente y necesitas densidad, tablas compartidas pueden funcionar—pero solo con salvaguardas estrictas.
2) ¿PostgreSQL es “mejor” para multi-tenancy?
PostgreSQL generalmente te da gobernanza a nivel de base de datos más fuerte (RLS, configuraciones por rol, introspección). Eso no reemplaza el buen comportamiento de la app, pero reduce la frecuencia con la que tienes que confiar en ella.
3) ¿Puede MariaDB hacer cumplir la equidad entre inquilinos?
MariaDB puede limitar conexiones por usuario y alguna actividad por usuario. No es un scheduler de equidad completo, pero basta para parar los peores comportamientos de “abrir 500 conexiones” y comprarte estabilidad.
4) ¿Cuál es la protección más efectiva contra vecinos ruidosos?
Control de conexiones más timeouts. Limita la concurrencia por inquilino y mata trabajo desbocado. Todo lo demás (indexado, tuning, particionado) es importante, pero esas dos cosas detienen la hemorragia.
5) ¿Row Level Security en PostgreSQL resuelve multi-tenancy?
Resuelve la aplicación de acceso a datos. No resuelve el aislamiento de rendimiento por sí sola. Un inquilino todavía puede ejecutar una consulta costosa que esté “permitida” pero sea destructiva.
6) ¿Debo poner inquilinos pesados en réplicas?
Pon cargas de lectura pesada y reporting en réplicas si los requisitos de consistencia lo permiten. Pero vigila el lag de réplica y asegúrate de que la app no vuelva silenciosamente a la primaria cuando el lag aumente.
7) ¿Cómo evito que inquilinos hagan cambios de esquema en horas pico?
No confíes en la cortesía. Hazlo cumplir: restringe privilegios DDL para roles de inquilino, enruta migraciones a través de un rol de despliegue controlado y usa lock timeouts para evitar amontonamientos.
8) ¿Cuál es la forma más rápida de encontrar al inquilino que causa problemas?
Usa usuarios/roles de base de datos por inquilino y atribución de consultas. Sin ese mapeo acabarás adivinando por IPs y logs de aplicación mientras el clúster arde.
9) ¿Cómo decido cuándo separar inquilinos en instancias separadas?
Separa cuando el patrón de carga de un inquilino es fundamentalmente diferente (escrituras pesadas, datos enormes, reporting constante) o cuando dispara incidentes repetidos. El aislamiento suele ser más barato que apagones recurrentes.
10) ¿Son útiles los controles a nivel de SO (cgroups) para bases de datos?
Son útiles como red de seguridad gruesa, especialmente en despliegues containerizados. No sustituyen la gobernanza a nivel de consulta y conexión, porque no entienden inquilinos ni SQL.
Próximos pasos que puedes hacer esta semana
- Mapea identidad de inquilino a identidad de base de datos: un rol/usuario por inquilino o por tier. Si no puedes hacer eso, la respuesta a incidentes seguirá siendo cara.
- Aplica timeouts:
- PostgreSQL: statement_timeout, lock_timeout, idle_in_transaction_session_timeout por rol de inquilino.
- MariaDB: límites por usuario y controles de ejecución donde sea seguro.
- Implementa pooling/presión hacia atrás: PgBouncer para Postgres; límites sensatos de workers y estrategia de pooling para stacks MariaDB.
- Activa la observabilidad correcta: pg_stat_statements o resúmenes de Performance Schema, más dashboards de bloqueos/sesiones.
- Escribe tu política de “cuándo expulsar a un inquilino”: no como castigo, como ingeniería. Si un inquilino necesita recursos dedicados, ofrécele un tier que refleje la realidad.
Si quieres un principio guía: diseña para que el peor inquilino no pueda tomar como rehén todo tu negocio. Las bases de datos son poderosas. No son negociadoras.