La dependencia que abría 24 hilos sin usarse
Los hostings compartidos con CloudLinux limitan cuántos procesos e hilos puede tener una cuenta a la vez. Cuando se alcanza el límite, las aplicaciones no fallan con un mensaje claro: dejan de responder, se reinician o devuelven errores intermitentes. En una cuenta con varias aplicaciones Node.js, una de ellas consumía mucho más que las demás, y la causa resultó ser una sola línea.
El síntoma
Cada proceso de aplicación Node tenía entre 7 y 11 hilos, excepto uno: el de SortealoPE, que tenía 35. Era el mayor consumidor del límite de la cuenta, y su código no hacía nada especialmente pesado en cada petición. Solo generaba imágenes para compartir, de vez en cuando.
Encontrar al culpable en un minuto
En Linux, el número de hilos de un proceso se lee en
/proc/self/status. Con eso se puede medir cuántos hilos
agrega cada dependencia al cargarse:
node -e 'const t=()=>require("fs").readFileSync("/proc/self/status","utf8").match(/Threads:\s+(\d+)/)[1];
const a=t(); require("paquete-sospechoso"); console.log(a+" -> "+t())' Un Node vacío tenía 7 hilos. Cargar la base de datos, el cifrado de contraseñas o el cliente de imágenes en la nube no agregaba ninguno. Pero cargar la librería nativa que convertía SVG a PNG pasaba de 7 a 31: 24 hilos más, exactamente uno por cada núcleo visible del servidor.
Por qué pasa
Algunas librerías con código nativo crean un grupo de hilos de trabajo
en el momento en que se cargan, dimensionado según los núcleos de la
máquina. No importa si nunca se llama a la función que los usa: con
solo hacer require, los hilos existen y viven mientras viva
el proceso. En un computador personal nadie lo nota. En un servidor
compartido de 24 núcleos con límite por cuenta, es un problema serio.
Lo que no funcionó
La primera idea fue limitar el grupo de hilos con variables de entorno, como las que controlan el grupo de libuv o el de la librería de paralelismo que usaba el binding. No cambiaron nada: el proceso seguía en 31 hilos. Ese grupo no era el de libuv, y el binding lo dimensionaba con su propio conteo de núcleos ignorando las variables. Por suerte se probó de forma aislada en el servidor antes de tocar producción.
Lo que sí funcionó: cargar cuando se usa
La solución fue mover el require desde el inicio del módulo
al interior de la función que genera la imagen. Así los hilos solo
existen mientras se genera una imagen, y no durante toda la vida del
proceso. El proceso bajó de 35 a 11 hilos, el consumo total de la cuenta
bajó cerca de un tercio y las imágenes siguieron saliendo idénticas,
byte por byte.
Esta técnica sirve cuando el uso es esporádico: generar una imagen, un PDF o una exportación. Si la función se llama en cada petición, cargar diferido no ahorra nada después del primer uso.
Cómo elegir dependencias en adelante
- Antes de agregar una librería nativa en un hosting con límites, medir sus hilos con el comando de arriba.
- Sospechar primero de procesamiento de imágenes, compresión, criptografía nativa y motores de aprendizaje automático.
- Si no hay alternativa sin binding, planear la carga diferida desde el principio.
- Considerar si esa parte del sistema debería vivir en un proceso aparte que solo se ejecuta cuando hace falta.