MariaDB vs PostgreSQL: «Too many open files» — por qué ocurre y la solución real

¿Te fue útil?

Son las 02:14. La app aparece como “up” en el panel, pero cada petición que toca la base de datos devuelve un educado 500 y una línea de log nada educada: Too many open files. Subes un límite, reinicias y “funciona”. Durante tres días. Luego vuelve a ocurrir, durante la nómina, el cierre trimestral o cualquier ritual que tu negocio use para invocar el caos.

Este es uno de esos fallos que parece una pregunta de trivia del SO y en realidad es un problema de diseño de sistemas. MariaDB y PostgreSQL lo alcanzan de forma distinta, por razones distintas y con perillas distintas. La solución rara vez es “poner nofile a un millón y listo”. Eso no es una solución. Es una apuesta.

Qué significa realmente “Too many open files” (y por qué miente)

En Linux, “Too many open files” suele mapear a EMFILE: el proceso alcanzó su límite de descriptores de archivo por proceso. A veces es ENFILE: el sistema alcanzó el límite global de descriptores. Otras veces no es ninguno de los dos y estás ante un tope a nivel de aplicación que se registra como “open files” porque los ingenieros son optimistas y nombrar cosas es difícil.

Un descriptor de archivo (FD) es un manejador a una “cosa” abierta: un archivo regular, un directorio, un socket de dominio Unix, un socket TCP, una pipe, un eventfd, una watch de inotify, una instancia epoll. Las bases de datos usan todos ellos. Si solo piensas “archivos de tablas”, diagnosticarás mal el problema y lo arreglarás mal.

Dos verdades operativas importantes:

  • La agotación de FDs rara vez es un problema de una sola perilla. Es la interacción entre límites del SO, valores por defecto de systemd, configuración de la base de datos, comportamiento de conexiones y la forma de la carga de trabajo.
  • La agotación de FDs es un síntoma. La causa raíz suele ser: demasiadas conexiones, demasiadas relaciones (tablas/índices/particiones) o una configuración de caché que convirtió “reutilizar archivos abiertos” en “abrir todo y nunca cerrar”.

También: puedes “arreglar” EMFILE subiendo los límites hasta que el servidor pueda abrir suficientes archivos para avanzar, y luego empujar la falla a otro sitio: presión de memoria, agotamiento de inodos, churn de caché de dentry del kernel o pura complejidad operativa. La meta no es descriptores infinitos. La meta es uso controlado de recursos.

Una cita para pegar en un sticky: “Hope is not a strategy.” — General Gordon R. Sullivan. En operaciones, esto es menos un lema y más una herramienta de diagnóstico.

Cómo se consumen descriptores de archivo en servidores de bases de datos reales

Si estás depurando esto en producción, necesitas un modelo mental de qué mantiene realmente los FDs abiertos. Aquí está la lista no exhaustiva que importa.

Conexiones: la fábrica silenciosa de FDs

Cada conexión cliente consume al menos un FD en el lado del servidor (el socket), además de cierta plomería interna. Con TLS, añades sobrecarga de CPU; con pooling de conexiones mal hecho, añades churn y picos. Si ejecutas 5.000 conexiones activas porque “microservicios”, no eres moderno—solo estás pagando renta por socket.

Archivos de datos, índices y archivos de relaciones

Las bases de datos intentan evitar reabrir archivos constantemente. Las cachés existen en parte para mantener FDs alrededor, de modo que la caché de páginas del SO haga su trabajo y la BD evite la sobrecarga de llamadas al sistema. Pero las cachés pueden estar sobredimensionadas o mal ajustadas.

  • MariaDB/InnoDB: múltiples tablespaces, redo logs, undo logs, tablas temporales, archivos .ibd por tabla cuando innodb_file_per_table=ON.
  • PostgreSQL: cada fork de una relación (main, FSM, VM) mapea a archivos; las relaciones grandes se segmentan en múltiples archivos; los archivos temporales aparecen bajo base/pgsql_tmp o en directorios temporales por tablespace.

Archivos temporales y comportamiento de volcado a disco

Ordenamientos, hashes, agregados grandes y ciertos planes de consulta vuelcan a disco. Eso genera archivos temporales. Suficientes consultas paralelas y obtienes una pequeña tormenta de descriptores abiertos.

Replicación y workers en segundo plano

Hilos de replicación, WAL senders/receivers, hilos de I/O y workers en background mantienen sockets y archivos abiertos. Normalmente no son el mayor consumidor, pero en un clúster ocupado con múltiples réplicas, suma.

Logs, slow logs, audit logs y “más observabilidad”

Los logs son archivos. Algunas configuraciones de logging abren múltiples archivos (patrones de rotación, logs de auditoría separados, logs de errores, logs generales). Si haces tail a logs con herramientas que abren manejadores adicionales o ejecutas sidecars que hacen lo mismo, puedes contribuir a la presión de FDs. No suele ser el principal culpable, pero forma parte de la cuenta.

