Эта проблема
Мы написали некоторый новый код в форме задач 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 ,
В конечном итоге, лучшее решение — просто использовать меньше памяти. В нашем случае мы разбили работу на более мелкие куски (отдельные дни). Для некоторых задач это может оказаться невозможным или может потребовать сложной координации задач.