Что мы называем «утечкой памяти» в Linux
На практике «linux memory leak» почти всегда сводится к одному из сценариев:
- Настоящая утечка: приложение удерживает ссылки на объекты/буферы и реально накапливает память.
- Кэш/арены аллокатора (glibc malloc, jemalloc, tcmalloc): память возвращается приложению, но не всегда отдаётся ядру; RSS выглядит «липким».
- Рост mmap: приложение мапит анонимные регионы (например, большие буферы, JIT, базы, очереди) и не освобождает или удерживает их слишком долго.
- File-backed страницы: процесс активно читает файлы, RSS растёт, но значительная часть — страницы, которые ядро может вытеснить при давлении по RAM.
- Shared memory: tmpfs, SysV/Posix SHM, memfd; RSS «у всех», а физически это может быть одна копия.
Проблема в том, что мониторинги часто показывают один показатель (обычно RSS), а он не отвечает на вопрос «кто виноват» и «куда ушла память». Дальше — набор инструментов, которые помогают разложить потребление по составляющим.
RSS vs PSS: почему один и тот же процесс «ест» по-разному
RSS (Resident Set Size) — сколько страниц процесса сейчас находится в RAM. Если страница шарится (общие библиотеки, shared memory), она попадёт в RSS каждого процесса, который её мапит.
PSS (Proportional Set Size) — «пропорциональный» RSS: шаренная страница делится между процессами. Если страницу делят 4 процесса, то каждому добавится по 1/4 страницы в PSS. Для оценки «кто сколько реально занимает физической памяти» PSS почти всегда полезнее.
Если у вас несколько воркеров (web/queue/cron), ориентируйтесь на PSS для оценки реального вклада процесса. RSS хорош как индикатор «что-то происходит», но плохо подходит для распределения ответственности.
Утечка часто выглядит как рост RSS, но PSS при этом растёт гораздо медленнее (или почти не растёт), если увеличивается file-backed часть или шаренные маппинги.
В продакшене удобнее разбирать такие истории на изолированном стенде, где вы контролируете swap/overcommit и легко снимаете слепки. Для этого обычно берут отдельный инстанс на VDS.

Быстрая проверка: PID, status и «кто главный по памяти»
Начните с простого: найдите кандидатов по RSS и посмотрите, кто стабильно наверху. Это не «истина», но быстрый старт для дальнейшего разложения на anon/file/shmem.
ps -eo pid,comm,rss,etimes --sort=-rss | head
Затем загляните в /proc/PID/status — там удобные агрегаты, которые дают направление расследования.
pid=1234
cat /proc/$pid/status | egrep 'Name|VmRSS|VmHWM|VmSize|RssAnon|RssFile|RssShmem'
Ключевые поля:
VmRSS— текущий RSSVmHWM— пик RSS (High Water Mark), полезно понять «это давно так» или «выросло недавно»RssAnon— анонимные страницы (типично heap/stack/anon mmap)RssFile— file-backed (библиотеки, mmap файлов)RssShmem— shared memory
Если растёт RssAnon, чаще всего это heap/anon mmap. Если растёт RssFile, смотрим mmap файлов и поведение IO (иногда это норма для читающего сервиса).
smaps_rollup: самый полезный «одной командой»
Файл /proc/PID/smaps_rollup — агрегированная версия smaps. Это лучший старт для «профиля памяти» без парсинга сотен регионов.
pid=1234
cat /proc/$pid/smaps_rollup
Что смотреть в первую очередь:
Rss— общий RSS процессаPss— общий PSS процессаPss_Anon,Pss_File,Pss_Shmem— PSS по типам памятиAnonymous— сколько анонимных страниц в маппингахSwap— иногда «утечка» уходит в swap, а RSS выглядит терпимо
Если растёт именно Pss_Anon (и вместе с ним RssAnon в status) — это уже похоже на проблему внутри процесса: утечка, кэш аллокатора или активный mmap anon.
/proc/PID/smaps: когда нужно понять, какие регионы растут
/proc/PID/smaps — детализация по каждому VMA (виртуальному региону памяти). Там видно, какие маппинги дают вклад: heap, stack, анонимные регионы, memfd, конкретные файлы, shared libs.
Главная боль smaps — объём и необходимость суммирования. Но для расследования утечки важнее не «всё», а топ по PSS/RSS и их изменение во времени.
Ниже — практичный способ сгруппировать данные по «заголовку региона» и сложить PSS. Это не идеально (разные anon-регионы могут иметь одинаковую шапку), но часто даёт быстрый ответ.
pid=1234
awk '
/^([0-9a-f]+-)+[0-9a-f]+/ {h=$0; next}
/^Pss:/ {
pss=$2
key=h
gsub(/^[0-9a-f-]++/, "", key)
sum[key]+=pss
}
END {
for (k in sum) printf "%10.0f kB %s\n", sum[k], k
}
' /proc/$pid/smaps | sort -nr | head -40
Как это читать:
- строки вида
[heap]— классический heap (malloc) [stack],[stack:tid]— стеки потоков[anon], пустой путь или безымянные регионы — это и есть «mmap anon»- пути к
.so— библиотеки (обычно shared; PSS будет умеренным) - пути к файлам данных — mmap файлов или file-backed страницы
/memfd:...или(deleted)— частая находка при утечках через временные объекты или JIT
Поля внутри smaps, на которые реально смотреть
Для каждого региона полезны:
Size— размер виртуального региона (не равно RAM)Rss— сколько страниц региона в RAMPss— пропорциональный вкладPrivate_Clean/Private_Dirty— приватные страницы (не разделяются). Рост приватных — сильный сигнал утечки/кэша.Shared_Clean/Shared_Dirty— шаренные страницыAnonymous— сколько страниц в этом регионе анонимные
Если «растёт утечка», часто растут Private_Dirty и/или Anonymous в анонимных регионах или [heap].
pmap: быстрый обзор карты памяти и RSS по маппингам
pmap удобен, когда нужно быстро глазами увидеть «что за регионы» и их размеры. Он не заменяет smaps, но хорош как первый «рентген».
pid=1234
pmap -x $pid | head
Самое полезное — итоговая строка total kB и столбцы RSS/Dirty для крупных маппингов.
Если вы видите большие безымянные регионы с заметным Dirty, это типичная история про mmap anon. Если большие регионы привязаны к конкретному файлу, это file-backed память.
smem: когда нужно сравнить процессы «честно», с PSS
smem — инструмент для ответа на вопрос «какие процессы реально занимают память», а не «у кого большой RSS». Он умеет показывать PSS/USS.
Установка (пример):
apt-get update && apt-get install -y smem
dnf install -y smem
Топ процессов по PSS:
smem -tk | head -30
Топ по USS (Unique Set Size) — только приватные страницы процесса (условно «то, что можно освободить, убив процесс», без shared):
smem -uk | head -30
Это ключ к разбору истории «20 воркеров, каждый показывает 500 MB RSS»: shared библиотеки и shared memory раздувают RSS каждого, а PSS/USS обычно ставят всё на место.
Практический сценарий расследования (чек-лист)
Последовательность, которая чаще всего экономит время:
- Подтвердить тренд: растёт ли память монотонно и на каком горизонте (минуты/часы/дни). Снимите несколько точек.
- Сверить RSS с PSS: если PSS не растёт, «утечка» может быть иллюзией RSS.
- Разделить anon vs file:
/proc/PID/status(RssAnon/RssFile) иsmaps_rollup(Pss_Anon/Pss_File). - Найти растущий тип маппинга: топ по PSS из
smapsили быстрый взгляд черезpmap -x. - Сопоставить с поведением приложения: всплески трафика, очереди, фоновые джобы, загрузка файлов, компиляция шаблонов, кэши.
- Понять, это «не отдаёт аллокатор» или «реально накапливает»: перезапуск процесса освобождает? есть ли plateau? как ведёт себя после нагрузки?
Если вы крутите много сайтов/проектов и важно предсказуемое соседство по ресурсам, часто проще вынести тяжёлый сервис на отдельный VDS и уже там спокойно измерять PSS/USS и влияние swap.
Типовые «подводные камни»: почему память не возвращается
glibc malloc и арены (много потоков, много воркеров)
glibc malloc может держать арены на поток, фрагментировать кучу и не спешить возвращать память ядру. В результате:
- после пика нагрузки RSS остаётся высоким;
- внутренне приложение «свободно», но ОС этого не видит;
- в
smapsрастёт[heap]и приватные dirty страницы.
Это не всегда утечка. Часто это цена за производительность. Но если на сервере тесно по RAM, становится проблемой: начинается swap, растут задержки, падает стабильность.
mmap anon: большие регионы, которые выглядят как утечка
Многие рантаймы и библиотеки берут память через mmap (не через brk/heap). Тогда рост будет не в [heap], а в безымянных регионах. В smaps это видно как большие anonymous-маппинги, а в smaps_rollup растёт Pss_Anon.
Частая причина — буферы, кэши, пул соединений, компрессия/архивация, обработка больших файлов, JIT, in-memory индексы. Отдельно обращайте внимание на /memfd:... и отметки (deleted): нередко там и лежит «пожиратель» RAM.
File-backed RSS: «кажется, что течёт», но это page cache
Если растёт RssFile/Pss_File, ищите mmap файлов и интенсивное чтение. Это может быть нормой: ядро держит часто используемые страницы в памяти. При давлении по RAM они будут вытеснены, а процесс продолжит работать (только медленнее).