Broma #1: “Too many open files” es la forma en que el servidor dice que ahora mismo no está emocionalmente disponible.

MariaDB vs PostgreSQL: cómo se comportan bajo presión de FDs

Modos de fallo de MariaDB (InnoDB): la caché de tablas se encuentra con la realidad del sistema de archivos

El dolor de FDs más común en MariaDB proviene del uso de archivos de tablas/índices y del comportamiento de la caché de tablas combinado con alta concurrencia. Históricamente, los servidores de la familia MySQL se apoyaban en cachés de tablas (table_open_cache, table_definition_cache) para reducir el churn de abrir/cerrar. Eso es bueno—hasta que deja de serlo.

Qué sucede en el caso “malo”:

  • Tienes muchas tablas, o muchas particiones (que son efectivamente objetos tipo tabla), o muchos esquemas.
  • Configuraste table_open_cache alto porque alguien dijo que mejora el rendimiento.
  • La carga de trabajo toca muchas tablas distintas en múltiples sesiones.
  • MariaDB intenta mantenerlas abiertas para cumplir hits de caché.
  • El proceso alcanza RLIMIT_NOFILE (por proceso), o el límite interno de archivos abiertos del servidor, y empieza a fallar operaciones.

InnoDB aporta sus propios ángulos:

  • innodb_open_files provee un objetivo de cuántos archivos InnoDB puede mantener abiertos, pero está acotado por límites del SO y otros usuarios de archivos en el proceso.
  • El uso de tablas temporales (basadas en disco) puede disparar los FDs.
  • Herramientas de backup (lógicas o físicas) pueden añadir carga y manejadores abiertos.

Modos de fallo de PostgreSQL: conexiones y sobrecarga por sesión

PostgreSQL usa un modelo proceso-por-conexión (con matices como background workers). Eso significa que cada conexión es su propio proceso con su propia tabla de FDs. La buena noticia: la agotación por proceso es menos probable si cada backend usa pocos FDs. La mala noticia: demasiadas conexiones implican demasiados procesos, demasiados sockets, demasiada memoria, demasiado cambio de contexto y una estampida de uso de recursos.

PostgreSQL suele alcanzar “too many open files” en estos escenarios:

  • Altos recuentos de conexiones más un límite bajo de FDs para el postmaster/backends bajo systemd.
  • Gran número de relaciones más patrones de consulta que tocan muchas relaciones en una sesión (piensa tablas particionadas con scans amplios).
  • Creación intensiva de archivos temporales por ordenamientos/hashes y consultas paralelas, agravado por un work_mem bajo (más spills) o un paralelismo demasiado alto (más spills concurrentes).
  • Autovacuum y mantenimiento en muchas relaciones, más la carga de usuarios. Muchos archivos abiertos.

PostgreSQL también tiene un comportamiento sutil pero real: incluso si aumentas el límite de FDs del SO, aún puedes estar limitado por expectativas internas o por otros límites del SO (como max processes, ajustes de memoria compartida o límites de cgroup). EMFILE rara vez viene solo.

La diferencia práctica que cambia tu arreglo

MariaDB tiende a alcanzar agotamiento de FDs por archivos de tablas abiertos y caches. La solución suele ser una combinación de LimitNOFILE correcto, open_files_limit apropiado y dimensionamiento sensato de la caché de tablas—además de atajar la explosión de tablas/particiones.

PostgreSQL tiende a alcanzar agotamiento de FDs por el comportamiento de conexiones y churn de archivos temporales. La solución suele ser: pooling de conexiones, reducir recuentos de conexiones, aumentar límites del SO de forma adecuada y ajustar memoria/paralelismo para reducir tormentas de spills.

Hechos interesantes y contexto histórico (que realmente importa)

  1. Los descriptores de Unix fueron diseñados como una abstracción unificadora para “todo es un archivo”, que es elegante hasta que tu BD trata todo como “abrir y nunca soltar”.
  2. Unix temprano tenía límites por defecto muy pequeños (a menudo 64), y el hábito de defaults conservadores nunca murió por completo—los valores por defecto de systemd aún tropiezan a servidores modernos.
  3. El modelo proceso-por-conexión de PostgreSQL es una decisión arquitectónica de larga data que intercambia algo de simplicidad y aislamiento por mayor sobrecarga en concurrencias muy altas.
  4. Los knobs de caché de tablas de MySQL vinieron de un mundo donde las operaciones de metadata del sistema de archivos eran caras y “mantener abierto” era una ganancia medible.
  5. El sistema de archivos /proc de Linux hizo que la introspección de FDs fuera dramáticamente más fácil; antes de eso, diagnosticar fugas de FDs era más parecido a la arqueología.
  6. cgroups y contenedores cambiaron el juego: puedes tener límites altos en el host pero límites bajos en el contenedor; el proceso ve el mundo más pequeño y falla allí.
  7. Los sistemas de archivos modernos hicieron que abrir/cerrar sea más barato que antes, pero “barato” no es “gratis” cuando se multiplica por miles de consultas por segundo.
  8. La replicación aumentó los patrones de uso de FDs en ambos ecosistemas, añadiendo más sockets y actividad de archivos de logs—especialmente en topologías con múltiples réplicas.

