Перейти к содержанию
  • Лента
  • Категории
  • Последние
  • Метки
  • Популярные
  • Пользователи
  • Группы
Свернуть
exlends
Категории
  1. Главная
  2. Категории
  3. Бекенд, разработка серверов
  4. WebSocket в Node.js жрёт память: утечки EventEmitter

WebSocket в Node.js жрёт память: утечки EventEmitter

Запланировано Прикреплена Закрыта Перенесена Бекенд, разработка серверов
websocketeventemitterутечки памяти
1 Сообщения 1 Постеры 3 Просмотры
  • Сначала старые
  • Сначала новые
  • По количеству голосов
Ответить
  • Ответить, создав новую тему
Авторизуйтесь, чтобы ответить
Эта тема была удалена. Только пользователи с правом управления темами могут её видеть.
  • hannadevH Не в сети
    hannadevH Не в сети
    hannadev
    написал отредактировано
    #1

    WebSocket-соединения в Node.js — это мина под performance вашего сервера. Казалось бы, простая подписка на события, переподключение при падении соединения, обработка данных. Но где-то между client.on('error') и рекурсивным вызовом функции подключения память начинает расти как на дрожжах. За пару часов работы 90% оперативки занято, приложение тупит, а в логах весело мигает предупреждение про EventEmitter.

    Проблема не в самом Node.js и даже не в WebSocket. Проблема в том, как мы обрабатываем события. EventEmitter держит все подписчики в памяти, пока они не удалены явно. И если вы забыли про removeAllListeners() в нужном месте — поздравляем, у вас классическая утечка памяти. Разберёмся, почему это происходит и как это вообще предотвратить.

    Как EventEmitter тихо душит вашу память

    На первый взгляд код выглядит безобидно. Есть функция переподключения, которая срабатывает каждую секунду, если соединение упало. Тайм-аут, снова попытка, и так по кругу. Но вот что происходит под капотом: каждый раз, когда вы вызываете client.on('error', callback), вы добавляете новый обработчик в внутренний массив слушателей объекта client.

    Этот массив живёт столько же, сколько живёт сам объект. И если объект долго живущий (например, socket, который переиспользуется для переподключений), то все эти обработчики накапливаются в памяти как снежный ком. Первая итерация — один обработчик, вторая — два, третья — три. Через час таких переподключений у вас уже тысячи мёртвых обработчиков.

    Вот типичный сценарий:

    var net = require('net');
    const client = new net.Socket();
    
    let _connect_ = function () {
      client.connect(port, host, function (err) {
        if (err) {
          console.log("error");
          console.log(err);
        } else {
          console.log("connected");
        }
      });
      
      client.on('error', function () {
        console.log("connection error");
        setTimeout(function(){_connect_();}, 1000);
      });
    };
    
    _connect_();
    

    Выглядит нормально, не так ли? Но нет. Каждый вызов _connect_() добавляет новый обработчик 'error'. Если сервер падает часто, обработчиков станет столько же, сколько попыток переподключения. И память будет расти линейно, пока не станет критичной.

    Проблема здесь двойная: во-первых, обработчики накапливаются. Во-вторых, если вы добавили ещё и обработчики на 'data', 'close', 'timeout' — их количество растёт кратно. Пять событий по тысяче обработчиков каждое — это уже 5000 функций в памяти, которые никогда не будут вызваны.

    Когда Node.js начинает кричать о проблеме

    Node.js имеет встроенный детектор утечек памяти через EventEmitter. По умолчанию максимальное количество обработчиков на один объект — 10. Как только вы превышаете этот лимит, система выплёвывает вот такое предупреждение:

    (node) warning: possible EventEmitter memory leak detected. 11 listeners added.
    Use emitter.setMaxListeners() to increase limit.
    

    Звучит как помощь, но это ловушка. Новички видят это предупреждение, гуглят и находят совет: просто вызови setMaxListeners() и забей. Вот так:

    client.setMaxListeners(100);
    

    Ой, проблема решена! Предупреждение исчезло. Но утечка-то остаётся. Вы просто заткнули рот детектору, но инструмент продолжает жрать память так же интенсивно. Это как отключить датчик масла в двигателе машины — проблема не исчезла, просто вы её не видите.

    Предупреждение — это сигнал, что что-то не так с вашей архитектурой. А не сигнал к тому, чтобы увеличить лимит:

    • Предупреждение означает, что вы привязали коротко живущие объекты к долго живущим.
    • Это признак того, что обработчики где-то не удаляются после использования.
    • Это подсказка, что нужно переписать логику переподключения или управления lifecycle’ом.

    Как правильно управлять переподключениями

    Решение простое — очищайте слушателей перед добавлением новых. Используйте removeAllListeners() перед тем, как снова вызывать on():

    var net = require('net');
    const client = new net.Socket();
    
    let _connect_ = function () {
      client.connect(port, host, function (err) {
        if (err) {
          console.log("error");
          console.log(err);
        } else {
          console.log("connected");
        }
      });
      
      client.on('error', function () {
        console.log("connection error");
        reconnect();
      });
    };
    
    function reconnect() {
      client.removeAllListeners();
      client.on('error', function () {
        console.log("connection error");
      });
      setTimeout(function(){_connect_();}, 1000);
    }
    
    _connect_();
    

    Теперь каждый раз, когда вы переподключаетесь, все старые обработчики удаляются. Масив слушателей очищается, и память не растёт бесконечно.

    Лучший подход — минимизировать переиспользование объектов. Вот альтернатива:

    • Создавайте новый socket для каждого переподключения вместо переиспользования старого.
    • Явно закрывайте соединение перед новой попыткой: client.destroy().
    • Используйте паттерн с Promise или async/await, где lifetime обработчиков ограничен одной операцией.
    async function connectWithRetry(host, port, maxRetries = 5) {
      for (let i = 0; i < maxRetries; i++) {
        try {
          return await new Promise((resolve, reject) => {
            const socket = new net.Socket();
            
            const timeout = setTimeout(() => {
              socket.destroy();
              reject(new Error('Connection timeout'));
            }, 5000);
            
            socket.once('connect', () => {
              clearTimeout(timeout);
              resolve(socket);
            });
            
            socket.once('error', reject);
            socket.connect(port, host);
          });
        } catch (err) {
          if (i === maxRetries - 1) throw err;
          await new Promise(r => setTimeout(r, 1000));
        }
      }
    }
    

    Здесь каждая попытка создаёт новый socket и использует once() вместо on(). Это автоматически удаляет обработчик после первого срабатывания. Нет накопления, нет утечек.

    Что ещё создаёт скрытые утечки

    EventEmitter-утечки — это не единственная проблема. Есть и другие подводные камни:

    Забытые таймауты и интервалы. Если вы создаёте setInterval() или setTimeout() в обработчике события, но забываете их очищать, они тоже накапливаются в памяти. Каждый таймер — это ссылка на функцию, которая удерживает scope в памяти.

    Замыкания с большими объектами. Если ваш обработчик события сохраняет в замыкании большой объект (например, весь запрос или весь буфер данных), этот объект не будет освобождён, пока жив обработчик.

    Неудалённые слушатели в цепи. Если вы привязываете слушателей к слушателям (слушатель добавляет нового слушателя), и это происходит во вложенном коде, получится пирамида утечек.

    Socket-ы без явного закрытия. Если не вызвать socket.destroy() или socket.end() при обработке timeout-события, сокет останется в памяти в полу-мёртвом состоянии.

    Как отследить эти утечки? Есть несколько способов:

    • Heapdump. Модуль, который делает снимок памяти Node.js и позволяет анализировать его в Chrome DevTools. Можно сравнить два снимка и увидеть, какие объекты растут.
    • Node.js --inspect. Встроенный инструмент для профилирования. Запустите приложение с флагом --inspect и подключитесь к inspector’у.
    • Простой мониторинг. Выводите process.memoryUsage() каждые 5 секунд. Если числа растут монотонно и никогда не падают — утечка.

    За пределами EventEmitter

    Меморная оптимизация WebSocket-соединений в Node.js — это целая область. Вы можете глубже упасть в кроличью нору и наткнуться на проблемы типа утечек в буферах данных, неправильного управления потоками (streams), или неудачной компиляции V8. Но если вы начали с проблемы расхода памяти — 90% вероятности, что виноват именно EventEmitter и забытые обработчики.

    Суть простая: каждый on() должен иметь парный removeListener() или removeAllListeners(), или вы должны использовать once() вместо on(). Это базовый принцип работы с событиями в Node.js, и его неуважение ведёт к той самой ситуации, когда сервер жрёт 90% памяти и падает через час работы.

    1 ответ Последний ответ
    0

    Здравствуйте! Похоже, вас заинтересовала эта беседа, но у вас ещё нет аккаунта.

    Надоело каждый раз пролистывать одни и те же посты? Зарегистрировав аккаунт, вы всегда будете возвращаться на ту же страницу, где были раньше, и сможете выбирать, получать ли уведомления о новых ответах (по электронной почте или в виде push-уведомлений). Вы также сможете сохранять закладки и ставить лайки постам, чтобы выразить свою благодарность другим участникам сообщества.

    С вашими комментариями этот пост мог бы стать ещё лучше 💗

    Зарегистрироваться Войти

    Категории

    • Главная
    • Новости
    • Фронтенд
    • Бекенд
    • Языки программирования

    Контакты

    • Сотрудничество
    • info@exlends.com

    © 2024 - 2026 ExLends, Inc. Все права защищены.

    Политика конфиденциальности
    • Войти

    • Нет учётной записи? Зарегистрироваться

    • Войдите или зарегистрируйтесь для поиска.
    • Первое сообщение
      Последнее сообщение
    0
    • Лента
    • Категории
    • Последние
    • Метки
    • Популярные
    • Пользователи
    • Группы