Як виявити та запобігти витокам пам'яті в Node.js?
Витоки пам'яті в Node.js
Витік пам'яті виникає, коли додаток виділяє пам'ять, але не звільняє її, коли вона більше не потрібна. З часом споживання пам'яті зростає, поки процес не зламається з помилкою недостатньо пам'яті.
Загальні причини витоків пам'яті
1. Глобальні змінні та кеші без обмежень
// ❌ Зростає безкінечно
const cache = {};
app.get('/data/:id', (req, res) => {
cache[req.params.id] = fetchExpensiveData(req.params.id);
res.json(cache[req.params.id]);
});Виправлення: Використовуйте LRU кеш з максимальним розміром:
// ✅ Обмежений кеш
const LRU = require('lru-cache');
const cache = new LRU({ max: 500, ttl: 1000 * 60 * 5 });2. Не видалені обробники подій
// ❌ Новий обробник додається на кожен запит
app.get('/stream', (req, res) => {
process.on('data', handler); // Ніколи не видаляється!
});Виправлення: Завжди видаляйте обробники:
// ✅ Видалити при відключенні
req.on('close', () => {
process.removeListener('data', handler);
});3. Замикання, що утримують великі об'єкти
function processData() {
const hugeArray = new Array(1e6).fill('data');
// ❌ Замикання утримує hugeArray, хоча потрібна лише довжина
return function getLength() {
return hugeArray.length;
};
}Виправлення: Витягніть лише те, що потрібно:
function processData() {
const hugeArray = new Array(1e6).fill('data');
const length = hugeArray.length;
// hugeArray тепер можна зібрати сміття
return function getLength() {
return length;
};
}4. Забуті таймери та інтервали
// ❌ Інтервал ніколи не очищається
setInterval(() => {
doSomething();
}, 1000);Виправлення: Зберігайте та очищайте посилання:
const interval = setInterval(() => doSomething(), 1000);
// Коли закінчено:
clearInterval(interval);5. Непосилені потоки
Не споживання читабельних потоків призводить до накопичення буферизованих даних у пам'яті.
Виявлення витоків пам'яті
Використання process.memoryUsage()
setInterval(() => {
const mem = process.memoryUsage();
console.log({
rss: `${Math.round(mem.rss / 1024 / 1024)} MB`,
heapUsed: `${Math.round(mem.heapUsed / 1024 / 1024)} MB`,
heapTotal: `${Math.round(mem.heapTotal / 1024 / 1024)} MB`,
external: `${Math.round(mem.external / 1024 / 1024)} MB`
});
}, 5000);Використання --inspect з Chrome DevTools
node --inspect server.jsВідкрийте chrome://inspect → Зробіть снімки купи → Порівняйте їх з часом.
Використання clinic.js
npx clinic doctor -- node server.js
npx clinic heapprofiler -- node server.jsВикористання --max-old-space-size
# Обмежте купу до 512MB, щоб швидше виявляти витоки в розробці
node --max-old-space-size=512 server.jsКонтрольний список запобігання
| Практика | Опис |
|---|---|
| Використовуйте обмежені кеші | LRU з максимальним розміром та TTL |
| Видаляйте обробники подій | Завжди .removeListener() або .off() |
| Уникайте глобального стану | Обмежте дані життєвим циклом запиту |
| Очищайте таймери | clearInterval, clearTimeout |
| Споживайте потоки | Завжди підключайте або читайте потоки |
| Моніторте пам'ять | Відстежуйте process.memoryUsage() у виробництві |
| Використовуйте WeakMap/WeakRef | Для кешів об'єктів, які не повинні заважати GC |
Порада: У виробництві використовуйте інструменти моніторингу, такі як Prometheus + Grafana, Datadog або New Relic, щоб відстежувати тенденції пам'яті та налаштовувати сповіщення перед аваріями OOM.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.