Guía rápida de diagnóstico

Esta es la parte que sigues cuando estás de guardia, medio despierto y tu cerebro intenta negociar un alto el fuego con la realidad.

Primero: confirma qué límite estás alcanzando (proceso vs sistema)

  1. Revisa la fuente del error: logs de la base de datos, logs del sistema y logs de la aplicación. Determina si el propio proceso de BD no puede abrir archivos, o si los clientes no pueden conectar.
  2. Revisa el límite por proceso: inspecciona el Max open files del proceso de la BD desde /proc. Si es bajo (a menudo 1024/4096), has encontrado una causa inmediata probable.
  3. Revisa la presión global de manejadores de archivo: /proc/sys/fs/file-nr. Si el total del sistema está cerca del máximo, subir límites por proceso no ayudará sin ampliar la capacidad global y encontrar el consumidor.

Segundo: identifica quién está reteniendo los FDs

  1. Cuenta FDs abiertos por PID e identifica los mayores consumidores. Si es la BD, sigue. Si es un sidecar, shipper de logs o agente de backup, tienes un incidente distinto.
  2. Clasifica tipos de FD: ¿son mayormente sockets (conexiones) o archivos regulares (tablas, temporales, logs)? Eso te dice qué knobs de BD importan.

Tercero: determina si es “pico” o “fuga”

  1. Pico: los FDs se disparan durante un aumento de tráfico o un job por lotes, luego bajan. Solución: capacidad y control de concurrencia.
  2. Fuga/crecimiento persistente: los FDs tienden a subir y no regresan. Solución: identifica qué se mantiene abierto (caché demasiado grande, bug, conexiones atascadas, fuga de manejadores en tooling).

Cuarto: detener la hemorragia de forma segura

  1. Corto plazo: sube límites solo si estás seguro de que el kernel tiene margen y no inducirás presión de memoria. Prefiere un reinicio controlado con límites corregidos sobre jugar con ulimit al tuntún.
  2. Reduce la concurrencia: limita jobs por lotes, reduce el número de workers de la app o habilita pooling. Una base de datos que no puede abrir archivos tampoco puede servir consultas.

Tareas prácticas: comandos, salidas y decisiones (12+)

Estas son las tareas que convierten “creo” en “sé”. Cada una incluye un comando, un snippet de salida realista, qué significa y qué decides a continuación.

Tarea 1: Confirma el proceso DB y el PID

cr0x@server:~$ ps -eo pid,comm,args | egrep 'mariadbd|mysqld|postgres' | head
  1287 mariadbd /usr/sbin/mariadbd
  2140 postgres  /usr/lib/postgresql/16/bin/postgres -D /var/lib/postgresql/16/main
  2142 postgres  postgres: checkpointer

Significado: Tienes MariaDB en PID 1287 y PostgreSQL postmaster en PID 2140 (más workers). Sabe cuál está fallando; no “arregles” ambos.

Decisión: Elige el/los PID(s) que inspeccionarás en pasos posteriores. Si el error está en la app, confirma qué endpoint DB está usando.

Tarea 2: Revisa max open files por proceso (el que suele morder)

cr0x@server:~$ cat /proc/1287/limits | egrep -i 'open files|max processes'
Max open files            1024                 1048576              files
Max processes             127636               127636               processes

Significado: El límite soft es 1024; el hard 1048576. MariaDB está con una dieta de inanición.

Decisión: Arregla la unidad del servicio o los límites PAM para que la BD arranque con un soft limit sensato (por ejemplo, 65535 o más según dimensionado). No subas solo el hard y lo olvides.

Tarea 3: Cuenta FDs abiertos actuales para un PID

cr0x@server:~$ ls -1 /proc/1287/fd | wc -l
1008

Significado: El proceso está cerca del techo de 1024. EMFILE es inminente o ya está ocurriendo.

Decisión: Remediación inmediata: reduce carga y prepara un reinicio con límites corregidos. También encuentra qué está consumiendo los FDs (próximas tareas).

Tarea 4: Identifica qué tipos de FDs están abiertos (archivos vs sockets)

cr0x@server:~$ ls -l /proc/1287/fd | awk '{print $11}' | sed -e 's/.*socket:.*/socket/' -e 's/.*pipe:.*/pipe/' -e 's/.*anon_inode:.*/anon_inode/' | sort | uniq -c | sort -nr | head
  612 socket
  338 /var/lib/mysql/db1/orders.ibd
   42 anon_inode
   16 pipe

Significado: Mayormente sockets y archivos de InnoDB. No es solo “demasiadas tablas” ni solo “demasiadas conexiones”. Es ambos.

