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

Индекс LLMS: [llms.txt](/llms.txt)

---

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

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

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

```bash
pip install psutil
```

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

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

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

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

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

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

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

```python
import tracemalloc
tracemalloc.start()
# ... работа сервиса ...
snapshot = tracemalloc.take_snapshot()
top_stats = snapshot.statistics('lineno')
for stat in top_stats[:10]:
    print(stat)
```

> [!TIP] `tracemalloc` добавляет небольшой оверхед (~1–2 %). Включайте его только на стенде или при явных подозрениях на утечку.

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

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

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

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

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

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

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

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

```bash
pip install objgraph
```

```python
import objgraph
# Самые частые типы объектов в памяти
objgraph.most_common_types(limit=20)
# Поиск циклических ссылок
objgraph.find_backref_chains(some_object, 'owner')
```

> [!WARNING] `objgraph` работает только с объектами Python. Для нативного C‑расширения или ctypes придется использовать `gdb` или `valgrind`.

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

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

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

> [!NOTE] Если сервис запущен в контейнере с ограничениями `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)` или внешние хранилища с экспирацией |
| Накопление объектов в глобальных списках/модулях | «Смерть» памяти при длительном работе | Периодически проверяйте длину списков, выносите в периодические задачи очистку |

> [!WARNING] Один из самых коварных паттернов — `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** — если сервис ещё жив, выполните одну команду:

```bash
python3 -c "import psutil, os; p=psutil.Process(os.getpid()); print(p.memory_info().rss // 1024, 'KB')"
```

3. **Сбор да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')`
4. **Анализ** — используйте `objgraph.most_common_types()` или `gdb`‑команды `info files`, `bt` для вызова стека. Ищите unexpected количество `dict`, `list`, или объектов вашего доменного класса.
5. **Фиксация** — устраните причину: добавьте `maxsize` к кэшу, устраните циклические импорты, замените `global`‑списки на с ограниченным циклом, настройте `weakref` для наблюдателей.
6. **Профилактика** — добавьте алерт на `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", "
