Перейти до основного вмісту

Event Loop в Node.js

Event Loop в Node.js: архітектура, фази, черги

Event Loop — це серце асинхронності в Node.js. Якщо ви знайомі з Java, то знаєте про багатопотоковість та Thread Pool. Node.js працює інакше: замість створення окремих потоків для кожного запиту, він використовує єдиний потік і Event Loop для обробки всіх операцій.

Порівняння з Java/Spring Boot

Java/Spring Boot (традиційна модель):

  • Кожен HTTP-запит обробляється окремим потоком з Thread Pool
  • Якщо потік блокується на I/O операції (база даних, файл), він чекає
  • Обмежена кількість потоків (наприклад, 200) = максимум 200 одночасних запитів

Node.js (асинхронна модель):

  • Один основний потік (Event Loop)
  • I/O операції делегуються системі (libuv), потік не блокується
  • Може обробляти тисячі одночасних підключень на одному потоці
// Java - блокуючий виклик
String data = readFile("file.txt"); // потік чекає
System.out.println(data);

// Node.js - неблокуючий виклик
fs.readFile('file.txt', (err, data) => {
  console.log(data); // виконається пізніше
});
console.log('Продовжую роботу'); // виконається одразу

Архітектура Event Loop

Event Loop в Node.js базується на бібліотеці libuv, яка надає асинхронні I/O операції для різних операційних систем.

Основні компоненти:

  • Call Stack — стек викликів JavaScript (як у Java)
  • Node APIs — нативні асинхронні API (fs, http, timers)
  • Event Loop — механізм, що координує виконання коду
  • Callback Queue — черга для callback'ів
  • Microtask Queue — пріоритетна черга для Promise

Фази Event Loop

Event Loop працює циклічно, проходячи через шість фаз у порядку:

   ┌───────────────────────────┐
┌─>│           timers          │ <- setTimeout, setInterval
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │     pending callbacks     │ <- I/O callbacks (TCP errors)
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │       idle, prepare       │ <- внутрішні операції
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │           poll            │ <- нові I/O події, виконання I/O callbacks
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │           check           │ <- setImmediate callbacks
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
└──┤      close callbacks      │ <- socket.on('close', ...)
   └───────────────────────────┘

1. Фаза Timers

Виконує callback'и, заплановані через setTimeout() та setInterval().

console.log('1. Старт');

setTimeout(() => {
  console.log('3. Timeout виконано');
}, 0);

console.log('2. Синхронний код');

// Вивід:
// 1. Старт
// 2. Синхронний код
// 3. Timeout виконано

Аналогія з Java: схоже на ScheduledExecutorService.schedule(), але без окремого потоку.

2. Фаза Poll

Найважливіша фаза. Тут Event Loop:

  • Отримує нові I/O події
  • Виконує I/O-related callbacks (крім close, timers, setImmediate)
  • Може блокуватися в очікуванні нових подій
const fs = require('fs');

fs.readFile('data.txt', (err, data) => {
  console.log('Файл прочитано'); // Poll phase
});

3. Фаза Check

Виконує callback'и setImmediate(). Ця функція унікальна для Node.js.

setImmediate(() => {
  console.log('Immediate');
});

setTimeout(() => {
  console.log('Timeout');
}, 0);

// Порядок може відрізнятися!
// У I/O циклі setImmediate завжди виконується раніше

Microtasks vs Macrotasks

Це ключова концепція для розуміння пріоритетів виконання.

Macrotasks (Task Queue):

  • setTimeout / setInterval
  • setImmediate
  • I/O операції

Microtasks (Microtask Queue):

  • Promise.then / catch / finally
  • process.nextTick (найвищий пріоритет!)
  • queueMicrotask

Правило виконання: після кожної macrotask Event Loop виконує ВСІ microtasks перед наступною macrotask.

console.log('1. Синхронний');

setTimeout(() => {
  console.log('5. setTimeout (macrotask)');
}, 0);

Promise.resolve().then(() => {
  console.log('3. Promise (microtask)');
});

process.nextTick(() => {
  console.log('2. nextTick (найвищий пріоритет)');
});

setImmediate(() => {
  console.log('6. setImmediate (macrotask)');
});

Promise.resolve().then(() => {
  console.log('4. Promise 2 (microtask)');
});

// Вивід:
// 1. Синхронний
// 2. nextTick (найвищий пріоритет)
// 3. Promise (microtask)
// 4. Promise 2 (microtask)
// 5. setTimeout (macrotask)
// 6. setImmediate (macrotask)

process.nextTick — особливий випадок

process.nextTick() має найвищий пріоритет і виконується ДО microtasks.

Promise.resolve().then(() => console.log('Promise'));
process.nextTick(() => console.log('nextTick'));

