Anatomía de las 6 Fases del Event Loop en libuv, epoll/IOCP y Inanición de I/O por Microtasks (55 min)
Anatomía de las 6 Fases del Event Loop en libuv, epoll/IOCP e Inanición de I/O por Microtasks
1. El Ciclo Determinista de 6 Fases de uv_run() en C
Contrario al mito popular, el Event Loop de Node.js no es una única cola FIFO, sino una máquina de estados cíclica implementada en libuv que recorre seis fases estrictas, drenando entre cada fase y entre cada callback las dos colas de microtareas de alta prioridad (process.nextTick seguida de la cola de Promise de V8):
flowchart TD
T["1. Timers Phase (Min-Heap: setTimeout / setInterval vencidos)"] --> P["2. Pending Callbacks (Errores TCP diferidos ECONNREFUSED)"]
P --> ID["3. Idle, Prepare (Uso interno libuv)"]
ID --> POLL["4. Poll Phase (epoll_wait / kqueue / IOCP: I/O de Red y Sockets)"]
POLL --> CHK["5. Check Phase (setImmediate callbacks)"]
CHK --> CLS["6. Close Callbacks (socket.on('close'))"]
CLS --> T
2. Por qué UV_THREADPOOL_SIZE NO Afecta a Sockets TCP/HTTP pero Sí a DNS y Criptografía
- Red TCP/UDP/HTTP: Es $100\%$ no bloqueante a nivel de kernel (epoll en Linux, kqueue en macOS, IOCP en Windows). No utiliza el Thread Pool de libuv.
- Operaciones que SÍ compiten por el Thread Pool de libuv (por defecto solo $4$ hilos):
1. fs.* (todas las operaciones asíncronas de sistema de archivos local).
2. dns.lookup() (invoca getaddrinfo(3) síncrono de libc; en alta carga bloquea los $4$ hilos si no se usa dns.resolve4() o caché DNS).
3. crypto.pbkdf2, crypto.scrypt, crypto.randomBytes y zlib.
3. Detector de Bloqueo de Event Loop y Lag p99 en Producción
import { monitorEventLoopDelay } from 'perf_hooks';export interface EventLoopHealthSnapshot {
p50LagMs: number;
p99LagMs: number;
maxLagMs: number;
shouldShedLoad: boolean;
}
export class EventLoopCircuitBreaker {
private histogram = monitorEventLoopDelay({ resolution: 10 });
constructor(private readonly maxAllowedP99Ms = 85) {
this.histogram.enable();
}
public inspect(): EventLoopHealthSnapshot {
const p50LagMs = Number((this.histogram.percentile(50) / 1e6).toFixed(2));
const p99LagMs = Number((this.histogram.percentile(99) / 1e6).toFixed(2));
const maxLagMs = Number((this.histogram.max / 1e6).toFixed(2));
this.histogram.reset();
return {
p50LagMs,
p99LagMs,
maxLagMs,
shouldShedLoad: p99LagMs > this.maxAllowedP99Ms
};
}
}