NNODE LOOP LABruntime observatoryNNEON · Статьи на 90 языках
CONNECTING
v24.18.0linux/x64
Скрытый параллелизм
05

Пул потоков libuv

Шесть PBKDF2-задач показывают очередь нативного thread pool и возврат результатов в Event Loop.

PROCESS IDтекущий сервер
UPTIMEпосле запуска
LOOP DELAY P95perf_hooks
UTILIZATIONevent loop
HTTP ROUNDTRIPbrowser → server
LIVE TRACE

Временная шкала

ГОТОВ
0 ms
События появятся здесьЗапустите выбранный сценарий
#ВРЕМЯИСТОЧНИКСОБЫТИЕ
Ожидаю запуск эксперимента…
ГЛАВА 05
ПОДРОБНЫЙ РАЗБОР · ОТ БАЗЫ К КОДУ

Разбираем: Пул потоков libuv

Этот раздел можно читать до запуска опыта. После теории вернитесь к live trace и сопоставьте каждый шаг с реальным событием.

СНАЧАЛА ПРОСТЫМИ СЛОВАМИ

Это небольшая кухня за залом. Официант — главный JavaScript-поток — быстро передаёт шесть заказов. На кухне только четыре повара, поэтому два заказа ждут, даже если официант свободен и продолжает принимать гостей.

ТЕХНИЧЕСКАЯ ОСНОВА

Thread pool libuv выполняет определённые нативные операции, для которых нет удобной неблокирующей готовности ОС или которые вычислительно дороги. JavaScript приложения не исполняется внутри этого пула. Пул общий для процесса, а после завершения нативной функции libuv возвращает callback в Event Loop.

Зачем это знатьОбщий ограниченный пул может стать скрытым bottleneck: тяжёлая crypto-задача способна увеличить задержку независимого fs или DNS-вызова.
ГДЕ ВЫПОЛНЯЕТСЯ РАБОТА
01ВАШ JS-КОДfunctions · callbacks
02NODE APIsfs · crypto · timers
03V8 + LIBUVheap · loop · pool
04ОПЕРАЦИОННАЯ СИСТЕМАI/O · threads · memory
01 · СЛОВАРЬ

Термины этого эксперимента

Сначала поймите слова — затем порядок выполнения.

01

Native operation

Код на C/C++ внутри Node или библиотеки, а не JavaScript приложения.

02

Thread pool

Фиксированное число переиспользуемых потоков и очередь заданий перед ними.

03

UV_THREADPOOL_SIZE

Переменная окружения, задающая размер общего пула при старте Node-процесса.

04

PBKDF2

Вычислительно дорогая функция получения ключа; здесь она создаёт заметную работу для пула.

02 · МЕХАНИКА

Что происходит по шагам

Каждый шаг соответствует наблюдаемому состоянию runtime.

  1. 01
    Шесть отправок

    JS быстро вызывает асинхронный pbkdf2 шесть раз.

  2. 02
    Очередь пула

    Свободные native threads забирают задания, остальные ожидают.

  3. 03
    Main свободен

    Event Loop может обслуживать HTTP, пока нативные потоки считают.

  4. 04
    Первая волна

    При стандартном пуле близко завершаются примерно первые четыре задачи.

  5. 05
    Callbacks результатов

    Завершение каждого native job возвращается в основной Event Loop.

03 · КОНТЕКСТ

Где результат требует оговорки

Эти детали объясняют, почему похожий код иногда даёт другой trace.

01

Размер пула — не число физических ядер

UV_THREADPOOL_SIZE=4 разрешает до четырёх pool jobs, но ОС и CPU решают, сколько работы реально идёт одновременно. На загруженной или малоядерной машине красивых «волн» может не быть.

02

Переменную задают до запуска Node

Пул инициализируется заранее, поэтому UV_THREADPOOL_SIZE следует передавать окружением процессу или контейнеру. Изменение process.env после начала работы не является надёжной перенастройкой.

03

Свободный Event Loop ещё может тормозить

