Короткий ответ
Нужно сопоставить рост памяти процесса во времени с ростом числа ключей в подозрительных структурах (кэши, реестры подписчиков, сессии), а не искать только циклические ссылки — неограниченный словарь-кэш растёт без всякого цикла, просто за счёт накопления новых уникальных ключей.
Как это работает подробнее
В отличие от утечки через циклические ссылки, здесь объекты абсолютно «легитимно» достижимы — они лежат в живом словаре кэша, на который есть обычная сильная ссылка из долгоживущего объекта приложения; сборщик мусора здесь не поможет, так как объекты не являются мусором с точки зрения интерпретатора.
Пример кода
example-279.py
# Намеренно проблемный паттерн - для анализа и диагностики
_user_sessions_cache = {} # растёт на каждого нового пользователя, никогда не очищается
def get_session(user_id):
if user_id not in _user_sessions_cache:
_user_sessions_cache[user_id] = load_heavy_session_data(user_id)
return _user_sessions_cache[user_id]
# Диагностика: метрика len(_user_sessions_cache) растёт линейно с числом уникальных user_id
# Исправление: functools.lru_cache(maxsize=N) или TTL-кэш (например, cachetools.TTLCache)Что использовать на практике
- Практика. Добавляют метрику
len(cache)в мониторинг и смотрят на её рост во времени параллельно с RSS процесса; если метрика растёт линейно с числом уникальных запросов и никогда не уменьшается — это и есть причина. Решение — ограничить кэш по размеру и времени жизни записей (LRU сmaxsize, TTL).
Подводные камни
- Риски. Ищут исключительно циклические ссылки через
gc, хотя проблема вообще не связана со сборщиком мусора; добавляют данные в глобальный словарь «на всякий случай для отладки» в production коде без TTL и ограничения размера.