Промышленные записки

Как устранить проблемы с памятью в Python

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

Обнаружение того, что приложению не хватает памяти, - одна из худших реалий, которые может иметь разработчик. Проблемы с памятью сложно диагностировать и исправить в целом, но я бы сказал, что в Python это еще сложнее. Автоматическая сборка мусора Python позволяет легко встать и продолжить работу с языком, но он настолько хорош, что, когда он не работает, как ожидалось, разработчики могут не знать, как определить и исправить проблему. .

В этом посте я покажу, как мы диагностировали и исправляли проблему с памятью в EvalML, библиотеке AutoML с открытым исходным кодом, разработанной Alteryx Innovation Labs. Не существует волшебного рецепта решения проблем с памятью, но я надеюсь, что разработчики, особенно разработчики Python, смогут узнать об инструментах и ​​передовых методах, которые они могут использовать, когда столкнутся с подобными проблемами в будущем.

Прочитав этот пост в блоге, вы должны уйти со следующим:

  1. Почему так важно находить и устранять проблемы с памятью в ваших программах,
  2. Что такое циклические ссылки и почему они могут вызывать утечку памяти в Python, и
  3. Знание инструментов профилирования памяти Python и некоторых шагов, которые вы можете предпринять, чтобы определить причину проблем с памятью.

Подготовка сцены

Команда EvalML запускает набор тестов производительности перед выпуском новой версии нашего пакета, чтобы выявить любые падения производительности. Эти тесты производительности включают запуск нашего алгоритма AutoML на различных наборах данных, измерение результатов, полученных нашим алгоритмом, а также времени выполнения и сравнение этих показателей с нашей ранее выпущенной версией.

Однажды я проводил тесты, и вдруг приложение вылетело. Что случилось?

Шаг 0. Что такое память и что такое утечка?

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

Ядро определяет интерфейс программ для доступа к процессорам, памяти, дискам и т. Д. Компьютера. Каждый язык программирования предоставляет способы попросить ядро ​​выделить и освободить блоки памяти для использования выполняющейся программой.

Утечки памяти происходят, когда программа просит ядро ​​выделить часть памяти для использования, но затем из-за ошибки или сбоя программа никогда не сообщает ядру, когда она закончила использовать эту память. В этом случае ядро ​​будет продолжать думать, что забытые фрагменты памяти все еще используются запущенной программой, а другие программы не смогут получить доступ к этим фрагментам памяти.

Если одна и та же утечка происходит неоднократно во время работы программы, общий размер забытой памяти может вырасти настолько, что потребляет большую часть памяти компьютера! В этой ситуации, если программа затем попытается запросить больше памяти, ядро ​​выдаст ошибку «недостаточно памяти», и программа перестанет работать, или, другими словами, «выйдет из строя».

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

Шаг 1. Определите, что это проблема с памятью

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

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

Я снова провел тесты производительности, но на этот раз с включенным профилировщиком памяти Python, чтобы получить график использования памяти во времени. Тесты снова вылетели, и когда я посмотрел на график памяти, я увидел следующее:

Наше использование памяти остается стабильным с течением времени, но затем оно достигает 8 гигабайт! Я знаю, что у нашего сервера приложений 8 гигабайт оперативной памяти, поэтому этот профиль подтверждает, что нам не хватает памяти. Более того, когда память стабильна, мы используем около 4 ГБ памяти, но наша предыдущая версия EvalML использовала около 2 ГБ памяти. Итак, по какой-то причине текущая версия использует примерно вдвое больше памяти, чем обычно.

Теперь мне нужно было выяснить, почему.

Шаг 2. Воспроизведите локальную проблему с памятью на минимальном примере

Выявление причины проблемы с памятью требует множества экспериментов и повторений, потому что ответ обычно неочевиден. Если бы это было так, вы бы, вероятно, не записали это в код! По этой причине я считаю важным воспроизвести проблему с помощью как можно меньшего количества строк кода. Этот минимальный пример позволяет вам быстро запускать его под профилировщиком, пока вы изменяете код, чтобы увидеть, добиваетесь ли вы прогресса.

