Утечки памяти в asyncio-приложениях — та ещё головная боль. Стандартные
pympler или objgraph часто показывают не то, потому что объекты висят в стеках корутин или в циклических ссылках внутри Task-ов. Есть два рабочих подхода, которые я сам использую.Трассировка стека через Task.get_stack()
Когда таск зависает в бесконечном ожидании (например, из-за Future, который никогда не завершится), его кадры стека держат ссылки на большие объекты. Берёшь
asyncio.all_tasks(), для каждого вызываешь get_stack(), ищешь кадры с подозрительными локальными переменными. Прямо видишь, что висит и сколько весит:async def leaky_task():
data = [0] * 10_000_000
await asyncio.Future()
tasks = asyncio.all_tasks()
for t in tasks:
stack = t.get_stack()
if stack and 'data' in stack[0].f_locals:
obj = stack[0].f_locals['data']
print(f'Task {t} держит {type(obj)} размером {sys.getsizeof(obj)} байт')
Поиск забытых ссылок через gc.get_objects
Снимаешь снапшот до операции, потом после, ищешь разницу. Только не забудь вызвать
gc.collect() перед каждым снимком, иначе поймаешь кучу короткоживущих объектов.def find_growth(snapshot_before):
new_objects = []
for obj in gc.get_objects():
if id(obj) not in snapshot_before and hasattr(obj, '__class__'):
new_objects.append(obj.__class__.__name__)
return Counter(new_objects).most_common(5)
Почему это работает? В asyncio event loop держит ссылки на все незавершённые таски. Если корутина содержит локальные переменные или замыкания — они не утилизируются, пока таск не завершится.
gc.get_objects находит объекты, которые держатся циклическими ссылками в колбэках или корутинах.Практические советы:
- Включи
gc.set_debug(gc.DEBUG_SAVEALL) — он сохраняет недостижимые объекты, можно посмотреть, кто не собирается.- Для единичных подозрительных корутин
sys.getrefcount до и после — быстро, но грубо.- Для продакшена есть
tracemalloc, который не требует остановки приложения.Типичная ошибка: Не вызывать
gc.collect() перед снимком — тогда разница будет забита короткоживущими объектами из недавних операций, что маскирует реальную утечку.Вывод: Трассировка стека тасков ловит утечки внутри активных корутин, а
gc.get_objects — глобальные забытые объекты и циклические ссылки, и без них вы будете сидеть с логами и гадать, где память утекает.