Как узнать, что использует память в процессе Python в производственной системе?

Моя производственная система иногда обнаруживает утечку памяти, которую я не смог воспроизвести в среде разработки. Я использовал профилировщик памяти Python (в частности, Heapy) с некоторым успехом в среде разработки, но он не может помочь мне с вещами, которые я не могу воспроизвести, и я не хочу оснащать нашу производственную систему с помощью Heapy, потому что на это требуется время, а его многопоточный удаленный интерфейс плохо работает на нашем сервере.

Я думаю, что мне нужен способ сделать дамп снимка производственного процесса Python (или, по крайней мере, gc.get_objects), а затем проанализировать его в автономном режиме, чтобы увидеть, где он использует память. Как получить дамп ядра такой процесс Python? Как мне сделать с ним что-нибудь полезное, если он у меня есть?


person keturn    schedule 26.09.2008    source источник
comment
Какая у вас производственная платформа, * nix или NT?   -  person Constantin    schedule 27.09.2008


Ответы (7)


Я расширю ответ Бретта из своего недавнего опыта. Пакет Dozer - это в хорошем состоянии, и, несмотря на такие улучшения, как добавление tracemalloc в stdlib в Python 3.4, его gc.get_objects диаграмма подсчета - это мой инструмент для устранения утечек памяти. Ниже я использую dozer > 0.7, который не был выпущен на момент написания (ну, потому что недавно я внес туда пару исправлений).

Пример

Давайте посмотрим на нетривиальную утечку памяти. Я воспользуюсь Celery 4.4 здесь и в конечном итоге обнаружу функцию, которая вызывает утечку (и поскольку это ошибка / особенность, это можно назвать простой неправильной конфигурацией, вызванной незнанием). Итак, есть Python 3.6 venv, где я pip install celery < 4.5. И есть следующий модуль.

demo.py

import time

import celery 


redis_dsn = 'redis://localhost'
app = celery.Celery('demo', broker=redis_dsn, backend=redis_dsn)

@app.task
def subtask():
    pass

@app.task
def task():
    for i in range(10_000):
        subtask.delay()
        time.sleep(0.01)


if __name__ == '__main__':
    task.delay().get()

В основном задача, которая планирует кучу подзадач. Что может пойти не так?

Я буду использовать procpath для анализа потребления памяти узлами Celery. pip install procpath. У меня 4 терминала:

  1. procpath record -d celery.sqlite -i1 "$..children[?('celery' in @.cmdline)]" для записи статистики дерева процессов узла Celery
  2. docker run --rm -it -p 6379:6379 redis для запуска Redis, который будет выступать в роли брокера Celery и конечного результата.
  3. celery -A demo worker --concurrency 2 для запуска узла с 2 рабочими
  4. python demo.py, чтобы, наконец, запустить пример

(4) закончится менее чем за 2 минуты.

Затем я использую sqliteviz (предварительно созданная версия), чтобы увидеть, что procpath имеет записывающее устройство. Я бросаю туда celery.sqlite и использую этот запрос:

SELECT datetime(ts, 'unixepoch', 'localtime') ts, stat_pid, stat_rss / 256.0 rss
FROM record 

А в sqliteviz я создаю трассировку линейного графика с помощью X=ts, Y=rss и добавляю преобразование разделения By=stat_pid. График результатов:

Утечка узлов сельдерея

Эта форма, вероятно, знакома всем, кто боролся с утечками памяти.

Поиск протекающих предметов

Пришло время dozer. Я покажу неинструментированный случай (и вы можете аналогичным образом инструментировать свой код, если сможете). Чтобы внедрить сервер Dozer в целевой процесс, я буду использовать Pyrasite. Об этом нужно знать две вещи:

  • Для его запуска необходимо настроить ptrace как классический ptrace. разрешения: echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope, что может представлять угрозу безопасности
  • Есть ненулевые шансы, что ваш целевой процесс Python выйдет из строя

С этой оговоркой я:

  • pip install https://github.com/mgedmin/dozer/archive/3ca74bd8.zip (это будет 0.8, о которой я говорил выше)
  • pip install pillow (который dozer используется для построения графиков)
  • pip install pyrasite

После этого я могу получить оболочку Python в целевом процессе:

pyrasite-shell 26572

И введите следующее, которое запустит приложение Dozer WSGI с использованием сервера wsgiref в stdlib.

import threading
import wsgiref.simple_server

import dozer


def run_dozer():
    app = dozer.Dozer(app=None, path='/')
    with wsgiref.simple_server.make_server('', 8000, app) as httpd:
        print('Serving Dozer on port 8000...')
        httpd.serve_forever()

threading.Thread(target=run_dozer, daemon=True).start()

Открыв http://localhost:8000 в браузере, вы должны увидеть что-то вроде:

бульдозер

После этого я снова запускаю python demo.py из (4) и жду его завершения. Затем в Dozer я устанавливаю Floor на 5000, и вот что я вижу:

дозатор показывает утечку сельдерея

Два типа, связанных с сельдереем, растут по расписанию подзадачи:

  • celery.result.AsyncResult
  • vine.promises.promise

weakref.WeakMethod имеет такую ​​же форму и номера и должен быть вызван одним и тем же.

Поиск первопричины

На этом этапе по типам утечек и тенденциям уже может быть ясно, что происходит в вашем случае. Если это не так, Dozer имеет ссылку TRACE для каждого типа, которая позволяет отслеживать (например, видеть атрибуты объекта) рефереры (gc.get_referrers) и референты (gc.get_referents) выбранного объекта и продолжить процесс, снова перемещаясь по графу.

Но картинка говорит тысячу слов, верно? Итак, я покажу, как использовать objgraph для визуализации графа зависимостей выбранного объекта.

  • pip install objgraph
  • apt-get install graphviz

Потом:

  • Я снова запускаю python demo.py из (4)
  • в Dozer я установил floor=0, filter=AsyncResult
  • и нажмите TRACE, который должен дать

след

Затем в оболочке Pyrasite запустите:

objgraph.show_backrefs([objgraph.at(140254427663376)], filename='backref.png')

Файл PNG должен содержать:

backref chart

По сути, есть некий Context объект, содержащий list под названием _children, который, в свою очередь, содержит множество экземпляров celery.result.AsyncResult, которые протекают. Меняя Filter=celery.*context в Dozer, вот что я вижу:

Контекст сельдерея

Итак, виноват celery.app.task.Context. Поиск этого типа обязательно приведет вас к странице задачи Celery. Быстро ищу там детей, вот что там написано:

trail = True

Если этот параметр включен, запрос будет отслеживать подзадачи, запущенные этой задачей, и эта информация будет отправлена ​​с результатом (result.children).

Отключение следа, установив trail=False как:

@app.task(trail=False)
def task():
    for i in range(10_000):
        subtask.delay()
        time.sleep(0.01)

Затем перезапуск узла Celery из (3) и python demo.py из (4) еще раз показывает это потребление памяти.

решено

Проблема решена!

person saaj    schedule 16.04.2020
comment
Классная отладка @saaj! Из вашего минимального примера это выглядит как ошибка / утечка в функции следа celery 4.4, знаете ли вы, сообщалось ли об этом когда-либо сельдерею? - person tutuDajuju; 25.04.2020
comment
@tutuDajuju Спасибо. Что касается сообщения о поведении / ошибке / утечке, учитывая разнообразие случаев использования сельдерея, я не был уверен, что это будет воспринято как ошибка. Функция отслеживания описывается как отслеживание подзадач, запущенных этой задачей, что означает, что некоторые объекты должны сохраняться на время выполнения задачи. Последнее может быть ошибкой / утечкой или работать по назначению, в зависимости от точки зрения. Так что я не сообщил об этом и нашел существующий отчет, когда сам столкнулся с таким поведением. - person saaj; 25.04.2020

Используя gc интерфейс сборщика мусора и sys.getsizeof() Python, можно выгружать все объекты Python и их размеры. Вот код, который я использую в производственной среде для устранения утечки памяти:

rss = psutil.Process(os.getpid()).get_memory_info().rss
# Dump variables if using more than 100MB of memory
if rss > 100 * 1024 * 1024:
    memory_dump()
    os.abort()

def memory_dump():
    dump = open("memory.pickle", 'wb')
    xs = []
    for obj in gc.get_objects():
        i = id(obj)
        size = sys.getsizeof(obj, 0)
        #    referrers = [id(o) for o in gc.get_referrers(obj) if hasattr(o, '__class__')]
        referents = [id(o) for o in gc.get_referents(obj) if hasattr(o, '__class__')]
        if hasattr(obj, '__class__'):
            cls = str(obj.__class__)
            xs.append({'id': i, 'class': cls, 'size': size, 'referents': referents})
    cPickle.dump(xs, dump)

Обратите внимание, что я сохраняю данные только от объектов с атрибутом __class__, потому что это единственные объекты, которые меня волнуют. Должна быть возможность сохранить полный список объектов, но вам нужно будет позаботиться о выборе других атрибутов. Кроме того, я обнаружил, что получение рефереров для каждого объекта было очень медленным, поэтому я решил сохранить только референты. В любом случае, после сбоя полученные маринованные данные можно будет прочитать следующим образом:

with open("memory.pickle", 'rb') as dump:
    objs = cPickle.load(dump)

Добавлено 2017-11-15

Версия Python 3.6 находится здесь:

import gc
import sys
import _pickle as cPickle