Main thread не выполняет PBKDF2, но native threads конкурируют за тот же CPU. Кроме того, занятый общий pool задерживает другие использующие его fs, crypto и некоторые DNS-операции.

04

Волны — наблюдение, а не контракт

Одинаковое число итераций не гарантирует строгий порядок завершения. Планировщик ОС, частоты CPU и другая нагрузка могут смешать callbacks.

01
Теория

Сначала разберитесь, какие части Node участвуют в выполнении.

02
Упрощённый код

Затем уберите служебные детали и рассмотрите только главную идею.

03
Runtime-код

После этого сопоставьте модель с кодом, который создаёт live trace.

04 · Упрощённый код

Минимальная модель без служебного кода

src/demos.js · учебный фрагментJavaScript
for (let i = 0; i < 6; i++) {
  crypto.pbkdf2('secret', 'salt', 120_000, 32, 'sha256',
    () => console.log('ready', i)
  );
}
05 · Runtime-код

Полный код, который выполняет сценарий

Это не альтернативный пример: ниже показаны функции и файлы, используемые кнопкой запуска.

ФАКТИЧЕСКИЙ SOURCE

Код сформирован из реальной серверной функции. Для сценариев с отдельным процессом или Worker показаны все участвующие файлы.

src/demos.js
сценарий52 строк
import { pbkdf2 } from 'node:crypto';
import { performance } from 'node:perf_hooks';

async function libuvThreadPool(emit) {
  const jobs = 6;
  const iterations = 120_000;
  const startedAt = performance.now();

  emit(
    'call-stack',
    'sync',
    `Синхронно запускаем ${jobs} вызовов crypto.pbkdf2`,
  );
  emit(
    'libuv',
    'info',
    `UV_THREADPOOL_SIZE=${process.env.UV_THREADPOOL_SIZE ?? '4 (по умолчанию)'}`,
  );

  const tasks = Array.from({ length: jobs }, (_, index) => {
    const job = index + 1;
    emit('libuv-queue', 'schedule', `PBKDF2 #${job} отправлен в пул`);

    return new Promise((resolve, reject) => {
      pbkdf2('node-loop-lab', `salt-${job}`, iterations, 32, 'sha256', (error) => {
        if (error) {
          reject(error);
          return;
        }

        emit(
          'poll',
          'callback',
          `PBKDF2 #${job} вернулся через ${Math.round(performance.now() - startedAt)} мс`,
        );
        resolve();
      });
    });
  });

  emit(
    'call-stack',
    'info',
    'Главный JS-поток сразу свободен; вычисления идут в пуле libuv',
  );
  await Promise.all(tasks);
  emit(
    'result',
    'result',
    `Все ${jobs} задач завершены за ${Math.round(performance.now() - startedAt)} мс`,
  );
}

Именно вызовы emit(...) превращаются в строки live trace. await и Promise удерживают HTTP-поток открытым до завершения сценария.

06 · НЕ ПЕРЕПУТАЙТЕ

Популярные заблуждения

Миф слева, корректная модель справа.

МИФ

Пул libuv и Worker Threads — одно и то же.

НА САМОМ ДЕЛЕ

В libuv pool работает нативная функция API; Worker исполняет ваш JavaScript в отдельном isolate.

МИФ

Чем больше UV_THREADPOOL_SIZE, тем быстрее.

НА САМОМ ДЕЛЕ

После насыщения CPU дополнительные потоки увеличивают конкуренцию и переключения контекста.

МИФ

Любой асинхронный Node API использует pool.

НА САМОМ ДЕЛЕ

Сетевые сокеты обычно используют механизмы готовности ОС без потока на каждую операцию.

МИФ

Свободный main thread означает, что нагрузка не влияет на HTTP.

НА САМОМ ДЕЛЕ

CPU contention и общий pool всё равно способны увеличить latency, даже когда JavaScript не заблокирован.

07 · САМОПРОВЕРКА

Ответьте своими словами

Если ответ получается объяснить без терминов из документации, ментальная модель уже начала складываться.

  1. Почему пятая задача может ждать, хотя Event Loop свободен?
  2. Что произойдёт с fs.readFile при полностью занятом общем пуле?