Decisión: Investiga recuentos de conexiones y configuración de caché de tablas en paralelo. Arreglar solo un lado puede desplazar el cuello de botella.

Tarea 5: Revisa uso de manejadores a nivel sistema (presión global)

cr0x@server:~$ cat /proc/sys/fs/file-nr
38144	0	9223372036854775807

Significado: Los manejadores asignados a nivel sistema están bien; el límite global es efectivamente enorme. Esto es un problema por proceso, no global.

Decisión: Enfócate en límites de systemd/PAM y configuración de BD, no en kernel fs.file-max.

Tarea 6: Inspecciona límites del servicio systemd (el culpable oculto)

cr0x@server:~$ systemctl show mariadb -p LimitNOFILE -p LimitNPROC -p TasksMax
LimitNOFILE=1024
LimitNPROC=127636
TasksMax=4915

Significado: systemd está explicitando LimitNOFILE=1024. Puedes editar /etc/security/limits.conf todo lo que quieras; systemd seguirá ganando para servicios.

Decisión: Añade un override de systemd con un LimitNOFILE mayor y reinicia el servicio. Considera también TasksMax si estás en PostgreSQL con muchos backends.

Tarea 7: Aplica un override de systemd para MariaDB o PostgreSQL

cr0x@server:~$ sudo systemctl edit mariadb
# (opens editor)
cr0x@server:~$ sudo cat /etc/systemd/system/mariadb.service.d/override.conf
[Service]
LimitNOFILE=65535

Significado: Has establecido un nuevo límite de FDs a nivel de servicio. Esta es la capa correcta para servicios.

Decisión: Recarga systemd y reinicia MariaDB en una ventana controlada. Luego verifica /proc/<pid>/limits.

Tarea 8: Recargar systemd y validar que el nuevo límite está activo

cr0x@server:~$ sudo systemctl daemon-reload
cr0x@server:~$ sudo systemctl restart mariadb
cr0x@server:~$ systemctl show mariadb -p LimitNOFILE
LimitNOFILE=65535

Significado: El servicio ahora arranca con un techo de FDs mayor.

Decisión: Si aún ves EMFILE, no es que “el límite sea demasiado bajo”—es que la carga consume demasiados FDs. Continúa diagnosticando.

Tarea 9: MariaDB—revisa variables actuales de archivos abiertos y caché de tablas

cr0x@server:~$ mariadb -e "SHOW VARIABLES WHERE Variable_name IN ('open_files_limit','table_open_cache','table_definition_cache','innodb_open_files');"
+------------------------+--------+
| Variable_name          | Value  |
+------------------------+--------+
| innodb_open_files      | 2000   |
| open_files_limit       | 65535  |
| table_definition_cache | 4000   |
| table_open_cache       | 8000   |
+------------------------+--------+

Significado: MariaDB puede abrir muchos archivos y está configurada para mantener muchas tablas abiertas. Eso puede ser apropiado—o una optimización desmedida—dependiendo del número de tablas y la memoria.

Decisión: Compáralo con la realidad: número de tablas/particiones, patrón de acceso y uso de FDs. Si estás abriendo 30k archivos en estado estable, 65k puede estar bien; si estás en 60k y sigue subiendo, necesitas cambios de diseño.

Tarea 10: MariaDB—estima el conteo de tablas y explosión de particiones

cr0x@server:~$ mariadb -N -e "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema NOT IN ('mysql','information_schema','performance_schema','sys');"
18432

Significado: Dieciocho mil tablas (o particiones representadas como tablas en metadata) es mucho. Cachés de tablas configuradas en 8000 pueden churnear o mantener miles abiertas, según el patrón de acceso.

Decisión: Si esto es una estrategia de particionado descontrolada, considera consolidar particiones, usar menos esquemas o desplazar datos de archivo fuera de la BD caliente. Si es legítimo, dimensiona límites y caches deliberadamente y monitoriza.

Tarea 11: PostgreSQL—revisa max connections y sesiones activas

cr0x@server:~$ sudo -u postgres psql -c "SHOW max_connections; SELECT count(*) AS current_sessions FROM pg_stat_activity;"
 max_connections 
-----------------
 800
(1 row)

 current_sessions 
------------------
 742
(1 row)

Significado: Estás cerca del tope de conexiones configurado. Cada conexión es un proceso. Incluso si los límites de FDs son altos, esto huele a “presión de recursos”.

Decisión: Si la app abre cientos de conexiones inactivas, implementa pooling (PgBouncer en transaction mode es la elección adulta habitual) y reduce max_connections a un número que puedas sostener.

Tarea 12: PostgreSQL—revisa rápidamente uso de FDs por backend

cr0x@server:~$ for p in $(pgrep -u postgres -d ' ' postgres); do printf "%s " "$p"; ls -1 /proc/$p/fd 2>/dev/null | wc -l; done | sort -k2 -n | tail
3188 64
3191 68
3201 71
3210 74
3222 91

