Программирование PythonПамять и производительностьИнженер по производительности Python

В приложении RSS процесса растёт, но снимки tracemalloc почти не меняются: какой механизм это объясняет?

В приложении RSS процесса растёт, но снимки tracemalloc почти не меняются: какой механизм это объясняет?

Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

tracemalloc отслеживает не весь объём памяти процесса, а только трассируемые блоки памяти, выделенные через поддерживаемые Python-аллокаторы. Поэтому рост RSS при почти неизменном tracemalloc обычно означает, что память выделяется нативным кодом, через неотслеживаемый malloc или mmap, либо расходуется самим аллокатором и библиотечными кэшами.

Эти метрики отвечают на разные вопросы: tracemalloc помогает найти источники Python-аллокаций, а RSS показывает, сколько страниц процесса фактически находится в оперативной памяти. Для диагностики утечки их нужно сопоставлять, а не считать взаимозаменяемыми.

Исторический контекст

Управление памятью в Python скрывает детали выделения объектов от разработчика. Для поиска источников роста Python-объектов в стандартную библиотеку добавили tracemalloc: он сохраняет трассировки мест, где выделялись отслеживаемые блоки, и позволяет сравнивать снимки.

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

Постановка проблемы

Предположим, сервис принимает данные, создаёт временные объекты Python и передаёт их библиотеке на C. Через несколько часов RSS увеличивается на гигабайты, но число живых Python-объектов и объём, показанный tracemalloc, остаются примерно постоянными.

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

Подробное решение

RSS — это объём страниц процесса, резидентных в физической памяти в данный момент. Он зависит не только от живых Python-объектов, но и от нативных выделений, загруженных библиотек, отображений файлов, страниц стеков, копирования при записи и поведения системного аллокатора.

tracemalloc собирает информацию о трассируемых блоках и связывает их с местами выделения в Python-коде. Он полезен для ответа на вопрос «какие участки Python-программы удерживают или создают больше отслеживаемой памяти», но не обязан отражать произвольные вызовы malloc, mmap и внутренние хранилища сторонних C-библиотек.

Возможны несколько причин расхождения:

  • расширение на C выделяет данные напрямую и не использует отслеживаемый Python-аллокатор;
  • библиотека удерживает внутренний кэш или пул буферов;
  • системный аллокатор получил память у ОС, но пока не вернул её, даже если пользовательские данные освобождены;
  • память отображена через mmap или файл и учитывается в RSS, но не представлена как обычная Python-аллокация;
  • tracemalloc был запущен после выделения значительной части памяти или его область наблюдения не совпадает с измеряемым периодом.

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

У tracemalloc есть цена: хранение трассировок увеличивает потребление памяти и может влиять на производительность. Поэтому его обычно включают в воспроизводимом тесте или на ограниченный период, а не безусловно оставляют в высоконагруженном production-процессе.

Ситуация из практики

Сервис преобразования изображений после каждой операции освобождал Python-объекты, но RSS постепенно рос. Снимки tracemalloc показывали небольшой стабильный объём, а профилирование времени выполнения указывало на нативную библиотеку обработки изображений.

Рассматривались варианты:

  • оптимизировать контейнеры и строки Python — дёшево и безопасно, но не объясняло основной рост;
  • принудительно вызывать сборку мусора — могло уменьшить число циклических объектов, но не освобождало нативные буферы и ухудшало задержки;
  • ограничить внутренний кэш библиотеки — эффективно при кэшировании, но бесполезно при настоящем удержании буферов;
  • использовать нативный профилировщик и измерять RSS по этапам обработки — требовало отдельного тестового стенда, зато позволяло увидеть источник вне Python.

Выбрали последний вариант и дополнительно ограничили размер рабочих пакетов. Профилирование показало, что библиотека сохраняла буферы для повторного использования; после настройки её кэша RSS перестал расти без необходимости увеличивать сборку мусора или переписывать Python-код.

Что кандидаты часто упускают

  1. Означает ли неизменный tracemalloc, что утечки памяти нет?

Нет. Это означает только отсутствие заметного роста среди отслеживаемых им блоков. Утечка может находиться в нативном расширении, в буферах библиотеки, в отображениях памяти или в ресурсах, которые не представлены обычными Python-объектами. Для вывода об отсутствии утечки нужно сопоставить Python-профилирование с RSS и, при необходимости, нативным инструментом.

  1. Почему RSS может не уменьшиться сразу после освобождения нативной памяти?

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

  1. Достаточно ли сравнить только текущее значение RSS со снимком tracemalloc?

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