Суть Читай реальные Node-обработчики и сигнал perf, предсказывай, как каждый взаимодействует с event loop, и выбирай фикс, к которому первым тянется сеньор.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на senior-высоте — в орбите
◷ 14 min
Блокировку диагностируют в обработчиках и в гистограмме lag, а не абстрактно. Читай каждый сниппет, предсказывай, что он делает с единственным потоком loop, и выбирай изменение, которое сеньор сделал бы прежде, чем тянуться к любому knob.
Цель
Отработай цикл, который ты запускаешь в каждом инциденте заморозки: замечай синхронный участок или неограниченный fan-out на горячем пути, называй, почему он стопорит loop, и тянись к фиксу с наибольшим рычагом — async API, worker thread или лимит конкурентности.
Сниппет 1 — синхронный логин
import bcrypt from "bcrypt";import fs from "fs";app.post("/login", (req, res) => { const policy = fs.readFileSync("./password-policy.json", "utf8"); // sync read const hash = bcrypt.hashSync(req.body.password, 12); // ~250 ms CPU // ...verify and respond...});
Викторина
Completed
Под нагрузкой логинов этот обработчик роняет пропускную способность каждого route, не только /login. Что происходит и фикс с наибольшим рычагом?
Heads-up Cost factor 12 — разумный выбор безопасности; баг в том, что он выполняется синхронно на loop, а не в факторе. Async bcrypt выносит ту же работу на пул libuv и держит loop свободным.
Heads-up Хеширование — это CPU-работа, но async bcrypt гонит её на потоке пула libuv, так что loop продолжает обслуживать другие запросы. Блокировать loop — это выбор, а не необходимость.
Heads-up Синхронные вызовы завершаются инлайн; корректность в порядке. Дефект в том, что они монополизируют единственный поток loop, head-of-line-блокируя каждый другой запрос.
Сниппет 2 — ресайз на пуле libuv
// "Фикс" команды после профилирования медленного эндпоинта картинок:process.env.UV_THREADPOOL_SIZE = "32";app.post("/resize", (req, res) => { const out = resizeImageSync(req.body.buffer, 1024, 768); // pure-JS pixel loop res.send(out);});
Викторина
Completed
Команда подняла UV_THREADPOOL_SIZE до 32, ожидая распараллелить ресайз. Ничего не изменилось. Почему и какой фикс верный?
Heads-up Даже если бы оно вступило в силу, пул не выполняет JavaScript, так что не смог бы запустить JS-цикл по пикселям. Фикс — worker thread, а не пул.
Heads-up Переразмер пула действительно вызывает contention, но сломано не поэтому — пул вообще не выполняет JS. Ресайзу нужен worker thread.
Heads-up Цикл по пикселям CPU-bound, а не I/O; именно поэтому он блокирует loop и поэтому ответ — worker thread (или разбиение на чанки).
Сниппет 3 — голодание worker-пула
// Worker-пул под 4 ядра машины:const pool = new WorkerPool({ size: 4 });app.get("/report/:id", async (req, res) => { // Каждый отчёт = одна CPU-тяжёлая задача агрегации на пуле. const result = await pool.run("aggregate", req.params.id); // may take ~2 s res.json(result);});
Викторина
Completed
Под всплеском запросов отчётов каждый эндпоинт — включая дешёвые, тоже использующие пул — видит рост задержки до секунд, затем таймауты. Какой это режим отказа?
Heads-up Здесь ничего не течёт; симптом — очередь, а не рост. Фиксированный пул из 4 просто не может выполнять больше 4 тяжёлых задач сразу, остальные ждут.
Heads-up await уступает loop; loop остаётся свободным. Узкое место — ограниченный набор worker'ов, а не поток loop.
Heads-up Больше worker'ов, чем ядер, не добавляет CPU; 64 worker'а на 4 ядрах лишь нарезают время и добавляют накладные расходы. Нужна ограниченная очередь с таймаутами/сбросом, в идеале больше ядер или инстансов.
Сниппет 4 — измеряем заморозку
import { monitorEventLoopDelay } from "node:perf_hooks";const h = monitorEventLoopDelay();h.enable();setInterval(() => { console.log("loop delay p99 (ms):", h.percentile(99) / 1e6);}, 1000);// Пример вывода во время плохого запроса отчёта:// loop delay p99 (ms): 812
Викторина
Completed
CPU спокойно сидит на ~55%, пока это печатает p99 loop delay 812 мс. Какое прочтение верно?
Heads-up Умеренный CPU — это и есть ловушка: один толстый синхронный участок может стопорить loop, пока CPU остаётся средним. 812 мс p99 lag — реальная, видимая пользователю заморозка.
Heads-up Loop delay — это насколько поздно сработал запланированный колбэк (разрыв до того, как loop до него добрался), а не задержка на запрос. Он измеряет, как долго loop был монополизирован.
Heads-up Больше ядер не разблокирует единственный поток loop; один Node loop — одно ядро JS. Фикс — найти и вынести синхронный участок, затем перемерить lag.
Итог
Любую заморозку читают в обработчиках и в гистограмме lag: синхронный I/O и sync crypto на пути запроса стопорят loop и должны переехать на async API (которые используют пул libuv); CPU-bound JS не лечится большим пулом libuv и живёт в worker thread; фиксированный worker-пул голодает под всплеском тяжёлых задач, поэтому ограничивай очередь таймаутами и держи быстрые задачи вне него; а event-loop delay — а не CPU — это метрика, совпадающая с таймаутами, которые чувствуют пользователи. Диагностируй по сигналу, чини блокирующий участок, затем перемерь.