Significado: Los backends no son consumidores individuales enormes de FDs (docenas cada uno), pero multiplicados por 700 sesiones aún suman muchos sockets y manejadores internos entre procesos.

Decisión: Si el postmaster o un subsistema compartido está alcanzando un límite, aumenta LimitNOFILE del servicio. Si el sistema está generalmente sobrecargado, arregla la estrategia de conexiones primero.

Tarea 13: PostgreSQL—encuentra presión por archivos temporales (spills)

cr0x@server:~$ sudo -u postgres psql -c "SELECT datname, temp_files, temp_bytes FROM pg_stat_database ORDER BY temp_bytes DESC LIMIT 5;"
  datname  | temp_files |  temp_bytes  
-----------+------------+--------------
 appdb     |      18233 | 429496729600
 postgres  |          0 |            0
 template1 |          0 |            0
 template0 |          0 |            0
(4 rows)

Significado: Muchos archivos temporales y cientos de GB volcados desde el reset de estadísticas. Esto se correlaciona con churn de FDs y tormentas de I/O en consultas pesadas.

Decisión: Identifica consultas que provocan spills, ajusta work_mem cuidadosamente y/o reduce concurrencia/paralelismo. Menos spills reducen archivos temporales y manejadores abiertos.

Tarea 14: Ver quién más consume FDs (procesos top)

cr0x@server:~$ for p in $(ps -e -o pid=); do n=$(ls -1 /proc/$p/fd 2>/dev/null | wc -l); echo "$n $p"; done | sort -nr | head
18421 1287
 2290 1774
 1132  987
  640 2140

Significado: MariaDB es la que más consume FDs (18421). El postmaster de PostgreSQL está mucho más abajo. Probablemente el incidente es relacionado con MariaDB, no “el host”.

Decisión: Enfoca la solución. Si un shipper de logs o proxy ocupa el segundo lugar, inspecciónalo también—a veces el “problema BD” es un sidecar mal comportado.

Tarea 15: Revisa mensajes del kernel por fallos relacionados con FDs

cr0x@server:~$ sudo dmesg -T | tail -n 10
[Wed Dec 31 02:13:51 2025] mariadbd[1287]: EMFILE: too many open files
[Wed Dec 31 02:13:52 2025] mariadbd[1287]: error opening file ./db1/orders.ibd (errno: 24)

Significado: Confirmación clara: errno 24 (EMFILE). No es un error de almacenamiento; es un límite de FDs.

Decisión: Trátalo como un problema de capacidad/configuración. No pierdas tiempo en chequeos de filesystem salvo que veas errores de I/O.

Tres micro-historias del mundo corporativo

Micro-historia 1: El incidente causado por una suposición equivocada

Migraron un monolito a “servicios”, mantuvieron el mismo backend MariaDB y celebraron la primera semana de dashboards verdes. El equipo de servicios tenía una costumbre ordenada: cada servicio mantenía un pool cálido de conexiones “por rendimiento”. Nadie coordinó; todos hicieron lo que funcionó localmente.

En el cierre de mes, corrió un job por lotes que tocó un gran conjunto de tablas. Mientras tanto, los servicios mantenían su actividad—más tormentas de reintentos porque la latencia subió. MariaDB comenzó a lanzar “Too many open files”. El ingeniero de guardia asumió que era un límite del kernel y subió fs.file-max. El error continuó.

El verdadero limitador fue LimitNOFILE=1024 en systemd para el servicio MariaDB. Y aun después de aumentarlo, el servidor siguió en zona de peligro porque el conteo de conexiones se había duplicado, elevando los FDs de sockets. La “asunción equivocada” fue creer que el ajuste a nivel de sistema anularía límites a nivel de servicio, y que los pools de conexiones son gratis.

Lo arreglaron correctamente: pusieron LimitNOFILE explícito, dimensionaron caches de MariaDB a valores realistas e introdujeron un layer de pooling adecuado en el borde de la app. También hicieron una regla: los tamaños de pool deben presupuestarse como memoria—porque lo son, y también lo son los descriptores de archivo.

Micro-historia 2: La optimización que salió mal

Otra compañía corría PostgreSQL y tenía un problema crónico de latencia en consultas analíticas. Un ingeniero bienintencionado aumentó settings de consultas paralelas y subió algunos knobs del planner. El primer benchmark se vio genial. Todos aplaudieron, en voz baja.

Luego llegó la carga real: muchos usuarios de reporting concurrentes, cada uno ejecutando consultas que volcaron a disco. Los workers paralelos multiplicaron el número de creadores de archivos temporales. Los archivos temporales explotaron. El I/O se disparó. Y sí, el uso de FDs subió porque cada worker abrió su propio set de archivos.

