Добрый день, уважаемые хабровчане
Вопрос для людей, глубоко разбирающихся в технологиях и внутренностях ноды по управлению памятью. Процесс отъедает неоправданно много памяти, при этом не течёт



Есть node.js приложение, которое принимает на вход (tcp или unix socket) поток данных, разбивает его на строки, строки передаёт в пул, который в свою очередь раскидывает эти строки по разным файлам. Каждый такой файл полностью реализует интерфейс stream.Writable (но не использует его) и уменьшает свой счётчик length только при выходе из fs.write. Пул также имеет свой буфер, тоже полностью реализует интерфейс stream.Writable (но не использует его). Все строки — объекты типа Buffer, к настоящим строкам данные не приводятся. Одновременных файловых объектов — порядка 3 тысяч.

При работе приложения с некоторым интервалом логирую использование памяти кэшами всех узлов системы. Кэши сокета, парсера строк и самого пула в рабочем режиме всегда пусты, все данные сразу уходят в кэши файлов. Заполнить их удавалось только при нагрузочных тестированиях. Причём кэши соекта и парсера принебрежимо малы, их можно даже не рассматривать. Для кэша пула и кэшей файлов установлен суммарный лимит в 1 ГБ, после которого pool.write начинает возвращать false, и другие участники stream-а ждут от него события «drain».

При нагрузочных тестированиях на локальной машине всё прекрасно работает, общая память процесса даже при полной заполненности всех кэшей не превышает 1.2-1.3 ГБ.

На боевой же по суточным логам объём файловых кэшей не превышает 30 МБ, при этом процесс жрёт 1.4 ГБ оперативки, вместо ожидаемых пусть даже 300-400 МБ. Причём отъедает не сразу, а спустя какое-то время работы, как правило часы. Я сначала думал, что приложение течёт. Сделал heapdump на старте и уже разжиревшему. Оба дампа оказались примерно одинаковыми по объёму и очень маленькими, порядка 5 МБ, и не показали каких бы то ни было утечек. Кроме того, отъев 1.4 ГБ приложение прекращает разрастаться и держится сутками примерно на одной и той же отметке.

Ощущение такое, что приложение сильно фрагментирует память, из-за этого аллокирует её до какого-то своего внутреннего максимума, после чего работает с тем, что есть.

Кто-нибудь сталкивался с подобным поведением нодовских приложений?
Чем объяснить такое странное поведение? Как выяснить, что на самом деле там происходит? Какие инструменты нужно использовать? И что можно сделать, чтобы объёмы памяти были сопоставимы с объёмами входных данных (ограничивать --max-old-space-size не вариант, так как объём входящего потока заранее не известен)?

Окружение — Ubuntu 16.04
Node 0.10 (Знаю, старая, но на более новые пока перейти не можем из-за модулей. К тому же к сути вопроса это отношение не имеет)

