Перейти к содержимому

исправление утечек памяти в python-сервисах: диагностика и сбор дампов

Python-сервисы под нагрузкой могут silently расходовывать память до hitting cgroup limit и OOM-kill. Без систематического сбора дампов и интроспекции root-cause hunt превращается в перебор гипотез. Ниже — проверенный набор команд и скриптов для диагностики, сбора хит-дампов и устранения типичных утечек.

1. Команды диагностики потребления памяти

Базовый уровень — psutil. Устанавливается одной строкой и работает без перезапуска процесса.

pip install psutil

Текущее потребление текущего процесса:

import psutil, os
p = psutil.Process(os.getpid())
info = p.memory_info()
print(f"RSS: {info.rss}  VMS: {info.vms}")

Расширенная структура одной командой:

full = p.memory_full_info()
print(full)
# Attributes: python, rss, vms, shared, text, lib, data, dt

Краткая таблица ключевых атрибутов psutil.Process.memory_full_info():

АтрибутОписание
pythonПамять, выделенная внутри интерпретатора Python
rssResident Set Size — физическая память в RAM
vmsVirtual Memory Size — virtual address space
sharedОбщая память (shared libraries, mmap)
text, lib, dataСегменты кода, библиотек, данных ELF

Для отслеживания динамики роста за короткий интервал можно использовать psutil в цикле или обертку watch -n 1 psutil ..., но на production чаще включают tracemalloc, встроенный в CPython.

import tracemalloc
tracemalloc.start()
# ... работа сервиса ...
snapshot = tracemalloc.take_snapshot()
top_stats = snapshot.statistics('lineno')
for stat in top_stats[:10]:
    print(stat)
tracemalloc добавляет небольшой оверхед (~1–2 %). Включайте его только на стенде или при явных подозрениях на утечку.

2. Инструменты для отслеживания роста и сбора дампов

Когда RSS начинает расти незаметно, нужны более глубокие инс펙ции. Набор зависит от доступности отладчика и разрешения на ptrace.

gdb + gcore — классический способ выгрузить полный дамп процесса без его остановки (при условии включенного coredump).

# 1. Найдите PID
pgrep -f your_service

# 2. Подключите gdb и выгрузите дамп
gdb -p <PID>
# внутри gdb:
(gdb) gcore /tmp/heap_dump.core
(gdb) quit

Результат gcore — бинарный файл, который можно анализировать локально:

# Показать статистику по сегментам
gdb -ex "info files" -ex "quit" /tmp/heap_dump.core
# Или через python-плагины gdb (см. ниже)

objgraph — утилита для быстрого ответа на вопрос «кто держит объект».

pip install objgraph
import objgraph
# Самые частые типы объектов в памяти
objgraph.most_common_types(limit=20)
# Поиск циклических ссылок
objgraph.find_backref_chains(some_object, 'owner')
objgraph работает только с объектами Python. Для нативного C‑расширения или ctypes придется использовать gdb или valgrind.

tracemalloc + heapdump — встроенный механизм Python 3.4+ может сохранить снимок кучи в файл.

import tracemalloc, sys
tracemalloc.start()
# ... ...
snapshot = tracemalloc.take_snapshot()
snapshot.dump('/tmp/tracemalloc.dump')

Дамп можно открыть в визуализаторе (например, python -m pdb или сторонние GUI), но для глубокого анализа все же удобнее gdb.

Если сервис запущен в контейнере с ограничениями cap_sys_ptrace, сбор gcore может требовать привилегий или использования kubectl exec с доступом к хосту.

3. Типичные антипаттерны и чего избегать

АнтипatternПоследствиеРекомендация
Игнорирование gc.collect() как «волшебной кнопки»Ложное чувство безопасности, утечка продолжаетсяИспользуйте gc.collect() для очистки временных ссылок, но не как лечение логической утечки
Опора только на __del__ для освобождения ресурсовНерегулярное освобождение, circular referencesПредпочитайте contextlib.contextmanager, try/finally или weakref
Отсутствие лимитов памяти (cgroup/ulimit)Острый OOM-kill без дампа, потери контекстаВсегда ставьте memory.limit в манифестах и проверяйте ulimit -v
Кэши без TTL или роста без пределовМощленный рост RSSИспользуйте functools.lru_cache(maxsize=N) или внешние хранилища с экспирацией
Накопление объектов в глобальных списках/модулях«Смерть» памяти при длительном работеПериодически проверяйте длину списков, выносите в периодические задачи очистку
Один из самых коварных паттернов — del obj в __del__ при наличии circular references. Python’s GC eventually collects them, но порядок не гарантирован, что приводит к пикам памяти между коллекциями.

4. Сценарий реагирования на OOM-инцидент

  1. Подтверждение — проверьте событие в кластере: kubectl get events -n <ns> | grep OOM или dmesg | grep out of memory. Убедитесь, что процесс terminated с кодом 137.
  2. Быстрый взгляд на RSS — если сервис ещё жив, выполните одну команду:
python3 -c "import psutil, os; p=psutil.Process(os.getpid()); print(p.memory_info().rss // 1024, 'KB')"
  1. Сбор даmpа — выберите метод в зависимости от доступности инструментов:
    • Если есть gdb и права: gdb -p <PID> -batch -ex gcore -ex quit /tmp/oom_dump.core
    • Если нужно быстро: python3 -c "import tracemalloc; tracemalloc.start(); ..." и tracemalloc.take_snapshot().statistics('traceback')
  2. Анализ — используйте objgraph.most_common_types() или gdb‑команды info files, bt для вызова стека. Ищите unexpected количество dict, list, или объектов вашего доменного класса.
  3. Фиксация — устраните причину: добавьте maxsize к кэшу, устраните циклические импорты, замените global‑списки на с ограниченным циклом, настройте weakref для наблюдателей.
  4. Профилактика — добавьте алерт на rss > 80% от лимита в Prometheus/Grafana и документируйте команду сбора дампа в runbook.

Завершение диагностики на этом этапе позволяет либо восстановить сервис, либо собрать достаточно данных для тикета в трекере с конкретными типами объектов и стеками вызовов. Here’s a thinking process:

  1. Analyze the Request:
  • Topic: Fixing memory leaks in Python services: diagnostics and dump collection
  • Role: Practicing Lead DevOps
  • Format: IT notes for a Hugo blog (Mana theme)
  • Style: Short paragraphs, concrete commands, flag tables, code with fences, no fluff, no marketing, no emojis, no author signature at end
  • Allowed: GitHub admonitions (> [!NOTE], > [!TIP], > [!WARNING])
  • Prohibited: Made-up flags, URLs, versions; uncertain facts → cautious tone; no YAML/TOML front matter; no wrapping in ```; start with a lid (2-4 sentences); sections with ##; practical commands, tables if needed; 800-1600 words; end on last substantive section; don’t repeat rules, don’t write “User wants”, "