Пошук уроків, статей та іншого контенту
Налаштуйте ліміти ресурсів, визначте реакцію Node.js на нестачу пам’яті та CPU й запобігайте деградації сервісу.
Node.js-сервіс може деградувати задовго до повної відмови:
необмежене зростання пам’яті призводить до частих циклів GC, а потім до Out Of Memory;
надмірне споживання CPU збільшує затримки обробки запитів;
блокування основного потоку зупиняє обробку таймерів, мережевих подій і нових запитів;
один процес може використати всі ресурси контейнера та вплинути на інші процеси.
Обмеження потрібно налаштовувати на двох рівнях:
у Node.js — для контролю купи V8 і спостереження за станом процесу;
на рівні контейнера або оркестратора — для жорсткого обмеження всієї пам’яті та CPU.
Обмеження Node.js не замінюють ліміти контейнера. І навпаки, контейнерні ліміти не пояснюють, яка саме частина програми споживає пам’ять.
Пам’ять процесу складається не лише з JavaScript-об’єктів у купі V8. Важливі категорії:
heapUsed — пам’ять, яку використовують JavaScript-об’єкти;
heapTotal — пам’ять, виділена V8 під купу;
external — пам’ять за межами купи V8, пов’язана з JavaScript-об’єктами;
ArrayBufferBufferrss — загальний обсяг пам’яті процесу в RAM.
Buffer особливо важливий: його вміст не належить до звичайної JavaScript-купи, але все одно збільшує споживання пам’яті процесом.
Для обмеження старої області купи використовується параметр --max-old-space-size. Значення задається в мегабайтах:
node --max-old-space-size=1536 server.jsУ цьому прикладі V8 може використовувати приблизно до 1536 MiB для old space. Це не є лімітом усієї пам’яті процесу. Поза ним залишаються:
інші області V8;
нативні структури Node.js;
Buffer та ArrayBuffer;
пам’ять модулів;
стек потоків;
службові витрати операційної системи.
Параметр можна передати через NODE_OPTIONS:
NODE_OPTIONS="--max-old-space-size=1536" node server.jsНе слід встановлювати значення, рівне всьому контейнерному ліміту. Процесу потрібен запас для пам’яті поза old space. Точне значення визначають навантажувальним тестуванням і вимірюваннями.
Коли купа наближається до ліміту, V8 частіше запускає збірку сміття. Це може спричинити:
зростання затримок;
зменшення пропускної здатності;
тимчасове блокування основного потоку.
Якщо звільнити достатньо пам’яті не вдається, процес зазвичай завершується з помилкою на кшталт:
FATAL ERROR: Ineffective mark-compacts near heap limit
Allocation failed - JavaScript heap out of memoryЦе не помилка, яку надійно можна перехопити та продовжити роботу. Після досягнення критичного стану процес слід завершити, а його перезапуск має виконувати зовнішній менеджер процесів або оркестратор.
Якщо загальне споживання пам’яті перевищує ліміт контейнера, процес може бути завершений операційною системою з боку OOM killer. У такому випадку JavaScript-код не отримує можливості виконати очищення або graceful shutdown.
Для базової діагностики використовуйте process.memoryUsage():
import http from 'node:http';
import { monitorEventLoopDelay } from 'node:perf_hooks';
const port = Number(process.env.PORT ?? 3000);
const eventLoopDelay = monitorEventLoopDelay({ resolution: 20 });
eventLoopDelay.enable();
const server = http.createServer((request, response) => {
if (request.url === '/health') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(JSON.stringify({ ok: true }));
return;
}
response.writeHead(404);
response.end('Not found');
});
server.listen(port, () => {
console.log(`Server is listening on port ${port}`);
});
const metricsInterval = setInterval(() => {
const memory = process.memoryUsage();
const cpu = process.cpuUsage();
console.log({
memoryMiB: {
rss: Math.round(memory.rss / 1024 / 1024),
heapTotal: Math.round(memory.heapTotal / 1024 / 1024),
heapUsed: Math.round(memory.heapUsed / 1024 / 1024),
external: Math.round(memory.external / 1024 / 1024),
arrayBuffers: Math.round(memory.arrayBuffers / 1024 / 1024),
},
eventLoopDelayMs: Number((eventLoopDelay.mean / 1e6).toFixed(2)),
eventLoopMaxDelayMs: Number((eventLoopDelay.max / 1e6).toFixed(2)),
cpuTimeMs: {
user: Math.round(cpu.user / 1000),
system: Math.round(cpu.system / 1000),
},
});
eventLoopDelay.reset();
}, 10_000);
function shutdown(signal) {
console.log(`Received ${signal}, stopping server`);
clearInterval(metricsInterval);
eventLoopDelay.disable();
server.close((error) => {
if (error) {
console.error(error);
process.exitCode = 1;
}
process.exit();
});
setTimeout(() => {
console.error('Graceful shutdown timeout');
process.exit(1);
}, 10_000).unref();
}
process.once('SIGTERM', () => shutdown('SIGTERM'));
process.once('SIGINT', () => shutdown('SIGINT'));Запустіть приклад із лімітом купи:
node --max-old-space-size=256 server.jsheapUsed постійно зростає між циклами GC — можлива витік пам’яті;
heapUsed коливається, а rss зростає — проблема може бути в Buffer, нативному модулі або фрагментації;
високий eventLoopMaxDelayMs означає, що основний потік надовго блокується;
велике rss при помірному heapUsed означає, що аналізувати лише купу V8 недостатньо.
Разове значення не є доказом витоку. Спостерігайте за трендом після кількох циклів навантаження та збору сміття.
Контейнерний ліміт задає верхню межу для всього процесу, а не лише для JavaScript-купи.
Приклад запуску Docker-контейнера:
docker run --rm \
--memory=512m \
--cpus=1.0 \
-p 3000:3000 \
-e NODE_OPTIONS="--max-old-space-size=320" \
my-node-serviceУ цьому прикладі:
контейнер має жорсткий ліміт пам’яті 512 MiB;
контейнер може використовувати не більше одного CPU;
V8 має нижчий ліміт old space — 320 MiB;
решта пам’яті залишається для Buffer, нативних структур Node.js, бібліотек і системних витрат.
Значення 320 MiB не є універсальним. Якщо сервіс активно працює з файлами, потоками, зображеннями або великими буферами, запас має бути більшим.
Ліміт пам’яті контейнера може призвести до двох різних сценаріїв:
V8 раніше досягає власного ліміту та завершує процес із повідомленням про heap out of memory.
Загальний RSS перевищує контейнерний ліміт, і процес завершується OOM killer без контрольованого завершення.
Тому в журналах потрібно розрізняти помилку V8, завершення сигналом і завершення контейнера через OOM.
Node.js не має загального прапорця, який обмежує CPU конкретного JavaScript-процесу так само, як --max-old-space-size обмежує купу. CPU зазвичай обмежують середовищем виконання:
параметром контейнера --cpus;
CPU-лімітом у системі оркестрації;
квотами операційної системи.
CPU-ліміт не спричиняє помилки JavaScript. Якщо процес хоче використовувати більше CPU, операційна система просто обмежує його час виконання. Наслідки:
запити завершуються повільніше;
таймери спрацьовують із затримкою;
збільшується черга очікування;
зростає затримка event loop;
за нестачі CPU health check може почати помилково вважати сервіс нездоровим.
Node.js обробляє JavaScript-код на основному потоці. Синхронний CPU-інтенсивний код блокує цей потік:
function calculateChecksum(iterations) {
let result = 0;
for (let index = 0; index < iterations; index += 1) {
result = (result + index * 31) % 1_000_000_007;
}
return result;
}
calculateChecksum(500_000_000);Поки виконується цей цикл, процес не може нормально обробляти інші події на тому самому потоці. Обмеження CPU контейнера може посилити проблему, але не усуває її.
Для виявлення деградації контролюйте:
затримку event loop;
час відповіді запитів;
кількість запитів у черзі;
фактичне CPU-споживання процесу;
кількість перезапусків.
monitorEventLoopDelay() вимірює затримку між моментом, коли подія мала бути оброблена, і фактичним виконанням. Це корисніший сигнал для Node.js-сервісу, ніж лише відсоток CPU: процес може мати високі затримки через блокування event loop навіть без повного завантаження всіх CPU.
Не встановлюйте --max-old-space-size на рівні контейнерного ліміту. Приблизна схема:
визначте максимальний RSS під реальним навантаженням;
визначте частку heapUsed, external і arrayBuffers;
залиште запас для пікових алокацій і нативної пам’яті;
встановіть контейнерний ліміт вище робочого піка;
встановіть ліміт old space нижче контейнерного ліміту.
Це лише початкова конфігурація. Її потрібно перевірити навантажувальним тестом.
Обробник uncaughtException не є способом відновити процес після нестачі пам’яті. У критичному стані:
стан процесу може бути непередбачуваним;
нова спроба логування або очищення може потребувати додаткової пам’яті;
продовження роботи може призвести до пошкоджених результатів або повторного падіння.
Надійніша стратегія:
не допускати неконтрольованого зростання буферів;
обмежувати розмір вхідних даних;
завершувати нездоровий процес;
запускати новий екземпляр зовнішнім менеджером.
Оркестратор зазвичай надсилає SIGTERM перед зупинкою контейнера. Обробник сигналу має:
припинити приймати нові з’єднання;
завершити поточні операції в обмежений час;
закрити сервер;
вийти з процесу після тайм-ауту.
Це допомагає під час планового перезапуску або перевищення політики здоров’я, але не гарантує виконання коду при OOM killer.
Встановіть контейнерні ліміти пам’яті та CPU.
Встановіть --max-old-space-size нижче ліміту пам’яті контейнера.
Додайте метрики rss, heapUsed, external, arrayBuffers.
Додайте вимірювання затримки event loop.
Перевірте сервіс під навантаженням, близьким до production.
Перевірте поведінку під час перевищення пам’яті та CPU.
Налаштуйте автоматичний перезапуск і обмежений graceful shutdown.
Аналізуйте не лише середні значення, а й піки та довготривалі тренди.
heapUsed усією пам’яттю процесуheapUsed не враховує значну частину пам’яті Node.js. Для контейнерних лімітів орієнтуйтеся на rss і додатково аналізуйте external та arrayBuffers.
Це залишає недостатньо пам’яті для нативних структур, буферів і службових витрат. У результаті контейнер може бути завершений OOM killer.
CPU-ліміт не генерує виняток. Він збільшує час очікування та затримки. Для виявлення проблеми потрібні метрики latency та event loop delay.
Часті цикли GC можуть означати, що сервіс працює на межі heap-ліміту. Навіть якщо пам’ять звільняється, продуктивність уже може бути неприйнятною.
Збільшення --max-old-space-size лише відкладає падіння та може збільшити час GC. Спочатку перевірте тренд heapUsed, життєвий цикл кешів, черг і об’єктів, що утримуються посиланнями.
Великі завантаження або черги Buffer можуть збільшувати RSS, не наближаючи heapUsed до очевидного ліміту. Встановлюйте максимальний розмір вхідних даних і контролюйте загальний обсяг буферів.
--max-old-space-size обмежує лише old space V8, а не всю пам’ять процесу.
Для оцінювання пам’яті потрібно контролювати rss, heapUsed, external та arrayBuffers.
При досягненні heap-ліміту V8 може завершити процес із JavaScript heap out of memory.
При перевищенні контейнерного ліміту процес може бути примусово завершений OOM killer.
CPU обмежують на рівні контейнера або оркестратора; Node.js не викидає виняток через CPU throttling.
Затримка event loop — ключовий сигнал блокування основного потоку.
Ліміти потрібно встановлювати із запасом і перевіряти під реальним навантаженням.
Відновлення після критичної нестачі ресурсів має забезпечувати зовнішній механізм перезапуску, а не сам процес Node.js.