Как решить проблему утечки памяти в 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)
  • Регулярно запускаю нагрузочное тестирование с профилированием памяти

Ответ 18+ 🔞

А, ну это же классика, ёпта! Слушай, как я с утечками памяти в 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);

Смотрю на цифры и думаю: «Ни хуя себе, heapUsed растёт как на дрожжах, а должен в определённых рамках болтаться». Вот тут уже подозрение ебать чувствую — где-то тут собака зарыта.

2. А причины-то, блядь, обычно одни и те же, как заезженная пластинка. Самые частые грабли, на которые все наступают:

  • Глобальные переменные: Какой-нибудь распиздяй запихнул огромный объект в глобал, и он висит там до скончания времён.
  • Таймеры и обработчики событий: setInterval запустили, а очистить забыли. Или слушатель события прицепили, а отписаться — ну не, не царское это дело. В итоге куча мусора копится.
  • Замыкания: Вот это хитрая жопа. Вроде функция маленькая, а она в замыкании тащит за собой здоровенный контекст, который уже никому не нужен.
  • Кэши без ограничений: Сделали простой кэш в виде объекта {}, он растёт, растёт, а удалять из него старьё — ну не, мы же не жадные. Пока память не кончится.
  • Соединения: Базу данных открыли, поработали, а закрыть — западло. Или файл прочитали, а дескриптор не освободили. Классика жанра.

3. Ну а теперь, как я с этим борюсь — практические приёмы. Вот смотри, вместо того чтобы делать кэш на обычном объекте, который всё помнит, я юзаю WeakMap. Он умный, сам мусор собирает, когда ссылки теряются.

// Вместо обычного объекта для кэша использую 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)
  );
}

С таймерами — дисциплина, блядь. Записал ID, потом при завершении работы всё почистил. И с потоками работаю — не гружу гигабайтный файл в память, а гоняю его чанками, как по трубе.

4. Инструменты — без них вообще никуда, ядрёна вошь.

  • Запускаю node --inspect, открываю Chrome DevTools, там вкладка Memory — и начинаю делать снапшоты. Сравниваю, что между ними появилось лишнего, кто не удалился.
  • Clinic.js — вообще огонь, он сразу показывает, где узкие места, причём в красивом графическом виде.
  • node --trace-gc включаю, смотрю, как часто сборщик мусора работает. Если он без перерыва трудится — дело плохо, память подтекает.
  • Раньше memwatch-next использовал, чтобы ловить резкие скачки потребления. Удобная штука.

5. Ну и профилактика, куда без неё.

  • В линтеры правила добавляю, чтобы ругались на потенциально опасные паттерны.
  • В продакшене обязательно лимиты памяти выставляю через --max-old-space-size, чтобы приложение не сожрало всю оперативку на сервере.
  • И периодически устраиваю нагрузочное тестирование, специально смотрю на график памяти. Если после отбоя нагрузки память не возвращается к исходному уровню — всё, пиши пропало, где-то течь. Надо искать.

В общем, чувак, тут главное — системный подход. Не ждать, когда всё еб*нется, а заранее смотреть, мониторить и приучать себя к чистоте кода. А то потом разгребать — волнение ебать, и терпения ноль ебать.