Python-сервис расходует память: как найти живые выделения через tracemalloc

После перезапуска сервис занимает немного памяти, затем растёт с каждой выгрузкой. Первая реакция — чаще вызывать сборщик мусора. Но если приложение продолжает хранить ссылки на результаты, сборщик ничего не освободит: объекты всё ещё нужны с точки зрения программы. Полезнее сначала найти, какие выделения пережили обработку нескольких одинаковых заданий, и только потом выбирать изменение.

Сравнивайте одинаковые стадии работы

У сервиса есть прогрев: импорты, первый вызов библиотеки, создание соединений и заполнение небольших справочников. Снимок до прогрева и снимок после первой выгрузки смешают нормальное создание постоянных объектов с возможным накоплением. Лучше сначала выполнить типичную операцию, дождаться её завершения, а затем взять начальную точку. Повторите тот же сценарий несколько раз и снова дождитесь окончания работы.

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

Небольшое накопление, которое можно увидеть

import gc
import tracemalloc

retained = []

def export_batch():
    retained.extend(bytearray(4096) for _ in range(30))

tracemalloc.start(5)
gc.collect()
before = tracemalloc.take_snapshot()
export_batch()
export_batch()
gc.collect()
after = tracemalloc.take_snapshot()
changes = after.compare_to(before, "lineno")
positive = [item for item in changes if item.size_diff > 0]
assert len(retained) == 60
assert sum(item.size_diff for item in positive) > 60 * 4096
for item in positive[:3]:
    print(item)
retained.clear()
gc.collect()
print("retained:", len(retained))
tracemalloc.stop()

В отчёте появится строка создания bytearray с положительным приростом. Размер включает служебные расходы, поэтому точное число байтов зависит от реализации и версии Python. Проверка опирается на нижнюю границу полезной нагрузки, а не на заранее выписанный красивый вывод. Список retained объясняет причину: приложение само сохраняет каждый блок. Очистка меняет владение объектами; дополнительный вызов gc до очистки не мог удалить их.

Строка выделения и причина удержания — разные места

Tracemalloc указывает, где появилась память. Это хорошая точка входа, но строка создания объекта может быть совершенно правильной. Дальше проследите, куда объект передаётся после завершения запроса: общий список результатов, кэш, callback, задача, замыкание. Например, функция создаёт корректный отчёт, а вызывающая сторона добавляет его в историю без ограничения. Менять построение отчёта в таком случае бессмысленно.

Выберите несколько записей с наибольшим положительным size_diff и сопоставьте с ожидаемым временем жизни данных. Если справочник должен жить весь процесс, его наличие нормально. Если содержимое предыдущего запроса больше нигде не используется, найдите владельца ссылки. Удобно записать ответ одним предложением: «отчёт остаётся в словаре recent_reports до перезапуска». После этого решение становится конкретным: ограничение размера, срок жизни либо отказ от хранения.

Когда снимок не объясняет рост процесса

Tracemalloc отслеживает выделения памяти Python, а показатель памяти процесса включает и другие источники. Нативная библиотека может выделять память по собственным правилам. Кроме того, освобождение Python-объектов не обязано сразу уменьшить память, которую показывает операционная система. Поэтому сравнение снимков tracemalloc используйте для ответа о живых отслеживаемых выделениях. Если они стабильны, а память процесса растёт, продолжайте исследование на уровне используемой библиотеки и процесса.

Начинайте трассировку до подозрительного сценария. Увеличение глубины стека помогает отличить одинаковый вызов из разных путей приложения, но добавляет расходы. На рабочей системе сначала оцените возможность воспроизвести проблему на отдельном экземпляре. Цель первого запуска — связать рост с обработкой задания; для этого обычно полезнее несколько сопоставимых снимков, чем долгий сбор всего происходящего без выбранного вопроса.

Проверка исправления

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

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

Если подозрение связано с временем жизни ресурсов, посмотрите закрытие генератора после break и управление несколькими ресурсами через ExitStack. Они помогают отделить удержание объектов от незавершённой очистки файлов и контекстов.

Освоить диагностику памяти последовательно

В платном курсе «Python для профессионалов» есть урок о tracemalloc и memory_profiler, раздел о сборке мусора и подсчёте ссылок, а также приёмы оптимизации кода. Следующая задача после этого разбора — понять владельцев объектов и выбрать инструмент измерения для своего приложения. Откройте программу курса и изучите раздел «Оптимизация и профилирование».

Продолжить на практике

  • Python для профессионалов ↗

    Python для профессионалов — курс для людей, стремящихся углубить свои знания языка. Вы изучите итераторы, генераторы, декораторы, современные возможности Python (3.8+), освоите метапрограммирование, асинхронность, внутреннее устройство интерпретатора, инструменты оптимизации и профилирования, а также получите уверенность в использовании продвинутых возможностей стандартной библиотеки.