NNODE LOOP LABruntime observatoryNNEON · Статьи на 90 языках
CONNECTING
v24.18.0linux/x64
Concurrency model → подходящий workload
08

Node.js против Java, Go и Python

Разберите, почему Node эффективен на I/O, где заканчивается преимущество Event Loop и какие модели используют Java, Go и Python.

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

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

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

Разбираем: Node.js против Java, Go и Python

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

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

Node похож не на «одного медленного работника», а на диспетчера, который не стоит рядом с каждым ожидающим заказом. Пока база данных, сеть или диск выполняют работу, главный JavaScript-поток может обслуживать другие готовые события. Это экономит потоки именно на ожидании I/O, но не превращает тяжёлый JavaScript в параллельный.

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

Один Node-процесс содержит V8 isolate с главным JavaScript Event Loop, служебные потоки V8 и возможности libuv. Сокеты обычно наблюдаются механизмами готовности ОС, часть fs/crypto/DNS выполняется в ограниченном libuv pool, а CPU-bound JavaScript можно вынести в Worker Threads или процессы. Поэтому фраза «Node однопоточный» описывает выполнение JavaScript в одном isolate, а не весь процесс.

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

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

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

01

Concurrency

Несколько задач находятся в работе и продвигаются с перекрытием по времени; это ещё не означает одновременное выполнение инструкций.

02

Parallelism

Инструкции действительно выполняются одновременно на нескольких ядрах или аппаратных потоках.

03

I/O-bound

Нагрузка, где большая часть времени уходит на ожидание сети, диска, базы данных или другого внешнего ресурса.

04

CPU-bound

Нагрузка, ограниченная вычислениями: сериализацией, компрессией, изображениями, криптографией или большими циклами.

05

V8 isolate

Изолированная среда V8 со своим heap и JavaScript execution state. Worker Thread создаёт отдельный isolate.

06

Throughput

Количество успешно обработанных операций за единицу времени; не то же самое, что latency одного запроса.

02 · МЕХАНИКА

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

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

  1. 01
    Классифицируйте работу

    Отделите короткий JavaScript от сетевого ожидания, нативной операции и тяжёлого CPU-кода.

  2. 02
    Node регистрирует I/O

    Главный поток передаёт ожидание ОС/libuv и возвращается к готовым callbacks.

  3. 03
    Готовность возвращает continuation

    Когда операция готова, callback или Promise continuation ждёт свободного JavaScript-стека.

  4. 04
    CPU меняет картину

    Долгий синхронный JavaScript занимает isolate и задерживает все его соединения.

  5. 05
    Выбирается граница параллелизма

    Worker Threads подходят для CPU и общей памяти, процессы — для изоляции, очередь jobs — для распределённой работы.

  6. 06
    Модель проверяется метриками

    Сравнивайте throughput, p95/p99 latency, Event Loop delay, CPU, память и поведение под перегрузкой.

03 · КОНТЕКСТ

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

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

01

Java — не только тяжёлый thread-per-request

Java поддерживает platform threads, неблокирующий NIO/reactive-подход и virtual threads. Virtual thread при blocking I/O может освободить carrier OS thread; это масштабирует привычный синхронный стиль, но не ускоряет CPU-задачу.

02

Go — не «по ОС-потоку на goroutine»

Лёгкие goroutines мультиплексируются scheduler-ом Go на набор OS threads. Параллелизм регулируется runtime и доступными ядрами, а блокирующие операции интегрируются с scheduler-ом.

03

Python зависит от реализации и сборки

asyncio также использует Event Loop для I/O. Обычная CPython-сборка сериализует большую часть Python bytecode через GIL, но multiprocessing, native extensions и опциональная free-threaded сборка меняют границы параллелизма.

04

Node эффективен не для всего

Много ожидающих сокетов — сильный сценарий. Большая синхронная JSON-сериализация или расчёт на каждом запросе может превратить один isolate в bottleneck.

05

Архитектура важнее языка в вакууме

