Disco lleno es el tipo de incidente que hace que todos recuerden de golpe cuántos logs existen, cuántas exportaciones “temporales” son permanentes y lo rápido que una base de datos castiga el optimismo. La aplicación se ve “lenta”. Luego se ve “caída”. Luego parece que tu pager intenta alcanzar velocidad de escape.
Esto es una comparativa en campo: cuando el sistema de ficheros llega al 100%, ¿qué motor te devuelve a verde más rápido—y cuál se recupera más limpio (es decir, con menos efectos secundarios raros, menos riesgos silenciosos sobre los datos, menos seguimientos de “lo arreglamos pero está embrujado”)?
La tesis directa
Si mides “quién se recupera más rápido” por “quién vuelve a estar en línea con mínima supervisión humana”, PostgreSQL suele ganar en claridad y modos de fallo predecibles, especialmente en lo relativo a WAL y recuperación tras crash. Si mides “quién se recupera más limpio” por “quién es menos propenso a seguir cojeando silenciosamente con riesgo oculto de corrupción”, PostgreSQL de nuevo tiende a sentirse más seguro—porque es más ruidoso, más transaccional con su metadata y se niega a hacer como si todo estuviera bien.
MySQL (InnoDB) también puede recuperarse limpiamente y es muy bueno en recuperación tras crash, pero los incidentes por disco lleno tienen talento para convertirse en fallos parciales desordenados: tablas temporales, redo logs, binlogs y el SO peleando por los últimos megabytes como si fuera el último asiento en un tren de cercanías.
Mi guía operativa con opinión:
- PostgreSQL: prioriza hacer que WAL y
pg_walsean resilientes y acotados (archivado, disciplina en replication slots, volumen separado). Disco lleno suele verse como “no se puede escribir WAL / no se puede checkpoint”, que da miedo pero es legible. - MySQL: prioriza limitar binlogs, tmpdir y crecimiento de undo/redo, y evita sorpresas a nivel de sistema de ficheros (thin provisioning, snapshots). Disco lleno a menudo parece “todo está medio roto a la vez”, lo cual es caro operacionalmente.
- Para ambos: mantén el espacio libre como si fuera una característica, no una sugerencia. “Operamos al 92% de uso” es cómo terminas haciendo respuesta a incidentes mientras negocias con un array de almacenamiento.
Una cita para tener pegada en el monitor, de Gene Kranz: “Failure is not an option.” Cuando gestionas almacenamiento en producción, trátalo como una idea parafraseada, no como una promesa.
Datos interesantes y contexto histórico
- Línea WAL de PostgreSQL: el enfoque de write-ahead logging de PostgreSQL maduró desde raíces de investigación de Postgres hasta un modelo de durabilidad robusto y conservador que marca fuertemente el comportamiento ante disco lleno (WAL primero, todo lo demás después).
- InnoDB no siempre fue “el predeterminado”: InnoDB se convirtió en el motor práctico por defecto en MySQL porque aportó recuperación tras crash y semántica transaccional que MyISAM antiguo no tenía—cambiando cómo se presentan los incidentes por disco lleno (redo/undo frente a caos a nivel de tabla).
- El dolor histórico de ibdata1 en MySQL: las primeras configuraciones de InnoDB usaban a menudo un espacio de tablas compartido (ibdata1) que podía crecer y no reducirse fácilmente, una cicatriz operativa de largo recorrido para incidentes de “borramos datos, ¿por qué sigue lleno el disco?”.
- MVCC de PostgreSQL cuesta espacio por diseño: el MVCC de PostgreSQL crea tuplas muertas que deben vaciarse con vacuum, así que la presión de disco no es sorpresa; es una factura que pagas rutinariamente o con interés más tarde.
- Los replication slots cambiaron modos de fallo: los replication slots de PostgreSQL son potentes, pero pueden fijar WAL indefinidamente; los incidentes modernos de “disco lleno” a menudo rastrean hasta un slot olvidado que retiene WAL como rehén.
- La retención de binlogs en MySQL es una palanca de outage: en MySQL, los binary logs son a la vez un activo de recuperación y una bomba de disco. Los valores por defecto de retención y las prácticas operativas pueden decidir si disco lleno es “menor” o “de varias horas”.
- Los sistemas de ficheros importan más de lo que te gustaría: XFS, ext4 y ZFS se comportan diferente ante ENOSPC. La base de datos no puede optar por no verse afectada por la personalidad del kernel.
- El comportamiento de checkpoints es un diferenciador importante: los checkpoints de PostgreSQL y la política de flush de MySQL crean patrones de “picos de escritura” distintos; bajo alto llenado, esos picos son donde descubres que patinabas sobre hielo fino.
Qué significa realmente “disco lleno” en producción
“Disco lleno” rara vez es una sola cosa. Es una discusión entre capas:
- El sistema de ficheros devuelve ENOSPC. O devuelve EDQUOT porque alcanzaste una cuota que olvidaste que existía.
- El backend de almacenamiento engaña educadamente (thin provisioning) hasta que deja de ser educado, y entonces es problema de todos.
- El kernel puede mantener vivos a algunos procesos mientras otros fallan en fsync, rename o allocate.
- La base de datos tiene múltiples caminos de escritura: WAL/redo, ficheros de datos, ficheros temporales, spill de ordenaciones, autovacuum/vacuum, binary logs, metadata de replicación y trabajadores en segundo plano.
La definición práctica para manejo de incidentes es: ¿la base de datos aún puede garantizar durabilidad y consistencia? Cuando el almacenamiento está al 100%, la respuesta se vuelve “no de forma fiable” mucho antes de que el proceso realmente muera.
Además, “disco lleno” no es solo capacidad. Es bloques libres, inodos, margen de IOPS, amplificación de escritura y la cantidad de espacio contiguo que tu sistema de ficheros puede asignar bajo fragmentación.
Broma corta #1: Los incidentes de disco lleno son como niños pequeños—silencio está bien, gritos están mal, pero lo peor es cuando se quedan callados de nuevo y te das cuenta de que encontraron los rotuladores.
Cómo se comportan PostgreSQL y MySQL cuando se acaba el espacio
PostgreSQL: WAL manda, y te dirá cuando el reino está roto
La durabilidad de PostgreSQL gira en torno al WAL. Si no puede escribir WAL, no puede confirmar de forma segura. Eso no es negociable. Cuando el disco se llena en pg_wal (o en el sistema de ficheros que lo aloja), a menudo verás:
- Transacciones fallando con “could not write to file … No space left on device”.
- Advertencias de checkpoint que escalan hasta “PANIC” en los peores casos (dependiendo de qué falló exactamente).
- Retraso en replicación volviéndose irrelevante porque los primarios no pueden generar WAL de forma fiable.
Esto es brutal pero honesto: Postgres tiende a fallar de maneras que te obligan a arreglar la limitación subyacente de almacenamiento, no de formas que te inviten a seguir escribiendo “solo un poco más” hasta crear un segundo incidente.
Patrones de recuperación más limpios en eventos de disco lleno en PostgreSQL:
- Mensajes de error claros que apuntan a segmentos WAL, ficheros temporales o al directorio base.
- La recuperación tras crash suele ser determinística una vez restauras la capacidad de escritura.
- Conjunto acotado de sospechosos habituales:
pg_wal, spill temporales, autovacuum, replication slots.
Patrones desordenados que aún ves en Postgres:
- Los replication slots fijan WAL hasta que el disco se agota.
- Transacciones de larga duración impiden vacuum, inflan tablas e índices, y luego aparece el problema de espacio.
- Explosiones de ficheros temporales por consultas malas: sorts, hashes, CTE grandes o índices faltantes.
MySQL (InnoDB): múltiples caminos de escritura, múltiples formas de sufrir
InnoDB tiene redo logs, undo logs, doublewrite buffer, ficheros de datos, tablespaces temporales, binary logs (a nivel de servidor), relay logs (replicación) y luego tu sistema de ficheros. Cuando el disco se aprieta, puedes golpear el fallo en un área mientras otra sigue escribiendo—creando funcionalidad parcial y síntomas confusos.
Patrones comunes de disco lleno en MySQL:
- Los binary logs llenan la partición, especialmente con logging basado en filas y cargas de escritura intensas.
- tmpdir se llena por ordenaciones grandes o tablas temporales; las consultas empiezan a fallar de maneras extrañas mientras el servidor “parece funcionar”.
- InnoDB no puede extender un tablespace (file-per-table o shared tablespace), produciendo errores en insert/update.
- La replicación se rompe asimétricamente: el origen sigue funcionando pero el réplica se detiene al escribir relay log, o viceversa.
Patrones de recuperación más limpios en MySQL:
- Una vez liberas espacio, la recuperación de crash de InnoDB suele ser sólida.
- La purga de binary logs es una palanca rápida si eres disciplinado y entiendes los requisitos de replicación.
Patrones más desordenados en MySQL:
- ibdata1 y los tablespaces de undo pueden permanecer grandes incluso después de deletes; “liberar espacio” no siempre es inmediato sin reconstrucciones.
- La sospecha de corrupción de tablas aumenta cuando se escribieron archivos parcialmente y el sistema de ficheros estaba bajo presión—raro, pero el miedo es caro.
- Los hilos en segundo plano pueden seguir martillando el IO intentando flush/merge mientras intentas estabilizar el sistema.
¿Entonces quién se recupera más rápido?
Si tu on-call necesita una sola frase: PostgreSQL suele darte un camino más directo de “disco lleno” a “seguro otra vez”, siempre que entiendas WAL, checkpoints y slots. MySQL te da más “palancas rápidas” (purga de binlogs, mover tmpdir), pero también más maneras de cortar la rama en la que estás sentado accidentalmente.
¿Y quién se recupera más limpio?
La recuperación más limpia se trata de confianza: después de liberar espacio, ¿confías en el sistema, o programas un fin de semana para “verificar”? La postura de PostgreSQL—detener el mundo cuando no se puede garantizar WAL—tiende a producir menos situaciones de “está corriendo pero…”. MySQL también puede quedar perfecto, pero los incidentes por disco lleno con más frecuencia te dejan con una lista de comprobación de “perdimos posición de replicación, truncamos algo, movimos tmpdir, rompió la purga de binlogs?”.
Guion de diagnóstico rápido
Este es el orden que encuentra el cuello de botella rápido. No “teóricamente correcto”, sino “termina la caída”.
1) Confirma qué está realmente lleno (bloques vs inodos vs cuota)
- Comprueba uso de bloques del sistema de ficheros.
- Comprueba agotamiento de inodos.
- Comprueba cuotas (usuario/proyecto).
- Comprueba thin provisioning / LVM / margen del array.
2) Identifica el consumidor dominante de espacio (y si sigue creciendo)
- Encuentra qué directorio es enorme (
/var/lib/postgresql,/var/lib/mysql,/var/log). - Comprueba ficheros abiertos pero borrados que siguen ocupando espacio.
- Comprueba si el crecimiento es “logs continuos” o “derrame repentino de temporales”.
3) Determina si la base de datos aún puede garantizar durabilidad
- Postgres: ¿puede escribir WAL? ¿están fallando los checkpoints?
- MySQL: ¿están fallando escrituras de redo/binlog/tmp? ¿está comprometida la replicación?
4) Aplica primero un alivio de espacio reversible y de bajo riesgo
- Eliminar/purgar logs rotados, dumps antiguos, paquetes viejos.
- Purga binlogs de MySQL solo si las réplicas están seguras.
- Resolver replication slots de Postgres que fijan WAL.
- Mover directorios temporales a otro volumen como solución temporal.
5) Tras estabilizar, realiza el trabajo de corrección
- Ejecuta checks de consistencia adecuados al motor.
- Arregla políticas de retención y alertas de capacidad.
- Programa vacuum/reindex o rebuild de tablas si la inflación lo causó.
Tareas prácticas: comandos, salidas y decisiones (12+)
Estos son los comandos que ejecutas a las 3 a.m. Cada uno incluye qué significa la salida y la decisión que tomas.
Tarea 1: Comprobar capacidad del sistema de ficheros (bloques)
cr0x@server:~$ df -hT
Filesystem Type Size Used Avail Use% Mounted on
/dev/nvme0n1p2 ext4 80G 79G 120M 100% /
/dev/nvme1n1p1 xfs 500G 410G 90G 83% /var/lib/postgresql
tmpfs tmpfs 16G 1.2G 15G 8% /run
Significado: El filesystem raíz está al 100% con solo 120M disponibles; el volumen de datos de Postgres está bien. Muchos servicios fallan cuando / está lleno (journald, actualizaciones de paquetes, ficheros temporales).
Decisión: Libera espacio en / inmediatamente (logs, caches). No toques los ficheros de la base de datos todavía si no son el culpable.
Tarea 2: Comprobar agotamiento de inodos
cr0x@server:~$ df -ih
Filesystem Inodes IUsed IFree IUse% Mounted on
/dev/nvme0n1p2 5.0M 5.0M 0 100% /
/dev/nvme1n1p1 20M 1.2M 18.8M 6% /var/lib/postgresql
Significado: Los inodos en / están agotados. Esto se parece a “disco lleno” pero borrar un archivo enorme no ayudará.
Decisión: Encuentra directorios con millones de ficheros pequeños (a menudo logs, temporales o caches de aplicación mal gestionados). Limpia esos primero.
Tarea 3: Encontrar a los mayores consumidores de espacio de forma segura
cr0x@server:~$ sudo du -xhd1 /var | sort -h
120M /var/cache
2.4G /var/log
8.1G /var/tmp
55G /var/lib
Significado: /var/lib domina. Ahí viven las bases de datos. Pero /var/log y /var/tmp son no triviales y a menudo lo más fácil de recortar.
Decisión: Si la base de datos está caída, primero recupera margen recortando logs y temporales. Luego investiga los directorios de la base de datos con más precisión.
Tarea 4: Detectar ficheros abiertos pero borrados (clásico “df dice lleno, du no”)
cr0x@server:~$ sudo lsof +L1 | head
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NLINK NODE NAME
rsyslogd 812 syslog 7w REG 259,2 2147483648 0 12345 /var/log/syslog.1 (deleted)
java 1552 app 12w REG 259,2 1073741824 0 12346 /var/log/app.log (deleted)
Significado: Procesos mantienen descriptores de ficheros borrados, por lo que el espacio no se liberará hasta que esos procesos reinicien o cierren los FDs.
Decisión: Reinicia el/los servicio(s) específicos tras confirmar el impacto, o rota logs correctamente. No reinicies a lo loco a menos que disfrutes de downtime extendido.
Tarea 5: Comprobar uso de journald
cr0x@server:~$ sudo journalctl --disk-usage
Archived and active journals take up 1.8G in the file system.
Significado: Los journals están ocupando espacio real. En raíces pequeñas, esto importa.
Decisión: Vacía logs antiguos si es necesario; luego arregla la retención para que esto no sea tu hobby recurrente.
Tarea 6: Vaciar journald rápidamente (alivio de espacio)
cr0x@server:~$ sudo journalctl --vacuum-time=7d
Vacuuming done, freed 1.2G of archived journals from /var/log/journal.
Significado: Recuperaste 1.2G. Eso suele ser suficiente para permitir que las bases de datos hagan checkpoint, roten logs o se reinicien correctamente.
Decisión: Usa el espacio recuperado para estabilizar la base de datos (o crear un buffer temporal), luego arregla la causa raíz.
Tarea 7: PostgreSQL—comprobar si el directorio WAL es el culpable
cr0x@server:~$ sudo -u postgres du -sh /var/lib/postgresql/16/main/pg_wal
86G /var/lib/postgresql/16/main/pg_wal
Significado: WAL es enorme. Eso suele significar (a) replication slot fijando WAL, (b) archivado roto, (c) réplica muy atrasada, o (d) checkpointing impedido.
Decisión: Investiga replication slots y estado de archivado antes de borrar nada. Borrar archivos WAL manualmente es cómo conviertes “incidente” en “cambio de carrera”.
Tarea 8: PostgreSQL—listar replication slots y localizar la fijación de WAL
cr0x@server:~$ sudo -u postgres psql -x -c "SELECT slot_name, slot_type, active, restart_lsn, wal_status FROM pg_replication_slots;"
-[ RECORD 1 ]--+------------------------------
slot_name | analytics_slot
slot_type | logical
active | f
restart_lsn | 2A/9F000000
wal_status | reserved
-[ RECORD 2 ]--+------------------------------
slot_name | standby_1
slot_type | physical
active | t
restart_lsn | 2F/12000000
wal_status | extended
Significado: analytics_slot está inactivo pero aún reserva WAL vía restart_lsn. Esa es una causa común de crecimiento de WAL hasta la muerte del disco.
Decisión: Si el consumidor se ha ido o puede resetearse, elimina el slot para liberar retención de WAL. Si se necesita, arregla al consumidor y deja que se ponga al día, o mueve WAL a un volumen más grande.
Tarea 9: PostgreSQL—eliminar un slot lógico sin usar (solo si estás seguro)
cr0x@server:~$ sudo -u postgres psql -c "SELECT pg_drop_replication_slot('analytics_slot');"
pg_drop_replication_slot
--------------------------
(1 row)
Significado: El slot fue eliminado; PostgreSQL ahora puede reciclar WAL una vez que no existan otras restricciones de retención.
Decisión: Monitoriza el tamaño de pg_wal y el uso de disco; coordina con el equipo que usaba el slot porque su pipeline se romperá (mejor roto que lleno).
Tarea 10: PostgreSQL—buscar ficheros temporales descontrolados (spill de consultas)
cr0x@server:~$ sudo -u postgres find /var/lib/postgresql/16/main/base -maxdepth 2 -type f -name "pgsql_tmp*" -printf "%s %p\n" | head
2147483648 /var/lib/postgresql/16/main/base/16384/pgsql_tmp16384.0
1073741824 /var/lib/postgresql/16/main/base/16384/pgsql_tmp16384.1
Significado: Existen ficheros temporales grandes, sugiriendo ordenaciones/hashes que derramaron a disco. Normalmente desaparecen cuando las sesiones terminan, pero durante un incidente pueden ser el propio incidente.
Decisión: Identifica las sesiones culpables (tarea siguiente), cancélalas si es necesario, y ajusta consulta/indexado. Para alivio inmediato, matar unas pocas sesiones puede liberar gigabytes rápidamente.
Tarea 11: PostgreSQL—encontrar sesiones pesadas y cancelar la peor
cr0x@server:~$ sudo -u postgres psql -c "SELECT pid, usename, state, now()-query_start AS age, left(query,120) AS q FROM pg_stat_activity WHERE state <> 'idle' ORDER BY query_start ASC LIMIT 5;"
pid | usename | state | age | q
------+--------+--------+---------+------------------------------------------------------------
4412 | app | active | 00:34:12 | SELECT ... ORDER BY ...
4520 | app | active | 00:21:03 | WITH ... JOIN ...
cr0x@server:~$ sudo -u postgres psql -c "SELECT pg_cancel_backend(4412);"
pg_cancel_backend
-------------------
t
(1 row)
Significado: Encontraste consultas de larga duración; el cancel funcionó para PID 4412. Si estaba derramando a disco, los ficheros temporales deberían dejar de crecer y pueden eliminarse.
Decisión: Si la presión de disco es aguda, cancela/termina las sesiones que causan el spill. Luego corrige el plan de consulta con ojos calmados.
Tarea 12: MySQL—comprobar dónde se va el espacio (datadir y logs)
cr0x@server:~$ sudo mysql -e "SHOW VARIABLES WHERE Variable_name IN ('datadir','tmpdir','log_bin','general_log_file','slow_query_log_file');"
+--------------------+---------------------------+
| Variable_name | Value |
+--------------------+---------------------------+
| datadir | /var/lib/mysql/ |
| tmpdir | /tmp |
| log_bin | ON |
| general_log_file | /var/lib/mysql/general.log|
| slow_query_log_file| /var/lib/mysql/slow.log |
+--------------------+---------------------------+
Significado: tmpdir está en /tmp (a menudo respaldado por la raíz). Si / se llena, las operaciones temporales de MySQL fallan de maneras sorprendentemente extrañas.
Decisión: Si / está limitado, mueve tmpdir a un volumen más grande y reinicia (o configura con antelación). También comprueba si el general log se habilitó por accidente.
Tarea 13: MySQL—ver inventario y tamaño de binlogs
cr0x@server:~$ sudo mysql -e "SHOW BINARY LOGS;"
+------------------+-----------+
| Log_name | File_size |
+------------------+-----------+
| binlog.000231 | 104857600 |
| binlog.000232 | 104857600 |
| binlog.000233 | 104857600 |
| binlog.000234 | 104857600 |
+------------------+-----------+
Significado: Existen binlogs y pueden acumularse. Los tamaños aquí son consistentes, pero el recuento puede ser enorme.
Decisión: Antes de purgar, verifica el estado de replicación. Purga binlogs que todas las réplicas ya hayan consumido.
Tarea 14: MySQL—confirmar posición de replicación antes de purgar binlogs
cr0x@server:~$ sudo mysql -e "SHOW MASTER STATUS\G"
*************************** 1. row ***************************
File: binlog.000234
Position: 89234122
Binlog_Do_DB:
Binlog_Ignore_DB:
Executed_Gtid_Set: 3f1c2c3a-aaaa-bbbb-cccc-111111111111:1-982341
cr0x@server:~$ sudo mysql -e "SHOW SLAVE HOSTS;"
+-----------+-----------+------+-------------------+-----------+
| Server_id | Host | Port | Rpl_recovery_rank | Master_id |
+-----------+-----------+------+-------------------+-----------+
| 12 | replica01 | 3306 | 0 | 1 |
+-----------+-----------+------+-------------------+-----------+
Significado: Tienes al menos una réplica. Necesitas asegurarte de que está lo bastante puesta al día (GTID o file/pos) antes de eliminar logs.
Decisión: Comprueba el estado de cada réplica (o mediante monitorización). Purga solo logs más antiguos de lo que todas las réplicas han consumido.
Tarea 15: MySQL—purgar binlogs con conservadurismo
cr0x@server:~$ sudo mysql -e "PURGE BINARY LOGS TO 'binlog.000233';"
Significado: Los binlogs anteriores a binlog.000233 se eliminan. Si una réplica aún los necesitaba, se detendrá y requerirá re-seed u otra reparación.
Decisión: Purga solo tras verificar replicación. Si dudas, libera espacio en otro lado primero. Los binlogs son una motosierra, no un bisturí.
Tarea 16: MySQL—comprobar explosiones de tablas temporales y presión en tmpdir
cr0x@server:~$ sudo mysql -e "SHOW GLOBAL STATUS LIKE 'Created_tmp%';"
+-------------------------+----------+
| Variable_name | Value |
+-------------------------+----------+
| Created_tmp_disk_tables | 184203 |
| Created_tmp_files | 91203 |
| Created_tmp_tables | 3312849 |
+-------------------------+----------+
Significado: Muchas tablas temporales en disco sugieren patrones de consulta que derraman a disco. En un sistema de ficheros lleno, esas consultas fallan y a veces bloquean a otras.
Decisión: Mitiga la presión inmediata del disco (mueve tmpdir, añade espacio, mata las peores consultas), luego optimiza consultas/índices y ajustes de tablas temporales.
Tarea 17: Confirmar que el thin provisioning no te está mintiendo (ejemplo LVM)
cr0x@server:~$ sudo lvs -a -o +data_percent,metadata_percent
LV VG Attr LSize Pool Data% Meta%
mysql vg0 Vwi-aotz-- 300.00g thinpool 98.44 92.10
thinpool vg0 twi-aotz-- 500.00g 98.44 92.10
Significado: El thin pool está casi lleno. Incluso si el sistema de ficheros parece “bien”, las asignaciones pueden fallar pronto. A las bases de datos les encanta descubrir esto en tiempo de máxima escritura.
Decisión: Expande el thin pool o libera extents inmediatamente. Trátalo como “disco lleno pendiente”.
Tarea 18: Verificar que no has alcanzado una cuota (ejemplo XFS project quota)
cr0x@server:~$ sudo xfs_quota -x -c "report -p" /var/lib/mysql
Project quota on /var/lib/mysql (/dev/nvme2n1p1)
Project ID: 10 (mysql)
Used: 498.0G Soft: 0 Hard: 500.0G Warn/Grace: [--------]
Significado: Alcanzaste una cuota hard a 500G. El sistema de ficheros puede tener espacio libre en conjunto, pero tu directorio MySQL no puede crecer.
Decisión: Aumenta la cuota o migra datos a un proyecto más grande. Deja de culpar a la base de datos por una decisión de política.
Broma corta #2: Lo único que crece más rápido que tu WAL es la confianza de la persona que dice “no necesitamos alertas de disco”.
Tres micro-historias corporativas (anonimizadas, dolorosamente plausibles)
1) El incidente provocado por una suposición errónea: “Tenemos 20% libre, estamos bien”
Ejecutaban PostgreSQL en una VM con un disco de datos separado y un disco raíz pequeño. El panel mostraba el disco de datos al 78% usado. Todos se sintieron responsables. Nadie se preocupó.
Luego un despliegue activó logging de aplicación muy verboso durante una sesión de depuración. Los logs iban a /var/log en el disco raíz. A medianoche, el filesystem raíz llegó al 100% y journald empezó a eliminar mensajes. A las 00:30, PostgreSQL empezó a fallar escrituras temporales y luego tuvo problemas con escrituras relacionadas con checkpoints porque incluso “no es el disco de datos” aún importa para el SO y servicios del sistema.
On-call hizo lo habitual: comprobó el volumen de base de datos, vio margen y asumió que el problema estaba en otro lado. Mientras tanto, el “otro lado” era la partición raíz, que además alojaba /tmp y algunos scripts administrativos que escribían sus propios ficheros temporales. El fallo parecía un problema de base de datos, pero la causa raíz fue la disposición de la infraestructura y la suposición de que “disco de base de datos” equivale a “todos los discos que importan”.
La recuperación fue rápida una vez que alguien ejecutó df -h en vez de mirar el dashboard de la base de datos. Vaciaron journald, truncaron el log desbocado, reiniciaron el servicio ruidoso y Postgres se recuperó sin drama. La acción del postmortem fue aburrida: redimensionar particiones y retención de logs. Funcionó.
2) La optimización que se volvió en contra: “Mantengamos los binlogs más tiempo, por si acaso”
Un equipo de MySQL quería mejor recuperación punto-en-el-tiempo y reconstrucciones de réplicas más suaves. La perilla más fácil fue retención de binlogs. La extendieron. Nadie registró el nuevo tamaño máximo, porque la planificación de capacidad es lo que haces cuando tienes tiempo, y nadie tiene tiempo.
El almacenamiento estaba thin-provisioned. El filesystem todavía reportaba espacio libre, así que las alertas permanecieron silenciosas. Las escrituras continuaron. Los binlogs se acumularon. El thin pool se acercó al lleno. Un día, las asignaciones comenzaron a fallar en ráfagas—primero durante picos de escritura, luego con más frecuencia. MySQL lanzó errores sobre escritura en binary log, y de repente commits empezaron a fallar intermitentemente.
Lo peor fue el patrón: el servidor no murió de inmediato, se volvió poco fiable. Algunas transacciones confirmaban, otras no. La aplicación empezó a reintentar. Reintentar aumentó carga de escritura. La carga generó más presión de binlogs. Es un bucle de realimentación que no quieres.
Se recuperaron expandiendo el thin pool y purgando binlogs con conservadurismo tras confirmar que las réplicas estaban sanas. La optimización que salió mal no fue “mantener binlogs más tiempo”. Fue hacerlo sin un presupuesto acotado, sin alertas a nivel de almacenamiento y sin un procedimiento práctico de purga.
3) La práctica aburrida pero correcta que salvó el día: separar WAL/redo, imponer presupuestos, ensayar limpieza
Otra organización ejecutaba PostgreSQL con WAL en un volumen dedicado y monitorización estricta: alertas de llenado del volumen al 70/80/85%, y un runbook que incluía “comprobar replication slots, comprobar archivado, comprobar transacciones largas”. No era glamuroso. Fue efectivo.
Una tarde, un consumidor de replicación lógica se quedó parado tras un cambio de red. El replication slot quedó inactivo. WAL comenzó a acumularse. Saltó la alerta del 70%. On-call no entró en pánico; ejecutaron el runbook. Confirmaron que el slot era la causa, verificaron que el consumidor estaba realmente caído, luego eliminaron el slot y notificaron al equipo propietario.
La base de datos nunca pasó a modo solo lectura, nunca se cayó, nunca tuvo un outage. El “incidente” fue un hilo de Slack y un ticket. La práctica que los salvó no fue genialidad. Fue la disciplina silenciosa de poner la ruta de escritura más peligrosa (WAL) en un volumen con presupuesto, más alertas que dieron tiempo a los humanos para comportarse como humanos.
Si quieres recuperación más rápida ante disco lleno, no empiezas durante el incidente. Empiezas cuando decides dónde viven WAL/binlogs y cuánto holgura mantienes.
Errores comunes: síntomas → causa raíz → solución
1) “df dice 100%, pero borré ficheros y nada cambió”
Síntoma: El uso de disco se mantiene alto tras borrar logs o dumps grandes.
Causa raíz: Ficheros abiertos pero borrados (proceso sigue manteniendo el FD).
Solución: Usa lsof +L1, reinicia el servicio específico y confirma que df -h baja. No reinicies todo a lo loco salvo que sea necesario.
2) PostgreSQL: “pg_wal está enorme y no se reduce”
Síntoma: El directorio WAL crece hasta que el disco queda casi lleno; el archivado puede estar “funcionando” a veces.
Causa raíz: Slot de replicación inactivo, archiver atascado o réplica que no consume WAL.
Solución: Inspecciona pg_replication_slots, arregla el consumidor o elimina el slot; verifica la salud de archive_command; asegúrate de que el volumen WAL tenga holgura.
3) PostgreSQL: “No hay espacio” pero el disco de datos tiene espacio
Síntoma: Errores al escribir ficheros temporales, fallos durante sorts/joins; el montaje principal de datos parece OK.
Causa raíz: Ficheros temporales en otro filesystem (a menudo /tmp o raíz), o root lleno afectando operaciones del sistema.
Solución: Revisa temp_tablespaces y uso de tmp del SO; libera espacio raíz; opcionalmente mueve tablespaces temporales a un montaje más grande.
4) MySQL: “El servidor está arriba pero las escrituras fallan aleatoriamente”
Síntoma: Algunas transacciones fallan, otras tienen éxito; los errores mencionan binlog o ficheros tmp.
Causa raíz: Binary logs o tmpdir en un filesystem lleno; thin provisioning casi lleno causando fallos intermitentes de asignación.
Solución: Libera espacio donde viven los binlogs; valida el thin pool; mueve tmpdir; limita la retención de binlogs (binlog_expire_logs_seconds) y monitoriza.
5) “Liberamos 5G, pero la base de datos lo llena inmediatamente”
Síntoma: Borras cosas, liberas espacio brevemente y luego se va en minutos.
Causa raíz: Un proceso en segundo plano que amplifica escrituras: checkpoints bajo presión, autovacuum poniéndose al día, backlog de replicación generando logs, o una consulta descontrolada produciendo spill.
Solución: Identifica el vector de crecimiento (WAL/binlog/temp). Detén la hemorragia: cancela la consulta, arregla slot/réplica, pausa un job o limita temporalmente la ingesta.
6) “Purgamos binlogs de MySQL y ahora una réplica está muerta”
Síntoma: La réplica falla con errores de ficheros binlog faltantes.
Causa raíz: Purgaste binlogs que la réplica aún necesitaba; visibilidad pobre del lag/estado GTID de la réplica.
Solución: Re-seed o usa recuperación basada en GTID si está disponible. Evita recurrencia: automatiza la purga de binlogs basada en retención segura y monitoriza el lag de réplicas.
7) PostgreSQL: “Vacuum no nos salvó; el disco sigue lleno”
Síntoma: Se hicieron deletes, vacuum corrió, pero el disco no se redujo.
Causa raíz: MVCC libera espacio dentro de los ficheros de relación para reutilización; no lo devuelve necesariamente al SO. También hay bloat en índices.
Solución: Para encoger de verdad: VACUUM FULL (bloqueante) o estrategias de reescritura de tablas, además de REINDEX donde haga falta. Planifícalo; no lo improvises en medio del incidente.
Listas de comprobación / plan paso a paso
Durante el incidente: estabilizar primero, reparar después
- Detén el crecimiento: pausa jobs por lotes, deshabilita logs ruidosos, limita la ingesta o bloquea temporalmente al mayor culpable.
- Confirma qué está lleno:
- Ejecuta
df -hTydf -ih. - Comprueba cuotas si tu organización usa muros invisibles.
- Ejecuta
- Recupera margen rápido (apunta a 5–10% libre, no “unos MB”):
- Vacía journald y poda logs antiguos.
- Elimina dumps/artifacts antiguos de ubicaciones conocidas.
- Aborda ficheros abiertos pero borrados y reinicia el servicio específico.
- Triage específico de la base de datos:
- Postgres: inspecciona
pg_wal, slots, archivado, transacciones largas, spill temporales. - MySQL: inspecciona binlogs, ubicación de tmpdir, relay logs en réplicas y estado del thin pool.
- Postgres: inspecciona
- Trae la base de datos de vuelta de forma segura:
- Prefiere un reinicio controlado sobre bucles repetidos de crash.
- Verifica que las rutas de durabilidad funcionen (WAL/binlog escribibles).
- Valida la salud de la aplicación: tasas de error bajan, latencia se normaliza, replicación se pone al día.
Después del incidente: hazlo más difícil de repetir
- Separa rutas de escritura críticas: WAL o redo/binlog en volúmenes con alertas y presupuestos de crecimiento predecibles.
- Establece retenciones explícitas:
- Postgres: gestiona slots; asegura monitorización del archivado si se usa.
- MySQL: configura
binlog_expire_logs_seconds; verifica rotación de logs.
- Pon temporales donde puedan respirar: volumen temporal dedicado o límites sensatos; evita la raíz para cargas temporales pesadas.
- Alerta sobre tasa de cambio, no solo sobre porcentaje usado: “+20G/h” vence a “92% usado” cada vez.
- Ensaya un runbook de disco lleno trimestralmente. El objetivo es memoria muscular, no heroísmo.
Preguntas frecuentes
1) Si el disco está lleno, ¿debo reiniciar la base de datos inmediatamente?
No. Primero libera suficiente espacio para permitir que la base de datos arranque y complete la recuperación tras crash/checkpoints. Reiniciar en modo ENOSPC puede empeorar el riesgo de corrupción y alargar el downtime.
2) ¿Es seguro borrar ficheros WAL de PostgreSQL para liberar espacio?
Casi nunca. La eliminación manual puede hacer que la recuperación sea imposible. El enfoque correcto es eliminar la razón por la que WAL se retiene (slots, backlog de archivado, lag de réplicas) o añadir espacio.
3) ¿Es seguro purgar binlogs de MySQL durante un incidente?
Sí, pero solo con conciencia de replicación. Confirma que todas las réplicas han consumido los logs (GTID o file/pos). Purga con conservadurismo y vigila las réplicas de cerca.
4) ¿Por qué PostgreSQL consume tanto disco incluso después de borrar filas?
MVCC mantiene versiones antiguas de filas hasta que vacuum puede reclamarlas para reutilización. Ese espacio suele reutilizarse dentro de los mismos ficheros, no se devuelve al sistema de ficheros. Encoger requiere reescrituras (por ejemplo, VACUUM FULL) o estrategias de rebuild.
5) ¿Por qué MySQL “responde” pero las consultas fallan bajo presión de disco?
Porque distintos subsistemas fallan de forma independiente: tmpdir puede estar lleno, escrituras de binlog pueden fallar o los tablespaces no pueden extenderse. El puerto TCP abierto no es lo mismo que “la base de datos está sana”.
6) ¿Qué base de datos tolera más funcionar cerca del lleno?
Ninguna. PostgreSQL es más propenso a negarse a confirmar operaciones inseguras cuando no puede escribir WAL. MySQL puede seguir funcionando más tiempo, pero puede ser operativamente más desordenado. Tu mejor característica de tolerancia es espacio libre.
7) ¿Cuál es el control preventivo único más efectivo?
Limitar el crecimiento con presupuestos explícitos: retención de WAL/binlog, rotación de logs y cuotas donde proceda—más alertas sobre tasa de crecimiento. “Nos daremos cuenta” no es un control.
8) ¿Cuánto espacio libre debería mantener?
Mantén suficiente para sobrevivir tu peor crecimiento hasta que los humanos reaccionen: típicamente 10–20% en volúmenes de base de datos, más en volúmenes que alojan WAL/binlogs/temporales, y extra si tienes picos de escritura.
9) ¿La elección de sistema de ficheros cambia los resultados de recuperación?
Cambia el comportamiento de fallo y las herramientas. Los bloques reservados de ext4 pueden enmascarar un “lleno” para root; XFS y sus cuotas son comunes en entornos multi-tenant; ZFS tiene su propia amplificación copy-on-write y reglas de “no llenar la pool”. Elige intencionalmente y monitoriza en consecuencia.
10) ¿Cuál es el movimiento más rápido para recuperar espacio que suele ser seguro?
Limpiar artefactos no relacionados con la base de datos: logs rotados, dumps antiguos, cachés de paquetes, vacuum de journald y limpieza de ficheros abiertos pero borrados. Toca la interna de la base de datos solo una vez que entiendas por qué creció.
Conclusión: qué hacer la próxima semana
Los incidentes de disco lleno no tratan de bases de datos frágiles. Tratan de que nosotros somos optimistas. PostgreSQL tiende a recuperarse más limpio porque es estricto con WAL y consistencia. MySQL también puede recuperarse rápido, pero te da más palancas operacionales—y más oportunidades para cortarte.
Próximos pasos que rinden de inmediato:
- Separa y presupuestiza tus rutas de escritura críticas: WAL de Postgres, binlogs/redo de MySQL y ubicaciones de spill temporales.
- Instrumenta alertas de tasa de crecimiento y añade “tiempo hasta llenado” a tus dashboards.
- Escribe y ensaya un runbook de disco lleno que comience con
df,df -iylsof +L1antes de tocar cualquier fichero de base de datos. - Normaliza higiene aburrida: retención de logs, limpieza de dumps, disciplina de slots/binlog y gestión periódica del bloat.
Si haces esas cuatro cosas, “disco lleno” deja de ser un thriller y se convierte en una tarea de mantenimiento. Ese es el tipo de degradación que deberías perseguir activamente.