Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Що таке libuv і як він забезпечує асинхронний ввід/вивід у Node.js?». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**libuv** - це C-бібліотека, яка дає Node.js event loop, пул потоків і асинхронний I/O, загортаючи epoll, kqueue і IOCP в один API. Мережевий I/O використовує OS polling напряму; файловий I/O, DNS і crypto - пул потоків (4 потоки за замовчуванням). ```js fs.readFile('./file.txt', (err, data) => console.log(data.length)); // async через пул потоків libuv console.log('виконується першим'); // JS-потік не блокується ``` **Ключове:** libuv - це причина, чому Node.js обробляє тисячі з'єднань в одному потоці без блокування.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**libuv** - це крос-платформна C-бібліотека, яка дає Node.js event loop, пул потоків і асинхронні I/O-інтерфейси, загортаючи OS-механізми epoll на Linux, kqueue на macOS і IOCP на Windows в один уніфікований API. ## Теорія ### TL;DR - libuv знаходиться між JS-кодом Node.js і ОС, приховуючи різниці між платформами за одним API - Event loop опитує OS handles щодо завершення I/O без виділення потоку на кожен запит - Блокуючі операції (файловий I/O, DNS, crypto) йдуть у пул потоків - за замовчуванням 4 потоки - Мережевий I/O (TCP, UDP) минає пул потоків і використовує OS async polling напряму - Повільні відповіді під навантаженням crypto або файловим I/O - першим перевіряй `UV_THREADPOOL_SIZE` ### Швидкий приклад ```javascript const fs = require('fs'); // fs використовує libuv під капотом fs.readFile('data.txt', (err, data) => { if (err) throw err; console.log('Файл прочитано:', data.length, 'байт'); // спрацює після завершення I/O }); console.log('Це виконується першим'); // event loop продовжує роботу одразу // Виведення: // Це виконується першим // Файл прочитано: 1048576 байт ``` `fs.readFile` передає роботу у пул потоків libuv і одразу повертається. Callback спрацьовує пізніше, коли ОС закінчує читання. JS-потік ніколи не чекає. ### Як libuv вписується в Node.js JS-модулі Node.js (`fs`, `net`, `crypto`) викликають C++ bindings, які викликають функції libuv. Event loop libuv (`uv_run`) далі або реєструє операцію в OS poller для мережевого I/O, або кладе її в пул потоків для блокуючих операцій. ``` Node.js (JS / V8) | C++ Bindings | libuv event loop (uv_run) | | OS pollers Thread pool epoll / kqueue (4 потоки за замовчуванням) IOCP (Windows) fs, dns, crypto, zlib ``` JS-потік циклічно перевіряє, чи є завершена робота, і відправляє callbacks. Він не сидить в очікуванні диску чи мережі. ### Пул потоків vs OS polling Не всі async-операції йдуть одним шляхом. Розуміння цієї різниці пояснює більшість несподіванок у продуктивності Node.js. **OS polling (без потоків):** - TCP/UDP сокети: `http.get`, `net.connect`, `net.createServer` - Pipes, signals, таймери **Пул потоків (4 потоки за замовчуванням):** - Файлова система: `fs.readFile`, `fs.writeFile`, `fs.stat` - Crypto: `crypto.pbkdf2`, `crypto.scrypt` - DNS: `dns.lookup` (але не `dns.resolve`) - Стиснення: `zlib.gzip`, `zlib.deflate` Мережевий I/O проходить через `epoll`/`kqueue`/`IOCP` без виділення потоку на з'єднання. Саме тому Node.js обслуговує тисячі одночасних HTTP-з'єднань в одному потоці. Про те, як callbacks проходять через фази, детальніше у статті [як працює event loop в Node.js](/questions/nodejs-event-loop). ### Фази event loop Event loop libuv виконується фазами при кожній ітерації. З режимом `UV_RUN_DEFAULT` цикл такий: 1. **Timers** - запускає callbacks `setTimeout` і `setInterval`, чий час настав 2. **Pending callbacks** - I/O callbacks, відкладені з попередньої ітерації 3. **Idle / Prepare** - для внутрішнього використання libuv 4. **Poll** - блокується в очікуванні I/O-подій; відправляє готові I/O callbacks 5. **Check** - виконує callbacks `setImmediate` 6. **Close callbacks** - cleanup для закритих handles: `socket.destroy()` тощо Фаза poll - місце, де loop насправді зупиняється. Якщо є активні I/O-операції, loop чекає тут до сигналу ОС або наближення таймера. `setImmediate` виконується у фазі check, тому завжди спрацьовує після I/O callbacks тієї ж ітерації. ### Handles і requests libuv використовує дві внутрішні абстракції, які варто знати на senior-рівні. **Handles** - довгоживучі об'єкти: TCP-сервер, таймер, watcher файлів. Вони зберігаються між ітераціями loop і тримають процес живим. **Requests** - одноразові операції: читання файлу, запис у сокет, DNS-запит. Завершуються один раз і зникають. ```javascript const net = require('net'); const server = net.createServer(() => {}); // Handle - живе до server.close() server.listen(3000); const fs = require('fs'); fs.readFile('./config.json', cb); // Request - одноразовий, не тримає процес живим ``` Коли дебажиш "чому скрипт завис" - виклич `process._getActiveHandles()`. TCP-сервер або забутий таймер одразу видно. Детальніше про цей патерн у [lifecycle процесу Node.js](/questions/nodejs-process-lifecycle). ### Типові помилки **Sync crypto в обробниках запитів** ```javascript // Блокує всі інші запити на ~300мс app.post('/login', (req, res) => { const hash = crypto.pbkdf2Sync(req.body.password, salt, 100000, 512, 'sha512'); // Поки виконується цей рядок, жодні інші запити не обробляються res.send('ok'); }); ``` `pbkdf2Sync` тримає JS-потік до завершення. При 10 паралельних логінах вони обробляються послідовно. Використовуй async-варіант, який іде в пул потоків: ```javascript crypto.pbkdf2(req.body.password, salt, 100000, 512, 'sha512', (err, hash) => { res.send('ok'); }); ``` **Ігнорування виснаження пулу потоків** Чотири потоки здаються достатніми, поки не запустиш хешування паролів під навантаженням. Кожен виклик `pbkdf2` займає 200-300мс. При дефолтному пулі 5-й паралельний запит чекає в черзі за першими чотирма. Я бачив, як це додавало дві секунди до p99 latency на сервісі, який виглядав цілком здоровим. Підвищуй `UV_THREADPOOL_SIZE` до старту процесу: ```javascript // Через env variable перед запуском node: // UV_THREADPOOL_SIZE=32 node server.js // Або на самому початку entry file, до будь-яких require() process.env.UV_THREADPOOL_SIZE = '32'; ``` **Встановлення UV_THREADPOOL_SIZE занадто пізно** ```javascript // НЕПРАВИЛЬНО - пул може бути вже ініціалізований setTimeout(() => { process.env.UV_THREADPOOL_SIZE = '64'; }, 0); ``` Пул ініціалізується при першому використанні. Будь-який `require` модулів `crypto`, `dns` або `fs` до встановлення змінної фіксує дефолтні 4 потоки. **Пошук проблем event loop у V8** Коли API "рандомно зависає", розробники зазвичай дивляться на garbage collection або V8. Але V8 виконує JavaScript, а libuv керує event loop - це різні речі. Реальна причина зазвичай одна з: насичений пул потоків затримує callbacks, тривала sync-операція блокує JS-потік, або handle тримає loop від завершення. Профілюй через `clinic.js` або `0x`. Додай `console.log(process.hrtime.bigint())` біля підозрілих callbacks - одразу видно, де зникає час. ### Де зустрічається в продакшені - Маршрути Express.js з `fs.readFile` або `res.sendFile`: читання диску через пул потоків libuv, TCP-відповідь через OS polling - Next.js використовує chokidar для hot module replacement; chokidar обгортає file watcher handles libuv - `node-redis` передає TCP-дані через handles libuv без пулу потоків - Виклики AWS SDK до S3 або DynamoDB - мережевий I/O, обробляється OS polling напряму - Масштабне хешування паролів через `crypto.pbkdf2` на рівні продакшн-логінів вимагає підняття `UV_THREADPOOL_SIZE` ### Follow-up питання **Q:** Що відбувається з операціями пулу потоків, коли всі 4 потоки зайняті? **A:** Нові запити ставляться в чергу всередині libuv і чекають, поки звільниться потік. Затримка зростає пропорційно глибині черги. 5-й паралельний виклик `pbkdf2` займає приблизно в 2 рази більше часу ніж перші чотири. **Q:** Чому мережевий I/O не використовує пул потоків? **A:** TCP і UDP нативно асинхронні на рівні ОС. `epoll`, `kqueue` і IOCP підтримують polling тисяч сокетів без блокування потоку. libuv реєструє сокет у OS poller і отримує сповіщення, коли приходять дані. Потоки для цього не потрібні. **Q:** В якому порядку виконуються фази event loop? **A:** Timers, pending callbacks, idle/prepare, poll, check, close callbacks. Фаза poll блокується в очікуванні I/O. `setImmediate` виконується у фазі check, тому завжди спрацьовує після I/O callbacks тієї ж poll-ітерації. **Q:** Як дебажити event loop, який не завершується? **A:** Виклич `process._getActiveHandles()` і `process._getActiveRequests()` - побачиш, що тримає його живим. Типово це забутий таймер або відкритий TCP-сокет. У продакшені `clinic.js` дає flamegraph використання event loop по фазах. **Q (senior):** Яка різниця між `UV_RUN_DEFAULT`, `UV_RUN_ONCE` і `UV_RUN_NOWAIT`? **A:** `UV_RUN_DEFAULT` запускає loop до тих пір, поки не залишиться активних handles або requests, блокуючись у poll за потреби. `UV_RUN_ONCE` виконує одну ітерацію і блокується у poll максимум один раз. `UV_RUN_NOWAIT` виконує одну ітерацію, але не блокується у poll навіть якщо немає готових подій. Node використовує `UV_RUN_DEFAULT`. Вбудовування libuv в Electron або шар сумісності Deno іноді вимагає `UV_RUN_NOWAIT`, щоб не стопорити хост-loop. ## Приклади ### Базовий: порядок виконання при неблокуючому I/O ```javascript const fs = require('fs'); console.log('1: до readFile'); fs.readFile('./package.json', 'utf8', (err, data) => { if (err) throw err; console.log('3: розмір файлу:', data.length, 'символів'); }); console.log('2: після виклику readFile'); // Виведення: // 1: до readFile // 2: після виклику readFile // 3: розмір файлу: 432 символи ``` `readFile` реєструє request у libuv і повертається без очікування. JS-потік доходить до рядка 2 до того, як диск прочитано. Коли ОС сигналізує про завершення, libuv ставить callback в чергу і event loop його виконує. Цей порядок - основа всіх async-патернів у Node.js. ### Середній: виснаження пулу потоків в HTTP-сервері ```javascript const crypto = require('crypto'); const http = require('http'); // UV_THREADPOOL_SIZE = 4 за замовчуванням // 5 паралельних викликів pbkdf2 на запит насичують пул const server = http.createServer((req, res) => { const start = Date.now(); let count = 0; for (let i = 0; i < 5; i++) { crypto.pbkdf2('secret', 'salt', 100000, 512, 'sha512', () => { count++; if (count === 5) { res.end(`Зайняло ${Date.now() - start}мс`); } }); } }); server.listen(3000); // Перші 4 виклики pbkdf2 виконуються паралельно в 4 потоках // 5-й чекає в черзі - загальний час приблизно подвоюється // Рішення: UV_THREADPOOL_SIZE=8 node server.js ``` Звернись до цього endpoint і побачиш стрибок часу на 5-му завданні. Подвоєння розміру пулу вдвічі скорочує очікування. Це найпоширеніша продакшн-проблема, безпосередньо пов'язана з пулом потоків libuv. ### Просунутий: lifecycle handles і час завершення процесу ```javascript const net = require('net'); const fs = require('fs'); // Handle тримає процес живим const server = net.createServer(() => {}); server.listen(4000); // Request сам по собі не тримає процес живим fs.readFile('./data.txt', () => { console.log('файл прочитано'); }); // Без server.close() процес ніколи не завершиться: // TCP handle досі активний у event loop libuv setTimeout(() => { server.close(() => { console.log('сервер закрито - процес може завершитись'); }); }, 2000); console.log('активних handles:', process._getActiveHandles().length); // 2: сервер + таймер ``` libuv тримає `uv_run` активним поки є активні handles. Читання файлу - це request, не handle, тому воно не блокує вихід. TCP-сервер - це handle. Закрий його і loop завершиться. Розуміння різниці між handles і requests економить години при дебагінгу "чому мій скрипт завис після завершення роботи."Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.