// Вивід:
// nextTick
// Promise

Увага: надмірне використання process.nextTick() може заблокувати Event Loop!

// ПОГАНО - блокує Event Loop
function recursiveNextTick() {
  process.nextTick(recursiveNextTick);
}
recursiveNextTick(); // Event Loop ніколи не перейде до наступної фази

Практичний приклад з HTTP сервером

const http = require('http');
const fs = require('fs');

const server = http.createServer((req, res) => {
  console.log('1. Запит отримано (Poll phase)');
  
  // Microtask
  Promise.resolve().then(() => {
    console.log('2. Promise - microtask');
  });
  
  // Macrotask
  setTimeout(() => {
    console.log('4. setTimeout - macrotask');
  }, 0);
  
  // process.nextTick
  process.nextTick(() => {
    console.log('3. nextTick - найвищий пріоритет');
  });
  
  // Асинхронна I/O операція
  fs.readFile('data.txt', (err, data) => {
    console.log('5. Файл прочитано (Poll phase, наступна ітерація)');
    res.end(data);
  });
});

server.listen(3000);

Порівняння з Spring Boot @Async

У Spring Boot для асинхронності використовується анотація @Async:

// Spring Boot - окремий потік з ThreadPoolTaskExecutor
@Async
public CompletableFuture<String> processAsync() {
    // Виконується в окремому потоці
    return CompletableFuture.completedFuture("Done");
}

// Node.js - той самий потік, але неблокуюча операція
async function processAsync() {
    // Event Loop делегує I/O, продовжує інші задачі
    const result = await someAsyncOperation();
    return result;
}

Візуалізація черг

┌─────────────────────────────────────────┐
│          Call Stack (основний потік)    │
└─────────────────────────────────────────┘
                    ↓
┌─────────────────────────────────────────┐
│   process.nextTick Queue (пріоритет 1)  │ ← виконується завжди першою
└─────────────────────────────────────────┘
                    ↓
┌─────────────────────────────────────────┐
│   Microtask Queue (пріоритет 2)         │ ← Promise.then/catch
└─────────────────────────────────────────┘
                    ↓
┌─────────────────────────────────────────┐
│   Macrotask Queue (пріоритет 3)         │ ← setTimeout, setImmediate, I/O
└─────────────────────────────────────────┘

Поширені помилки

1. Блокування Event Loop синхронним кодом:

// ПОГАНО - блокує Event Loop
const data = fs.readFileSync('large-file.txt'); // синхронна операція

// ДОБРЕ - не блокує Event Loop
fs.readFile('large-file.txt', (err, data) => {
  // асинхронна операція
});

2. Нерозуміння порядку виконання:

// Код
setTimeout(() => console.log('timeout'), 0);
Promise.resolve().then(() => console.log('promise'));

// Багато хто думає: timeout, promise
// Насправді: promise, timeout (microtask має пріоритет!)

Коли використовувати що

Механізм Використання
setTimeout / setInterval Затримка виконання, періодичні задачі
setImmediate Виконання після завершення поточної фази Poll
process.nextTick Критично важливі операції, що мають виконатися негайно (використовуйте обережно!)
Promise Асинхронні операції, що повертають результат

Практичні рекомендації

  • Уникайте синхронних операцій у продакшені (readFileSync, execSync)
  • Використовуйте Promise/async-await замість callback'ів
  • Обмежуйте process.nextTick — він може заблокувати Event Loop
  • Для CPU-intensive задач використовуйте Worker Threads або винесіть в окремий сервіс
  • Моніторте Event Loop lag за допомогою бібліотек як blocked або event-loop-lag

Висновки

Event Loop — це те, що робить Node.js ефективним для I/O-intensive додатків. Розуміння його роботи критично важливе для написання продуктивного коду.

Ключові відмінності від Java:

  • Node.js: один потік + асинхронність = висока пропускна здатність для I/O
  • Java: багато потоків + блокування = простіша модель, але менша ефективність для I/O
  • Node.js погано підходить для CPU-intensive задач (криптографія, обробка зображень)
  • Java/Spring Boot краще для складної бізнес-логіки з багатопотоковістю

Коментарі

Популярні публікації

Встановлення Visual Studio Code на Ubuntu