В моем случае я знал по опыту, что наше приложение запускает набор данных такси с 1,5 миллионами строк примерно в то время, когда я увидел большой всплеск. Я сократил наше приложение до части, которая запускает этот набор данных. Я увидел всплеск, похожий на описанный выше, но на этот раз использование памяти достигло 10 гигабайт!

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

Шаг 3. Найдите строки кода, которые выделяют больше всего памяти

Как только мы изолировали проблему до как можно меньшего фрагмента кода, мы можем увидеть, где программа выделяет больше всего памяти. Это может быть дымящийся пистолет, который вам понадобится для рефакторинга кода и устранения проблемы.

Я думаю, что filprofiler - отличный инструмент Python для этого. Он отображает распределение памяти для каждой строки кода в вашем приложении в момент пикового использования памяти. Это результат моего локального примера:

Filprofiler ранжирует строки кода в вашем приложении (и код ваших зависимостей) по распределению памяти. Чем длиннее и краснее линия, тем больше выделяется памяти.

Строки, которые выделяют больше всего памяти, создают фреймы данных pandas (pandas / core / алгоритмы.py и pandas / core / internal / manager.py) и составляют 4 гигабайта данных! Я усек здесь вывод filprofiler, но он может отслеживать код pandas для кодирования в EvalML, который создает фреймы данных pandas.

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

Шаг 4. Найдите протекающие объекты

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

Такие объекты сложно найти, но есть некоторые инструменты Python, которые вы можете использовать для облегчения поиска. Первый инструмент - это флаг gc.DEBUG_SAVEALL сборщика мусора. Установив этот флаг, сборщик мусора сохранит недоступные объекты в списке gc.garbage. Это позволит вам исследовать эти объекты дальше.

Второй инструмент - это библиотека objgraph. Как только объекты окажутся в списке gc.garbage, мы можем отфильтровать этот список по фреймам данных pandas и использовать objgraph, чтобы увидеть, какие другие объекты ссылаются на эти фреймы данных и сохранить их в памяти. Я получил идею такого подхода, прочитав эту запись в блоге О’Рейли.

Это подмножество графа объектов, которое я увидел, когда визуализировал один из этих фреймов данных:

Это дымящийся пистолет, который я искал! Фрейм данных делает ссылку на себя через нечто, называемое PandasTableAccessor, которое создает циклическую ссылку, так что это будет держать объект в памяти до тех пор, пока сборщик мусора Python не запустится и не сможет освободить его. (Вы можете отслеживать цикл с помощью dict, PandasTableAccessor, dict, _dataframe.) Это было проблематично для EvalML, потому что сборщик мусора сохранял эти фреймы данных в памяти так долго, что нам не хватало памяти!

Мне удалось отследить PandasTableAccessor до библиотеки Woodwork и довести эту проблему до сопровождающих. Они смогли исправить это в новом выпуске и отправить соответствующую проблему в репозиторий pandas - отличный пример сотрудничества, которое возможно в экосистеме с открытым исходным кодом.

После выпуска обновления Woodwork я визуализировал граф объектов того же фрейма данных, и цикл исчез!

Шаг 5 - Убедитесь, что исправление работает

После того, как я обновил деревянную версию в EvalML, я измерил объем памяти, занимаемый нашим приложением, и с радостью сообщаю, что использование памяти сейчас меньше половины от того, что было раньше!

Заключительные мысли

Как я сказал в начале этого поста, волшебного рецепта для решения проблем с памятью не существует, но в этом тематическом исследовании предлагается общая структура и набор инструментов, которые вы можете использовать, если столкнетесь с такой ситуацией в будущем. Я обнаружил, что memory-profiler и filprofiler являются полезными инструментами для отладки утечек памяти в Python.

Я также хочу подчеркнуть, что циклические ссылки в Python могут увеличить объем памяти, занимаемый вашими приложениями. Сборщик мусора в конечном итоге освободит память, но, как мы видели в этом случае, возможно, только когда станет слишком поздно!

Циклические ссылки в Python на удивление легко ввести непреднамеренно. Мне удалось найти непреднамеренный в EvalML, scikit-optimize и scipy. Я призываю вас не спускать глаз, и если вы видите круговую ссылку в дикой природе, начните разговор, чтобы узнать, действительно ли она нужна!

Оригинал опубликован в Блоге лаборатории инноваций Alteryx