V8, libuv и thread pool
Вопрос «из чего вообще состоит Node.js» отсеивает тех, кто воспринимает рантайм как чёрный ящик.
V8 — движок Google, который компилирует и исполняет JavaScript и управляет памятью. libuv — библиотека на C, дающая event loop, асинхронный ввод-вывод и пул потоков. Node — это связка из них плюс собственные модули (
http,fs,crypto) и стандартная библиотека на JavaScript.
Что проверяет интервьюер
Главным образом — знаете ли вы про thread pool и его размер по умолчанию. Это не эрудиция ради эрудиции: незнание приводит к реальному инциденту, когда сервис с интенсивным хешированием паролей упирается в четыре потока и отвечает всё медленнее, хотя процессор загружен наполовину.
Что делает V8
V8 берёт исходный JavaScript, разбирает его и запускает интерпретатором Ignition, попутно собирая статистику. Горячие функции переводит в машинный код оптимизирующий компилятор TurboFan. Он же управляет кучей и сборкой мусора: молодое поколение (scavenger) собирается часто и дёшево, старое — реже и дороже. Размер старого поколения по умолчанию ограничен (порядка 1,5–4 ГБ в зависимости от версии и платформы) и меняется флагом --max-old-space-size. Важно: ни event loop, ни файлов, ни сети в V8 нет — это чистый движок языка, тот же самый, что в Chrome.
Что делает libuv
libuv отвечает за всё, чего в языке нет: цикл событий с фазами, работу с сокетами, файлами, таймерами, процессами. Для сети она использует механизмы уведомлений ядра — epoll, kqueue, IOCP. Для операций, у которых нет нормального асинхронного API на уровне ОС, libuv держит пул потоков: там операция выполняется блокирующе, но в стороне от главного потока.
Что попадает в thread pool, а что нет
| Операция | Куда идёт |
fs.readFile, fs.writeFile и прочий файловый ввод-вывод | thread pool |
crypto.pbkdf2, scrypt, randomBytes, bcrypt (асинхронные версии) | thread pool |
zlib.gzip и сжатие вообще | thread pool |
dns.lookup (и любое разрешение имён через getaddrinfo) | thread pool |
| TCP, HTTP, запросы к базе по сети | epoll/kqueue, без потоков |
dns.resolve (напрямую к DNS-серверу) | сеть, без потоков |
Формулировка, которую ждут: «сетевые операции асинхронны на уровне ядра, поэтому потоки им не нужны; файлы и криптография потоков требуют».
Ловушка с размером пула
const crypto = require('node:crypto');
const start = Date.now();
for (let i = 0; i < 5; i++) {
crypto.pbkdf2('пароль', 'соль', 200000, 64, 'sha512', () => {
console.log('задача', i, 'завершена через', Date.now() - start, 'мс');
});
}
Примерный вывод (числа зависят от машины):
задача 0 завершена через 210 мс
задача 1 завершена через 212 мс
задача 2 завершена через 213 мс
задача 3 завершена через 215 мс
задача 4 завершена через 420 мс
Первые четыре задачи выполняются одновременно — по одной на каждый поток пула. Пятая ждёт освобождения потока и завершается примерно вдвое позже. Размер пула меняется переменной окружения до запуска процесса:
UV_THREADPOOL_SIZE=8 node server.js
Но увеличивать вслепую бессмысленно: потоков больше, чем ядер, — только рост переключений контекста. И ещё одна деталь для сильного ответа: dns.lookup тоже сидит в этом пуле, поэтому шквал файловых операций способен затормозить резолвинг имён и, как следствие, исходящие HTTP-запросы — связь совершенно неочевидная, пока не знаешь про общий пул.
Как всё складывается вместе
- Ваш код (V8) вызывает
fs.readFile. - Node передаёт задачу libuv, та ставит её в thread pool и немедленно возвращает управление.
- Event loop продолжает работать: принимает запросы, выполняет таймеры.
- Поток пула дочитал файл — libuv кладёт колбэк в очередь фазы
poll. - Цикл доходит до этой фазы и выполняет колбэк уже в главном потоке, в V8.
Типичные ошибки кандидатов
- Говорят, что event loop находится в V8.
- Уверены, что HTTP-запросы занимают потоки из thread pool.
- Не знают размера пула по умолчанию (четыре) и переменной
UV_THREADPOOL_SIZE. - Путают thread pool libuv с
worker_threads— это разные механизмы: первый обслуживает I/O внутри рантайма, второй вы создаёте сами для своего JS-кода.
Как ответить кратко
V8 исполняет JavaScript и управляет памятью, libuv даёт event loop и асинхронный ввод-вывод. Сеть асинхронна на уровне ядра через epoll/kqueue и потоков не требует, а файловые операции,
zlib,cryptoиdns.lookupидут в thread pool libuv, где по умолчанию четыре потока. Размер задаётся переменнойUV_THREADPOOL_SIZEдо старта процесса. Пул libuv не надо путать сworker_threads: первый — внутренний механизм рантайма, второй — инструмент для вашего собственного CPU-кода.