В микросервисной архитектуре и телеком-ботах на Python (таких как боты управления на python-telegram-bot или демоны синхронизации) процессы работают непрерывно месяцами. На виртуальных серверах с 1–2 GB RAM постепенная утечка даже 2–3 мегабайт в день рано или поздно приводит к срабатыванию механизма ядра Linux OOM Killer, который без предупреждения завершает процесс.
Типичные источники утечек в asyncio
В отличие от классического синхронного кода, в асинхронных приложениях утечки чаще всего связаны не с открытыми файловыми дескрипторами, а с жизненным циклом корутин и ссылочными связями:
- Незавершённые фоновые Task: Создание задач через
asyncio.create_task()без сохранения ссылки в множестве (set) или без обработки исключений приводит к накоплению "висячих" корутин в event loop. - Замыкания в Callback-функциях: Привязка обработчиков событий, удерживающих ссылки на тяжелые объекты контекста (например, полные JSON-конфиги или декодированные медиафайлы).
- Глобальные кэши без TTL: Неограниченно растущие словари (dicts) истории пользовательских сессий.
Инструментарий диагностики: tracemalloc
Встроенный модуль tracemalloc позволяет отслеживать точные строки кода, аллоцирующие память в рантайме, практически без снижения производительности сервиса:
import asyncio
import tracemalloc
import logging
logger = logging.getLogger("mem-sentinel")
async def memory_monitor_daemon(interval_sec: int = 3600):
tracemalloc.start(25) # сохраняем до 25 кадров стека вызовов
snapshot_prev = None
while True:
await asyncio.sleep(interval_sec)
snapshot_curr = tracemalloc.take_snapshot()
if snapshot_prev:
top_stats = snapshot_curr.compare_to(snapshot_prev, 'lineno')
logger.info("--- ТОП 5 ПРИРОСТА ПАМЯТИ ПО СТРОКАМ ---")
for stat in top_stats[:5]:
logger.info(str(stat))
snapshot_prev = snapshot_curr
Ограничение ресурсов через Systemd
Чтобы процесс не мог занять всю память операционной системы и уронить критические службы (Nginx, Sing-Box), в системный юнит .service всегда добавляются лимиты cgroups:
# /etc/systemd/system/bot.service
[Service]
MemoryMax=150M
MemoryHigh=120M
Restart=always
RestartSec=5
При достижении отметки MemoryHigh ядро начинает активно вытеснять неиспользуемые страницы процесса в swap, а если утечка прогрессирует до MemoryMax, сервис перезапускается за доли секунды без остановки всей ОС.