El fallo no fue “too many open files” inmediato cada vez. Fue intermitente: algunas sesiones fallaban, algunas consultas se colgaban y la app hacía timeouts. La línea temporal del incidente fue un desastre porque el síntoma pareció “almacenamiento lento”, luego “malos planes”, y finalmente “flakiness aleatoria del SO”.

La optimización salió mal porque aumentó concurrencia en el peor sitio: dentro del motor DB, durante operadores con muchos spills. La solución fue reducir el paralelismo, subir work_mem con cuidado para el rol de reporting y aplicar límites de conexiones para ese tier. El rendimiento mejoró y los picos de FDs dejaron de ser un evento.

Micro-historia 3: La práctica aburrida pero correcta que salvó el día

Un equipo tenía un estándar operativo que sonaba soso: cada host de BD tenía un presupuesto de FDs documentado, con alarmas al 60% y 80% del límite efectivo por servicio. También registraban “top consumidores de FDs” como métrica periódica, no sólo durante incidentes.

Pareció burocracia hasta que una actualización de aplicación de un proveedor desplegó un cambio sutil: abría una nueva conexión por petición cuando cierta feature flag se activaba. El conteo de conexiones subió gradualmente durante una semana. Aún no había outage—solo un aumento continuo de sockets.

La alerta del 60% saltó en horario laboral. Investigaron sin presión, vieron la tendencia y la relacionaron con la feature flag. La revirtieron, luego implementaron PgBouncer y limitaron la creación de conexiones en la app.

No pasó nada en llamas. Nadie tuvo que explicar una caída evitable a finanzas. Fue el informe de SRE menos emocionante que hayan presentado, que es el mayor cumplido que puedes darle a una práctica.

La solución real: dimensionado, límites y las perillas que realmente importan

“Subir ulimit” es la aspirina. A veces necesitas aspirina. Pero si tomas aspirina todos los días, no estás tratando la enfermedad.

Paso 1: Establece límites sensatos en SO/servicio (capa correcta, persistencia correcta)

Para despliegues Linux modernos, la verdad es: systemd es la fuente de la realidad para servicios. Configura LimitNOFILE en un drop-in override para el servicio de base de datos. Verifica después del reinicio vía /proc/<pid>/limits.

Elige un número con intención:

  • Servidores pequeños (instancia única, esquema moderado): 65535 es una base común.
  • MariaDB grande con muchas tablas/particiones o alta concurrencia: 131072+ puede ser razonable.
  • PostgreSQL con pooling y conexiones controladas: puede que no necesites valores enormes, pero no lo dejes en 1024. Eso es sabotaje deliberado.

Además: evita ponerlo a “infinito” solo porque puedes. Cada FD tiene overhead en el kernel. Y límites enormes esconden fugas hasta que se convierten en catástrofes.

Paso 2: Reduce la demanda real de FDs

Aquí es donde MariaDB y PostgreSQL divergen en la práctica.

MariaDB: deja de acumular tablas como si fuera 2009

MariaDB puede mantener miles de tablas abiertas si se lo dices. Si tu esquema tiene decenas de miles de tablas/particiones, “mantener muchas abiertas” se convierte en un riesgo estructural.

Qué hacer:

  • Dimensiona correctamente table_open_cache y table_definition_cache. Más grande no siempre es mejor. Si no tienes memoria suficiente para mantener metadata y handlers calientes, solo cambiarás el tipo de thrash.
  • Configura open_files_limit y innodb_open_files de forma coherente. No dejes uno pequeño y otro gigante. Así te creas falsa confianza de “debería funcionar”.
  • Vigila la explosión de particiones. Miles de particiones se sienten ordenadas hasta que se convierten en problema de FDs y de planificación de consultas.

PostgreSQL: arregla conexiones primero, luego spills

La victoria fácil en FDs para PostgreSQL no es un knob de FDs. Es pooling de conexiones. Si estás exponiendo cientos o miles de sesiones cliente directamente a Postgres, tratas la base de datos como un servidor web. No lo es.

Qué hacer:

  • Usa un pooler (PgBouncer es la elección común) y reduce max_connections a un número sostenible.
  • Arregla tormentas de reintentos. Si los clientes se reconectan agresivamente en errores transitorios, pueden crear tormentas de sockets que empujen los FDs al límite.
  • Reduce spills temporales. Los spills crean archivos temporales; los archivos temporales consumen FDs durante su vida. Ajusta memoria por clase de carga y reduce fan-out de workers paralelos si genera más concurrencia de spills de la que puedes manejar.

Broma #2: Poner LimitNOFILE a un millón es como comprar un armario más grande en vez de tirar tu colección de swag de conferencias.

Paso 3: Valida que no acabas de mover el cuello de botella

