Статьи

Диагностика памяти «Утечки» в Python

Эта проблема

Мы написали некоторый новый код в форме задач Celery, который мы ожидаем выполнить до пяти минут и использовать несколько сотен мегабайт памяти. Промойте и повторите для тысячи различных наборов данных. Мы успешно просмотрели несколько наборов данных, но как только мы начали просматривать ВСЕ из них, мы заметили, что память о процессе Celery продолжает расти.

В Celery каждая задача выполняется в одном из фиксированного числа процессов, которые сохраняются между задачами. Мы предположили, что у нас была утечка памяти в наших руках; почему-то мы оставляли ссылки на наши структуры данных, которые оставались в памяти и не собирались мусором между задачами. Но как вы исследуете, что именно происходит?

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

Утилиты Linux

Утилиты командной строки top или более приятный htop должны быть вашей первой остановкой для любого исследования загрузки процессора или памяти. В нашем случае мы заметили, что машине не хватит памяти и начнется подкачка при выполнении наших задач. Поэтому мы снова их запустили и смотрели процессы в htop. На самом деле, процессы росли с первоначального размера в 100 МБ, медленно, вплоть до 1 ГБ, прежде чем мы их убили. Из журналов видно, что на этом пути успешно выполнялись отдельные задачи.

Мы смогли воспроизвести поведение в нашей среде разработки, хотя у нас было достаточно данных, чтобы процесс достигал нескольких сотен мегабайт. После того, как мы имели поведение воспроизводимые в сценарии , который может быть запущен на своем внешнем сельдерей ( с помощью CELERY_ALWAYS_EAGER), мы могли бы с помощью GNU timeкоманды для отслеживания использования памяти пики, то есть /usr/bin/time -v myscript.py.

Примечание: мы указываем полный путь ко времени, чтобы получить команду времени GNU, а не встроенную в bash.

Примечание: в некоторых версиях утилиты есть ошибка, которая неправильно сообщает об использовании памяти , умножая ее в четыре раза. Дважды проверьте, используя top.

Ресурсный модуль

Фактически вы можете получить объем памяти, используемый вашим процессом, изнутри вашего процесса Python, используя модуль ресурсов:

import resource
print 'Memory usage: %s (kb)' % resource.getrusage(resource.RUSAGE_SELF).ru_maxrss

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

Objgraph

После того как вы определили место в своем коде сразу после того, как возникла проблема с памятью, вы также можете запросить объекты, находящиеся в памяти, прямо из Python. Вам, вероятно, нужно сделать pip install objgraphсначала.

import gc
gc.collect()  # don't care about stuff that would be garbage collected properly
import objgraph
objgraph.show_most_common_types()
tuple                      5224
function                   1329
wrapper_descriptor         967
dict                       790
builtin_function_or_method 658
method_descriptor          340
weakref                    322
list                       168
member_descriptor          167
type                       163

бесформенный

Возможно, вам повезет, и вы увидите пользовательский класс, который вы определили в верхней части списка. Но если нет, что именно находится в этих корзинах общего типа? Введите гуппи , который похож show_most_common_typesна стероиды. Опять же, вам, вероятно, нужно будет установить это через pip install guppy. Самое замечательное в guppy / heapy заключается в том, что вы можете сделать снимок кучи до критической секции и после нее, и отразить их, просто получая объекты, которые были добавлены в кучу между ними.

from guppy import hpy
hp = hpy()
before = hp.heap()

# critical section here

after = hp.heap()
leftover = after - before
import pdb; pdb.set_trace()

Вам, вероятно, нужен сеанс pdb , поэтому вы можете в интерактивном режиме исследовать динамическую кучу Лучший учебник по heapy, который я нашел, — Как использовать гуппи / heapy для отслеживания использования памяти .

>leftover
Partition of a set of 134243 objects. Total size = 65671752 bytes.
 Index  Count   %     Size   % Cumulative  % Kind (class / dict of class)
     0  16081  12 45332744  69  45332744  69 unicode
     1  18714  14  5493360   8  50826104  77 dict (no owner)
     2  47441  35  3925672   6  54751776  83 str
     3  21300  16  1786080   3  56537856  86 tuple
     4    344   0   820544   1  57358400  87 dict of module
     5    654   0   685392   1  58043792  88 dict of django.db.models.related.RelatedObject
     6   5543   4   665160   1  58708952  89 function
     7    708   1   640992   1  59349944  90 type
     8   4946   4   633088   1  59983032  91 types.CodeType
     9    705   1   442776   1  60425808  92 dict of type

>leftover.byrcs[0].byid
Set of 16081 <unicode> objects. Total size = 45332744 bytes.
 Index     Size   %   Cumulative  %   Representation (limited)
     0       80   0.0        80   0.0 'media-plugin...re20051219-r1'
     1       76   0.0       156   0.0 'app-emulatio...4.20041102-r1'
     2       76   0.0       232   0.0 'dev-php5/ezc...hemaTiein-1.0'
     3       76   0.0       308   0.0 'games-misc/f...wski-20030120'
     4       76   0.0       384   0.0 'mail-client/...pt-viewer-0.8'
     5       76   0.0       460   0.0 'media-fonts/...-100dpi-1.0.0'
     6       76   0.0       536   0.0 'media-plugin...gdemux-0.10.4'
     7       76   0.0       612   0.0 'media-plugin...3_pre20051219'
     8       76   0.0       688   0.0 'media-plugin...3_pre20051219'
     9       76   0.0       764   0.0 'media-plugin...3_pre20060502

Примечание: дампы памяти были изготовлены для защиты невинных.

GDB

Интересная вещь произошла, когда мы использовали heapy. Мы заметили, что heapy сообщает о 128 МБ объектов в памяти, тогда как модуль ресурсов и top согласились с тем, что используется почти 1 ГБ.

Чтобы получить представление о том, что включает в себя оставшиеся 800+ МБ, мы обратились к gdb, а именно к помощнику по python под названием gdb-heap .

sudo apt-get install libc6-dev
sudo apt-get install libc6-dbg
sudo apt-get install python-gi
sudo apt-get install libglib2.0-dev
sudo apt-get install python-ply

# assuming 7458 is the PID of your memory hogging python process
sudo gdb -p 7458
>generate-core-file

# this will save a .core file, which you can then examine in gdb
sudo gdb python myfile.core -x ~/gdb-heap-commands

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

объяснение

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

Примечание: это поведение может быть специфичным для Linux; Есть неподтвержденные сообщения, что у Python в Windows нет этой проблемы.

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

Сельдерей имеет тенденцию выявлять такое поведение для многих пользователей.

AFAIK это просто, как работает Python. Я предполагаю, что операционная система все равно будет повторно использовать память, поскольку она может просто заменить ее, если она не используется. Если вы выделили часть памяти, есть большая вероятность, что она вам понадобится снова, и лучше делегировать управление памятью операционной системе. … Я не знаю решения, которое заставило бы Python освободить память… Спроси Солема, автора Celery

обходные

В частности, для сельдерея вы можете регулярно проверять рабочие процессы сельдерея. Это именно то, что CELERYD_MAX_TASKS_PER_CHILDделает настройка. Тем не менее, вам может понадобиться сворачивать рабочих так часто, что вы столкнетесь с нежелательными накладными расходами.

Для не-Celery систем вы можете использовать multiprocessingмодуль для запуска любой функции в отдельном процессе. Существует простой вид процедуры, называемый processify, который делает именно это.

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

Вы также можете запускать задания Python, используя Jython, который использует Java JVM и не демонстрирует такого поведения. Кроме того, вы можете обновить до Python 3.3 ,

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