Я расширю ответ Бретта из своего недавнего опыта. Пакет 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 терминала:
procpath record -d celery.sqlite -i1 "$..children[?('celery' in @.cmdline)]"
для записи статистики дерева процессов узла Celery
docker run --rm -it -p 6379:6379 redis
для запуска Redis, который будет выступать в роли брокера Celery и конечного результата.
celery -A demo worker --concurrency 2
для запуска узла с 2 рабочими
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
. График результатов:
![Утечка узлов сельдерея](https://i.stack.imgur.com/US20P.png)
Эта форма, вероятно, знакома всем, кто боролся с утечками памяти.
Поиск протекающих предметов
Пришло время 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
в браузере, вы должны увидеть что-то вроде:
![бульдозер](https://i.stack.imgur.com/5hDUb.png)
После этого я снова запускаю python demo.py
из (4) и жду его завершения. Затем в Dozer я устанавливаю Floor на 5000, и вот что я вижу:
![дозатор показывает утечку сельдерея](https://i.stack.imgur.com/VoTDD.png)
Два типа, связанных с сельдереем, растут по расписанию подзадачи:
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, который должен дать
![след](https://i.stack.imgur.com/GRp77.png)
Затем в оболочке Pyrasite запустите:
objgraph.show_backrefs([objgraph.at(140254427663376)], filename='backref.png')
Файл PNG должен содержать:
![backref chart](https://i.stack.imgur.com/TrU1M.png)
По сути, есть некий Context
объект, содержащий list
под названием _children
, который, в свою очередь, содержит множество экземпляров celery.result.AsyncResult
, которые протекают. Меняя Filter=celery.*context
в Dozer, вот что я вижу:
![Контекст сельдерея](https://i.stack.imgur.com/sIplB.png)
Итак, виноват 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) еще раз показывает это потребление памяти.
![решено](https://i.stack.imgur.com/CzTin.png)
Проблема решена!
person
saaj
schedule
16.04.2020