Después de subir límites y reducir demanda, revisa los siguientes modos de fallo:

  • Presión de memoria: más conexiones y caches significan más RSS. Vigila el swap como un halcón; hacer swap con una base de datos es un cosplay de rendimiento.
  • CPU y cambio de contexto: demasiados backends de PostgreSQL pueden fundir CPU sin que haya una consulta “mala”.
  • Disco y uso de inodos: uso intensivo de archivos temporales puede consumir inodos y espacio en disco rápido, especialmente en volúmenes root pequeños.
  • Límites del kernel más allá de nofile: max processes, límite de pids de cgroup, agotamiento de puertos efímeros (lado cliente) y ajustes de backlog de red.

Errores comunes: síntoma → causa raíz → solución

Esta sección es intencionalmente directa. La mayoría de incidentes EMFILE son autoinfligidos, solo que no por la persona que ahora tiene el pager.

Error 1: “Subimos fs.file-max, ¿por qué no funcionó?”

Síntoma: “Too many open files” continúa después de subir /proc/sys/fs/file-max.

Causa raíz: El límite por proceso/servicio (RLIMIT_NOFILE) sigue bajo, a menudo impuesto por systemd.

Solución: Establece LimitNOFILE en el override de la unidad systemd, reinicia la BD y valida vía /proc/<pid>/limits.

Error 2: “Pusimos ulimit en /etc/security/limits.conf; sigue roto”

Síntoma: En sesiones de shell manuales ves un ulimit -n alto, pero el servicio no lo hereda.

Causa raíz: Los límites PAM afectan a sesiones de login; los servicios systemd no los heredan igual.

Solución: Configura la unidad systemd. Trata los límites PAM como relevantes para sesiones interactivas, no para daemons.

Error 3: “Aumentamos table_open_cache; ahora tenemos EMFILE” (MariaDB)

Síntoma: MariaDB da errores al abrir tablas; los logs muestran errno 24; el recuento de FDs sigue subiendo.

Causa raíz: Caché de tablas demasiado grande para el esquema/patrón de trabajo; el servidor intenta mantener demasiados handlers de tablas abiertos.

Solución: Reduce table_open_cache a un valor medido, aumenta LimitNOFILE para coincidir con necesidades realistas y afronta el conteo de tablas/particiones.

Error 4: “Postgres puede manejar 2000 conexiones, está bien”

Síntoma: Fallos de conexión aleatorios, carga alta, a veces EMFILE, a veces solo timeouts.

Causa raíz: Demasiados procesos backend; uso de FDs y overhead de memoria escalan con sesiones; los picos empujan los límites.

Solución: Añade pooling, reduce max_connections y aplica presupuestos de conexión por servicio.

Error 5: “La BD está fugando FDs” (cuando en realidad son tormentas de temporales)

Síntoma: El recuento de FDs se dispara durante ciertas consultas/lotes y luego vuelve a bajar.

Causa raíz: Archivos temporales por spills y paralelismo crean picos transitorios de FDs.

Solución: Identifica consultas spill-heavy; ajusta memoria/paralelismo; programa batches; limita concurrencia.

Error 6: “Es el almacenamiento” (cuando en realidad son descriptores)

Síntoma: Las consultas fallan al abrir archivos; la gente sospecha corrupción de filesystem o discos lentos.

Causa raíz: errno 24 (EMFILE) no es un error de I/O; es un límite de FDs.

Solución: Confirma errno vía logs/dmesg; revisa /proc limits; ajusta settings de servicio y base de datos.

Error 7: “Lo arreglamos reiniciando”

Síntoma: Reiniciar resuelve temporalmente; vuelve bajo carga.

Causa raíz: Reiniciar resetea uso de FDs y caches; la demanda subyacente no cambió.

Solución: Haz el trabajo de dimensionado: límites + estrategia de conexiones + sensatez en caches de esquema + monitorización.

Listas de comprobación / plan paso a paso

Checklist A: Estabilización de emergencia (15–30 minutos)

  1. Confirma si es MariaDB o PostgreSQL quien lanza EMFILE (logs + PID).
  2. Revisa /proc/<pid>/limits para Max open files.
  3. Cuenta FDs abiertos: ls /proc/<pid>/fd | wc -l.
  4. Clasifica tipos de FD: sockets vs archivos de tablas vs archivos temporales.
  5. Si el límite del servicio es bajo, aplica override de systemd (LimitNOFILE) y programa un reinicio controlado.
  6. Limita: reduce concurrencia de workers de la app, pausa jobs pesados y desactiva tormentas de reintentos si es posible.
  7. Tras reiniciar, valida que el límite se aplicó y que el uso de FDs se estabiliza por debajo del 60% del límite.

Checklist B: Causa raíz y solución durable (mismo día)

  1. Documenta uso base de FDs en reposo, pico normal y pico máximo.
  2. Para MariaDB: inventaria conteo de tablas/particiones; revisa table_open_cache, open_files_limit, innodb_open_files.
  3. Para PostgreSQL: mide recuentos de conexiones a lo largo del tiempo; identifica qué clientes crean más sesiones; despliega pooling.
  4. Revisa estadísticas de archivos temporales y consultas lentas; correlaciona picos de FDs con horarios de batches.
  5. Configura alertas sobre uso de FDs por PID de BD y sobre recuentos de conexiones.
  6. Ejecuta una prueba de carga controlada para confirmar la solución bajo concurrencia realista y huella de esquema.

