Alquilaste un VPS pequeño porque estás siendo responsable. Luego instalaste MariaDB porque eres ambicioso. Ahora tu “aplicación simple” tiene un demonio de base de datos, un buffer pool, un sistema de autenticación, tormentas de conexiones y una historia de copias de seguridad que no has probado. Mientras tanto, tu carga de trabajo es: “unos pocos usuarios, un trabajo cron y un proceso web que mayormente lee”.
Este es el momento de admitir una verdad aburrida: para una gran clase de proyectos en VPS pequeños, MariaDB no es una elección de base de datos. Es una elección operativa. SQLite suele ser la opción predeterminada mejor—hasta que deja de serlo.
La decisión que realmente estás tomando
La gente enmarca esto como “SQLite vs MariaDB.” Eso es una omisión engañosa. La decisión real es:
- Base de datos embebida en archivo (SQLite): menos piezas móviles, menos hilos en segundo plano, menos perillas, menos modos de fallo. A cambio sacrificas algunos patrones de concurrencia y algunas herramientas operativas.
- Servicio cliente-servidor (MariaDB): protocolo de red, autenticación, pooling de conexiones, mantenimiento en segundo plano, opciones de replicación y muchas cosas que puedes configurar incorrectamente a las 2 a.m.
En un VPS pequeño (1–2 vCPU, 1–4 GB RAM, SSD modesto), el factor limitante rara vez es el “conjunto de características SQL”. Normalmente es uno de:
- Presión de RAM e intercambio (el asesino silencioso del rendimiento).
- Picos de latencia de I/O (vecinos ruidosos, créditos de ráfaga, almacenamiento barato).
- Demasiadas conexiones o hilos para la CPU.
- Copias de seguridad que existen solo en tu imaginación.
- Un patrón de concurrencia que parece bien en desarrollo y se desmorona con un poco de contención real.
Si tu aplicación es de nodo único, con volumen de escrituras bajo a moderado, y prefieres lanzar producto a cuidar un demonio, SQLite merece la primera consideración. Si necesitas muchos escritores concurrentes, acceso remoto desde múltiples máquinas o cambios de esquema en línea sin tiempo de inactividad, MariaDB deja de parecer sobreingenerado y pasa a ser supervisión adulta.
Idea parafraseada de Werner Vogels (mentalidad de ingeniería/confiabilidad): “Todo falla, así que diseña esperando fallos.” Construye tu capa de datos alrededor de eso, no de sensaciones.
Broma #1: Ejecutar MariaDB para una app de hobby pequeña es como contratar a un contador a tiempo completo para gestionar tu cambio de bolsillo—impresionante, pero el papeleo ganará.
Hechos e historia que importan en producción
Un poco de contexto no es trivia; explica por qué estos sistemas se comportan como lo hacen.
- SQLite es embebido por diseño. Es una librería enlazada a tu proceso, no un demonio. Por eso el coste de “conexión” es microscópico y por qué los permisos de archivo de repente importan mucho.
- SQLite ha estado en uso en producción desde 2000. No es “tecnología de juguete.” Se usa en navegadores, teléfonos y un sinnúmero de sistemas embebidos porque es estable y aburrida.
- El modo WAL de SQLite fue un punto de inflexión. Write-Ahead Logging mejoró la concurrencia para muchas cargas de lectura pesada permitiendo lectores durante escrituras (con restricciones).
- MySQL llegó primero; MariaDB es un fork. MariaDB se separó de MySQL tras la adquisición de Sun por Oracle. Esa historia del fork importa cuando lees consejos antiguos de “MySQL” y asumes que aplican.
- InnoDB se convirtió en el motor por defecto. Semántica transaccional, recuperación tras fallos y bloqueo a nivel de fila son la razón por la que domina. También por eso el dimensionamiento de memoria y el comportamiento de fsync importan.
- El modelo de bloqueo de SQLite es centrado en el archivo. Ese archivo único es a la vez la magia y la limitación. Es simple, hasta que tu carga tiene muchos escritores o transacciones largas.
- MariaDB tiene pools de hilos y múltiples rutas de ejecución. Ajustar el manejo de hilos puede hacer o romper el rendimiento en CPUs pequeñas; las configuraciones por defecto pueden ser adecuadas o terribles según el comportamiento de las conexiones.
- Las columnas “tipadas” de SQLite son más flexibles de lo que parecen. La afinidad de tipos no es lo mismo que tipado estricto, lo cual puede ser una función o una trampa según la higiene de tus datos.
- MariaDB trae herramientas maduras. Logs de consultas lentas, alternativas del performance schema y opciones de replicación son ventajas reales cuando necesitas observabilidad y patrones de escala.
Formas de carga: quién gana dónde
Caso A: VPS único, una instancia de la app, mayormente lecturas
Elige SQLite a menos que tengas una razón específica para no hacerlo. Obtienes:
- Sin salto de red.
- Sin necesidad de un pool de conexiones (aunque aún necesitas uso sensato a nivel de aplicación).
- Un único archivo que puedes respaldar, verificar checksum y enviar.
- Menor huella de RAM; sin buffer pool que debas dimensionar.
Con modo WAL y transacciones cortas, SQLite puede manejar una cantidad sorprendente de tráfico. La trampa son las transacciones de larga duración y los hábitos de “escribir por cada petición”.
Caso B: muchos escritores concurrentes (cola, API habladora, escrituras tipo métricas)
MariaDB comienza a ganar porque está diseñada para programar cargas de escritura concurrentes con bloqueos a nivel de fila y controles internos de concurrencia. SQLite puede hacerlo, pero pasarás más tiempo peleando con la contención de bloqueos que disfrutando la vida.
Caso C: múltiples servidores de app o un job runner separado
Si varias máquinas deben escribir en la misma base de datos, SQLite suele ser la herramienta equivocada. Sí, puedes poner SQLite en almacenamiento de red. No, generalmente no deberías. El momento en que introduces sistemas de archivos en red en una arquitectura de “VPS pequeño”, has construido un simulador de fallos.
Caso D: necesitas migraciones en línea y perillas operativas
MariaDB te da más palancas: DDL en línea en muchos casos, mejor introspección en tiempo de ejecución y patrones de migración/replicación establecidos. SQLite puede hacer migraciones, pero no obtienes la misma flexibilidad operativa cuando estás en el barro.
Caso E: los datos son pequeños y valiosos
El archivo único de SQLite es atractivo para datos “pequeños y valiosos”: estado de configuración, tokens de facturación, flags de funciones, logs de auditoría para una app pequeña, cachés que te importan. Tu historia de backup y restore puede ser extremadamente sencilla—si lo haces correctamente.
Cuándo MariaDB es exagerado (y por qué duele)
MariaDB no es “pesado” en el sentido empresarial. Es pesado en el sentido en que lo sienten los servidores pequeños: un servicio en segundo plano con apetito de memoria y muchos hilos concurrentes que no se reducen educadamente.
El impuesto oculto: memoria y swap
En VPS pequeños, la falla más común de MariaDB no es “índice equivocado”. Es thrashing de swap. Cuando MariaDB + app + caché de páginas del SO exceden la RAM, el sistema se degrada hasta un colapso en cámara lenta. La latencia se vuelve no lineal. Las peticiones se encolan. Los timeouts disparan reintentos. La carga aumenta. Lo ves arder.
SQLite no evita mágicamente los problemas de memoria, pero su huella base es menor y no ejecuta un demonio que quiera ayudar almacenando en caché el universo.
Conexiones, hilos y la falacia “es solo un VPS”
Las apps pequeñas a menudo usan un tamaño de pool ORM por defecto de 10–50 por proceso porque eso dijo un post. En un VPS pequeño, eso puede significar:
- Demasiados hilos de BD
- Sobrehead de cambio de contexto
- Contención de bloqueos amplificada por la concurrencia
- Overhead de memoria por conexión
SQLite en gran medida evita las “tormentas de conexiones” porque las conexiones son en proceso y baratas. Aun así puedes dispararte en el pie con concurrencia, pero es más difícil crear accidentalmente 400 conexiones TCP a ti mismo.
Superficie operativa
MariaDB trae privilegios, exposición de red, TLS, gestión de usuarios, propiedad del directorio de datos, rotación de logs, actualizaciones con compatibilidad y tareas en segundo plano. Ninguno de estos es malo. Pero para un proyecto pequeño, cada uno es un posible pozo de tiempo.
Test de olor a sobreingenería
MariaDB probablemente es exagerado si la mayoría de estas son verdaderas:
- VPS único, instancia única de la app.
- Las escrituras son ocasionales (segundos a minutos entre escrituras, o lotes pequeños).
- El conjunto de datos cabe cómodamente en la caché de páginas del SO (o es pequeño).
- No necesitas acceso remoto desde múltiples hosts.
- No necesitas alta concurrencia de escrituras.
- Prefieres backups y restores súper simples.
SQLite: los bordes afilados que debes respetar
La concurrencia trata sobre transacciones, no deseos
SQLite puede manejar muchos lectores y un solo escritor a la vez. El modo WAL mejora la coexistencia lectura/escritura, pero no hace que “muchos escritores” sean gratis. El peor patrón son las transacciones de escritura largas o cualquier transacción que mantenga bloqueos mientras haces I/O de red o trabajo complejo de aplicación.
Regla de diseño: mantiene las transacciones cortas, haz el trabajo fuera de la transacción y luego escribe.
El modo WAL no es opcional para la mayoría de cargas web
Si estás construyendo una app web y te quedas con el modo rollback journal por defecto, estás eligiendo voluntariamente más dolor de bloqueo. WAL suele ser la decisión correcta. También cambia la semántica de respaldo: debes tener en cuenta los archivos WAL.
Las configuraciones de durabilidad son verdaderos trade-offs
SQLite hace fácil ajustar pragmas como synchronous=NORMAL y sentirte un mago del rendimiento. También estás intercambiando algunas garantías de durabilidad. Si tu VPS puede perder energía o tu hipervisor puede reiniciar de forma no ordenada (puede), debes saber a qué te expones.
Los sistemas de archivos en red son una trampa común
SQLite espera semánticas POSIX razonables del sistema de archivos. En muchos sistemas de archivos en red, el bloqueo y el comportamiento de fsync son… un baile interpretativo. SSD local en el VPS: bien. NFS/SMB/“alguna cosa compartida”: normalmente no.
Un archivo significa un conjunto de permisos
La base de datos es un archivo. Eso es maravillosamente simple y también brutalmente literal. Si tu despliegue cambia usuarios, contenedores o directorios de trabajo, puedes romper la app con un chmod.
MariaDB: los bordes afilados que encontrarás
Las configuraciones por defecto no son “seguras para pequeños”
Los valores por defecto de MariaDB suelen ser sensatos para servidores de propósito general, pero “propósito general” asume más recursos que tu VPS de 5 dólares. El buffer pool, los buffers por conexión y los hilos en segundo plano pueden exceder lo que puedes permitirte.
La memoria por conexión es la fuga silenciosa de RAM
No es una fuga, es peor: es “funcionamiento según diseño”. Cada conexión puede asignar buffers para sort, join, tablas temporales, etc. El resultado: las tormentas de conexiones se convierten en tormentas de memoria. Ves swap, luego timeouts, luego pánico.
fsync y durabilidad: el rendimiento depende de la realidad del almacenamiento
La durabilidad de InnoDB depende del vaciado de logs. En almacenamiento barato de VPS, la latencia de fsync puede dispararse. Verás bloqueos periódicos y tu app acusará a la “base de datos lenta” como si fuera un defecto de personalidad.
Las copias de seguridad son un proceso, no un archivo
Con SQLite, el backup puede ser una operación a nivel de archivo (hecha correctamente). Con MariaDB, los backups consistentes requieren herramientas y pensamiento: dumps lógicos, copias físicas, comportamiento de bloqueo, posicionamiento si usas replicación. Nada de esto es difícil, pero es fácil no hacerlo.
Huella de seguridad
SQLite no tiene puerto de escucha. MariaDB sí. Si lo expones a Internet sin querer, aprenderás lo popular que es el password spraying. “Pero usé una contraseña fuerte” no es una estrategia; es una esperanza.
Broma #2: Abrir el puerto 3306 a Internet es una excelente manera de conocer bots que nunca duermen—al contrario de tu rotación on-call.
Tareas prácticas: comandos, salidas y decisiones
No eliges bases de datos leyendo matrices de características. Las eliges preguntándole a la máquina qué está haciendo. Abajo hay tareas concretas que ejecuto en VPS pequeños. Cada una incluye el comando, salida de ejemplo, qué significa y la decisión que tomas.
Tarea 1: ¿Está el VPS intercambiando?
cr0x@server:~$ free -h
total used free shared buff/cache available
Mem: 1.9Gi 1.6Gi 120Mi 32Mi 230Mi 170Mi
Swap: 1.0Gi 780Mi 244Mi
Qué significa: El swap está muy usado. En un VPS pequeño, esto se correlaciona con stalls aleatorios de varios segundos.
Decisión: Si estás en MariaDB, reduce la huella de memoria (buffer pool, count de conexiones) o migra a SQLite si la carga encaja. Si usas SQLite, reduce la memoria de la app o añade RAM; SQLite no es la causa, pero igualmente puede sufrir.
Tarea 2: ¿Estamos en el infierno de I/O wait?
cr0x@server:~$ vmstat 1 5
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
r b swpd free buff cache si so bi bo in cs us sy id wa st
2 1 798720 122880 11264 215040 0 5 120 980 520 840 18 7 55 20 0
1 2 798720 118432 11264 213120 0 12 210 1460 600 920 12 6 48 34 0
Qué significa: Alto wa (I/O wait) y swap indican latencia de almacenamiento y presión de memoria.
Decisión: Arregla primero el almacenamiento y la RAM. No ajustes SQL mientras el host se está ahogando.
Tarea 3: ¿Es MariaDB el mayor consumidor de memoria?
cr0x@server:~$ ps -eo pid,comm,rss,pcpu --sort=-rss | head
2481 mariadbd 612340 18.2
3022 python3 248120 9.1
1102 nginx 45200 0.3
Qué significa: El RSS de MariaDB es ~600MB; en 2GB esto puede estar bien, en 1GB no.
Decisión: Si tu conjunto de datos es pequeño y la concurrencia de escritura baja, considera SQLite para recuperar RAM. Si no, ajusta el buffer pool de InnoDB y el uso de conexiones.
Tarea 4: ¿Cuántas conexiones usamos realmente (MariaDB)?
cr0x@server:~$ sudo mariadb -e "SHOW GLOBAL STATUS LIKE 'Threads_connected'; SHOW VARIABLES LIKE 'max_connections';"
Variable_name Value
Threads_connected 87
Variable_name Value
max_connections 151
Qué significa: Estás rondando el techo. Cada conexión tiene overhead.
Decisión: Arregla el tamaño del pool de la app, añade un pooler o reduce la concurrencia. No aumentes simplemente max_connections en un VPS pequeño a menos que también presupuestes RAM.
Tarea 5: ¿El log de consultas lentas dice la verdad (MariaDB)?
cr0x@server:~$ sudo mariadb -e "SHOW VARIABLES LIKE 'slow_query_log'; SHOW VARIABLES LIKE 'long_query_time';"
Variable_name Value
slow_query_log OFF
Variable_name Value
long_query_time 10.000000
Qué significa: El logging está apagado y el umbral es demasiado alto. Estás a oscuras.
Decisión: Habilita temporalmente el log de consultas lentas con un umbral bajo (por ejemplo 0.2–0.5s) para atrapar a los verdaderos culpables. Luego ajústalo para evitar spam de logs.
Tarea 6: ¿Estamos limitados por CPU o I/O (vista rápida)?
cr0x@server:~$ top -b -n 1 | head -15
top - 12:00:11 up 21 days, 2:14, 1 user, load average: 3.12, 2.44, 1.98
Tasks: 132 total, 2 running, 130 sleeping, 0 stopped, 0 zombie
%Cpu(s): 72.0 us, 8.0 sy, 0.0 ni, 12.0 id, 8.0 wa, 0.0 hi, 0.0 si, 0.0 st
MiB Mem : 2048.0 total, 140.0 free, 1550.0 used, 358.0 buff/cache
Qué significa: Alta CPU de usuario sugiere ejecución de consultas o lógica de la app; un I/O wait no trivial sugiere latencia de almacenamiento también.
Decisión: Si la CPU es alta con pocas consultas, revisa planes de consulta e índices. Si los picos de I/O wait se correlacionan con commits, verifica el comportamiento de fsync y la salud del disco.
Tarea 7: Chequeo rápido de integridad en SQLite
cr0x@server:~$ sqlite3 /var/lib/myapp/app.db "PRAGMA integrity_check;"
ok
Qué significa: El archivo de la base de datos es estructuralmente consistente.
Decisión: Si no es “ok”, detén las escrituras, toma una copia y planea la recuperación. La corrupción es rara pero no imaginaria—especialmente con almacenamiento inseguro o cierres bruscos.
Tarea 8: Modo journal y ajustes synchronous de SQLite
cr0x@server:~$ sqlite3 /var/lib/myapp/app.db "PRAGMA journal_mode; PRAGMA synchronous;"
wal
2
Qué significa: El modo WAL está habilitado; synchronous=2 significa FULL (durable, más lento).
Decisión: Para muchas apps en VPS, WAL es imprescindible. Mantén synchronous FULL si la corrección de datos importa. Si eliges NORMAL, hazlo sabiendo y documentando el riesgo.
Tarea 9: Síntomas de contención de bloqueos en SQLite (busy timeouts)
cr0x@server:~$ sqlite3 /var/lib/myapp/app.db "PRAGMA busy_timeout;"
0
Qué significa: Sin busy timeout; los escritores pueden fallar inmediatamente bajo contención.
Decisión: Configura un busy timeout razonable en la app (o mediante PRAGMA por conexión) y arregla la longitud de las transacciones. Los busy timeouts son curitas; las transacciones largas son la infección.
Tarea 10: Comprobación del tamaño del buffer pool InnoDB en MariaDB
cr0x@server:~$ sudo mariadb -e "SHOW VARIABLES LIKE 'innodb_buffer_pool_size';"
Variable_name Value
innodb_buffer_pool_size 1073741824
Qué significa: Buffer pool de 1GB. En un sistema de 1–2GB esto puede ser demasiado grande una vez que incluyes SO, caché de páginas y memoria de la app.
Decisión: En VPS pequeños, comienza conservador. Si ocurre swapping, encojelo. La RAM que no tienes no es una caché, es un generador de timeouts.
Tarea 11: ¿Estamos usando tablas temporales en disco en MariaDB?
cr0x@server:~$ sudo mariadb -e "SHOW GLOBAL STATUS LIKE 'Created_tmp_disk_tables'; SHOW GLOBAL STATUS LIKE 'Created_tmp_tables';"
Variable_name Value
Created_tmp_disk_tables 1842
Variable_name Value
Created_tmp_tables 9621
Qué significa: Una fracción significativa de tablas temporales llega a disco, lo cual puede ser lento en almacenamiento económico.
Decisión: Revisa consultas que provoquen sorts/joins; añade índices; considera aumentar límites de tablas temporales solo si tienes RAM libre (raro en VPS pequeños).
Tarea 12: Instantánea rápida de las consultas más lentas (processlist) en MariaDB
cr0x@server:~$ sudo mariadb -e "SHOW FULL PROCESSLIST;"
Id User Host db Command Time State Info
412 app user@localhost mydb Query 12 Sending data SELECT * FROM events WHERE user_id=...
431 app user@localhost mydb Sleep 55 NULL
Qué significa: Una consulta ha estado ejecutándose 12 segundos; muchas conexiones en sleep pueden indicar problemas de pool.
Decisión: Encuentra esa consulta, explícalo, añade un índice o reescríbela. Para los sleepers, reduce el tamaño del pool o la vida; deja de acaparar conexiones inactivas en una máquina pequeña.
Tarea 13: Verifica que MariaDB no esté expuesto en red
cr0x@server:~$ sudo ss -lntp | grep -E '3306|mysqld|mariadbd'
LISTEN 0 80 127.0.0.1:3306 0.0.0.0:* users:(("mariadbd",pid=2481,fd=21))
Qué significa: Vinculado solo a localhost, lo cual es bueno para apps de un solo host.
Decisión: Si está escuchando en 0.0.0.0, corrige bind-address y las reglas de firewall inmediatamente. No lo “planees para después.” Después es cuando empieza el credential stuffing.
Tarea 14: Hacer backup de SQLite de la forma correcta (segura en línea)
cr0x@server:~$ sqlite3 /var/lib/myapp/app.db ".backup '/var/backups/app.db.bak'"
Qué significa: SQLite crea un backup consistente incluso mientras la BD está en uso (dentro de las garantías de SQLite).
Decisión: Usa .backup o la API de backup, no un cp ingenuo, a menos que hayas parado escritores y tengas en cuenta los archivos WAL.
Tarea 15: Sanidad de un backup lógico de MariaDB
cr0x@server:~$ sudo mysqldump --single-transaction --quick --routines mydb | head
-- MySQL dump 10.19 Distrib 10.11.6-MariaDB, for debian-linux-gnu (x86_64)
--
-- Host: localhost Database: mydb
-- ------------------------------------------------------
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
Qué significa: Puedes producir un volcado lógico consistente sin bloquear tablas agresivamente (para InnoDB).
Decisión: Si este dump tarda demasiado o impacta el rendimiento, puede que necesites mejor planificación o que estés superando el plan del VPS.
Tarea 16: Espacio en disco: ¿estás a punto de fallar de forma tonta?
cr0x@server:~$ df -h /var/lib
Filesystem Size Used Avail Use% Mounted on
/dev/vda1 25G 24G 0.9G 97% /
Qué significa: Estás a un pico de logs de sufrir downtime. Las bases de datos odian “disco lleno.” Te castigarán creativamente.
Decisión: Limpia logs, mueve backups fuera del host, amplía el disco. Esto no es mantenimiento opcional; es disponibilidad.
Guía de diagnóstico rápido
Esta es la secuencia “dejar de adivinar”. Ejecútala cuando tu app en VPS pequeño se sienta lenta y la gente empiece a culpar “la base de datos” como si fuera un compañero de trabajo que nunca responde.
Primero: restricciones a nivel de host (60 segundos)
- Swap y RAM:
free -h. Si el swap está activo y la RAM es baja, ese es tu principal sospechoso. - I/O wait:
vmstat 1 5oiostat -xz 1 5(si está instalado). Altowasignifica que el almacenamiento es parte de la historia. - Espacio en disco:
df -h. Discos casi llenos causan stalls, comportamiento extraño y a veces corrupción tras caídas.
Segundo: realidad a nivel de procesos (2–3 minutos)
- Quién usa CPU y memoria:
psordenado por RSS ytop. Si MariaDB domina la RAM en un host de 1GB, ajusta o reconsidera la arquitectura. - Conteo de conexiones (MariaDB): revisa
Threads_connectedfrente a la configuración de tu pool. - Descriptores de archivos abiertos (ambos): si tu app alcanza límites, SQLite puede fallar al abrir la BD; MariaDB puede fallar al aceptar conexiones.
Tercero: cuellos de botella específicos de la base de datos (5–15 minutos)
- MariaDB: revisa processlist por consultas largas; habilita el log de consultas lentas brevemente; busca tablas temporales en disco y stalls de log de InnoDB.
- SQLite: verifica modo WAL; revisa busy_timeout y logs de la app por “database is locked”; identifica transacciones largas; ejecuta integrity_check si sospechas corrupción.
- Capa de la app: confirma que tu ORM no esté haciendo N+1 queries y que no inicies una transacción por petición sin razón.
Si haces solo una cosa: arregla swap e I/O wait antes de tocar el tuning SQL. La mayoría de problemas de “rendimiento de base de datos” en VPS pequeños son en realidad problemas de “agotamiento del host” disfrazados de SQL.
Tres mini-historias corporativas (del archivo de cicatrices)
1) Incidente causado por una suposición errónea: “SQLite no puede hacer concurrencia”
Una pequeña herramienta interna vivía en una sola VM. Manejaba un flujo modesto de eventos, escribía unas filas por segundo y servía dashboards a quizá una docena de personas. Empezó en SQLite, funcionó bien y nadie se preocupó por meses—hasta que un ingeniero leyó un hilo que decía “SQLite no es para producción”.
Lo cambiaron a MariaDB con prisa, porque producción equivale a cliente-servidor, ¿no? Montaron una instancia MariaDB por defecto, migraron el esquema y apuntaron la app. Funcionó en staging. Funcionó en el portátil. Funcionó un día.
Entonces la VM empezó a degradarse. Los dashboards hicieron timeout. La cola de trabajos se acumuló. El equipo persiguió “consultas lentas”, añadió un par de índices y obtuvo una mejora breve. Pero el verdadero culpable era simple: la instancia MariaDB y la app competían por 2GB de RAM, y bajo carga el host usó swap. Picos de latencia causaron reintentos. Los reintentos causaron más conexiones. Más conexiones causaron más memoria. Espiraló.
La suposición equivocada no era sobre características SQL. Era sobre comportamiento de sistemas. SQLite había sido suficiente porque tenía una huella menor y no activaba swap. MariaDB podría haber estado bien también, pero no con el pool de conexiones por defecto y sin presupuestar memoria.
La solución no fue heroica: revirtieron a SQLite, habilitaron modo WAL, añadieron busy timeout y arreglaron la app para agrupar escrituras. El incidente terminó no con un breakthrough de escalado, sino con el recordatorio de que “listo para producción” significa “apropiado operativamente”, no “el más popular en ofertas de empleo”.
2) Optimización que rebotó: “Hacer SQLite más rápido”
Una API en un VPS económico usaba SQLite. Las escrituras eran pequeñas pero frecuentes. Alguien notó latencias ocasionales de escritura y decidió “optimizar” cambiando pragmas: synchronous=OFF, caches más grandes, almacenamiento temporal agresivo en memoria. Los benchmarks en la máquina del desarrollador se vieron fantásticos. Lo desplegaron un viernes, porque el optimismo es una fuente de energía.
Por unos días se sintió más rápido. Luego el VPS tuvo un reinicio no planificado—mantenimiento del hipervisor, de esos sin invitación en el calendario. La app volvió y empezó a fallar peticiones. El archivo SQLite no estaba catastróficamente corrupto, pero tenía estado de aplicación inconsistente. Faltaban algunas filas críticas de una tabla que debía ser append-only.
La depuración fue fea porque la base de datos estaba “bien” estructuralmente. Sin corrupción obvia. Solo escrituras faltantes que nunca llegaron a almacenamiento durable. Ese es el coste de synchronous=OFF: pediste a SQLite que te mintiera sobre durabilidad, y lo hizo.
Restauraron desde backups y reprocesaron algunos eventos. Luego volvieron los pragmas a valores sensatos, mantuvieron WAL y en su lugar optimizaron el comportamiento real de la aplicación: menos commits, más batching, transacciones más cortas e índices adecuados. El rendimiento se mantuvo. Los datos dejaron de desaparecer.
3) Práctica aburrida pero correcta que salvó el día: “Probar la restauración”
Un pequeño SaaS corría en un VPS con MariaDB. Nada sofisticado: un primario, sin réplicas, sin clusters, sin herramientas elaboradas. Pero el operador tenía un ritual: pruebas semanales de restauración en una VM desechable. No un plan teórico, una restauración real.
Una mañana, el disco del VPS empezó a devolver errores de I/O. El servicio de BD comenzó a crashear. El sistema de archivos se remontó en modo solo-lectura. Llegaron tickets de soporte, luego se detuvieron, porque la app estaba caída. El operador no intentó reparaciones heroicas. Paró el servicio, capturó los logs que pudo y levantó un nuevo VPS.
Porque las pruebas de restauración eran rutinarias, los pasos de recuperación estaban escritos y correctos. Restauraron el volcado de la noche anterior, reprodujeron un pequeño conjunto de eventos de la cola y pusieron el servicio en marcha. El downtime fue molesto, pero acotado. Sin improvisación bajo estrés.
La práctica aburrida no fue “usar MariaDB.” Fue “practicar restauraciones.” Las bases de datos no fallan según tu horario, así que tu recuperación no puede ser una hipótesis no probada.
Errores comunes: síntomas → causa raíz → solución
1) Errores “Database is locked” en SQLite con tráfico ligero
Síntomas: 500s esporádicos, logs muestran database is locked, picos durante trabajos en background.
Causa raíz: transacciones largas (a menudo “BEGIN; hacer trabajo; llamar a servicio externo; COMMIT”), sin busy timeout, modo rollback journal, o demasiados escritores.
Solución: habilita modo WAL; acorta transacciones; establece busy timeout; agrupa escrituras; asegura solo un camino escritor si es necesario.
2) MariaDB “Too many connections” en un VPS pequeño
Síntomas: errores de la app al conectar; logs de BD muestran límites de conexión; CPU sube durante incidentes.
Causa raíz: pools ORM sobredimensionados, falta de reutilización de conexiones o reintentos que causan tormentas.
Solución: reduce el tamaño del pool; implementa pooling correctamente; limita reintentos con backoff; monitorea Threads_connected.
3) Stalls periódicos de 2–10 segundos en escrituras de MariaDB
Síntomas: picos de latencia alrededor de commits; la CPU no está saturada; los usuarios se quejan de “lentitud aleatoria”.
Causa raíz: picos de latencia de fsync del almacenamiento, disco barato en VPS, comportamiento de flush de InnoDB logs.
Solución: mueve a tier de almacenamiento mejor; reduce amplificación de escritura (batching); asegura configuración sensata de logs InnoDB; evita saturar el disco con trabajos no relacionados.
4) SQLite “funciona en dev” pero falla en despliegue en contenedor
Síntomas: no puede abrir archivo DB, permiso denegado, o el archivo DB se reinicia inesperadamente.
Causa raíz: volumen montado incorrecto, UID/GID equivocado o sistema de archivos efímero en contenedor.
Solución: volumen persistente explícito; propiedad correcta; configura la ruta del archivo vía configuración; asegura que despliegues atómicos no reemplacen el archivo DB.
5) MariaDB usa toda la RAM lentamente con el tiempo
Síntomas: uso de memoria crece; swap comienza; rendimiento se degrada tras días.
Causa raíz: buffer pool demasiado grande, buffers por conexión activados por consultas complejas, demasiadas conexiones concurrentes.
Solución: reduce buffer pool; disminuye la concurrencia; reescribe consultas costosas; monitorea tablas temporales y actividad de sort.
6) “Existe el backup” pero la restauración falla
Síntomas: ejecutas un drill de restauración y falla, o los datos restaurados son inconsistentes.
Causa raíz: backup de SQLite tomado incorrectamente mientras WAL estaba activo; dump de MariaDB tomado sin consistencia transaccional; routines faltantes; supuestos de charset/collation equivocados.
Solución: SQLite: usa .backup o detén escritores y copia DB+WAL; MariaDB: usa --single-transaction para tablas InnoDB y prueba la restauración regularmente.
7) “SQLite es lento” durante reportes grandes
Síntomas: SELECTs de larga duración bloquean escrituras; peticiones web tiran timeout.
Causa raíz: transacciones de lectura largas que mantienen snapshots; índices insuficientes; escaneos grandes en discos baratos.
Solución: añade índices; pagina; mueve reporting a una réplica/archivo ETL; considera MariaDB si reporting y escrituras deben coexistir con mayor concurrencia.
8) La actualización de MariaDB rompe la app inesperadamente
Síntomas: consultas se comportan diferente; cambian las estricturas; mismatch de plugin de autenticación.
Causa raíz: valores por defecto específicos de versión y diferencias de compatibilidad entre variantes MySQL/MariaDB; pruebas insuficientes en staging.
Solución: fija versiones; prueba actualizaciones en staging con datos reales; registra diffs de configuración; ten plan de rollback.
Listas de verificación / plan paso a paso
Paso a paso: elegir SQLite en un VPS pequeño (la ruta “ship it”)
- Confirma la forma de la carga: un nodo, escrituras de baja a moderada, sin necesidad de escritores multi-host.
- Habilita modo WAL: configura
PRAGMA journal_mode=WAL;al inicializar la BD. - Establece durabilidad sensata: prefiere
synchronous=FULLpara corrección; consideraNORMALsolo si aceptas pérdida de datos ante fallo de energía. - Configura busy timeout: evita fallos inmediatos bajo contención; sigue corrigiendo la longitud de transacciones.
- Diseña para un escritor: serializa caminos de código con muchas escrituras o agrupa mediante una cola en proceso.
- Backups: usa
.backup; envía backups fuera del host; prueba restauraciones mensualmente. - Observabilidad: registra latencia de consultas en la app; captura errores “database is locked” con contexto.
Paso a paso: si insistes en MariaDB en un VPS pequeño (hazlo seguro)
- Vincular a localhost: expón la BD solo si realmente necesitas acceso remoto.
- Presupuesta RAM: dimensiona buffer pool de forma conservadora; deja RAM para SO y app.
- Arregla el pooling de conexiones: tamaños de pool pequeños; capea la concurrencia; evita conectar/desconectar por petición.
- Habilita observabilidad mínima: log de consultas lentas con umbral sensato durante troubleshooting; vigila tablas temporales en disco.
- Backups y drills de restauración: automatiza dumps; rota; prueba restauraciones en instancias nuevas.
- Planifica actualizaciones: fija versión; prueba; programa; ten rollback.
Paso a paso: ruta de migración (SQLite ahora, MariaDB después) sin drama
- Mantén SQL portable: evita rarezas propias de SQLite si esperas migrar (p. ej., depender de tipado flexible como “característica”).
- Usa migraciones explícitas: versiona tu esquema, no confíes en “crear tablas al inicio”.
- Abstrae el acceso a BD: mantén una pequeña capa de acceso a datos; no esparzas SQL crudo a menos que te guste la arqueología.
- Plan de export/import: usa un formato reproducible (CSV para tablas, o export a nivel de aplicación) y verifica conteos de filas y checksums.
- Ejecuta en dual brevemente: escribe en una, lee de la otra solo durante una ventana de corte planificada, si la app lo soporta.
- Corte con congelación: detén escrituras, haz el sync final, cambia, valida y luego reanuda.
Preguntas frecuentes
1) ¿SQLite es “apto para producción” para una app web en VPS?
Sí, si tu carga encaja: nodo único, concurrencia de escrituras modesta, transacciones cortas, modo WAL y backups reales. “Apto para producción” trata sobre comportamiento ante fallos, no sobre si tiene o no un demonio.
2) ¿Cuántos usuarios concurrentes puede manejar SQLite?
Unidad equivocada. Piensa en escritores concurrentes y duración de transacciones. Muchos lectores están bien; muchos escritores con transacciones largas dañarán. Si tu app escribe en cada petición, estás construyendo una carga de escritura aunque no lo pretendieras.
3) ¿El modo WAL soluciona el bloqueo en SQLite?
Mejora el caso común permitiendo lectores durante escrituras, pero no hace que las escrituras sean concurrentes. Aún tienes un escritor a la vez. Debes mantener transacciones cortas y evitar locks de larga duración.
4) ¿Puedo poner SQLite en NFS para compartir entre servidores?
Puedes, pero apuestas tus datos a semánticas del sistema de archivos y estabilidad de la red. Para la mayoría de equipos pequeños, es una mala apuesta. Si necesitas escritores multi-host, estás en territorio de MariaDB (u otra BD cliente-servidor).
5) ¿Por qué MariaDB se siente más lenta en un VPS pequeño que SQLite?
No siempre es más lenta, pero puede serlo: más overhead de memoria, más hilos, más presión de fsync y un protocolo de red incluso en localhost. En hosts limitados, el overhead se vuelve latencia visible para el usuario.
6) ¿Cuál es la copia de seguridad más simple y segura para SQLite?
sqlite3 app.db ".backup 'app.db.bak'", luego copia el backup fuera del host. Prueba las restauraciones. Si simplemente copias el archivo en vivo sin tener en cuenta WAL, puedes obtener backups inconsistentes.
7) ¿Cuál es la copia de seguridad más simple y segura para MariaDB en un VPS único?
mysqldump --single-transaction para tablas InnoDB, programado en horario de baja carga, rotado, copiado fuera del host y probado en restauración. Los backups físicos son más rápidos pero añaden complejidad operativa.
8) ¿Cuándo debo moverme de SQLite a MariaDB?
Cuando necesitas acceso multi-host, alta concurrencia de escrituras, herramientas operativas más ricas o estás gastando tiempo ingenierizando alrededor de la limitación de “un escritor” de SQLite. También cuando necesitas escalar de manera fiable más allá de un nodo único.
9) ¿MariaDB es más segura que SQLite?
No automáticamente. Ambos pueden ser seguros o inseguros según configuración y disciplina operativa. Los riesgos de SQLite suelen ser bloqueo y corrección de backups; los riesgos de MariaDB a menudo están en el tuning de recursos, exposición y complejidad operativa.
10) ¿Puedo usar ambos?
Sí, y a veces es la aproximación más limpia: SQLite para estado local y colas; MariaDB para datos transaccionales compartidos. Solo sé honesto sobre la complejidad que añades.
Próximos pasos que puedes hacer hoy
- Si estás indeciso: mide tu concurrencia de escrituras y la duración de transacciones. Esa es la variable clave. No “características”.
- Si estás en MariaDB en un VPS pequeño: revisa swap, tamaño del buffer pool y conteo de conexiones. Capea pools. Vincula a localhost. Habilita el log de consultas lentas brevemente cuando soluciones problemas.
- Si estás en SQLite: habilita modo WAL, establece un busy timeout y audita tu código por transacciones largas. Implementa la rutina de
.backupy ejecuta una prueba de restauración. - Si planeas crecer: mantén tus migraciones de esquema disciplinadas y tu SQL portable para que mover a MariaDB después sea un cambio planificado, no una reescritura a medianoche.
La base de datos “correcta” para un proyecto en VPS pequeño es la que se mantiene fuera de tu camino y, aun así, es honesta sobre fallos. SQLite suele ser esa base. MariaDB es excelente cuando realmente la necesitas. No pagues el impuesto operativo temprano a menos que estés seguro de que usarás los servicios que compraste.