Почему ps и top показывают разную память одного процесса и кому верить
Открываете top, видите у процесса RES 340 МБ. Тут же смотрите ps aux, а там VSZ у того же PID — 1.8 ГБ. Запускаете smem, и он показывает PSS 210 МБ. Три инструмента, один процесс — три разных числа, и все три правильные. Проблема не в инструментах, а в том, что «память процесса» — это не одно число, а несколько разных вопросов, на которые вы неосознанно пытаетесь ответить одним взглядом на монитор.
Содержание
VSZ — это обещание, а не потребление
VSZ (Virtual Size, столбец VSZ в ps, VIRT в top) — это сумма всех областей адресного пространства процесса, независимо от того, сопоставлены ли им реальные физические страницы. Сюда входит:
- вся зарезервированная, но нетронутая heap-память (например, если приложение сделало
mallocбольшого куска и ещё не записало в него ни байта); - все замапленные библиотеки и файлы целиком, даже если реально используется одна функция из библиотеки на 20 МБ;
- зарезервированные, но не закоммиченные регионы (
mmapсMAP_NORESERVE, стек с гарантированным запасом); - любые области, которые процесс освободит по
munmap, даже не начав их использовать.
Проверить это легко:
ps -o pid,vsz,rss,comm -p $(pgrep -n nginx)
У воркера nginx VSZ обычно заметно больше RSS — часть адресного пространства зарезервирована под буферы, которые ещё не понадобились. У Java или Go-процессов разрыв ещё сильнее: рантайм резервирует гигабайты виртуального адресного пространства под будущий heap заранее, но реально коммитит и трогает физические страницы по мере необходимости.
Практический вывод: VSZ почти бесполезен для вопроса «хватит ли серверу RAM». Процесс с VSZ в 4 ГБ на сервере с 2 ГБ RAM может прекрасно работать, если реально трогает лишь пару сотен мегабайт. Резкий скачок VSZ без роста RSS — не повод для паники, это часто нормальное поведение аллокатора или рантайма, который резервирует адресное пространство про запас.
RSS — резидентная память, но она тоже врёт
RSS (Resident Set Size, RES в top) — количество физических страниц RAM, которые сейчас реально отображены в адресное пространство процесса. Это ближе к «реальному потреблению», но у RSS есть два системных искажения, о которые спотыкается почти каждый, кто мониторит память по top.
Первое искажение: RSS считает разделяемые страницы полностью, для каждого процесса. Если библиотеку libc.so или общий модуль подключили 20 воркеров одного и того же приложения, физически в RAM она лежит один раз, но в RSS каждого из 20 процессов войдёт её полный размер. Просуммируйте RSS всех 20 процессов — и получите число заметно больше, чем реально занято в RAM, потому что общую библиотеку вы посчитали 20 раз. Это классическая ошибка при мониторинге пулов воркеров (PHP-FPM, Gunicorn, Unicorn, preforking-модели вообще): «RSS × количество процессов» почти всегда завышает реальное потребление, и чем больше у процессов общих замапленных файлов, тем сильнее искажение.
Второе искажение: RSS включает страницы, отображённые из page cache файлов, которые ядро в любой момент может вытеснить под давлением памяти. Если процесс мапит файл через mmap() и читает его, страницы попадают в RSS процесса, но физически это те же страницы page cache, которые ядро использует для кэширования файлового ввода-вывода и готово отдать первыми при нехватке RAM — без OOM и без потери данных приложения. Про то, куда вообще девается «свободная» память и как её на самом деле использует ядро, подробнее в статье про page cache — RSS файловых mmap-страниц концептуально ближе к этому кэшу, чем к «настоящему» потреблению приложения.
Проверить разбивку RSS по типам можно через /proc/PID/status:
grep -E 'Vm(RSS|Size)|Rss(Anon|File|Shmem)' /proc/PID/status
RssAnon— приватная анонимная память: heap, стек, всё, что процесс аллоцировал сам и ни с кем не делит. Это самая «честная» часть RSS.RssFile— страницы, отображённые из файлов (бинарник, библиотеки, mmap-файлы). Разделяются с другими процессами, мапящими те же файлы, и вытесняемы под давлением памяти.RssShmem— System V shared memory, POSIX shm, tmpfs. Тоже разделяемая память, но, в отличие отRssFile, обычно не бэкается обычным файлом на диске и не вытесняется так же легко.
Если у процесса RssAnon составляет основную часть RSS — это действительно его собственная память, и рост RssAnon во времени — надёжный сигнал утечки. Если основная часть — RssFile, число может расти и падать вместе с активностью page cache, и это не утечка.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверОт SHR к PSS: честная доля процесса в общей памяти
Столбец SHR в top — это грубая прикидка разделяемой части RSS, третье поле в /proc/PID/statm (size resident shared text lib data dt). У похожих процессов (несколько воркеров одного приложения) SHR обычно почти одинаковый и заметный — они мапят один бинарник и одни библиотеки. Но statm не умеет корректно делить страницы, которые мапят не все, а только часть процессов из группы, — для точной картины нужен следующий уровень детализации: PSS.
PSS (Proportional Set Size) решает проблему двойного счёта иначе, чем SHR: каждая разделяемая страница делится поровну между процессами, которые её реально мапят. Если библиотеку в 40 МБ разделяют 20 процессов, в PSS каждого войдёт по 2 МБ от неё, а не все 40 МБ, как в RSS. Просуммируйте PSS всех процессов на сервере — и получите число, близкое к реально занятой физической памяти, без переучёта общих страниц.
PSS считается по /proc/PID/smaps (полная детализация по каждой VMA) или по более дешёвому в чтении /proc/PID/smaps_rollup (появился в ядре 4.14, отдаёт агрегат сразу, без перечисления сотен отдельных областей — заметно быстрее для скрипта мониторинга, который опрашивает /proc каждые несколько секунд):
grep -E '^(Pss|Rss|Private|Shared)' /proc/PID/smaps_rollup
Классический инструмент, который считает PSS сразу по всем процессам и выдаёт таблицу — smem:
smem -k -p -s pss | head -20
Ни ps, ни top, ни базовая версия htop этого по умолчанию не показывают — они читают агрегированные VmRSS из /proc/PID/status, а не построчный /proc/PID/smaps, потому что построение полного smaps для сотен процессов ощутимо дороже по CPU, чем чтение status. Некоторые сборки htop умеют добавлять колонку PSS вручную через настройку отображаемых полей, но это не поведение по умолчанию — стоит проверить в вашей версии, прежде чем полагаться на неё.
USS — сколько реально освободится при убийстве процесса
USS (Unique Set Size, иногда называют Private) — это память, которая принадлежит только этому процессу и ни с кем не разделяется: Private_Clean + Private_Dirty из smaps. Это ответ на конкретный практический вопрос: «если я убью процесс прямо сейчас, сколько RAM освободится в системе?» Разделяемые библиотеки и файлы никуда не денутся, пока их мапит хотя бы один живой процесс — освободится только приватная часть.
USS обычно меньше RSS и меньше PSS. Разница между RSS и USS — грубая мера того, насколько процесс «завязан» на разделяемые ресурсы: у процесса с маленьким бинарником и статическим heap она невелика, у типового воркера с общим рантаймом и библиотеками — заметна.
| Метрика | Что показывает | Где взять | Когда полезна |
|---|---|---|---|
| VSZ / VIRT | Зарезервированное адресное пространство | ps, top, VmSize | Почти никогда для оценки RAM; полезна для отладки лимитов адресного пространства |
| RSS / RES | Резидентные страницы, с двойным счётом общих | ps, top, VmRSS | Тренд одного процесса во времени (рост = подозрение на утечку) |
| SHR | Разделяемая часть RSS | top, statm поле 3 | Грубая прикидка, сколько в RSS — общее |
| PSS | Доля процесса в общей памяти, без двойного счёта | smaps, smaps_rollup, smem | Сумма по всем процессам ≈ реально занятая физическая память |
| USS / Private | Только приватная память процесса | smaps (Private_Clean+Dirty) | «Сколько освободится, если убить процесс» |
Как это выглядит на практике: пул воркеров и общий рантайм
Возьмём типичную ситуацию: 10 воркеров PHP-FPM или 10 процессов Gunicorn под одним приложением. Каждый воркер — форк родителя, поэтому первое время после старта они массово разделяют страницы с родителем и друг с другом (почему fork() не копирует память сразу, а страницы становятся приватными только при записи — в статье про fork и copy-on-write). Со временем воркеры начинают отличаться: буферы запросов, состояние конкретных обработчиков — это приватная анонимная память, уникальная для каждого воркера.
Если вы смотрите ps aux | grep php-fpm и складываете RSS всех десяти — получите число, которое включает 10-кратный учёт общего рантайма, расширений и общих библиотек. Реальное потребление этого пула ближе к: (RSS одного воркера, представляющий общую часть) + (сумма приватных частей всех десяти). Именно поэтому smem -P php-fpm часто показывает суммарный PSS заметно меньше суммы RSS по ps.
Это не абстрактная разница на бумаге — она напрямую влияет на решения о лимитах. Если вы считаете, сколько RAM резервировать под сервис, по сумме RSS воркеров, вы почти наверняка закладываете лишний запас. Если считаете по сумме USS — рискуете недооценить, потому что при масштабировании (запуске ещё N воркеров) разделяемая часть тоже вырастет пропорционально, просто не так резко, как приватная. О том, как вообще подходить к расчёту запаса RAM на сервер, — в статье сколько оперативной памяти закладывать с запасом.
Что из этого важно для мониторинга и OOM
Ядро при выборе жертвы OOM killer ориентируется в первую очередь на RSS-подобную метрику (badness score учитывает резидентную память процесса и его потомков), а не на PSS — «тяжёлый» процесс с точки зрения ядра это тот, у кого много резидентных страниц, независимо от того, разделяет он их с кем-то или нет (подробнее — в статье как ядро выбирает жертву OOM killer). Отсюда практический вывод: RSS в top — ровно та метрика, на которую стоит смотреть, пытаясь предсказать, кого убьёт OOM killer следующим, даже если для честной оценки суммарного потребления системы она искажена двойным счётом.
Для дашбордов мониторинга имеет смысл развести три разные метрики по разным целям:
- Тренд RSS конкретного процесса во времени — дешёвый сигнал для алерта «процесс растёт и не останавливается» (подозрение на утечку). Читается из
/proc/PID/statusкаждые несколько секунд без заметной нагрузки на CPU. - Сумма PSS по группе процессов одного сервиса (или
memory.currentиз cgroup v2, если сервис запущен как systemd-unit или контейнер) — метрика для вопроса «сколько реально RAM занимает этот сервис целиком». Cgroup-счётчик устроен иначе, чем PSS: он засчитывает физическую страницу той cgroup, которая обратилась к ней первой, а не делит её поровну — ещё один, третий способ считать ту же память, с собственными нюансами на границе между control groups. Подробнее — в статье как cgroups ограничивают контейнер. /proc/meminfo(илиfree -m) — самая надёжная метрика для вопроса «хватает ли серверу памяти прямо сейчас»: считается ядром напрямую по физическим страницам, без суммирования по процессам, и не подвержена двойному счёту общих библиотек в принципе. Её стоит проверять первой, если тревога сработала по серверу в целом, а не по конкретному процессу.
Полное чтение /proc/PID/smaps по всем процессам заметно дороже по CPU, чем чтение /proc/PID/status, особенно на сервере с сотнями процессов. smaps_rollup снижает эту стоимость, но не убирает полностью. Разумный компромисс — опрашивать RSS часто (для трендов и быстрых алертов), а PSS/USS — реже, раз в минуту или по требованию при разборе конкретного инцидента с памятью.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверНужны сами нейросети для контента?
Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.
Частые вопросы
Почему free -m показывает мало свободной памяти, хотя сумма RSS всех процессов заметно меньше total?
Разница — это page cache, буферы ядра и прочие некасающиеся конкретных процессов структуры. free считает по /proc/meminfo напрямую от ядра, а не суммированием per-process RSS, поэтому эти два числа не обязаны совпадать и сравнение между ними напрямую малоинформативно.
Если сложить RSS всех процессов, получится ли реальный total used памяти на сервере?
Нет, и почти всегда с превышением — из-за двойного счёта разделяемых библиотек и файлов. Сумма PSS ближе к реальности, но тоже не идентична used из free, потому что не включает память ядра, слэб-аллокатор и прочие несвязанные с конкретными процессами структуры.
Какую метрику проверять первой при алерте "мало памяти на сервере"?
Сначала /proc/meminfo (или free -m) — понять, действительно ли свободной памяти мало, а не занята page cache, которая освобождается по требованию. Дальше — RSS процессов по top, чтобы найти конкретного виновника, и уже потом, при необходимости точной картины, PSS через smem.
Почему у только что запущенного процесса RSS иногда почти такой же большой, как у процесса, который работает часами?
Если процесс сразу мапит и трогает большие файлы (например, загружает модель или индекс в память при старте) или наследует много общих страниц от fork() родителя, RSS может быть высоким с первых секунд. Высокий начальный RSS сам по себе не говорит об утечке — важен тренд после старта, а не абсолютное значение в момент запуска.
Стоит ли ставить алерты по VSZ?
Обычно нет для абсолютного значения — слишком много легитимных причин для большого VSZ. Резкий скачок VSZ без соответствующего роста RSS может быть сигналом, что приложение зациклилось в mmap-вызовах и не освобождает адресное пространство, но это редкий и специфичный кейс, не универсальное правило мониторинга.
Обсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →