Парадокс расхода памяти- что показывает RSS и почему tracemalloc молчит
Когда процесс в Linux отчитывается о RSS примерно в 3 ГБ, а tracemalloc показывает лишь 180 МБ, многие разработчики приходят в замешательство. На первый взгляд кажется, что tracemalloc "теряет" почти три гигабайта - но разница объясняется тем, что оба инструмента измеряют разные аспекты использования памяти.
RSS (Resident Set Size) отражает общий объём физической памяти, удерживаемой процессом, включая стек, сегменты кода, распределения, выделенные вне Python (библиотеки, буферы ОС) и кучи C-партий.
Tracemalloc же отслеживает только распределения, сделанные через Python-аллокаторы - то есть объекты Python и внутренние буферы, которые проходят через механизм выделения памяти интерпретатора.
Поэтому несоответствие в гигабайты не обязательно указывает на утечку в Python-коде: большая часть памяти может приходиться на внешние библиотеки, кэш ОС или аллокации на стороне C. Чтобы корректно оценивать проблему, важно разделять "видимое" Python-памяти и "скрытую" память вне ведения tracemalloc.
Нельзя полагаться лишь на один инструмент: нужен системный подход и комбинация утилит, позволяющих заглянуть в разные слои использования памяти.
Какую память не видит tracemalloc
Tracemalloc фиксирует только объекты, созданные через PyObject и аллокации внутри интерпретатора.
Поэтому следующие источники утекающей памяти останутся невидимыми для этого инструмента: крупные буферы и структуры, выделенные нативными библиотеками (numpy, pandas, sqlite, ssl), память, захваченная подкачкой или mmap-файлы, а также внутренние сегменты аллокатораов C (malloc, jemalloc) и пулы менеджера памяти Python.
Кроме того, статические данные и сегменты исполняемого файла, кеши динамических библиотек и стековые фреймы тоже увеличивают RSS, но не фиксируются tracemalloc.
Также многие популярные расширения для Python ведут выделение больших блоков через собственные механизмы, и затем разбивают их на более мелкие участки.
Tracemalloc увидит лишь мелкие объекты, но большую часть памяти останется учтённой в RSS. Понимание этого разрыва - ключ к правильной диагностике.
Практический план поиска недостающих гигабайтов
Начать стоит с общего обзора: какие категории памяти используют процесс больше всего. Для этого пригодятся системные утилиты: pmap, smem, /proc/<pid>/smaps.
Pmap быстро покажет распределение по областям памяти, smem даст информацию о proportional set size и других метриках, а smaps - подробную разбивку по mmap-сегментам и их флагам.
Сравнивая вывод этих инструментов с данными tracemalloc, можно локализовать крупные регионы, которые не учтены в tracemalloc. Если в smaps обнаружены большие анонимные mmap-области или сегменты, то вероятные виновники mmap-выделения в расширениях, буферы нативных библиотек или пул аллокатора.
В таком случае полезно изучить, какие библиотеки загружены (lsof, /proc/<pid>/maps) и есть ли в процессе выделения больших файлов или кешей.
Утилиты и методы диагностики
- Используйте pmap -x <pid> для быстрой карты областей памяти с размерами. Это даст представление, какие сегменты занимают большую часть RSS. - Откройте /proc/<pid>/smaps - там вы найдёте подробную информацию по каждому mmap-участку: размер, RSS, PSS, Swap, и атрибуты.
PSS особенно полезен при анализе совместного использования страниц между процессами. - Если подозреваете аллокации через malloc/jemalloc, можно подключить инструменты профилирования на уровне libc/allocator: mallinfo, jemalloc stats и соответствующие профайлеры. Они покажут, сколько памяти удерживается в пулах аллокатора и не возвращено ОС.
- Для нативных расширений применимы инструменты вроде ltrace/strace (отслеживание вызовов mmap/malloc), а также средства анализа heap нативных библиотек (valgrind/memcheck, massif). Эти инструменты дадут представление о выделениях вне Python-кучи.
При работе с крупными библиотеками типа numpy или pandas стоит проверить, используются ли они с внешними аллокаторами или создают ли большие массивы через mmap. Иногда массивы остаются в памяти дольше, чем ожидается, из-за ссылок или кешей.
Практические приёмы для устранения
После локализации крупных регионов можно предпринимать конкретные шаги: избавляться от ненужных ссылок в Python-коде, явно освобождать ресурсы библиотек (close, release, del и вызов gc. collect), переключаться на memory-mapped файлы при работе с очень большими данными либо использовать более аккуратные API, минимизирующие кучу C.
Если причина - пул аллокатора, настройка переменных окружения для jemalloc/malloc_trim или переход на другой аллокатор может существено снизить RSS. В случае с расширениями - пересмотреть жизненный цикл объектов и, если возможно, использовать интерфейсы, позволяющие возвращать память обратно ОС.
Важно помнить: снижение tracemalloc-отчётности не всегда уменьшит RSS, пока память удерживается вне Python. Комплексный анализ и понимание, какие слои отвечают за какие выделения, - залог эффективной оптимизации.