Connection pool, backpressure, алгоритм, batching, кэш, число процессов и лимиты часто сильнее влияют на результат, чем название runtime.

01
Теория

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

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

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

03
Runtime-код

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

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

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

src/demos.js · учебный фрагментJavaScript
const waits = Array.from({ length: 24 }, (_, index) =>
  delay(25 + (index % 4) * 10)
);

// Один Event Loop координирует все pending waits:
await Promise.all(waits);

// Но этот CPU-код занимает главный JavaScript isolate:
heavyCpuWork(180);
05 · Runtime-код

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

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

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

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

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

const sleep = (ms) =>
  new Promise((resolve) => setTimeout(resolve, ms));

function blockMainThread(durationMs) {
  const startedAt = performance.now();
  let iterations = 0;

  // Намеренная блокировка для учебного сценария. В production так делать нельзя.
  while (performance.now() - startedAt < durationMs) {
    iterations += Math.sqrt((iterations % 10_000) + 1);
  }

  return Math.round(iterations);
}

async function runtimeModelsComparison(emit) {
  const taskCount = 24;
  const startedAt = performance.now();
  emit(
    'main-js',
    'schedule',
    `Регистрируем ${taskCount} I/O-подобных ожиданий без ${taskCount} JavaScript-потоков`,
  );

  const waits = Array.from({ length: taskCount }, (_, index) =>
    sleep(25 + (index % 4) * 10).then(() => index),
  );
  emit(
    'main-js',
    'sync',
    `Все ожидания зарегистрированы за ${(
      performance.now() - startedAt
    ).toFixed(1)} мс; стек снова свободен`,
  );

  const results = await Promise.all(waits);
  emit(
    'event-loop',
    'result',
    `${results.length} continuations выполнены тем же Event Loop после готовности таймеров`,
  );

  const timerStartedAt = performance.now();
  const delayedTimer = new Promise((resolve) => {
    setTimeout(() => {
      emit(
        'event-loop',
        'warning',
        `Таймер после CPU-блокировки вошёл в стек через ${Math.round(
          performance.now() - timerStartedAt,
        )} мс`,
      );
      resolve();
    }, 20);
  });

  emit(
    'main-js',
    'warning',
    'Теперь 180 мс CPU-bound JavaScript блокируют один главный isolate',
  );
  blockMainThread(180);
  await delayedTimer;

  emit(
    'architecture',
    'result',
    'Вывод: дешёвое ожидание I/O даёт throughput, но CPU-параллелизм требует Worker, процессов или другого runtime-подхода',
  );
}

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

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

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

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

МИФ

Node.js полностью однопоточный.

НА САМОМ ДЕЛЕ

Обычно один поток исполняет JavaScript конкретного isolate, но процесс использует служебные потоки, libuv pool и при необходимости Worker Threads.

МИФ

Асинхронность автоматически использует все CPU-ядра.

НА САМОМ ДЕЛЕ

Она позволяет не блокировать поток ожиданием. CPU-параллелизм требует нескольких isolates/процессов или нативного параллельного API.

МИФ

Java всегда создаёт дорогой OS thread на запрос.

НА САМОМ ДЕЛЕ

Это лишь одна модель. Есть NIO, reactive frameworks и virtual threads, которые мультиплексируются JVM.

МИФ

У Python всегда и при любых условиях нет параллельных threads.

НА САМОМ ДЕЛЕ

Это слишком широкое утверждение: важны CPython/GIL, free-threaded build, native code и выбранная модель multiprocessing/asyncio.

МИФ

Высокий throughput означает низкую latency.

НА САМОМ ДЕЛЕ

Система может обрабатывать много запросов в секунду и одновременно иметь плохие хвостовые p95/p99 задержки.

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

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

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

  1. Почему десять тысяч открытых сокетов и десять тысяч активных CPU-расчётов — принципиально разные нагрузки?
  2. Когда вы выберете Worker Thread, а когда отдельный процесс или BullMQ Worker?
  3. Какие метрики нужны, чтобы доказать преимущество модели, а не повторить рекламный тезис?