Освобождение памяти node js
Утечки памяти похожи на сущности, паразитирующие на приложении. Они незаметно проникают в системы, поначалу не принося никакого вреда. Но если утечка оказывается достаточно сильной — она способна довести приложение до катастрофы. Например — сильно его замедлить или попросту «убить».
Автор статьи, перевод которой мы сегодня публикуем, предлагает поговорить об утечках памяти в JavaScript. В частности, речь пойдёт об управлении памятью в JavaScript, о том, как идентифицировать утечки памяти в реальных приложениях, и о том, как с бороться с утечками памяти.
Итоги
Две вышеописанные проблемы с памятью заставили нас притормозить развитие нашего проекта, которое до этого шло очень быстро, и проанализировать производительность сервера. Теперь мы понимаем особенности производительности сервера на гораздо более глубоком уровне, чем раньше, и мы знаем, сколько времени нужно для нормального выполнения отдельных функций, и сколько памяти они используют. У нас появилось гораздо лучшее понимание того, какие ресурсы нам нужны при дальнейшем масштабировании проекта. И, что самое важное, мы перестали бояться проблем с памятью и перестали ожидать их появления в будущем.
Если вы ломаете голову над тем, почему ваше приложение JavaScript преподносит неприятные сюрпризы в виде сильного торможения, низкой производительности, продолжительных задержек или частых сбоев, и все ваши старательные попытки выяснить причину ни к чему не приводят, то, скорее всего, в вашем коде происходят утечки памяти.
Это довольно распространенная проблема. Дело в том, что многие разработчики пренебрегают управлением памятью из-за неправильных представлений об ее автоматическом выделении и освобождении в современных высокоуровневых языках программирования, например JavaScript.
Своевременно же не решенный вопрос ее утечек может обернуться резким снижением производительности приложения вплоть до невозможности его нормального использования.
Пространство Интернета постоянно пополняется сложными жаргонизмами, в которых весьма непросто разобраться. Данная статья будет построена по другому принципу — просто и понятно. Вы узнаете о том, что такое утечки памяти и каковы их причины; как их легко обнаружить и диагностировать с помощью Chrome Developer Tools.
Начнем с того, что ключ к их пониманию лежит в понимании принципов управления памятью в Node.js. А это, в свою очередь, означает, что мы должны разобраться, как это управление осуществляется движком V8, используемым Node.js для JavaScript.
Вкратце напомню вам структуру памяти в V8.
Главным образом она делится на две области: стек (stack) и кучу (heap).
1.Стек — это область памяти, в которой хранятся статические данные, включающие фреймы методов/функций, примитивные значения и указатели на объекты. Он управляется ОС.
2.Куча — это самая большая область памяти, в которой V8 хранит объекты или динамические данные. Здесь же происходит сборка мусора.
Цитируя Дип К Сасидхаран, разработчика и одного из авторов книги “Развитие полного стека с JHipster”, отметим, что
“V8 управляет памятью кучи с помощью сборки мусора. Проще говоря, он освобождает память, используемую висячими объектами, т.е. объектами, на которые нет прямых или косвенных (через ссылку в другом объекте) ссылок из стека, для освобождения пространства с целью создания нового объекта.
Сборщик мусора в V8 отвечает за восстановление неиспользуемой памяти для повторного ее применения в процессе работы движка. Сборка мусора происходит по поколениям (объекты в куче распределяются по группам в зависимости от времени жизни и удаляются на разных этапах). В V8 существуют два этапа и три разных алгоритма сборки мусора”.
Простыми словами, утечка памяти — это не что иное, как фрагмент памяти в куче, который больше не нужен приложению, но который не был возвращен оперативной системе сборщиком мусора.
И вот мы имеем неиспользуемый фрагмент памяти. Со временем результатом накопления таких фрагментов станет ваше приложение, сигнализирующее о нехватке памяти для работы или даже ОС, требующая места для выделения памяти. А все это вместе чревато торможениями и/или выходом приложения или даже ОС из строя.
Автоматическое управление памятью, подразумевающее сборку мусора в V8, предназначено для предотвращения ее утечек. Например, циклические ссылки больше не вызывают беспокойства, но все-таки могут возникать из-за нежелательных ссылок в куче или по каким-либо другим причинам.
Рассмотрим несколько самых распространенных причин:
1.Глобальные переменные. Поскольку на них в JavaScript ссылается корневой узел (глобальный объект window или ключевое слово this ), то они никогда не подвергаются сборке мусора в течение всего жизненного цикла приложения и будут занимать память до тех пор, пока оно выполняется. И это относится ко всем объектам, на которые ссылаются глобальные переменные, а также к их потомкам. Наличие большого графа объектов со ссылками из корня может привести к утечке памяти.
2. Множественные ссылки. Ситуации, когда на один и тот же объект ссылаются несколько объектов, также могут вызвать утечку памяти при условии, что одна из ссылок становится висячей.
3. Замыкания. Замыкания JavaScript обладают превосходным свойством запоминать окружающий их контекст, вследствие чего ссылки на крупные объекты кучи, используемые в них, сохраняются дольше, чем требуется.
4. Таймеры и события. Использование setTimeout , setInterval , Observers и слушателей событий может вызвать утечки памяти в том случае, если ссылки на объекты кучи хранятся в их обратных вызовах без правильной обработки.
Теперь, когда мы разобрались в причинах возникновения утечек памяти, давайте посмотрим, как их избежать и какие практики взять на вооружение для эффективного использования памяти.
Сокращение использования глобальных переменных
Поскольку глобальные переменные не подвергаются сборке мусора, то лучше всего убедиться, что вы не злоупотребляете их использованием. Ниже речь пойдет о том, как это сделать.
Обходимся без случайных глобальных переменных
Когда вы присваиваете значение необъявленной переменной, JavaScript по умолчанию определяет ее как глобальную. Это может произойти по ошибке и привести к утечке памяти. Подобное может случиться в результате присвоения переменной к this .
Во избежание таких сюрпризов, всегда пишите код JavaScript в режиме strict и используйте нотацию 'use strict'; в начале файла JS.
В режиме strict выше приведенный пример приведет к ошибке. Однако при использовании ES модулей и компиляторов, таких как TypeScript или Babel, нет необходимости включать данный режим, так как он будет задействован по умолчанию. В последних версиях Node.js вы можете активировать режим strict глобально, сопроводив выполнение команды node флагом --use_strict .
И наконец, помните, что не следует привязывать глобальное this к функциям, использующим методы bind или call , так как это лишает использование режима strict всякого смысла.
Управление памятью
Управление памятью — это механизм выделения системной памяти приложению, которое нуждается в ней, и механизм возврата ненужной памяти операционной системе. Существует множество подходов к управлению памятью. То, какой именно подход применяется, зависит от используемого языка программирования. Вот обзор нескольких распространённых подходов к управлению памятью:
- Ручное управление памятью. При использовании этого метода за выделение и освобождение памяти отвечает программист. Язык не предоставляет автоматизированных средств для решения этих задач. Хотя это и даёт разработчику огромную гибкость, это усложняет его работу. Такой подход к управлению памятью используется в C и C++. В распоряжении программиста, использующего эти языки, есть методы malloc и free , позволяющие ему выделять и освобождать память.
- Применение концепции сборки мусора. Языки, в которых реализована идея сборки мусора, управляют памятью автоматически, полностью избавляя программиста от решения соответствующих задач. Программисту не нужно, в частности, заботиться об освобождении памяти, так как эту задачу решает встроенный сборщик мусора. То, как именно работает сборщик мусора, и то, когда именно выполняются сеансы освобождения памяти, скрыто от программиста. Концепция сборки мусора используется в большинстве современных языков программирования. Это — JavaScript, языки, основанные на JVM (Java, Scala, Kotlin), Golang, Python, Ruby и другие.
- Применение концепции владения памятью. При таком подходе каждая переменная должна иметь своего владельца. Как только владелец оказывается за пределами области видимости, значение в переменной уничтожается, освобождая память. Эта идея используется в Rust.
JavaScript — язык, без которого веб-программисты не представляют своей работы, использует идею сборки мусора. Поэтому мы подробнее поговорим о том, как работает этот механизм.
Загрузка снепшотов и определение типа проблемы с памятью
Следующий шаг заключается в загрузке снепшотов на закладке Memory (память) инструментов разработчика Chrome. Если вы использовали для создания снепшотов кучи удалённый отладчик Chrome, то они уже будут загружены. Если вы использовали heapdump, то вам понадобится загрузить их самостоятельно. Обязательно загружайте их в правильном порядке, а именно — в том, в котором они были сделаны.
Самое главное, на что надо обращать внимание на данном этапе работы, заключатся в том, чтобы понять — с чем именно вы столкнулись — с утечкой или с чрезмерным использованием памяти. Если перед вами утечка памяти, то вы, вероятно, уже получили достаточно данных для того, чтобы начать исследовать кучу в поисках источника проблемы. Однако, если перед вами — чрезмерное использование памяти, вам нужно попробовать некоторые другие методы анализа для того, чтобы получить содержательные данные.
Наша первая проблема с памятью выглядела, на закладке Memory инструментов разработчика Chrome, так, как показано ниже. Несложно заметить, что куча постоянно растёт. Это говорит об утечке памяти.
Куча увеличивается со временем — очевидная утечка памяти
Наша вторая проблема с памятью, которая возникла через пару месяцев после исправления утечки, в итоге, на тех же испытаниях, выглядела так, как показано на рисунке ниже.
Куча со временем не растёт — это не утечка памяти
Размер кучи со временем не меняется. Всё дело в том, что при чрезмерном использовании памяти её размер превышает некие ожидаемые показатели не всегда, а лишь при выполнении определённых операций. При этом снепшоты делаются в какие-то моменты, которые никак не привязаны к ситуациям с чрезмерным использованием памяти. Если в момент создания снепшота не происходило выполнения неправильно написанной ресурсоёмкой функции, тогда куча не будет содержать никакой ценной информации о памяти, используемой этой функцией.
Для выявления подобных проблем мы рекомендуем два способа, которые помогли нам обнаружить виновников проблемы — функцию и переменную. Это — запись профиля выделения памяти и создание снепшотов на сервере, находящемся под серьёзной нагрузкой.
Если вы используете версию Node 6.3 или более позднюю, вы можете записать профиль выделения памяти через удалённый отладчик Chrome, запустив Node с уже упоминавшимся ключом --inspect . Это даст сведения о том, как отдельные функции используют память с течением времени.
Запись профиля выделения памяти
Ещё один вариант заключается в отправке множества одновременных запросов к вашему серверу и в создании множества снепшотов во время обработки этих запросов (предполагается, что сервер работает асинхронно, как результат, некоторые снепшоты могут оказаться гораздо больше других, что укажет на проблему). Мы бомбардировали сервер запросами и делали снепшоты. Некоторые из них оказались очень большими. Исследованием этих снепшотов можно заняться для выявления источника проблемы.
Что такое утечка памяти?
Утечка памяти — это, в широком смысле, фрагмент памяти, выделенной приложению, который больше не нужен этому приложению, но не может быть возвращён операционной системе для дальнейшего использования. Другими словами это — блок памяти, который захвачен приложением без намерения пользоваться этой памятью в будущем.
Утечки памяти в Node.js-приложениях
К настоящему моменту мы разобрали достаточно теоретических понятий, касающихся утечек памяти и сборки мусора. А значит — мы готовы к тому, чтобы посмотреть на то, как всё это выглядит в реальных приложениях. В этом разделе мы напишем Node.js-сервер, в котором есть утечка памяти. Мы попытаемся выявить эту утечку, используя различные инструменты, а потом её устраним.
Итоги
В этом материале вы ознакомились с основами управления памятью, и с тем, как организовано управление памятью в разных языках. Здесь мы воспроизвели реальный сценарий утечки памяти и описали методику поиска и устранения проблемы.
Недавно в компании Reside Real Estate столкнулись с проблемами: в самые ответственные моменты начал падать Node.js-сервер. Подозрение пало на память. Сотрудники компании прибегли к временным мерам, что позволило избавить от неудобств пользователей, и занялись поисками источника проблем. В результате им удалось найти и устранить неполадки.
В этом материале они рассказывают о том, как искать и устранять ошибки, связанные с использованием памяти. А именно, речь пойдёт об утечках памяти, и о ситуациях, когда программы используют гораздо больше памяти, чем им на самом деле нужно. Этот рассказ поможет тем, кто столкнётся с чем-то похожим, сразу понять причину странного поведения сервера и быстро вернуть его в строй.
▍ Использование heapdump для создания снепшотов кучи
Мы, для создания снимков кучи, пользовались heapdump. Этот npm-пакет оказался весьма полезным. Его можно импортировать в код и обращаться к нему в тех местах программы, где нужно делать снепшоты. Например, мы делали снепшот каждый раз, когда сервер получал запрос, который мог вызвать процесс, интенсивно использующий память. Тут же мы формировали имя файла, содержащее текущее время. Таким образом мы могли воспроизводить проблему, отправляя на сервер всё новые и новые запросы. Вот как это выглядит в коде:
▍Знакомство с кодом, в котором есть утечка памяти
В демонстрационных целях я написал Express-сервер, у которого есть маршрут с утечкой памяти. Этот сервер мы и будем отлаживать.
Здесь имеется массив leaks , находящийся за пределами области видимости кода обработки запроса к API. В результате, каждый раз, когда выполняется соответствующий код, в массив просто добавляются новые элементы. Массив при этом никогда не очищается. Так как ссылка на этот массив после выхода из обработчика запроса никуда не девается, сборщик мусора никогда не освобождает занятую им память.
Управление памятью
Управление памятью — это механизм выделения системной памяти приложению, которое нуждается в ней, и механизм возврата ненужной памяти операционной системе. Существует множество подходов к управлению памятью. То, какой именно подход применяется, зависит от используемого языка программирования. Вот обзор нескольких распространённых подходов к управлению памятью:
- Ручное управление памятью. При использовании этого метода за выделение и освобождение памяти отвечает программист. Язык не предоставляет автоматизированных средств для решения этих задач. Хотя это и даёт разработчику огромную гибкость, это усложняет его работу. Такой подход к управлению памятью используется в C и C++. В распоряжении программиста, использующего эти языки, есть методы malloc и free , позволяющие ему выделять и освобождать память.
- Применение концепции сборки мусора. Языки, в которых реализована идея сборки мусора, управляют памятью автоматически, полностью избавляя программиста от решения соответствующих задач. Программисту не нужно, в частности, заботиться об освобождении памяти, так как эту задачу решает встроенный сборщик мусора. То, как именно работает сборщик мусора, и то, когда именно выполняются сеансы освобождения памяти, скрыто от программиста. Концепция сборки мусора используется в большинстве современных языков программирования. Это — JavaScript, языки, основанные на JVM (Java, Scala, Kotlin), Golang, Python, Ruby и другие.
- Применение концепции владения памятью. При таком подходе каждая переменная должна иметь своего владельца. Как только владелец оказывается за пределами области видимости, значение в переменной уничтожается, освобождая память. Эта идея используется в Rust.
JavaScript — язык, без которого веб-программисты не представляют своей работы, использует идею сборки мусора. Поэтому мы подробнее поговорим о том, как работает этот механизм.
▍Знакомство с кодом, в котором есть утечка памяти
В демонстрационных целях я написал Express-сервер, у которого есть маршрут с утечкой памяти. Этот сервер мы и будем отлаживать.
Здесь имеется массив leaks , находящийся за пределами области видимости кода обработки запроса к API. В результате, каждый раз, когда выполняется соответствующий код, в массив просто добавляются новые элементы. Массив при этом никогда не очищается. Так как ссылка на этот массив после выхода из обработчика запроса никуда не девается, сборщик мусора никогда не освобождает занятую им память.
Утечки памяти в Node.js-приложениях
К настоящему моменту мы разобрали достаточно теоретических понятий, касающихся утечек памяти и сборки мусора. А значит — мы готовы к тому, чтобы посмотреть на то, как всё это выглядит в реальных приложениях. В этом разделе мы напишем Node.js-сервер, в котором есть утечка памяти. Мы попытаемся выявить эту утечку, используя различные инструменты, а потом её устраним.
▍Выявление утечки памяти
И вот, сервер развёрнут. Он работает уже несколько дней. К нему поступает множество запросов (в нашем случае — лишь запросы одного вида) и мы обратили внимание на рост объёма памяти, потребляемой сервером. Заметить утечку памяти можно, пользуясь инструментами мониторинга наподобие Express Status Monitor, Clinic, Prometheus. После этого мы вызываем API для создания дампа кучи. Этот дамп будет содержать все объекты, которые не смог удалить сборщик мусора.
Вот как выглядит запрос, позволяющий создать дамп:
При создании дампа кучи принудительно запускается сборщик мусора. В результате нам не нужно беспокоиться о тех объектах, которые могут быть удалены сборщиком мусора в будущем, но пока находятся в куче. То есть — об объектах, при работе с которыми утечек памяти не происходит.
После того, как в нашем распоряжении окажутся оба дампа (дамп свежезапущенного сервера и дамп сервера, проработавшего некоторое время), мы можем приступить к их сравнению.
Получение дампа памяти — это блокирующая операция, на выполнение которой нужно много памяти. Поэтому выполнять её надо с осторожностью. Подробнее о возможных проблемах, возникающих в ходе этой операции, можно почитать здесь.
Запустим Chrome и нажмём клавишу F12 . Это приведёт к открытию инструментов разработчика. Тут нужно перейти на вкладку Memory и выполнить загрузку обоих снимков памяти.
Загрузка дампов памяти на вкладке Memory инструментов разработчика Chrome
После загрузки обоих снимков нужно изменить perspective на Comparison и щёлкнуть по снимку памяти сервера, который проработал некоторое время.
Начало сравнения снимков
Здесь мы можем проанализировать столбец Constructor и поискать объекты, которые не может удалить сборщик мусора. Большая часть таких объектов будет представлена внутренними ссылками, которые используют узлы. Тут полезно воспользоваться одним приёмом, который заключается в сортировке списка по полю Alloc. Size . Это позволит быстро найти объекты, использующие больше всего памяти. Если развернуть блок (array) , а затем — (object elements) , то можно будет увидеть массив leaks , содержащий огромное количество объектов, которые невозможно удалить с помощью сборщика мусора.
Анализ подозрительного массива
Эта методика позволит нам выйти на массив leaks и понять, что именно неправильная работа с ним является причиной утечки памяти.
Анализ снепшотов
Теперь у нас есть данные, которые вполне могут помочь найти виновников проблем с памятью. В частности, рассмотрим анализ ситуации, в которой размеры последовательно сделанных снепшотов растут. Вот один из снепшотов, который загружен на вкладке Memory инструментов разработчика Chrome.
Исследование утечки памяти — все функции указывают на наш сервис электронной почты
Показатель Retained Size — это размер памяти, освобождённой после того, как объект удалён вместе со своими зависимыми объектами, которые недостижимы из корневого объекта.
Анализ можно начать с сортировки списка по убыванию по параметру Retained Size, после чего приступить к исследованию больших объектов. В нашем случае имена функций указали нам на ту часть кода, которая вызывала проблему.
Так как мы были уверены в том, что перед нами утечка памяти, мы знали, что исследование стоит начать с поиска переменных с неподходящей областью видимости. Мы открыли файл index.js почтовой службы и тут же обнаружили переменную уровня модуля в верхней части файла.
Мы со всем этим разобрались, внесли необходимые изменения, протестировали проект ещё несколько раз и исправили в итоге утечку памяти.
Вторую проблему отлаживать было сложнее, но тут сработал тот же подход. Ниже показан профиль выделения памяти, который мы записали с использованием инструментов разработчика Chrome и ключа Node --inspect .
Поиск виновников чрезмерного использования памяти
Так же как при анализе данных в ходе поиска утечки памяти, многие имена функций и объектов с первого взгляда узнать не удаётся, так как находятся они на более низком уровне, чем код, который пишут для Node.js. В подобной ситуации следует, встретив незнакомое имя, записать его.
Профиль выделения памяти привёл нас к одной из функций, recordFromSnapshot , она стала хорошей отправной точкой. Наше исследование снепшота кучи, которое не особенно отличалось от исследования, выполняемого при поиске утечки памяти, позволило обнаружить очень большой объект target . Это была переменная, объявленная внутри функции recordFromSnapshot . Эта переменная осталась от старой версии приложения, она была больше не нужна. Избавившись от неё, мы исправили ситуацию с чрезмерным использованием памяти и ускорили выполнение процесса, которое раньше занимало 40 секунд, до примерно 10 секунд. При этом процессу не требовалась дополнительная память.
Что такое утечка памяти?
Утечка памяти — это, в широком смысле, фрагмент памяти, выделенной приложению, который больше не нужен этому приложению, но не может быть возвращён операционной системе для дальнейшего использования. Другими словами это — блок памяти, который захвачен приложением без намерения пользоваться этой памятью в будущем.
Типы проблем с памятью
▍Вызов утечки памяти
Здесь мы подходим к самому интересному. Написано много статей, рассказывающих о том, как, используя node --inspect , отлаживать серверные утечки памяти, предварительно завалив сервер запросами с помощью чего-то вроде artillery. Но у такого подхода есть один важный недостаток. Представьте, что у вас есть API-сервер, у которого имеются тысячи конечных точек. Каждая из них принимает множество параметров, от особенностей которых зависит то, какой именно код будет вызван. В результате, в реальных условиях, если разработчик не знает о том, где кроется утечка памяти, ему, чтобы переполнить память, придётся по много раз обращаться к каждому API, используя все возможные комбинации параметров. Как по мне — так сделать это непросто. Решение подобной задачи, правда, облегчается при использовании чего-то вроде goreplay — системы, которая позволяет записывать и «воспроизводить» реальный трафик.
Грамотное использование замыканий, таймеров и обработчиков событий
Как уже было отмечено ранее, замыкания, таймеры и обработчики событий — это те области, где могут произойти утечки памяти. Начнем с замыканий, как наиболее часто встречающихся в коде JavaScript. Посмотрим на следующий код команды Meteor, который приводит к утечке памяти, так как переменная longStr не подлежит удалению и увеличивает объём занимаемой памяти.
Выше обозначенный код создает несколько замыканий, которые удерживают ссылки на объекты. В этом случае устранение утечки памяти возможно путем определения originalThing как null в конце выполнения функции replaceThing . Подобных ситуаций тоже можно избежать, создавая копии объектов и соблюдая выше описанный немутабельный подход.
Когда дело касается таймеров, всегда передавайте копии объектов и обходитесь без мутаций. По окончании работы таймеров проводите их очистку с помощью методов clearTimeout и clearInterval .
Тоже самое относится к слушателям событий и наблюдателям. Как только они выполнили свою задачу, очистите их. Не оставляйте слушателей в постоянно работающем состоянии, особенно если они удерживают ссылки на объекты из родительской области видимости.
В настоящее время утечки памяти в JavaScript не являются такой уж большой проблемой, как бывало раньше, благодаря эволюции движков JS и улучшениям в языке. Но если мы не будем внимательны, то они по-прежнему будут происходить и приводить к снижению производительности и сбоям в работе приложения/ОС.
Во-первых, чтобы убедиться, что наш код не приводит к утечкам памяти в приложении Node.js, нужно разобраться в принципах ее управления движком V8. Во-вторых, важно понять, что послужило их причиной.
После этого в наших силах избежать развития таких ситуаций. Но если все-таки мы обнаружим утечку памяти/проблемы с производительностью, то теперь будем знать, что искать.
H ave you ever got involved in any large scale NodeJs application? If Yes, So you know what we are talking about, if No, then you need to continue reading as you might face it soon, as I did 😄. Memory leak is very common in NodeJs Applications especially large scale ones with reasonable traffic, and many big companies have suffered from it. let’s know why?
NodeJs applications are long-running processes, the application is initiated once, then it is up and running, handling all incoming requests and consuming resources continuously.
Unlike PHP which creates a child process for every incoming request, initiates the app for it and once the request is done the process is terminated and all resources are released.
This fact gives NodeJS applications advantages as the application is initiated only once not for each incoming request and at the initiation you can add all your configs, DB connections, routes, etc. This makes handling requests faster as it only does the logic for each request with no need for repetitive work, also it saves resources required for this initiation process.
But Nothing comes without a price as for this advantage you have to take care of your application memory usage as it will be consuming your memory continuously during its run-time, and if you aren’t handling your memory usage efficiently, you may run out of memory, Then you’ll have to restart your app to free up the memory.
Memory Management in NodeJs is done via V8: ‘which is the engine that makes it possible to run JavaScript on the server outside the browser’.
V8 is handling two main memory categories:
Stack: In which it stores the primitive data types: Number, String, boolean, Null, Undefined, Symbol and references to non-primitive data types Object .
Heap: stores the non-primitive data types: Object .
The V8 has a garbage collector runs mainly Mark and Sweep algorithm:
It checks for all objects’ reference paths to the root node ‘which is the global or window object’. If any reference has no path to the root node, it will be marked as garbage and will be swept later.
Important Note: When the Garbage Collector runs, it pauses your application entirely until it finishes its work. so you need to minimize its work by taking care of your objects’ references.
According to what we explained in Memory management, We can define three of the most common memory leak causes, you need to be careful while using them in your code.
Эффективное использование стека
Максимально возможное использование переменных стека способствует эффективной и производительной работе памяти, так как доступ к стеку происходит гораздо быстрее, чем к куче. Это также позволяет избежать случайных утечек памяти.
Конечно, использование исключительно статических данных непрактично. В реальных приложениях нам приходится работать со многими объектами и динамическими данными. Но мы можем оптимизировать применение переменных стека при помощи ряда приемов:
1.Избегайте использования ссылок переменных стека на объекты кучи по мере возможности и не храните неиспользуемые переменные;
2.Деструктуризируйте и используйте только необходимые поля объектов или массивов вместо того, чтобы целиком передавать их функциям, замыканиям, таймерам и обработчикам событий. Тогда ссылки на объекты уже не будут оставаться в замыканиях. Передаваемые поля могут быть в основном примитивами, которые будут храниться в стеке.
Quick Demo on Node-Inspector & Chrome DevTools:
- Firstly, run your app with options:
expose-gc to be able to run the garbage collector explicitly
inspect=9222 to be able to attach the Chrome Debugger to your app on port 9222
so the command should be:
node --expose-gc --inspect=9222 app.js - now open this URL chrome://inspect from your Chrome to open chrome devices inspector
then you need to check that you have your server host with debugging port added to the list of the target discovery servers.
after adding your server to the list, it will show up in the remote targets. just click inspect .
this will open a new dedicated window for debugging your app. in Memory tab you would find an option to collect garbage, take heap snapshots and monitor allocation timeline.
In the screenshot below you can find on the upper left corner the button to enforce running garbage collector and in the middle the button to take heap snapshots.
Now we need to take two snapshots one at the start and the other after making some calls to the server for a specific route if you wanna check specific logic or some random routes if you wanna check the overall performance.
and also don’t forget to enforce running the garbage collector before each snapshot.
After taking the second snapshot there’s a select menu on the top.
Choose from it Objects allocated between Snapshot1 and Snapshot2 this is to get the difference between the two snapshots as these objects are the potential leakage in your app.
You’ll find two sections for Constructors and Retainers, first you shall focus on constructors without braces then you may check others.
After checking the constructor, you may check its retainers, you shall find indications from your source code to where the leakage is, the leakage also may be from third parties you use.
Important notice: try to take many heap snapshots and between them make some requests to your server and check if the memory usage increasing or still the same if it’s stable so you’re safe otherwise you have an issue.
Finally, That was just a quick overview on the main parts of the memory leak problem, We may speak in more details for each part of those, but I wanted to make it a quick overview about this mysterious topic, So if you like it show your support by claps and share.
Утечки памяти похожи на сущности, паразитирующие на приложении. Они незаметно проникают в системы, поначалу не принося никакого вреда. Но если утечка оказывается достаточно сильной — она способна довести приложение до катастрофы. Например — сильно его замедлить или попросту «убить».
Автор статьи, перевод которой мы сегодня публикуем, предлагает поговорить об утечках памяти в JavaScript. В частности, речь пойдёт об управлении памятью в JavaScript, о том, как идентифицировать утечки памяти в реальных приложениях, и о том, как с бороться с утечками памяти.
▍ Чрезмерное использование памяти
В ситуации чрезмерного использования памяти программа занимает гораздо больше памяти, чем ей нужно для решения возложенной на неё задачи. Например, такое может возникнуть тогда, когда ссылки на большие объекты хранят дольше, чем нужно для правильной работы программы, что предотвращает уничтожение этих объектов сборщиком мусора. Подобное случается и тогда, когда в памяти держат большие объекты, которые попросту не нужны программе (это вызывает одну из двух основных проблем, которые мы рассмотрим ниже).
Итоги
В этом материале вы ознакомились с основами управления памятью, и с тем, как организовано управление памятью в разных языках. Здесь мы воспроизвели реальный сценарий утечки памяти и описали методику поиска и устранения проблемы.
Временное решение проблемы
Параметр $SIZE задаётся в мегабайтах и, теоретически, может быть любым числом, которое имеет смысл на конкретном компьютере. В нашем случае был использован параметр 8000, который, с учётом особенностей работы сервера, позволил выиграть достаточно времени на исследования. Кроме того, мы увеличили динамическую память. Мы пользуемся Heroku, там это делается просто.
Также мы воспользовались сервисом Twilio, настроили его так, чтобы нас оповещали каждый раз, когда на сервер приходит запрос, требующий особенно много памяти. Это позволило нам наблюдать за запросом и перезапускать сервер после его завершения. Такое решение неидеально, но для того, чтобы наши пользователи не сталкивались с отказами, мы были готовы на всё, даже на круглосуточные дежурства без выходных.
Multiple References
Setting multiple references to the same object may cause a problem also as you may remove one ref and forget the other which will keep your object still exists in the Heap.
▍Исправление утечки памяти
Для того чтобы проверить действенность принятых мер, достаточно повторить вышеописанные шаги и снова сравнить снимки кучи.
Умеренное использование глобальной области видимости
В целом будет лучше, если вы воздержитесь от использования глобальной области видимости и глобальных переменных насколько это возможно.
- Минимизируйте использование глобальной области видимости. Вместо этого рассмотрите возможность применения локальной области внутри функций, так как они будут удалены в процессе сборки мусора для освобождения памяти. Если вам вынужденно приходится прибегать к использованию глобальной переменной, то задайте ей значение null , когда в ней уже не будет необходимости.
- Используйте глобальные переменные только для констант, кэша и переиспользуемых объектов-одиночек. Не стоит применять их в целях избежания передачи значений в коде. Для обмена данными между функциями и классами передавайте значения в качестве параметров или атрибутов объектов.
- Не храните крупные объекты в глобальной области видимости. Если же вам приходится это делать, то не забудьте определить их как null, когда они более не нужны. В отношении объектов кэша рекомендуется установить обработчик для периодической очистки, препятствующий их неограниченному росту.
▍Исправление утечки памяти
Для того чтобы проверить действенность принятых мер, достаточно повторить вышеописанные шаги и снова сравнить снимки кучи.
▍ Снепшот кучи
«Утечка памяти» — это проблема, которая выражается в постоянно растущем размере кучи. В результате куча оказывается слишком большой для продолжения нормальной работы сервера. Поэтому в самом начале исследования нужно сделать несколько снепшотов (снимков состояния) кучи, с некоторым интервалом, и погрузиться в исследование этих снепшотов с использованием инструментов разработчика Chrome для того, чтобы понять, почему куча так велика и почему она растёт. Обратите внимание на то, что следует делать несколько снепшотов, через некоторое время, в результате можно будет изучить объекты, которые будут переходить из одного снепшота в другой. Эти объекты, вполне возможно, являются виновниками утечки памяти. Существует множество способов создать снепшот кучи.
▍ Использование удалённого отладчика Chrome для создания снепшотов кучи
Если вы работаете с Node 6.3. или с более поздней его версией, для создания снепшотов кучи можно использовать удалённый отладчик Chrome. Для того, чтобы это сделать, сначала запустите Node командой такого вида: node --inspect server.j s. Затем перейдите по адресу chrome://inspect . Теперь вы сможете удалённо отлаживать процессы Node. Чтобы сэкономить время, можете установить этот плагин Chrome, который автоматически откроет вкладку отладчика при запуске Node с флагом --inspect . После этого просто делайте снепшоты тогда, когда сочтёте это необходимым.
Средства удалённой отладки Chrome и создание снепшотов кучи
Global Variables
As they have a direct path to the root node, they will stay in memory as long as the application is running so you need to be careful when setting global variables and the amount of data you’ll set to them.
Closures
In closures simply you keep references to objects to be used later. this feature has many advantages but if it’s used without caution it may cause big issues as these references will keep objects in heap and these objects might be large ones, not just simple objects.
There’re many tools and libraries used to detect memory leaks in NodeJS, all following the same concept to detect memory leaks by comparing different heap dumps and check the results, and they try to force run the garbage collector before taking any heap dump to make sure that the leakage is real.
The difference between two heap dumps is an indicator of the amount of memory leak in your application. I would go with two of these tools, the most important ones from my point of view:
-
: Very helpful in the production as it emits specific events once there’s abnormal memory usage or potential leak in your app so you can handle these events or expect that there’s a problem coming😄. : Very useful especially when used with Chrome DevTools in the development phase as it enables you to perform stress tests on your application and monitor the memory usage performance to check if there’s any potential leak in your code and find where exactly that leak might happen.
▍Вызов утечки памяти
Здесь мы подходим к самому интересному. Написано много статей, рассказывающих о том, как, используя node --inspect , отлаживать серверные утечки памяти, предварительно завалив сервер запросами с помощью чего-то вроде artillery. Но у такого подхода есть один важный недостаток. Представьте, что у вас есть API-сервер, у которого имеются тысячи конечных точек. Каждая из них принимает множество параметров, от особенностей которых зависит то, какой именно код будет вызван. В результате, в реальных условиях, если разработчик не знает о том, где кроется утечка памяти, ему, чтобы переполнить память, придётся по много раз обращаться к каждому API, используя все возможные комбинации параметров. Как по мне — так сделать это непросто. Решение подобной задачи, правда, облегчается при использовании чего-то вроде goreplay — системы, которая позволяет записывать и «воспроизводить» реальный трафик.
Эффективное использование кучи
В любом реальном приложении мы так или иначе будем использовать кучу, но с помощью следующих рекомендаций можно сделать работу с ней более эффективной:
1.По возможности копируйте объекты вместо того, чтобы передавать ссылки. Их передача возможна только в том случае, если объект крупный или операция копирования требует больших затрат.
2.По максимуму обходитесь без мутаций объекта. Вместо этого для их копирования используйте распространение объекта или Object.assign .
3.Вместо создания множественных ссылок на объект просто его скопируйте.
4.Используйте переменные с коротким жизненным циклом.
5.Старайтесь не создавать огромные деревья объектов. Если же это неизбежно, то обеспечьте им короткий жизненный цикл в локальной области видимости.
▍Дамп кучи
Для того чтобы разобраться с тем, что такое «дамп кучи», нам сначала надо выяснить смысл понятия «куча». Если описать это понятие максимально просто, то окажется, что куча — это то место, куда попадает всё то, для чего выделяется память. Всё это находится в куче до тех пор, пока сборщик мусора не уберёт из неё всё то, что будет признано ненужным. Дамп кучи — это нечто вроде снимка текущего состояния кучи. Дамп содержит все внутренние переменные и переменные, объявленные программистом. В нём представлена вся память, выделенная в куче на момент получения дампа.
В результате, если бы мы могли как-то сравнить дамп кучи только что запущенного сервера с дампом кучи сервера, работающего уже долго и переполняющего память, то мы смогли бы идентифицировать подозрительные объекты, которые не нужны приложению, но не удаляются сборщиком мусора.
Прежде чем продолжать разговор — поговорим о том, как создавать дампы кучи. Для решения этой задачи мы воспользуемся npm-пакетом heapdump, который позволяет программно получить дамп кучи сервера.
Внесём в код сервера некоторые изменения, которые позволят нам воспользоваться данным пакетом:
Здесь мы воспользовались данным пакетом для получения дампа свежезапущенного сервера. Так же мы создали API /heapdump , предназначенный для создания кучи при обращении к нему. К этому API мы обратимся в тот момент, когда поймём, что сервер начал потреблять слишком много памяти.
Если ваш сервер работает в кластере Kubernetes, то вам не удастся, без дополнительных усилий, обратиться именно к тому поду, сервер, работающий в котором, потребляет слишком много памяти. Для того чтобы это сделать, можно воспользоваться перенаправлением портов. Кроме того, так как у вас не будет доступа к файловой системе, который нужен для загрузки файлов дампов, эти файлы лучше будет выгрузить во внешнее облачное хранилище (вроде S3).
Сборка мусора в JavaScript
Как уже было сказано, JavaScript — это язык, в котором используется концепция сборки мусора. В ходе работы JS-программ периодически запускается механизм, называемый сборщиком мусора. Он выясняет то, к каким участкам выделенной памяти можно получить доступ из кода приложения. То есть — то, на какие переменные имеются ссылки. Если сборщик мусора выясняет, что к какому-то участку памяти больше нет доступа из кода приложения, он освобождает эту память. Вышеописанный подход может быть реализован с помощью двух основных алгоритмов. Первый — это так называемый алгоритм пометок (Mark and Sweep). Он используется в JavaScript. Второй — это подсчёт ссылок (Reference Counting). Он применяется в Python и PHP.
Фазы Mark (пометка) и Sweep (очистка) алгоритма Mark and Sweep
При реализации алгоритма пометок сначала создаётся список корневых узлов, представленных глобальными переменными окружения (в браузере это — объект window ), а затем осуществляется обход полученного дерева от корневых к листовым узлам с пометкой всех встреченных на пути объектов. Память в куче, которая занята непомеченными объектами, освобождается.
Выявление проблем с памятью
Признаки утечки памяти, кроме того, включают в себя уменьшение производительности программы с течением времени. Если сервер периодически выполняет один и тот же процесс, который изначально быстр, а перед отказом постепенно становится медленнее, это, весьма вероятно, говорит об утечке памяти.
Признаки чрезмерного использования памяти обычно выражаются в низкой производительности программ. Однако, чрезмерное использование памяти без утечки со временем не приводит к падению производительности.
▍Выявление утечки памяти
И вот, сервер развёрнут. Он работает уже несколько дней. К нему поступает множество запросов (в нашем случае — лишь запросы одного вида) и мы обратили внимание на рост объёма памяти, потребляемой сервером. Заметить утечку памяти можно, пользуясь инструментами мониторинга наподобие Express Status Monitor, Clinic, Prometheus. После этого мы вызываем API для создания дампа кучи. Этот дамп будет содержать все объекты, которые не смог удалить сборщик мусора.
Вот как выглядит запрос, позволяющий создать дамп:
При создании дампа кучи принудительно запускается сборщик мусора. В результате нам не нужно беспокоиться о тех объектах, которые могут быть удалены сборщиком мусора в будущем, но пока находятся в куче. То есть — об объектах, при работе с которыми утечек памяти не происходит.
После того, как в нашем распоряжении окажутся оба дампа (дамп свежезапущенного сервера и дамп сервера, проработавшего некоторое время), мы можем приступить к их сравнению.
Получение дампа памяти — это блокирующая операция, на выполнение которой нужно много памяти. Поэтому выполнять её надо с осторожностью. Подробнее о возможных проблемах, возникающих в ходе этой операции, можно почитать здесь.
Запустим Chrome и нажмём клавишу F12 . Это приведёт к открытию инструментов разработчика. Тут нужно перейти на вкладку Memory и выполнить загрузку обоих снимков памяти.
Загрузка дампов памяти на вкладке Memory инструментов разработчика Chrome
После загрузки обоих снимков нужно изменить perspective на Comparison и щёлкнуть по снимку памяти сервера, который проработал некоторое время.
Начало сравнения снимков
Здесь мы можем проанализировать столбец Constructor и поискать объекты, которые не может удалить сборщик мусора. Большая часть таких объектов будет представлена внутренними ссылками, которые используют узлы. Тут полезно воспользоваться одним приёмом, который заключается в сортировке списка по полю Alloc. Size . Это позволит быстро найти объекты, использующие больше всего памяти. Если развернуть блок (array) , а затем — (object elements) , то можно будет увидеть массив leaks , содержащий огромное количество объектов, которые невозможно удалить с помощью сборщика мусора.
Анализ подозрительного массива
Эта методика позволит нам выйти на массив leaks и понять, что именно неправильная работа с ним является причиной утечки памяти.
Сборка мусора в JavaScript
Как уже было сказано, JavaScript — это язык, в котором используется концепция сборки мусора. В ходе работы JS-программ периодически запускается механизм, называемый сборщиком мусора. Он выясняет то, к каким участкам выделенной памяти можно получить доступ из кода приложения. То есть — то, на какие переменные имеются ссылки. Если сборщик мусора выясняет, что к какому-то участку памяти больше нет доступа из кода приложения, он освобождает эту память. Вышеописанный подход может быть реализован с помощью двух основных алгоритмов. Первый — это так называемый алгоритм пометок (Mark and Sweep). Он используется в JavaScript. Второй — это подсчёт ссылок (Reference Counting). Он применяется в Python и PHP.
Фазы Mark (пометка) и Sweep (очистка) алгоритма Mark and Sweep
При реализации алгоритма пометок сначала создаётся список корневых узлов, представленных глобальными переменными окружения (в браузере это — объект window ), а затем осуществляется обход полученного дерева от корневых к листовым узлам с пометкой всех встреченных на пути объектов. Память в куче, которая занята непомеченными объектами, освобождается.
▍ Утечка памяти
В информатике утечка памяти — это разновидность неконтролируемого использования ресурсов, которая возникает, когда программа неправильно управляет выделением памяти, в результате чего память, которая больше не нужна, не освобождается.
В низкоуровневых языках вроде C утечки памяти часто возникают в ситуации, когда память выделяют, например так: buffer = malloc(num_items*sizeof(double)); , но не освобождают после того, как память больше не нужна: free(buffer); .
В языках с автоматическим управлением освобождением памяти утечки возникают, когда к сущностям, которые больше не нужны, можно получить доступ из исполняющейся программы, или из некоего корневого объекта. В случае с JavaScript, любой объект, к которому можно обратиться из программы, не уничтожается сборщиком мусора, соответственно, место, которое он занимает в куче, не освобождается. Если размер кучи вырастет слишком сильно, возникнет ситуация нехватки памяти.
▍Дамп кучи
Для того чтобы разобраться с тем, что такое «дамп кучи», нам сначала надо выяснить смысл понятия «куча». Если описать это понятие максимально просто, то окажется, что куча — это то место, куда попадает всё то, для чего выделяется память. Всё это находится в куче до тех пор, пока сборщик мусора не уберёт из неё всё то, что будет признано ненужным. Дамп кучи — это нечто вроде снимка текущего состояния кучи. Дамп содержит все внутренние переменные и переменные, объявленные программистом. В нём представлена вся память, выделенная в куче на момент получения дампа.
В результате, если бы мы могли как-то сравнить дамп кучи только что запущенного сервера с дампом кучи сервера, работающего уже долго и переполняющего память, то мы смогли бы идентифицировать подозрительные объекты, которые не нужны приложению, но не удаляются сборщиком мусора.
Прежде чем продолжать разговор — поговорим о том, как создавать дампы кучи. Для решения этой задачи мы воспользуемся npm-пакетом heapdump, который позволяет программно получить дамп кучи сервера.
Внесём в код сервера некоторые изменения, которые позволят нам воспользоваться данным пакетом:
Здесь мы воспользовались данным пакетом для получения дампа свежезапущенного сервера. Так же мы создали API /heapdump , предназначенный для создания кучи при обращении к нему. К этому API мы обратимся в тот момент, когда поймём, что сервер начал потреблять слишком много памяти.
Если ваш сервер работает в кластере Kubernetes, то вам не удастся, без дополнительных усилий, обратиться именно к тому поду, сервер, работающий в котором, потребляет слишком много памяти. Для того чтобы это сделать, можно воспользоваться перенаправлением портов. Кроме того, так как у вас не будет доступа к файловой системе, который нужен для загрузки файлов дампов, эти файлы лучше будет выгрузить во внешнее облачное хранилище (вроде S3).
Отладка
Итак, благодаря настройкам Node и организации мониторинга сервера мы выиграли время, которое можно было потратить на то, чтобы дойти до первопричины неполадки. На первый взгляд может показаться, что «проблема с памятью сервера» — это нечто ужасное, а для избавления от этой «проблемы» потребуются фантастические инструменты и умения. Однако, на самом деле, всё не так уж и страшно. Есть вполне доступные инструменты для исследования приложений, существует множество материалов, в которых можно найти подсказки. Мы, для исследования памяти Node-сервера, будем пользоваться инструментами разработчика Chrome.
Читайте также: