Схема по материалу: Как я отличаю нехватку памяти Linux от нормального cache и фиксирую OOM до падения сервиса
Схема основана на материале «Как я отличаю нехватку памяти Linux от нормального cache и фиксирую OOM до падения сервиса» и его сигналах. Диагностическая цепочка от доступной памяти и давления PSI до границы cgroup, reclaim, swap и подтверждённого события OOM.

Почему свободная память вводит в заблуждение

В наших внедрениях я начинаю не с процента занятой RAM, а с вопроса, какую часть памяти ядро способно вернуть приложению без тяжёлых последствий. Linux сознательно использует незанятые страницы под page cache и некоторые возвращаемые структуры ядра. Поэтому низкий MemFree часто означает эффективное использование ресурсов, а не аварию. MemAvailable полезнее: ядро оценивает запас, доступный для нового запроса без ухода в swap, учитывая возможный reclaim cache и его безопасную долю. Это не точный обещанный объём, но гораздо содержательнее голого free. Я рядом сохраняю Cached, SReclaimable, SwapFree и динамику MemAvailable, чтобы видеть состав и направление изменения. Диагноз появляется только при согласованности слоёв: доступный запас устойчиво уменьшается, reclaim активизируется, задачи ждут память, задержка сервиса выходит из бюджета или срабатывает ограничение cgroup. Разовый минимум после пакетной обработки сам по себе недостаточен. Особенно осторожно читаю высокий used: в него попадает полезный cache, который при спросе может быть освобождён. Так я отделяю нормальную адаптацию ядра от накопления рабочего набора, которое действительно вытесняет систему к границе. Тревога должна описывать риск для нагрузки, а не эстетически неприятную цифру free.

Что именно говорит PSI

Файл `/proc/pressure/memory` показывает не объём занятой RAM, а время, когда задачи испытывали задержки из-за нехватки ресурса. Строка some означает, что хотя бы часть исполняемых задач была заторможена давлением памяти; full означает периоды, когда все не простаивающие задачи одновременно были заблокированы этим давлением, то есть полезная работа фактически останавливалась. Поля avg10, avg60 и avg300 — скользящие средние доли времени за соответствующие окна, а total — накопленное время задержек в микросекундах с момента загрузки. Для тренда я считаю приращение total между корректно упорядоченными снимками и отдельно смотрю averages. Их нельзя объявлять просто процентом занятой RAM: машина может иметь высокий used из-за cache и почти нулевое давление либо свободный запас на хосте при сильном давлении внутри ограниченной cgroup. Короткий some допустим для отдельных профилей, тогда как устойчивый full рядом с ростом прикладной задержки требует быстрой проверки. После перезагрузки total начинается заново, поэтому источник должен учитывать смену boot. Я не смешиваю линии some и full и не превращаю единичный всплеск avg10 в доказательство утечки. Сначала сопоставляю окно PSI с рабочим циклом сервиса, reclaim, swap и его границей памяти.

Граница хоста и cgroup v2

На контейнерном или systemd-хосте общий запас может выглядеть здоровым, пока конкретная служба упирается в собственную cgroup. Поэтому я сначала подтверждаю cgroup v2 и ControlGroup нужного unit, а уже затем читаю файлы в установленной границе. `memory.current` отражает текущий учитываемый объём cgroup. `memory.max` задаёт жёсткий предел либо содержит max, если такого предела нет. `memory.high` — мягкая граница регулирования: выше неё процессы подвергаются интенсивному reclaim и торможению, но само пересечение не равнозначно OOM. В `memory.events` счётчик high показывает воздействия этой границы, max — сколько раз распределение достигало жёсткого предела, oom — ситуации OOM в cgroup, а oom_kill — фактические убийства задач. Я читаю именно приращения счётчиков, потому что накопленное значение могло относиться к прошлому эпизоду. У хоста и дочерней группы разные области учёта: локальный oom_kill не доказывает исчерпание всей RAM, а благополучный MemAvailable хоста не отменяет малого memory.max. Обратная ситуация тоже важна: отсутствие лимита у службы не защищает её от глобального давления. Идентификатор процесса, путь cgroup и время события связываю до передачи вывода владельцу.

Reclaim, swap и альтернативные объяснения

Чтобы не свести разбор к одному индикатору, я добавляю конечную серию `vmstat` и счётчики `/proc/vmstat`. Поля si и so показывают поступление страниц из swap и выгрузку в него; устойчивое движение вместе с PSI и ростом задержки сильнее разового ненулевого значения. Сам факт настроенного или занятого swap не является ошибкой: давно неиспользуемые страницы могут оставаться там без текущей перегрузки. Major faults, представленные, например, `pgmajfault`, указывают на обращения, потребовавшие ввода, но их причина может быть файловым чтением, а не только swap. Семейства pgscan и pgsteal помогают увидеть работу сканирования и успешного отбора страниц reclaim. Если сканирование ускоряется, а эффективность возврата низка, я проверяю состав памяти и рабочий набор; если pgsteal следует за pgscan без PSI и сервис стабилен, ядро может штатно освобождать cache. Альтернативами остаются холодный запуск, резервное копирование, массовое чтение файлов, изменение потока запросов или локальный лимит контейнера. Для причинности нужны совпадающие временные окна. Один большой cumulative-счётчик после долгой работы ничего не говорит о текущей интенсивности, поэтому я рассчитываю разности и отмечаю перезагрузки.

Как подтверждаю OOM, не преувеличивая доказательство

Событие OOM я подтверждаю сообщением ядра через `journalctl -k` в узком временном окне, а при разрешённом доступе сверяю с `dmesg`. Ищу начало решения OOM killer, killed process, PID, имя процесса, сведения о памяти и упоминание cgroup. Такая запись доказывает, что ядро рассматривало нехватку памяти и могло завершить задачу, однако сама по себе не доказывает первопричину прикладного сбоя. Процесс мог быть побочным потребителем, журнал мог относиться к другой cgroup, а сервис — завершиться раньше по иной причине. PID также переиспользуются, поэтому я связываю событие со временем, unit, ControlGroup и прикладным журналом. При локальном OOM особенно важны приращения memory.events oom и oom_kill: они уточняют область, но не заменяют идентификацию жертвы. Отсутствие записи тоже не гарантирует отсутствия проблемы: журнал бывает ротирован, rate-limited или недоступен пользователю агента. В рабочем контуре я формулирую вывод строго: наблюдалось давление, пересекалась граница, зафиксировано убийство либо подтверждения пока нет. Такое разделение защищает от автоматического обвинения крупнейшего процесса и сохраняет пригодную для эскалации доказательную цепочку.

Безопасный сбор через Zabbix Agent

Для агента я применяю маленький read-only wrapper с фиксированным списком источников. Он не получает аргументов и не строит путь из пользовательского параметра, макроса или имени сервиса. Абсолютные пути `/proc/meminfo`, `/proc/pressure/memory`, `/proc/vmstat` и одной согласованной cgroup задаются администратором внутри файла; выход проверяется как ограниченный набор чисел и строк max. UserParameter определён точной записью из конфигурационного поля, поэтому оператор тестирует именно `itfresh.linux.memory_snapshot`, а не выдуманный ключ. AllowKey разрешает только его; DenyKey сохраняет общий запрет опасных вариантов. Я отклоняю wildcard, `system.run[*]`, широкий sudo, конвейер из оболочки и любой произвольный `cat $1`: такая гибкость позволяет читать чужие файлы или исполнять ввод. Если доступ к cgroup уже есть у пользователя агента, повышение прав не требуется. Если доступа нет, сначала меняют модель предоставления конкретных данных, а не выдают универсальный root. Wrapper возвращает ошибку при исчезновении ожидаемого каталога, чтобы миграция unit не превращалась в правдоподобный ноль. Частоту опроса выбираю после оценки длительности опасных эпизодов, стоимости чтения и риска; единого интервала для всех систем нет.

Контролируемая проверка и критерии остановки

Цепочку проверяю на тестовом хосте или в специально изолированной cgroup с лимитом, который не затрагивает соседей. Сначала фиксирую baseline: memory.current, high, max, events, обе строки PSI, MemAvailable хоста, vmstat и задержку тестового приложения. Затем контролируемая программа небольшими шагами выделяет память, удерживает её и освобождает. Я ожидаю понятную последовательность: current растёт; при дефиците ядро усиливает reclaim; some может увеличиться; пересечение memory.high добавляет событие high и способно замедлить группу; после освобождения current и давление снижаются. Доводить опыт до oom_kill необязательно. Если отдельно проверяется жёсткая граница, это допускается лишь в расходном контейнере с заранее известным процессом-жертвой и без внешнего трафика. Останавливаю выделение при неожиданном full, быстром росте swap-out, нарушении бюджета задержки, влиянии на хост или приближении его MemAvailable к рискованной зоне. На production стресс не запускаю. Результат теста — подтверждение семантики сбора и маршрута тревоги, а не универсальный порог. После освобождения проверяю отсутствие продолжающегося давления и возвращение тестового сервиса в исходное состояние; любые остаточные процессы удаляет владелец тестовой среды по её регламенту.

Решение, эскалация и эксплуатационные границы

В итоговом правиле я разделяю наблюдение и действие. Zabbix сообщает, какая граница исследована, как менялись MemAvailable и PSI, были ли swap/reclaim, какие счётчики memory.events прирастали и найдено ли подтверждение ядра. Он не перезапускает службу и не повышает memory.high или memory.max автоматически: оба действия могут скрыть утечку, усилить глобальное давление или нарушить план ёмкости. Первичное уведомление получает владелец сервиса, который сопоставляет период с нагрузкой и релизами; при подтверждённом системном либо cgroup-риске он эскалирует администратору платформы. Порог строю по baseline сопоставимых рабочих циклов, допустимой задержке, цене ложного пропуска и длительности окна. Для пакетной задачи приемлемый some может отличаться от интерактивного API, а короткий опрос не всегда оправдан стоимостью и шумом. В карточке события сохраняю область хост/cgroup, временное окно, изменения счётчиков и доступность источников. Ограничения признаю явно: PSI не назначает виновника, cache меняется естественно, журнал ядра неполон, а редкий опрос пропускает короткие эпизоды. Если новый сбор даёт ложные данные, откатываю только его конфигурацию; рабочие лимиты и процессы остаются под отдельным управлением.

Сигналы, которые сопоставляю

  • MemAvailable
  • MemFree
  • Cached
  • SReclaimable
  • SwapFree
  • PSI memory some avg10
  • PSI memory full avg10
  • PSI memory some total
  • PSI memory full total
  • memory.current
  • memory.max
  • memory.high
  • memory.events high
  • memory.events max
  • memory.events oom
  • memory.events oom_kill
  • vmstat si
  • vmstat so
  • major faults pgmajfault
  • pgscan
  • pgsteal
  • сообщения OOM killer в журнале ядра

Отвергнутые альтернативы

  • низкий MemFree объясняется возвращаемым page cache без заметного давления
  • рабочий набор хоста действительно приближается к доступной ёмкости
  • давление и OOM ограничены конкретной cgroup при достаточном запасе у хоста
  • интенсивный reclaim или swap создаёт задержки до исчерпания памяти
  • порог memory.high сдерживает cgroup без убийства процесса
  • событие OOM относится к другому процессу, контейнеру или временному окну

Конфигурация и сбор

Я использую один фиксированный ключ без параметров: `UserParameter=itfresh.linux.memory_snapshot,/usr/local/libexec/zabbix-memory-read`. Read-only wrapper `/usr/local/libexec/zabbix-memory-read` не принимает аргументы, пути, макросы или ввод; он читает только allowlist `/proc/meminfo`, `/proc/pressure/memory`, `/proc/vmstat` и заранее настроенный абсолютный каталог одной cgroup, проверяет ожидаемые имена `memory.current`, `memory.max`, `memory.high`, `memory.events`, затем выдаёт числовой JSON. Для ключа задаётся точечный `AllowKey=itfresh.linux.memory_snapshot`; более общий `DenyKey` сохраняет запрет остальных пользовательских команд. Широкие sudo, shell-конструкции, wildcard и пользовательский путь отвергаются: они превращают наблюдение в произвольное выполнение. Определение и wrapper сначала проходят штатную проверку конфигурации и ревью владельцем сервиса; тест ключа выполняется ровно как `zabbix_agentd -t itfresh.linux.memory_snapshot` либо соответствующей бинарной версией установленного агента.

Контролируемая проверка и свидетельство

Проверку давления я разрешаю только на тестовом хосте либо внутри изолированной cgroup/контейнера с отдельным лимитом. Контролируемое приложение постепенно выделяет память, затем освобождает её; параллельно снимаются MemAvailable, PSI some/full, memory.current и приращения memory.events. Ожидается рост current, возможный reclaim и рост PSI, а при достижении high — счётчика high; OOM не является обязательной целью. Критерии немедленной остановки: влияние на соседнюю нагрузку, неожиданное full-давление, swap-шторм, нарушение задержки тестового сервиса или приближение к лимиту хоста. Стресс-тест в рабочем контуре не запускается.

Фрагмент рабочего регламента

Сначала читаю хост: `grep -E '^(MemAvailable|MemFree|Cached|SReclaimable|SwapFree):' /proc/meminfo`; `cat /proc/pressure/memory`; `vmstat 1 10`; `grep -E '^(pgmajfault|pgscan|pgsteal)' /proc/vmstat`. Затем подтверждаю иерархию: `findmnt -t cgroup2`; для systemd-службы получаю путь командой `systemctl show example.service -p ControlGroup --value`. Только после проверки, что значение непустое, не содержит `..` и соответствует ожидаемой службе, оператор подставляет его в заранее просмотренный каталог `/sys/fs/cgroup/<проверенный-ControlGroup>/` и отдельно читает `cat memory.current`, `cat memory.max`, `cat memory.high`, `cat memory.events` из этого каталога. Я сопоставляю снимки с узким окном ядра: `journalctl -k --since '-15 minutes' --no-pager | grep -Ei 'oom|out of memory|killed process'`; при необходимости читаю `dmesg --ctime | grep -Ei 'oom|out of memory|killed process'`, если политика доступа это допускает. Сбор Zabbix проверяется только определённым ключом: `zabbix_agentd -t itfresh.linux.memory_snapshot`. Команды ничего не перезапускают и не меняют лимиты.

Откат

Если сбор создаёт ошибки или лишнюю нагрузку, я удаляю только точное определение `itfresh.linux.memory_snapshot`, связанную точечную AllowKey и wrapper по согласованной процедуре, затем проверяю конфигурацию агента и прекращение опроса элемента. Значения memory.high и memory.max, процессы и службы этот откат не меняет. Любое изменение или перезапуск агента выполняет ответственный администратор в своём окне, а не диагностический сценарий.

Ограничения вывода

Универсального порога и единственного интервала опроса нет: базовую линию и условия тревоги выбирают по профилю нагрузки, бюджету задержки, границе cgroup и риску пропустить короткое давление. PSI показывает время задержек задач, но не называет виновника; счётчики cgroup зависят от выбранной границы; kernel log может быть ротирован или ограничен правами. Мониторинг различает наблюдаемое состояние и действие: автоматический restart либо увеличение лимита запрещены, решение принимает владелец сервиса с эскалацией системному администратору.

Источники

  • https://docs.kernel.org/accounting/psi.html
  • https://www.zabbix.com/documentation/current/en/manual/config/items/userparameters