Как решить проблему утечки памяти в Node.js приложении?

«Как решить проблему утечки памяти в Node.js приложении?» — вопрос из категории Node.js Core, который задают на 26% собеседований Node.js Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Для диагностики и устранения утечек памяти в Node.js я использую комбинацию инструментов и следующих практик:

1. Профилирование и диагностика:

// Использую встроенный модуль v8 для снимков кучи
const v8 = require('v8');
const fs = require('fs');

function takeHeapSnapshot() {
  const snapshot = v8.getHeapSnapshot();
  const fileName = `heapdump-${Date.now()}.heapsnapshot`;
  const fileStream = fs.createWriteStream(fileName);
  snapshot.pipe(fileStream);
  console.log(`Heap snapshot saved to ${fileName}`);
}

// Мониторинг в реальном времени
setInterval(() => {
  const memoryUsage = process.memoryUsage();
  console.log({
    rss: `${Math.round(memoryUsage.rss / 1024 / 1024)} MB`,
    heapTotal: `${Math.round(memoryUsage.heapTotal / 1024 / 1024)} MB`,
    heapUsed: `${Math.round(memoryUsage.heapUsed / 1024 / 1024)} MB`,
    external: `${Math.round(memoryUsage.external / 1024 / 1024)} MB`
  });
}, 10000);

2. Типичные причины утечек в Node.js:

  • Глобальные переменные: Непреднамеренное присвоение больших объектов глобальным переменным
  • Таймеры и обработчики событий: Незакрытые setInterval, setTimeout или слушатели событий
  • Замыкания: Сохранение ссылок на большие объекты в замыканиях
  • Кэши без ограничений: Кэши, которые растут бесконечно без механизма инвалидации
  • Соединения: Незакрытые соединения с базами данных, сокеты или файловые дескрипторы

3. Практические решения:

// Вместо обычного объекта для кэша использую WeakMap
const cache = new WeakMap();

// Для очистки таймеров всегда сохраняю их ID
const timerIds = new Set();

function createManagedInterval(callback, delay) {
  const id = setInterval(callback, delay);
  timerIds.add(id);
  return id;
}

function cleanupTimers() {
  timerIds.forEach(id => clearInterval(id));
  timerIds.clear();
}

// Использую потоковую обработку для больших данных
const { pipeline } = require('stream');
const { createReadStream, createWriteStream } = require('fs');

async function processLargeFile(inputPath, outputPath) {
  await pipeline(
    createReadStream(inputPath),
    // Трансформации...
    createWriteStream(outputPath)
  );
}

4. Инструменты, которые я применяю:

  • node --inspect с Chrome DevTools Memory tab
  • Clinic.js для комплексного анализа производительности
  • node --trace-gc для отслеживания сборки мусора
  • memwatch-next для обнаружения утечек

5. Профилактика:

  • Использую линтеры с правилами для обнаружения потенциальных утечек
  • Настраиваю лимиты памяти в production (--max-old-space-size)
  • Регулярно запускаю нагрузочное тестирование с профилированием памяти