Комментарии (10)


  1. mwizard
    15.05.2018 13:19
    +6

    Даже не вздумайте переходить на Node 10! Всего пять лет с релиза прошло — разве можно говорить о какой-то там, прости господи, стабильности? Тем более с этой недавней возней с io.js. Вот устаканится все, баги пофиксят, и годиков через 8 можно будет поставить io.js


    1. TheRoSS Автор
      15.05.2018 13:23
      -4

      А какое это имеет отношение к моему вопросу? В новых нодах подобного не бывает? Вы можете дать гарантию?


      1. mwizard
        15.05.2018 13:27
        +3

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

        Более того, вы пользуетесь альфа-версией софта пятилетней давности, и не имеете рациональных причин, которые удерживают вас от перехода на современный продукт — иначе бы вы эти причины огласили.

        В чем же суть? Что вы хотите узнать этим постом?

        UPD: И да, «давать гарантию» может Joyent. Вы пробовали заплатить им денег?


        1. TheRoSS Автор
          15.05.2018 13:46
          -3

          Основная причина невозможности перехода — модули. В частности — libmysqlclient, который работает только на 0.10. Замена его на стандартный mysql ведёт к полной переработке архитектуры, так как использовались его синхронные вызовы. Увы. Кроме того, переход на последние версии ноды приведёт к потере производительности. Например, JSON.parse работает на 30% медленнее. Сейчас для нас это важно, хотя и не так критично, как модули.


          Этим постом хочу узнать, как вообще подходить к подобным задачам. Какие инструменты использовать. Пока что поток моих собственных идей иссяк. А отсылать просто к "устаревшей" версии, извините, но это не аргумент. На ноде 0.10 было написано не так уж мало вполне работоспособных продакшн приложений. Кроме того, кто Вам сказал, что версия 0.10 — это альфа-версия? Версия 0.4 уже была вполне стабильной.


          1. mwizard
            15.05.2018 14:01
            +1

            Ну вот мы и подошли к конкретике. Во-первых, забавно, что вы беспокоитесь о 30% замедлении JSON.parse, тогда как вы используете синхронные вызовы mysql, которые сводят производительность всей вашей нынешней системы к околонулевому значению — пока вы ждете ответа по одному запросу, в этом процессе не работает вообще больше никто, все сидят и ждут ответа от БД.

            Я понимаю ваше нежелание переписывать код, используя коллбэки (это никому не нравится) — посмотрите в сторону async/await, в node 10.1 они уже есть из коробки, а переработка кода с синхронного тривиальна.

            Дальше, как вы замеряли производительность JSON.parse? Вы уверены в том, что замедление на 30% вызвано именно парсингом JSON, а не внешними причинами?

            Если вы разбираете огромные строки, возможно, имеет смысл перейти на какой-нибудь protobuf? Также, возможно, имеет смысл вынести JSON.parse в отдельный поток или найти асинхронный парсер JSON.


            1. TheRoSS Автор
              15.05.2018 14:26

              Вот именно при синхронной архитектуре эти 30% и становятся значимыми)
              На самом деле вся эта синхронность мне досталась по наследству, и практически все компоненты системы, кроме основного, уже работают асинхронно. Так что JSON.parse для нас сейчас уже не критичен, держит только libmysqlclient в основном компоненте.


              Спасибо, в сторону async/await посмотрю


              Да, уверен, что именно JSON.parse. Проверяли на версиях 6.х. Возможно на 8.х и 10.х ситуация изменилась, не могу сказать.


              И всё таки хотелось бы получить ответ на мой первоначальный вопрос
              Предположим, моё приложение работает на последней ноде, использует самые последние стабильные версии модулей, и начинает наблюдаться подобное поведение. В какую сторону смотреть в такой ситуации?


              1. mwizard
                15.05.2018 14:38

                в сторону V8 Memory Profiler


              1. alexey-m-ukolov
                15.05.2018 14:41

                И всё таки хотелось бы получить ответ на мой первоначальный вопрос
                Тогда имеет смысл задавать его на предназначенных для этого ресурсах.


  1. mopsicus
    15.05.2018 13:35
    +4

    Лучше задайте свой вопрос на «Тостере», а эту запись уберите в черновики


  1. TheRoSS Автор
    15.05.2018 14:41

    Вообще забавна реакция аудитории )
    Какое-то время назад на стэковерфлоу задал вопрос о том, как добавить в андроид новый часовой пояс. Обычный вопрос на администрирование андроида. Но в вопросе упомянул, что нужен часовой пояс именно Северной Кореи, из-за чего получил массу минусов


    Уважаемые хабровчане, давайте не скатываться до холиваров, какая версия ноды истинно трушная.


    Вопрос ведь был не в версии ноды, а в том, как обрабатывать ситуации с разрастанием памяти в ноде при отсутствии утечек (как я предполагаю). У меня гипотеза, что причина — фрагментация памяти. Поверьте, такая причина вполне реальна на хайлоаде, сталкивался. Но что на самом деле, не знаю. Вот и приглашаю поделиться вашими гипотезами и методами, как эти гипотезы проверить