Visual Studio Code (VS Code) — це потужний, легкий та кросплатформний редактор коду, який став стандартом для розробників завдяки універсальності та швидкості роботи. Він підтримує Windows, Linux і macOS, запускається швидше за повноцінні IDE, але водночас може перетворюватися на повнофункціональне середовище розробки завдяки тисячам розширень. VS Code має продуману архітектуру, інтуїтивний інтерфейс, вбудований термінал та інтелектуальну систему автодоповнення на базі Language Server Protocol, що забезпечує високоякісну підтримку синтаксису для багатьох мов програмування. Серед ключових можливостей — розумне автодоповнення коду (IntelliSense), навігація між класами, символами та файлами, рефакторинг, форматування коду, інтеграція з Git, підтримка віддаленої розробки через SSH або контейнерні середовища (Dev Containers). Завдяки відлагоджувачу (debugger), який налаштовується під будь-яку мову, розробник може запускати код у контрольованому середовищі, ставити брейкпоїнти, переглядат...

Керуючі конструкції в Elixir

Elixir пропонує різноманітні керуючі конструкції для управління потоком виконання програми. На відміну від імперативних мов, всі конструкції в Elixir повертають значення, що робить код більш виразним. if та else Конструкція if перевіряє умову і виконує код відповідно до результату: # Базовий синтаксис if true do "Це буде виконано" else "Це не буде виконано" end # Однорядковий варіант if connected?, do: "Підключено", else: "Відключено" # Присвоєння результату message = if age >= 18 do "Ви повнолітній" else "Ви неповнолітній" end # Без else (повертає nil) if user_logged_in? do show_dashboard() end Примітка: В Elixir тільки false і nil вважаються хибними. Всі інші значення (включаючи 0, "", []) вважаються істинними. unless Конструкція unless — це протилежність if, виконується коли умова хибна: # ...

Шпаргалка по базових командах PostgreSQL

1. Підключення до PostgreSQL через командний рядок: psql -h <host> -p <port> -U <username> -d <database> 2. Підключення до бази без параметрів (якщо користувач і база мають однакове ім’я): psql 3. Показати список усіх баз даних: \l 4. Підключитися до іншої бази даних: \c <database_name> 5. Показати список таблиць у поточній базі: \dt 6. Показати всі об'єкти (таблиці, індекси, секвенції): \d 7. Показати таблиці з усіх схем: \dt *.* 8. Переглянути структуру конкретної таблиці: \d <table_name> 9. Виконати SQL-запит (приклад): SELECT * FROM users; 10. Вийти з psql: \q 11. Створити нову базу даних: CREATE DATABASE mydb; 12. Створити нову таблицю: CREATE TABLE users ( id SERIAL PRIMARY KEY, name TEXT NOT NULL, email TEXT UNIQUE ); 13. Додати новий запис: INSERT INTO users (name, email) VALUES ('Іван', 'ivan@example.com'); 14. Оновити дані в таблиці: UPDATE users SET name = 'Петро' WH...

Angular CLI

CLI (command-line interface) – інтерфейс командного рядка. Перед початком роботи має бути встановлений Node.js Встановлення: npm install -g @angular/cli Отримання допомоги: ng help Буде приблизно такий результат: add Adds support for an external library to your project. analytics Configures the gathering of Angular CLI usage metrics. See https://angular.io/cli/usage-analytics-gathering. build (b) Compiles an Angular app into an output directory named dist/ at the given output path. Must be executed from within a workspace directory. deploy Invokes the deploy builder for a specified project or for the default project in the workspace. config Retrieves or sets Angular configuration values in the angular.json file for the workspace. doc (d) Opens the official Angular documentation (angular.io) in a browser, and searches for a given keyword. e2e (e) Builds and serves an Angular app, then runs end-to-end tests. extract-i18n (i18n-extract, xi18n) Extracts i18n mes...

Дамп бази даних Postgresql локально

Що потрібно встановити локально (Ubuntu) Встанови клієнтські утиліти PostgreSQL: pg_dump , psql , pg_restore . sudo apt update sudo apt install postgresql-client Кроки міграції Ідея: не зберігати дамп на серверах. Зробити дамп з віддаленого сервера на свій локальний ПК, потім передати його на новий сервер і там відновити. 1) Створення дампу з віддаленого сервера локально pg_dump -h OLD_SERVER_IP -U OLD_USER -Fc -d OLD_DB -f dump_file.dump Пояснення ключів: -h (хост), -U (користувач), -Fc (custom format), -d (БД), -f (вихідний файл локально). 1.1) Варіант з розбиттям на частини (якщо треба) pg_dump -h OLD_SERVER_IP -U OLD_USER -Fc -d OLD_DB | split -b 10G - dump_file.dump.part_ Це створить локально файли типу dump_file.dump.part_aa , dump_file.dump.part_ab тощо. 2) Передача дампу на новий сервер Один файл: scp dump_file.dump NEW_USER@NEW_SERVER_IP:/tmp/ Або частинами: scp dump_file.dump.part_* NEW_USER@NEW_SERVER_IP:/tmp/ 3) Збирання файл...