def memory_dump():
    with open("memory.pickle", 'wb') as dump:
        xs = []
        for obj in gc.get_objects():
            i = id(obj)
            size = sys.getsizeof(obj, 0)
            #    referrers = [id(o) for o in gc.get_referrers(obj) if hasattr(o, '__class__')]
            referents = [id(o) for o in gc.get_referents(obj) if hasattr(o, '__class__')]
            if hasattr(obj, '__class__'):
                cls = str(obj.__class__)
                xs.append({'id': i, 'class': cls, 'size': size, 'referents': referents})
        cPickle.dump(xs, dump)
person gerdemb    schedule 05.03.2012
comment
Похоже, у sys.getsizeof есть некоторые важные ограничения, о которых следует помнить: docs.python. org / library / sys.html # sys.getsizeof А именно, новые в версии 2.6 и значения по умолчанию будут возвращены, если объект не предоставляет средства для получения размера. В противном случае возникнет ошибка TypeError. - person keturn; 06.03.2012
comment
@keturn Использование pickle.dump () несколько раз для одного и того же файла означает, что мы можем сэкономить много памяти, не сохраняя весь список объектов в памяти перед их обработкой. Это полезно в моей программе, потому что у нее уже заканчивается память, когда я запускаю дамп. - person gerdemb; 06.03.2012
comment
Отличный ответ. Обновление вместо psutil.Process (os.getpid ()). Get_memory_info (). Rss должно быть psutil.Process (os.getpid ()). Memory_info (). Rss на Python 2.7 - person Ramashish Baranwal; 17.06.2015

Не могли бы вы записать трафик (через журнал) на своем производственном сайте, а затем воспроизвести его на своем сервере разработки, оснащенном отладчиком памяти Python? (Я рекомендую дозатор: http://pypi.python.org/pypi/Dozer)

person Brett    schedule 26.09.2008
comment
Может, стоит попробовать. Существуют различные соображения, например, сколько операций ввода-вывода для записи на диск я бы использовал, и создание правильного моментального снимка базы данных для работы с записанными входными данными, но если бы мы могли заставить его работать, это был бы невероятно полезный инструмент. - person keturn; 27.09.2008

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

Мне никогда не приходилось этого делать, поэтому я не уверен на 100%, что это сработает, но, возможно, указатели будут полезны.

person Alex Coventry    schedule 27.09.2008
comment
Да, установка люка была бы невероятно полезной. К сожалению, это не приложение Twisted. - person keturn; 27.09.2008
comment
См. здесь, как можно использовать люк. в (например) приложении Flask. - person Webthusiast; 14.09.2016

Я не знаю, как сбросить все состояние интерпретатора Python и восстановить его. Было бы полезно, я буду следить за этим ответом, если у кого-то есть идеи.

Если у вас есть представление о том, где происходит утечка памяти, вы можете добавить проверки количества ссылок ваших объектов. Например:

x = SomeObject()
... later ...
oldRefCount = sys.getrefcount( x )
suspiciousFunction( x )
if (oldRefCount != sys.getrefcount(x)):
    print "Possible memory leak..."

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

Вот эссе с дополнительной информацией об отладке подобных вещей. Он больше ориентирован на авторов плагинов, но по большей части применим.

Отладка количества ссылок

person joeld    schedule 26.09.2008
comment
К сожалению, я пытаюсь понять, где происходит утечка памяти. Тем не менее, этот подход с поиском большого количества ссылок может быть полезен. - person keturn; 27.09.2008

В gc module есть некоторые функции, которые могут быть полезны, например, перечисление всех объектов, Сборщик мусора недоступен, но не может освободить его, или список всех отслеживаемых объектов.

Если у вас есть подозрение, какие объекты могут протекать, вам может пригодиться модуль weakref. чтобы узнать, собраны ли / когда объекты.

person Torsten Marek    schedule 26.09.2008
comment
Ага. Если бы я мог просто придумать, как взять что-то вроде gc.get_objects и экспортировать его для дальнейшего анализа. Я не думаю, что могу использовать для этого рассол, потому что не все можно мариновать. - person keturn; 27.09.2008

Meliae выглядит многообещающим:

Этот проект похож на heapy (в проекте 'guppy') в попытке понять, как распределяется память.

В настоящее время его основное отличие состоит в том, что он отделяет задачу вычисления сводной статистики и т. Д. Потребления памяти от фактического сканирования потребления памяти. Он делает это, потому что я часто хочу выяснить, что происходит в моем процессе, в то время как мой процесс потребляет огромное количество памяти (1 ГБ и т. Д.). Это также позволяет значительно упростить сканер, поскольку я не выделяю объекты python, пытаясь проанализировать потребление памяти объектами python.

person keturn    schedule 31.01.2010