Checklist C: Prevención (aquí ganan los adultos)

  1. Crea un presupuesto de descriptores por entorno: dev, staging, producción.
  2. Aplica presupuestos de conexión por servicio. No hay excepciones sin revisión.
  3. Monitorea crecimiento de esquema (tablas, particiones, índices) como métrica de capacidad de primera clase.
  4. Haz que los overrides de systemd sean parte del control de configuración, no conocimiento tribal.
  5. Prueba failover y comportamiento de reinicio con los límites elegidos para asegurar recuperación rápida.

Preguntas frecuentes

1) ¿Siempre es culpa de la base de datos “Too many open files”?

No. A menudo lo dispara la BD, pero puede ser un proxy (como HAProxy), un shipper de logs, un agente de backup o incluso el servidor de aplicaciones agotando sus propios FDs y reportándolo mal.

2) ¿Cuál es la diferencia entre EMFILE y ENFILE?

EMFILE significa que el proceso alcanzó su límite de FDs por proceso. ENFILE significa que el sistema alcanzó su límite global de manejadores de archivos. La mayoría de incidentes de BD son EMFILE.

3) ¿Por qué systemd ignora mis cambios en /etc/security/limits.conf?

Los límites PAM aplican generalmente a sesiones de login. Los servicios systemd usan sus propios límites a menos que se configuren. Arregla la unidad con LimitNOFILE.

4) ¿Cuál es un LimitNOFILE razonable para MariaDB?

Empieza con 65535 si no sabes. Luego dimensiona según: conexiones (sockets), tablas/particiones abiertas, archivos temporales y FDs para logs/auxiliares. Si tienes conteos enormes de particiones, puede que necesites 131072 o más—pero entonces deberías cuestionar por qué tienes tantas particiones.

5) ¿Cuál es un LimitNOFILE razonable para PostgreSQL?

A menudo 65535 es un buen punto de partida. La mayor mejora es controlar conexiones y reducir tormentas de archivos temporales. Si necesitas recuentos masivos de FDs para Postgres, probablemente tienes concurrencia descontrolada o churn extremo de relaciones.

6) ¿Puedo simplemente aumentar max_connections para arreglar errores de conexión?

Puedes, pero así cambias “conexión rehusada” por “servidor en llamas”. Para PostgreSQL, usa pooling y mantiene max_connections dentro de lo que memoria y CPU puedan manejar.

7) ¿Por qué veo muchos sockets en las listas de FDs?

Porque cada conexión cliente es un FD de socket. Si los sockets dominan, céntrate en recuentos de conexiones, pooling y comportamiento de reintentos. Si los archivos regulares dominan, céntrate en comportamiento de cache de tablas, huella de esquema y churn de archivos temporales.

8) ¿Subir límites de FDs tiene desventajas?

Sí. Límites más altos facilitan que una fuga o una carga desbocada consuman más recursos del kernel antes de fallar. Fallarás más tarde, posiblemente con mayor impacto. Sube límites pero también reduce demanda y monitoriza.

9) ¿Cómo diferencio fuga de pico?

Si el uso de FDs sube de forma sostenida y no baja tras bajar la carga, sospecha fuga o comportamiento de caché que mantiene cosas abiertas indefinidamente. Si se dispara durante un batch o surge de tráfico y luego vuelve a la base, es un pico de concurrencia/capacidad.

10) ¿Realmente importan las particiones para los FDs?

Sí. En ambos ecosistemas, las particiones aumentan el número de objetos tipo relación. Más objetos pueden significar más metadata, más manejadores abiertos y más overhead de planificación/mantenimiento. El particionado es una herramienta, no una personalidad.

Siguientes pasos prácticos

Si estás en medio de un incidente: aplica la guía rápida de diagnóstico, arregla el límite de FDs a nivel de servicio y limita la concurrencia. Eso te dará aire para respirar.

Luego haz el trabajo de adultos:

  • Mide uso de FDs por tipo (sockets vs archivos) y por estado estable vs picos.
  • MariaDB: dimensiona caches de tablas y afronta crecimiento de esquema/particiones; alinea open_files_limit y innodb_open_files con límites del SO.
  • PostgreSQL: pool de conexiones, reduce max_connections y ataca spills temporales ajustando memoria/paralelismo y arreglando las peores consultas.
  • Monitoriza uso de FDs y pon alertas antes de que llegues al borde. El borde no es una oportunidad de aprendizaje; es un generador de downtime.
← Anterior
Bucle de inicio de sesión de WordPress: te devuelve al login — Cómo solucionarlo
Siguiente →
Proxmox RBD «error opening»: errores de autenticación/keyring y soluciones

Deja un comentario