Снимки во времени: как увидеть, что именно растёт
Один снимок smaps_rollup полезен, но утечки — это динамика. Простейший «без агентов» способ — сохранить несколько слепков и сравнить.
pid=1234
ts=$(date +%F_%H%M%S)
cat /proc/$pid/smaps_rollup > /tmp/smaps_rollup_${pid}_${ts}.txt
cat /proc/$pid/status > /tmp/status_${pid}_${ts}.txt
Сравнение двух точек:
diff -u /tmp/smaps_rollup_1234_OLD.txt /tmp/smaps_rollup_1234_NEW.txt | sed -n '1,120p'
Если нужно сравнить распределение по регионам (heap/anon/file), делайте одинаковую агрегацию из smaps и сравнивайте топ-строки.
Когда пора лезть в приложение: профилирование аллокаций и рантайма
Если вы доказали, что растёт именно Pss_Anon и/или приватные dirty страницы (а не file cache), дальше два пути:
- системный уровень: понять, heap это или anon mmap, и какой компонент потребляет;
- уровень приложения: включить профилирование аллокаций и найти места выделения.
Для PHP-сервисов полезно параллельно исключить «память не причина, причина — зависшие воркеры». В этом помогает разбор медленных запросов через slowlog: как читать PHP-FPM slowlog в продакшене.
Если у вас JIT/VM и есть подозрение на рост через JIT-код или связанные буферы, посмотрите, как это обычно выглядит и какие метрики контролировать: PHP 8 JIT в продакшене: риски и диагностика.
Правильная последовательность: сначала PSS/anon/file на уровне ядра, затем выбор инструмента профилирования в рантайме. Иначе вы рискуете оптимизировать красивый график RSS, а не первопричину.
Мини-памятка: что использовать в какой момент
- Нужно быстро понять, кто виноват в RAM на хосте:
smem -tk(сортировка по PSS). - Нужно понять, из чего состоит память конкретного процесса:
/proc/PID/smaps_rollup. - Нужно найти конкретные растущие маппинги:
/proc/PID/smaps+ агрегация, и/илиpmap -x. - Нужно отделить утечку от «не отдаёт аллокатор»: сравнить несколько снимков, посмотреть рост приватных dirty страниц, проверить поведение после пика нагрузки (есть ли plateau), при необходимости — включить профилирование в рантайме/аллокаторе.
Диагностические вопросы, которые экономят часы
- Растёт
Pssили толькоRss? - Растёт
Pss_AnonилиPss_File? - Рост идёт в
[heap]или в безымянных регионах (mmap anon)? - Есть ли корреляция с нагрузкой, кроном, конкретным типом запросов?
- После снятия нагрузки память возвращается (полностью/частично/никогда)?
- Сколько процессов/воркеров, и какой их реальный вклад по PSS/USS?
Если вы ответили на эти вопросы, то в большинстве случаев уже понятно, куда копать дальше: настройки воркеров и аллокатора, конкретные кэши, утечка объектов в коде или неконтролируемые mmap-регионы.
Заключение
В поиске «утечки» главное — не застрять на RSS. Комбинация smem (PSS по процессам), smaps_rollup (сводка по процессу), smaps (детализация по регионам) и pmap (быстрый обзор) даёт понятную картину: что именно растёт — heap, mmap anon, shared memory или file-backed страницы. А уже после этого имеет смысл включать профилировщики и исправлять первопричину в приложении.
Если сервис крутится в продакшене и важны предсказуемые ресурсы, проще всего жить с такими расследованиями на отдельной машине/инстансе: на VDS вы быстрее воспроизводите нагрузку, сравниваете слепки и контролируете swap/overcommit без сюрпризов от соседей.


