Las ráfagas de escritura no llegan con educación. Aparecen en manada: runners de trabajos que despiertan a la vez, una cola que se descarga después de un deploy, clientes móviles que se reconectan tras salir de un túnel, o un backfill de “ups” que prometiste que correrá “despacio”. La pregunta no es si tu base de datos puede escribir. La pregunta es si puede escribir mucho, ahora mismo, sin convertir el on-call en un hobby.
MariaDB y SQLite pueden almacenar tus datos. Pero ante picos actúan como especies distintas. MariaDB es un servidor con controles de concurrencia, flushing en segundo plano, buffer pools y una larga historia de haber recibido cargas de producción. SQLite es una librería que vive dentro de tu proceso, brutalmente eficiente y maravillosamente de bajo mantenimiento—hasta que le pides hacer algo parecido a una tormenta de múltiples escritores.
La pregunta real: qué significa “ráfaga” para tu sistema
“Ráfaga de escritura” es una frase vaga que provoca malentendidos costosos. Hay al menos cuatro bestias diferentes que la gente llama ráfaga:
- Pico corto, alta concurrencia: 500 peticiones llegan a la vez, cada una haciendo un insert pequeño.
- Subida sostenida: 10× la tasa normal de escritura durante 10–30 minutos (jobs por lotes, backfills).
- Explosión de latencia en cola larga: el throughput medio parece correcto, pero cada 20 segundos los commits se detienen 300–2000 ms.
- Acantilado de I/O: el disco o el sistema de almacenamiento choca contra un muro de flush (comportamiento de fsync/flush cache), y todo se encola detrás.
MariaDB vs SQLite bajo “ráfagas” trata principalmente de cómo se comportan frente a la concurrencia y cómo pagan por la durabilidad. Si sólo tienes un escritor y toleras algo de encolamiento, SQLite puede ser ridículamente bueno. Si tienes muchos escritores, muchos procesos, o necesitas seguir sirviendo lecturas mientras las escrituras golpean, MariaDB suele ser el adulto en la sala.
Pero hay trampas en ambos lados. La trampa de SQLite es el bloqueo. La trampa de MariaDB es pensar que el servidor de la base de datos es el cuello de botella cuando en realidad es el subsistema de almacenamiento (o tu política de commits).
Algunos hechos e historia que realmente importan
Algunos puntos de contexto que son cortos, concretos y sorprendentemente predictivos del comportamiento ante ráfagas:
- SQLite es una librería, no un servidor. No hay un daemon separado; tu app la enlaza y lee/escribe directamente el archivo de BD. Eso es una superpotencia de rendimiento y una limitación operativa.
- El diseño original de SQLite se optimizó para sistemas embebidos. Se hizo popular en escritorio/móvil porque es “simplemente un archivo” y no necesita un DBA que lo cuide.
- El modo WAL en SQLite se introdujo para mejorar la concurrencia. Separa lecturas de escrituras mediante la escritura en un write-ahead log, permitiendo lectores durante escrituras—hasta cierto punto.
- SQLite aún tiene una regla de un solo escritor a nivel de base de datos. WAL ayuda a las lecturas, pero múltiples escritores concurrentes todavía se serializan en el bloqueo de escritura.
- MariaDB es un fork de MySQL. El fork ocurrió tras la adquisición de Sun por Oracle; MariaDB se convirtió en la opción “amigable con la comunidad” para muchas organizaciones.
- InnoDB se convirtió en el engine por defecto por una razón. Está construido alrededor de MVCC, redo logs, flushing en segundo plano y recuperación ante fallos—características que importan cuando golpean las ráfagas.
- El rendimiento de MariaDB durante ráfagas depende mucho del comportamiento de fsync. Tu política de flush del redo log puede desplazar el dolor de “cada commit se detiene” a “algunos commits se detienen pero el throughput mejora”. Es una compensación, no dinero gratis.
- La mayoría de incidentes de “la base de datos está lenta” durante picos de escritura son en realidad “el almacenamiento está lento.” La base de datos es lo primero que lo admite bloqueándose en fsync.
Anatomía de la ruta de escritura: MariaDB/InnoDB vs SQLite
SQLite: un archivo, un escritor, muy poca ceremonia
SQLite escribe en un único archivo de base de datos (además, en modo WAL, un archivo WAL y un archivo de índice de memoria compartida). Tu proceso emite SQL; SQLite lo traduce en actualizaciones de páginas. Durante el commit de una transacción, SQLite debe asegurar la durabilidad según tus pragmas. Esto normalmente significa forzar los datos a almacenamiento estable usando llamadas tipo fsync, según la plataforma y el sistema de archivos.
Bajo ráfagas, el detalle crítico de SQLite es qué tan rápido puede ciclar “adquirir bloqueo de escritura → escribir páginas/WAL → política de sync → liberar bloqueo”. Si los commits son frecuentes y pequeños, la sobrecarga la dominan las llamadas de sync y los traspasos de bloqueo. Si los commits se agrupan, SQLite puede volar.
El modo WAL cambia la forma: los escritores apenden al WAL y los lectores pueden seguir leyendo el snapshot principal. Pero sigue habiendo un solo escritor a la vez, y los checkpoints pueden convertirse en una segunda clase de ráfaga (más sobre eso después).
MariaDB/InnoDB: concurrencia, buffering y I/O en segundo plano
MariaDB es un proceso servidor con múltiples hilos trabajadores. InnoDB mantiene un buffer pool (cache) para páginas, un redo log (write-ahead), y a menudo un undo log para MVCC. Cuando haces commit, InnoDB escribe registros de redo y—dependiendo de la configuración—los flush a disco. Las páginas sucias se vacían en segundo plano.
Bajo ráfagas, la superpotencia de InnoDB es que puede aceptar muchos escritores concurrentes, encolar el trabajo y suavizarlo con flushing en segundo plano—suponiendo que lo hayas dimensionado y tu I/O pueda seguir el ritmo. Su debilidad es que aún puede golpear un muro duro donde el redo log o el flushing de páginas sucias se vuelven urgentes, y entonces los picos de latencia parecen un colapso sincronizado.
Hay una idea parafraseada de Werner Vogels (CTO de Amazon) que la gente de operaciones repite porque sigue siendo cierta: todo falla, así que diseña para la recuperación y minimiza el radio del impacto
(idea parafraseada). En el mundo de ráfagas, eso suele significar: espera amplificación de escritura y espera que el disco sea el primero en quejarse.
Quién maneja mejor los picos (y cuándo)
Si quieres una regla limpia y honesta: SQLite maneja las ráfagas sin drama cuando puedes moldear la carga de escritura en menos transacciones y no tienes muchos escritores entre procesos. MariaDB maneja las ráfagas sin drama cuando tienes muchos escritores concurrentes, múltiples instancias de aplicación y necesitas comportamiento predecible bajo contención—siempre que tu almacenamiento y configuración no te saboteen.
SQLite gana cuando
- Proceso único o escritores controlados: un hilo escritor, una cola, o un proceso escritor dedicado.
- Transacciones cortas, commits agrupados: puedes commitear cada N registros o cada T milisegundos.
- Disco local, fsync de baja latencia: NVMe, no un filesystem de red inestable.
- Quieres simplicidad: sin servidor, menos partes móviles, menos páginas que te despierten a las 3 a.m.
- Carga de lecturas con ráfagas ocasionales: el modo WAL puede mantener las lecturas ágiles mientras ocurren escrituras.
SQLite pierde (ruidosamente) cuando
- Muchos escritores concurrentes: se serializan y tus hilos de aplicación se amontonan tras “database is locked.”
- Múltiples procesos escriben al mismo tiempo: especialmente en hosts ocupados o contenedores sin coordinación.
- El checkpointing se vuelve una ráfaga: el WAL crece, se dispara un checkpoint y de repente tienes una tormenta de escritura dentro de tu tormenta.
- El almacenamiento tiene extrañas semánticas de fsync: algunos almacenamientos virtualizados o en red hacen que la durabilidad sea extremadamente cara o inconsistente.
MariaDB gana cuando
- Tienes concurrencia real: múltiples instancias de app, cada una escribiendo al mismo tiempo.
- Necesitas herramientas operativas: replicación, backups, cambios de esquema en línea, hooks de observabilidad.
- Necesitas aislar la carga: el buffer pool absorbe picos, los thread pools y colas pueden evitar un colapso total.
- Necesitas semánticas de aislamiento predecibles: MVCC con lecturas consistentes bajo carga de escritura.
MariaDB falla cuando
- Tu disco no puede vaciar lo suficientemente rápido: el flush del redo log detiene el mundo; la latencia se dispara.
- Dimensionas mal el buffer pool: demasiado pequeño y hay thrashing; demasiado grande y llega el drama de cache del SO y swapping.
- “Afinas” la durabilidad a ciegas: compras throughput vendiendo a tu yo del futuro un incidente de pérdida de datos.
- Tu esquema fuerza puntos calientes: contadores de fila única, índices pobres o inserts monotónicos peleando por las mismas estructuras.
Broma #1: SQLite es el amigo que siempre llega a tiempo—salvo que invites a tres amigos más a hablar a la vez, entonces simplemente cierra la puerta.
Perillas de durabilidad: qué compras realmente con fsync
Las ráfagas son donde las configuraciones de durabilidad dejan de ser teóricas. Se convierten en una factura que tu almacenamiento debe pagar, inmediatamente, en efectivo.
Palancas de durabilidad en SQLite
SQLite expone la durabilidad vía pragmas. Las grandes para ráfagas:
- journal_mode=WAL: usualmente la recomendación por defecto para lecturas concurrentes y rendimiento de escritura sostenido.
- synchronous: controla cuán agresivamente SQLite sincroniza datos al disco. Mayor durabilidad normalmente significa más coste de fsync.
- busy_timeout: no mejora el throughput, pero evita fallos inútiles al esperar bloqueos.
- wal_autocheckpoint: controla cuándo SQLite intenta checkpointear (mover el contenido del WAL al archivo principal DB).
Aquí está la parte sutil: en modo WAL, el sistema puede sentirse genial hasta que el WAL crece y el checkpointing se vuelve inevitable. Ese “impuesto de checkpoint” frecuentemente aparece como latencias periódicas que parecen el “hipo” de la base de datos. Si estás insertando logs o series temporales, esto puede morder fuerte.
Perillas de durabilidad en MariaDB/InnoDB
En InnoDB, las perillas críticas para ráfagas están relacionadas con el flush del redo log y qué tan rápido se pueden escribir las páginas sucias:
- innodb_flush_log_at_trx_commit: la clásica compensación durabilidad/throughput. Valor 1 es lo más seguro (flush en cada commit), 2 intercambia algo de durabilidad por velocidad, 0 es más rápido pero más arriesgado.
- sync_binlog: si usas binlogs para replicación, esto puede ser un coste adicional de fsync.
- innodb_redo_log_capacity (o dimensionado de archivos de log en versiones antiguas): demasiado pequeño y sufres checkpoints frecuentes; demasiado grande y cambia el tiempo de recuperación. Los picos suelen revelar logs subdimensionados.
- innodb_io_capacity / innodb_io_capacity_max: indica a InnoDB cuán agresivo ser con el flushing en segundo plano.
Para tolerancia a ráfagas, quieres que la base de datos absorba la ráfaga y haga flush de forma constante en vez de entrar en pánico y flushar. El panic flushing es donde la latencia se vuelve “interesante.”
Patrones comunes de ráfaga y qué falla primero
Patrón: transacciones mínimas a alta QPS
Este es el clásico “insertar una fila y commitear” en bucle, multiplicado por concurrencia. Es una tormenta de commits.
- SQLite: contención de bloqueos + coste de fsync. Verás “database is locked” o esperas largas a menos que encoles escrituras y agrupes commits.
- MariaDB: puede manejar concurrencia, pero el fsync por commit puede dominar la latencia. Verás muchos commits de transacciones, esperas de flush de log y saturación de I/O.
Patrón: backfill con índices pesados
Añades columnas, rellenas datos y actualizas índices secundarios. Ahora cada escritura se expande en múltiples actualizaciones de B-tree.
- SQLite: un solo escritor lo hace predecible pero lento; la ventana de bloqueo es más larga, así que todos los demás esperan más.
- MariaDB: el throughput depende del buffer pool y del I/O. Índices calientes pueden causar contención de latches; demasiados hilos pueden empeorar la situación.
Patrón: la ráfaga coincide con el ciclo de checkpoint/flush
Este es el escenario “está bien, está bien, está bien… ¿por qué se incendia cada 30 segundos?”.
- SQLite WAL checkpoint: ciclos largos de checkpoint pueden bloquear o ralentizar escrituras, dependiendo del modo y condiciones.
- InnoDB checkpoint: el redo log se llena, las páginas sucias deben vaciarse, y el trabajo de primer plano empieza a esperar al I/O en segundo plano.
Patrón: jitter de latencia del almacenamiento
Todo está normal hasta que el disco se pausa. Volúmenes en la nube, flushes de cache de RAID, ruido de vecinos, commits del journal del filesystem—elige tu villano.
- SQLite: tu hilo de aplicación es la base de datos; se bloquea. Los picos de latencia se propagan directamente a la latencia de la petición.
- MariaDB: puede encolar y paralelizar, pero eventualmente los hilos del servidor también se bloquean. La diferencia es que puedes verlo desde dentro del engine mediante contadores de estado y logs.
Broma #2: “Simplemente lo haremos síncrono y rápido” es el equivalente en bases de datos de “seré calmado y puntual durante la cola de seguridad del aeropuerto.”
Tres mini-historias corporativas desde las trincheras
Incidente causado por una suposición errónea: “SQLite puede manejar unos pocos escritores, ¿verdad?”
Un equipo de producto de tamaño medio lanzó un nuevo servicio de ingestión. Cada contenedor tomaba eventos de una cola y los escribía en un archivo SQLite local para “buffering temporal”; luego otro job enviaría el archivo a object storage. La suposición fue que “es disco local, así que será rápido.” Y lo fue—durante la demo del camino feliz.
Luego llegó producción. El autoscaling arrancó múltiples contenedores en el mismo nodo, todos escribiendo al mismo archivo de SQLite vía un hostPath compartido. En cuanto subió el tráfico, los escritores colisionaron. SQLite hizo lo que está diseñado para hacer: serializar escrituras. La aplicación hizo lo que está diseñada para hacer: entrar en pánico.
Los síntomas fueron desordenados: timeouts de petición, errores “database is locked”, y un bucle de reintentos que multiplicó la ráfaga. El host en sí parecía infrautilizado en CPU, lo que incentivó el instinto de depuración exactamente equivocado: “no puede ser la base de datos; la CPU está inactiva.”
La solución fue embarazosamente simple y operativamente adulta: un escritor por archivo de base de datos. Cambiaron a archivos SQLite por contenedor e introdujeron una cola de escritura explícita en proceso. Cuando necesitaron escrituras entre contenedores, movieron la capa de buffering a MariaDB con pooling de conexiones y batching de transacciones.
La lección: SQLite es increíble cuando controlas la serialización de escrituras intencionalmente. Es caos cuando descubres la serialización por accidente.
Optimización que salió mal: “Relajemos fsync y subamos hilos”
Una plataforma administrativa interna corrió sobre MariaDB. Durante un job de importación trimestral, vieron picos de latencia en commits. Alguien (bienintencionado, cansado) cambió innodb_flush_log_at_trx_commit de 1 a 2 y aumentó la concurrencia en el importador de 16 a 128 hilos. Querían “empujar el lote más rápido” y reducir la ventana de dolor.
El throughput mejoró durante unos cinco minutos. Luego el sistema golpeó otro muro: churn del buffer pool más amplificación de escritura por índices secundarios. Las páginas sucias se acumularon más rápido de lo que el flushing podía seguir. InnoDB empezó a hacer flushing agresivo. La latencia pasó de esporádica a consistentemente terrible, y el primario empezó a retrasar la replicación porque el patrón de fsync del binlog cambió bajo carga.
No perdieron datos, pero sí tiempo: la importación tomó más al final porque el sistema oscilaba entre ráfagas de progreso y largos stalls. Mientras tanto, el tráfico orientado al usuario sufrió porque la base de datos no pudo mantener tiempos de respuesta estables.
La solución eventual no fue “más tuning.” Fue moldeado disciplinado de la carga: limitar el importador, agrupar commits y programar el trabajo con un límite de velocidad predecible. Mantuvieron las configuraciones de durabilidad conservadoras y arreglaron el verdadero problema: el importador no tenía por qué comportarse como una prueba DDoS.
La lección: girar perillas sin controlar la concurrencia es cómo cambias un modo de fallo por uno más confuso.
Práctica aburrida pero correcta que salvó el día: “Medir fsync, mantener margen, ensayar restauraciones”
Un servicio adyacente a pagos (del tipo donde no puedes ser creativo con la durabilidad) usaba MariaDB con InnoDB. Cada pocas semanas tenían una ráfaga: jobs de conciliación más un pico de tráfico. Nunca causó una caída, y nadie celebró eso. Ese era el punto.
Tenían una rutina aburrida. Medían la latencia del disco (incluyendo fsync) continuamente, no sólo IOPS. Mantuvieron un margen en la capacidad del redo log y dimensionaron el buffer pool para que el sistema no hiciera thrash durante las ráfagas. También ensayaban restauraciones con una cadencia para que nadie aprendiera el comportamiento de backups durante un incidente.
Un día el jitter de latencia del almacenamiento se duplicó debido a un vecino ruidoso en el hardware subyacente. El servicio no se cayó. Se volvió más lento, las alarmas saltaron temprano y el equipo aplicó una mitigación conocida: limitar temporalmente la tasa de jobs y pausar escritores no críticos. El tráfico de usuarios se mantuvo dentro del SLO.
Más tarde, cuando migraron a otro almacenamiento, ya tenían líneas base que demostraban que la capa de almacenamiento era la culpable. Las reuniones de procurement son mucho más fáciles cuando muestras gráficas en vez de emociones.
La lección: la práctica “aburrida” de medir lo correcto y mantener margen es el seguro más barato contra ráfagas que puedes comprar.
Guion rápido de diagnóstico
Cuando una ráfaga de escritura golpea y todo se vuelve raro, no tienes tiempo para filosofar. Necesitas un árbol de decisiones rápido: ¿estamos limitados por bloqueos, CPU o I/O?
Primero: confirma la forma del dolor (latencia vs throughput)
- Si el throughput se mantiene alto pero la latencia p95/p99 explota: busca stalls por fsync/journal/checkpoint.
- Si el throughput colapsa: busca contención de bloqueos, agotamiento de hilos o saturación del almacenamiento.
Segundo: decide si es específico de SQLite o MariaDB
- SQLite: errores como “database is locked”, esperas largas, crecimiento del WAL, o stalls de checkpoint.
- MariaDB: hilos esperando flush de log, flushing de páginas sucias, esperas por bloqueos de fila, o lag de replicación que complica la presión.
Tercero: prueba o elimina el almacenamiento como cuello de botella
- Revisa latencia de disco, profundidad de cola y comportamiento de fsync bajo carga.
- Si el almacenamiento es inestable, casi cualquier base de datos parecerá culpable.
Cuarto: deja de empeorarlo
- Limita la fuente de la ráfaga (job por lotes, importador, bucle de reintentos).
- Agrupa commits. Reduce la concurrencia. Apaga “reintentos infinitos sin jitter”.
- Captura evidencia antes de reiniciar servicios. Los reinicios borran las pistas y rara vez arreglan la física.
Tareas prácticas: comandos, salidas y decisiones
Estas son las cosas que puedes ejecutar durante un incidente o una sesión de ajuste. Cada una incluye: comando, qué significa la salida y la decisión que tomas.
1) Ver si el sistema está saturado de I/O (Linux)
cr0x@server:~$ iostat -xz 1 3
Linux 6.5.0 (db01) 12/30/2025 _x86_64_ (8 CPU)
avg-cpu: %user %nice %system %iowait %steal %idle
12.1 0.0 4.3 31.7 0.0 51.9
Device r/s w/s rkB/s wkB/s rrqm/s wrqm/s %util await svctm
nvme0n1 20.0 900.0 640.0 42000.0 0.0 50.0 98.7 22.4 0.8
Significado: %util cercano a 100% junto con await alto indica que el dispositivo es el limitador; iowait también está elevado.
Decisión: Deja de “ajustar SQL” primero. Limita escritores, reduce la frecuencia de commits o muévete a almacenamiento más rápido. Si es un volumen compartido, investiga ruido de vecinos.
2) Identificar qué proceso está emitiendo más I/O de escritura
cr0x@server:~$ pidstat -d 1 3
Linux 6.5.0 (db01) 12/30/2025 _x86_64_ (8 CPU)
# Time UID PID kB_rd/s kB_wr/s kB_ccwr/s Command
12:01:01 999 2145 0.00 52000.00 0.00 mariadbd
12:01:01 1000 8812 0.00 1200.00 0.00 importer
Significado: MariaDB está haciendo la mayoría de las escrituras; importer contribuye pero no domina.
Decisión: Enfócate en el comportamiento de flushing/redo de InnoDB y la latencia de almacenamiento, no sólo en agrupar la app.
3) Revisar filesystem y opciones de montaje (sorpresas de durabilidad viven aquí)
cr0x@server:~$ mount | grep -E " /var/lib/mysql | /data "
/dev/nvme0n1p2 on /var/lib/mysql type ext4 (rw,relatime,commit=5)
/dev/nvme1n1p1 on /data type xfs (rw,relatime,attr2,inode64,logbufs=8)
Significado: el intervalo commit de ext4 y el comportamiento del journal pueden afectar picos periódicos de latencia.
Decisión: Si ves stalls periódicos alineados con commits del journal, considera tunear el filesystem o mover archivos DB a un volumen con mejores características de latencia.
4) Medir latencia cruda tipo fsync con una prueba simple de sync
cr0x@server:~$ dd if=/dev/zero of=/var/lib/mysql/sync-test.bin bs=4k count=4096 oflag=dsync
4096+0 records in
4096+0 records out
16777216 bytes (17 MB, 16 MiB) copied, 3.91 s, 4.3 MB/s
Significado: oflag=dsync fuerza sync por bloque; bajo throughput implica alto coste por sync. No es un modelo perfecto, pero revela “el almacenamiento engaña”.
Decisión: Si esto se ve terrible en discos “rápidos”, detente y arregla almacenamiento o settings de virtualización antes de culpar a la base de datos.
5) MariaDB: confirmar política de flush de InnoDB y tamaño de redo
cr0x@server:~$ mariadb -e "SHOW VARIABLES WHERE Variable_name IN ('innodb_flush_log_at_trx_commit','sync_binlog','innodb_redo_log_capacity','innodb_io_capacity','innodb_io_capacity_max');"
+------------------------------+-----------+
| Variable_name | Value |
+------------------------------+-----------+
| innodb_flush_log_at_trx_commit | 1 |
| sync_binlog | 1 |
| innodb_redo_log_capacity | 1073741824|
| innodb_io_capacity | 200 |
| innodb_io_capacity_max | 2000 |
+------------------------------+-----------+
Significado: Durabilidad total en redo y binlog (costosa durante ráfagas). La capacidad de redo puede ser pequeña depende de la carga.
Decisión: Si el p99 está muriendo y puedes tolerar pequeñas compensaciones de durabilidad, considera ajustar settings—pero sólo con aprobación de negocio. De lo contrario, mejora el rendimiento del almacenamiento y considera agrupar commits.
6) MariaDB: ver si esperas en flush de log
cr0x@server:~$ mariadb -e "SHOW GLOBAL STATUS LIKE 'Innodb_log_waits'; SHOW GLOBAL STATUS LIKE 'Innodb_os_log_fsyncs';"
+------------------+-------+
| Variable_name | Value |
+------------------+-------+
| Innodb_log_waits | 1834 |
+------------------+-------+
+----------------------+--------+
| Variable_name | Value |
+----------------------+--------+
| Innodb_os_log_fsyncs | 920044 |
+----------------------+--------+
Significado: Log waits significa que transacciones tuvieron que esperar al flush del redo log. Ráfagas + latencia de fsync = dolor.
Decisión: Reduce la frecuencia de commits (batch), reduce concurrencia o mejora latencia de fsync. No añadas sólo CPU.
7) MariaDB: comprobar presión de páginas sucias (deuda de flush)
cr0x@server:~$ mariadb -e "SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_pages_dirty'; SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_pages_total';"
+--------------------------------+--------+
| Variable_name | Value |
+--------------------------------+--------+
| Innodb_buffer_pool_pages_dirty | 412345 |
+--------------------------------+--------+
+--------------------------------+--------+
| Variable_name | Value |
+--------------------------------+--------+
| Innodb_buffer_pool_pages_total | 524288 |
+--------------------------------+--------+
Significado: Una ratio muy alta de páginas sucias sugiere que el sistema está atrasado en flushing; los checkpoints pueden forzar stalls.
Decisión: Aumenta innodb_io_capacity con cuidado, asegúrate de que el almacenamiento puede sostener escrituras y reduce la tasa de entrada hasta que las páginas sucias se estabilicen.
8) MariaDB: identificar esperas por bloqueos y tablas calientes
cr0x@server:~$ mariadb -e "SELECT * FROM information_schema.innodb_lock_waits\G"
*************************** 1. row ***************************
requesting_trx_id: 123456
blocking_trx_id: 123455
blocked_table: `app`.`events`
blocked_lock_type: RECORD
blocking_lock_type: RECORD
Significado: Tienes contención en una tabla/índice específico.
Decisión: Arregla el punto caliente: añade índice, cambia patrón de acceso, evita contadores de una sola fila o haz sharding por clave/tiempo. Meter más hilos en contención de bloqueos lo empeora.
9) MariaDB: inspeccionar estados de hilos actuales (¿qué están esperando?)
cr0x@server:~$ mariadb -e "SHOW PROCESSLIST;"
+-----+------+-----------+------+---------+------+------------------------+------------------------------+
| Id | User | Host | db | Command | Time | State | Info |
+-----+------+-----------+------+---------+------+------------------------+------------------------------+
| 101 | app | 10.0.0.12 | app | Query | 12 | Waiting for handler commit | INSERT INTO events ... |
| 102 | app | 10.0.0.13 | app | Query | 11 | Waiting for handler commit | INSERT INTO events ... |
| 103 | app | 10.0.0.14 | app | Sleep | 0 | | NULL |
+-----+------+-----------+------+---------+------+------------------------+------------------------------+
Significado: “Waiting for handler commit” comúnmente se correlaciona con presión de commit/fsync.
Decisión: Investiga settings de flush de redo/binlog y latencia de disco; considera agrupar escrituras.
10) SQLite: verificar journal mode y synchronous
cr0x@server:~$ sqlite3 /data/app.db "PRAGMA journal_mode; PRAGMA synchronous; PRAGMA wal_autocheckpoint;"
wal
2
1000
Significado: El modo WAL está habilitado; synchronous=2 es FULL (durable, más lento); autocheckpoint en 1000 páginas.
Decisión: Si estás teniendo picos y ves stalls, considera si realmente necesitas FULL. También planifica la estrategia de checkpoints (manual/controlado) en lugar de dejar que wal_autocheckpoint te sorprenda.
11) SQLite: detectar contención de bloqueo con una prueba controlada de escritura
cr0x@server:~$ sqlite3 /data/app.db "PRAGMA busy_timeout=2000; BEGIN IMMEDIATE; INSERT INTO events(ts, payload) VALUES(strftime('%s','now'),'x'); COMMIT;"
Significado: Si esto falla intermitentemente con “database is locked”, tienes escritores compitiendo o transacciones largas.
Decisión: Introduce una cola de un solo escritor, acorta transacciones y asegúrate de que los lectores no mantienen bloqueos más tiempo del esperado (por ejemplo, SELECTs de larga duración dentro de una transacción).
12) SQLite: vigilar crecimiento del WAL y salud de checkpoints
cr0x@server:~$ ls -lh /data/app.db /data/app.db-wal /data/app.db-shm
-rw-r--r-- 1 app app 1.2G Dec 30 12:05 /data/app.db
-rw-r--r-- 1 app app 3.8G Dec 30 12:05 /data/app.db-wal
-rw-r--r-- 1 app app 32K Dec 30 12:05 /data/app.db-shm
Significado: El WAL es más grande que la BD principal. No es automáticamente fatal, pero es una señal de que el checkpointing no está al día.
Decisión: Ejecuta un checkpoint controlado en una ventana tranquila, o ajusta la carga para que los checkpoints ocurran de forma predecible. Investiga lectores de larga duración que impidan el progreso del checkpoint.
13) SQLite: comprobar si los lectores bloquean los checkpoints (base ocupada)
cr0x@server:~$ sqlite3 /data/app.db "PRAGMA wal_checkpoint(TRUNCATE);"
0|0|0
Significado: Los tres números son (busy, log, checkpointed). Ceros después de TRUNCATE sugiere que el checkpoint tuvo éxito rápidamente y el WAL se truncó.
Decisión: Si “busy” no es cero o el WAL no se trunca, busca transacciones de lectura de larga duración y arréglalas (acorta lecturas, evita mantener transacciones abiertas).
14) MariaDB: confirmar dimensionado del buffer pool y presión
cr0x@server:~$ mariadb -e "SHOW VARIABLES LIKE 'innodb_buffer_pool_size'; SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_reads';"
+-------------------------+------------+
| Variable_name | Value |
+-------------------------+------------+
| innodb_buffer_pool_size | 8589934592 |
+-------------------------+------------+
+-------------------------+----------+
| Variable_name | Value |
+-------------------------+----------+
| Innodb_buffer_pool_reads| 18403921 |
+-------------------------+----------+
Significado: Si las lecturas de buffer pool suben rápidamente durante la ráfaga, estás perdiendo caché y haciendo más I/O físico del planeado.
Decisión: Aumenta buffer pool (si RAM lo permite), reduce el working set (índices, patrones de consulta) o shardea la carga. No ignores el SO; el swapping arruinará tu día.
15) Sospecha de almacenamiento en red: comprobar distribución de latencia rápidamente
cr0x@server:~$ ioping -c 10 -W 2000 /var/lib/mysql
4 KiB <<< /var/lib/mysql (ext4 /dev/nvme0n1p2): request=1 time=0.8 ms
4 KiB <<< /var/lib/mysql (ext4 /dev/nvme0n1p2): request=2 time=1.1 ms
4 KiB <<< /var/lib/mysql (ext4 /dev/nvme0n1p2): request=3 time=47.9 ms
...
--- /var/lib/mysql ioping statistics ---
10 requests completed in 12.3 s, min/avg/max = 0.7/6.4/47.9 ms
Significado: Ese pico de latencia máximo es exactamente lo que parece la latencia de commit cuando el disco hace un hipo.
Decisión: Si ves jitter como este, deja de perseguir micro-optimización en SQL. Arregla QoS de almacenamiento, mueve volúmenes o añade buffering/batching.
16) Encontrar tormentas de reintentos en logs de aplicación (la “ráfaga auto-amplificadora”)
cr0x@server:~$ journalctl -u app-ingester --since "10 min ago" | grep -E "database is locked|retrying" | tail -n 5
Dec 30 12:00:41 db01 app-ingester[8812]: sqlite error: database is locked; retrying attempt=7
Dec 30 12:00:41 db01 app-ingester[8812]: sqlite error: database is locked; retrying attempt=8
Dec 30 12:00:42 db01 app-ingester[8812]: sqlite error: database is locked; retrying attempt=9
Dec 30 12:00:42 db01 app-ingester[8812]: sqlite error: database is locked; retrying attempt=10
Dec 30 12:00:42 db01 app-ingester[8812]: sqlite error: database is locked; retrying attempt=11
Significado: No sólo estás experimentando contención; la estás multiplicando con reintentos.
Decisión: Añade backoff exponencial con jitter, limita los reintentos y considera una cola de un solo escritor. Reintentar agresivamente es cómo conviertes un pico en una caída.
Errores comunes (síntomas → causa raíz → solución)
1) Síntoma: errores “database is locked” durante picos (SQLite)
Causa raíz: Múltiples escritores concurrentes o transacciones de larga duración que mantienen bloqueos; la realidad de un solo escritor choca con carga multi-escritor.
Solución: Serializa escrituras explícitamente (un hilo/proceso escritor), usa WAL mode, establece un busy_timeout sensato y agrupa commits. Evita mantener transacciones de lectura abiertas mientras escribes.
2) Síntoma: stalls periódicos 200–2000 ms cada N segundos (SQLite)
Causa raíz: Ciclos de checkpoint WAL o commits del journal del filesystem que crean comportamiento de sync ráfaga.
Solución: Controla checkpoints (manuales en ventanas tranquilas), ajusta wal_autocheckpoint, reduce synchronous sólo con requisitos claros de durabilidad y valida jitter de latencia del almacenamiento.
3) Síntoma: p99 de MariaDB se dispara mientras la CPU está baja
Causa raíz: Commits ligados a I/O: la latencia de redo/binlog fsync domina; hilos esperan log flush o handler commit.
Solución: Agrupa transacciones, reduce concurrencia, revisa innodb_flush_log_at_trx_commit y sync_binlog con aprobación de negocio, y mejora la latencia del almacenamiento.
4) Síntoma: el throughput colapsa cuando “añades más workers” (MariaDB)
Causa raíz: Contención de locks/latches o presión de flushing amplificada por thrash de hilos; más concurrencia aumenta los context switches y la contención.
Solución: Limita la concurrencia, usa pooling de conexiones, arregla índices/tablas calientes y ajusta el flushing en segundo plano de InnoDB en lugar de añadir hilos.
5) Síntoma: el archivo WAL crece eternamente (SQLite)
Causa raíz: Lectores de larga duración impiden que el checkpoint complete; o wal_autocheckpoint no coincide con la carga.
Solución: Asegura que los lectores no mantienen transacciones abiertas, ejecuta wal_checkpoint en ventanas controladas y considera repartir la carga entre múltiples archivos DB si la contención es estructural.
6) Síntoma: picos de lag de replicación en MariaDB durante imports
Causa raíz: fsync del binlog y flush de redo bajo carga pesada de escritura; la aplicación en el replica (según la configuración) puede no seguir el ritmo.
Solución: Agrupa escrituras, programa imports, revisa settings de durabilidad del binlog y asegúrate de que la configuración de apply en réplicas coincide con la carga. No trates la replicación como “gratis”.
7) Síntoma: “Rápido en mi portátil, lento en prod” (ambos)
Causa raíz: Semánticas de almacenamiento diferentes: NVMe de portátil vs volumen compartido en la nube; fsync y jitter de latencia son universos distintos.
Solución: Benchmark en almacenamiento similar a producción, mide la distribución de latencias y establece SLOs alrededor de la latencia p99 de commit—no sólo el throughput medio.
Listas de verificación / plan paso a paso
Si eliges entre MariaDB y SQLite para escrituras ráfaga
- Cuenta escritores, no peticiones. ¿Cuántos procesos/hosts pueden escribir concurrentemente?
- Decide si puedes imponer un escritor único. Si sí, SQLite sigue siendo opción.
- Define requisitos de durabilidad claramente. “Podemos perder 1 segundo de datos” es un requisito real; “debe ser durable” no lo es.
- Mide la latencia de fsync del almacenamiento. Si es inestable, ambas bases parecerán poco fiables ante los picos.
- Planea los backfills. Si rutinariamente importas o reprocesas datos, diseña throttling y batching desde el día uno.
Plan práctico de endurecimiento para SQLite
- Habilita WAL mode y confirma que permanece habilitado.
- Configura busy_timeout a un valor no trivial (centenas a miles de ms) y maneja SQLITE_BUSY con backoff + jitter.
- Agrupa commits: commit cada N filas o cada T milisegundos.
- Introduce una cola de escritura con un hilo escritor. Si existen múltiples procesos, introduce un proceso escritor único.
- Controla checkpoints: ejecuta wal_checkpoint durante baja carga; ajusta wal_autocheckpoint.
- Vigila tamaño del WAL y éxito de checkpoints como métricas de primera clase.
Plan práctico de endurecimiento para MariaDB
- Confirma que usas InnoDB para tablas con escrituras ráfaga.
- Dimensiona buffer pool para que el working set quepa tanto como sea razonable sin hacer swapping.
- Revisa la capacidad del redo log; evita redos demasiado pequeños que fuerzan checkpoints frecuentes.
- Alinea innodb_io_capacity con la capacidad real del almacenamiento (no con deseos).
- Limita la concurrencia de la aplicación; usa pooling de conexiones; evita tormentas de hilos.
- Agrupa escrituras y usa inserts multi-fila cuando sea seguro.
- Mide y alerta sobre log waits, indicadores de latencia de fsync y ratio de páginas sucias.
Cuándo migrar de SQLite a MariaDB (o viceversa)
- Migrar SQLite → MariaDB cuando no puedes imponer un escritor único, necesitas escrituras multi-host o las herramientas operativas (replicación/backup online) importan.
- Migrar MariaDB → SQLite cuando la carga es local, de un solo escritor y estás pagando sobrecarga operativa innecesaria por un dataset pequeño embebido.
Preguntas frecuentes
1) ¿Puede SQLite manejar alto throughput de escritura?
Sí—si agrupas transacciones y mantienes los escritores serializados. SQLite puede ser extremadamente rápido por núcleo porque evita saltos de red y sobrecarga de servidor.
2) ¿Por qué SQLite dice “database is locked” en vez de encolar escritores?
El modelo de bloqueo de SQLite es simple e intencional. Espera que la aplicación controle la concurrencia (busy_timeout, reintentos y, idealmente, un escritor único). Si quieres que la base de datos gestione alta concurrencia multi-escritor, estás describiendo una BD servidor.
3) ¿Es el modo WAL siempre la opción correcta para SQLite ante ráfagas?
A menudo, pero no siempre. WAL ayuda a lecturas concurrentes durante escrituras y puede suavizar carga de escritura sostenida. También introduce comportamiento de checkpoint que debes gestionar. Si ignoras los checkpoints, obtendrás stalls periódicos y archivos WAL gigantes.
4) Para MariaDB, ¿qué configuración afecta más el comportamiento ante ráfagas?
innodb_flush_log_at_trx_commit y (si usas binlog) sync_binlog. Determinan directamente con qué frecuencia pagas el coste de fsync. Cambiarlas altera la durabilidad, así que trátalo como una decisión de negocio.
5) ¿Por qué los picos de escritura a veces empeoran después de añadir índices?
Los índices aumentan la amplificación de escritura. Un insert se convierte en múltiples actualizaciones de B-tree y más páginas sucias. En picos, la diferencia entre “una escritura” y “cinco escrituras” no es teórica; es tu p99.
6) ¿Debería poner SQLite en almacenamiento en red?
Normalmente no. SQLite depende de bloqueos y semánticas de sync correctas y de baja latencia. Filesystems en red y algunos volúmenes distribuidos pueden hacer el locking impredecible y fsync dolorosamente lento. Si debes hacerlo, prueba la implementación de almacenamiento exacta bajo carga.
7) Si MariaDB va lento durante ráfagas, ¿debo subir CPU?
Sólo después de probar que estás limitado por CPU. La mayoría del dolor en ráfagas es latencia I/O o contención. Añadir CPU a un cuello de botella por fsync es como poner más cajas registradoras cuando la tienda sólo tiene una única salida.
8) ¿Cuál es la forma más simple de hacer que cualquiera de las dos bases maneje ráfagas mejor?
Agrupa commits y limita la concurrencia. Las ráfagas suelen ser autoinfligidas por “trabajadores ilimitados” y “commit por fila”. Arregla eso primero.
9) ¿Cuál es más segura para durabilidad ante picos?
Ambas pueden ser seguras; ambas pueden configurarse de forma insegura. Los valores por defecto de MariaDB tienden a ser conservadores para workloads de servidor. SQLite también puede ser totalmente durable, pero el coste de rendimiento ante ráfagas es más visible porque está en el camino de la petición.
10) ¿Cómo sé si estoy limitado por checkpointing?
SQLite: el WAL crece y los checkpoints reportan “busy” o no se truncan. MariaDB: suben los log waits, aumentan las páginas sucias y ves stalls ligados al flushing. En ambos casos, correlaciona con picos de latencia del disco.
Conclusión: pasos prácticos siguientes
Si tienes escrituras ráfaga y estás decidiendo entre MariaDB y SQLite, no empieces por la ideología. Empieza por el modelo de escritura.
- Si puedes imponer un escritor, agrupar commits y mantener la base en almacenamiento local de baja latencia, SQLite manejará los picos de forma silenciosa y económica.
- Si tienes muchos escritores entre procesos/hosts y necesitas herramientas operativas como replicación y observabilidad robusta, MariaDB es la apuesta más segura—siempre que respetes la física de fsync y afines con cuidado.
Luego haz el trabajo poco glamoroso que evita el drama: mide la latencia de fsync, limita la concurrencia, agrupa escrituras y convierte checkpoints/flushing en una parte controlada del sistema en lugar de una sorpresa. Tu yo futuro seguirá estando cansado, pero al menos estará aburrido. Ese es el objetivo.