Load average 15 при простаивающем CPU - не сбой датчика. Что он считает, когда приходит OOM killer и какое решение действительно помогает.
Load average 18 при почти простаивающем процессоре обычно читают неправильно: «раз число большое, значит, процессору не хватает мощности» - и первым делом берут тариф с бОльшим числом vCPU. Половину таких случаев чинит не апгрейд ядер, а лишний гигабайт памяти или лимит на зарвавшийся контейнер. Эта статья разбирает именно память: что на самом деле показывает load average, когда за дело берётся OOM killer - механизм ядра, который принудительно убивает процессы, чтобы не дать системе зависнуть насмерть без памяти - и как отличить его работу от банальной нехватки CPU. Если ваш случай скорее про диск, сеть или CPU steal, начните с общего разбора в статье «Диагностика Linux-сервера: что делать, когда сервер тормозит» - здесь разбирается только память, и разбирается глубже, чем там.
Коротко. Load average - это не проценты загрузки CPU, а экспоненциально сглаженное среднее число процессов, которые либо считают на CPU, либо ждут его, либо застряли в состоянии D (uninterruptible sleep) - обычно из-за диска или подкачки. OOM killer срабатывает, когда ядро не может выделить память даже после сброса файлового кэша - либо когда исчерпаны RAM и своп всего хоста, либо когда контейнер или systemd-юнит с лимитом упирается в свой собственный memory.max, пока на хосте полно свободного места. Жертву выбирает по «badness»-оценке, а не по фиксированному рейтингу. Доказательство - строка в
dmesgилиjournalctl -k: «Out of memory» для хоста, «Memory cgroup out of memory» для контейнера - это разные события, и путать их не стоит. Своп откладывает убийство, но не лечит нехватку; лимиты на контейнеры ограничивают ущерб от одного зарвавшегося сервиса; апгрейд RAM - единственное решение, если нехватка не разовая, а постоянная.
Три числа в выводе uptime - это не «загрузка процессора в процентах», хотя выглядят они именно так на первый взгляд. Технически это экспоненциально сглаженные скользящие средние количества процессов в очереди планировщика за 1, 5 и 15 минут, а в эту очередь ядро Linux, в отличие от BSD, кладёт два разных типа процессов: тех, кто реально выполняется или ждёт свободного ядра CPU, и тех, кто застрял в состоянии D (uninterruptible sleep) - системном вызове, который нельзя прервать до его завершения, обычно чтении с диска, сетевой файловой системе или обмене со свопом.
Посмотрите на вывод сами:
uptime
w покажет то же самое плюс список залогиненных пользователей и их активные команды - полезно, когда нужно понять, кто на сервере что запустил именно сейчас.Отсюда главная ошибка: сравнивать load напрямую с числом ядер и делать вывод «CPU занят на 100%». Это верно только тогда, когда очередь состоит из процессов, реально готовых считать. Load 20 при простаивающем CPU - не сбой датчика, а двадцать процессов, застрявших в ожидании чего-то медленного, а не в очереди на ядро. Первая проверка - глянуть на строку %Cpu(s) в top: если id (простой) высокий, а wa (ожидание ввода-вывода) тоже высокий, дело не в процессоре вообще.
Диск - не единственная причина, из-за которой процессы застревают в D. Вторая частая причина - нехватка оперативной памяти: когда её не хватает, ядро начинает подкачку (перекладывает страницы памяти на диск и обратно), и каждое обращение процесса к своей памяти вместо мгновенного чтения из RAM превращается в операцию ввода-вывода на диск - процесс формально «ждёт диск», хотя корень проблемы в памяти.
Разложить эти два случая помогает vmstat - он показывает и очередь на CPU, и активность подкачки в одном выводе:
vmstat 1 5
vmstat -y 1 5, чтобы пропустить его.r - число процессов, реально готовых считать на CPU прямо сейчас. Если она устойчиво выше числа ядер (nproc) - конкуренция за процессор настоящая.b - число процессов в состоянии D. Устойчиво высокая b при низкой r - это не CPU, ищите, чего они ждут.si и so - скорость подкачки страниц в память и из памяти, КБ/с. Ноль означает, что прямо сейчас обмена со свопом нет; ненулевые и устойчивые значения - активная подкачка, и именно она обычно превращает «просто медленно» в «система как будто зависла».Дальше смотрите на саму память:
Важна колонка available, а не free - она показывает, сколько памяти реально можно отдать новым процессам без похода в своп, включая ту часть buff/cache (файловый кэш), которую ядро отдаст по первому требованию. Если available близко к нулю одновременно с ненулевыми si/so - это и есть подкачка-пробуксовка, а не нехватка CPU. Своп при этом может быть занят даже без активного обмена прямо сейчас - его объём проверяется отдельно:
Если этой команде нечего вывести - свопа на сервере нет вообще, и тогда нехватка памяти не сглаживается подкачкой, а сразу упирается в OOM killer.
OOM killer - часть ядра Linux, а не отдельная служба. Он вступает в дело, когда какому-то процессу нужно выделить память, а ядро физически не может её найти: страничный кэш уже сброшен, всё, что можно было переложить в своп, туда переложено, а своп либо тоже заполнен, либо его нет вовсе. В этот момент у ядра остаётся один вариант - принудительно завершить один или несколько процессов, чтобы освободить память и не дать системе зависнуть насмерть.
Жертву ядро выбирает не по алфавиту и не «самый новый процесс первый». Для каждого процесса на момент срабатывания оно вычисляет оценку badness - грубо говоря, долю системной памяти, которую занимает процесс, скорректированную значением oom_score_adj (диапазон от -1000, что полностью исключает процесс из отбора, до +1000, что делает его кандидатом номер один). Убивается процесс с наибольшей badness именно в этот момент - оценка пересчитывается заново при каждом срабатывании, а не берётся из какого-то заранее известного «рейтинга». Посмотреть текущие значения для конкретного процесса можно так:
На практике это значит: тяжёлый процесс базы данных с большим объёмом занятой памяти - частый кандидат в жертвы просто по объёму, даже если реальная утечка памяти происходит в соседнем, куда более лёгком процессе. Это одна из причин не доверять первому впечатлению «убило именно то, что и вызвало проблему» без дополнительной проверки, о которой ниже.
Классический симптом - процесс просто исчез, а в его собственных логах ни слова об ошибке: он не успел ничего записать, потому что получил SIGKILL мгновенно и без права на последнее слово. Доказательство ищите не в логах приложения, а в логе ядра:
Вот реальная строка, полученная 25 сентября 2026 намеренным вызовом OOM killer на тестовом сервере HIP («stress-ng --vm 1 --vm-bytes 2500M» без свопа) - структура совпадает с тем, что ожидалось по вторичным источникам; числа и имя процесса у вас будут другими:
Out of memory: Killed process 1068570 (stress-ng-vm) total-vm:2620784kB, anon-rss:1237080kB, file-rss:512kB, shmem-rss:0kB, UID:0 pgtables:2504kB oom_score_adj:1000
1068570 (stress-ng-vm) - PID и имя убитого процесса, вместе с временной меткой из -T.total-vm - весь виртуальный адресный диапазон процесса, обычно намного больше реально занятой памяти - не главный показатель.anon-rss - реально занятая процессом оперативная память (анонимные страницы, не файлы на диске) - вот эта цифра ближе всего к «сколько он на самом деле съел».oom_score_adj - тот самый параметр, который сдвигал вероятность стать жертвой в ту или иную сторону.Буфер dmesg ограничен по размеру и обнуляется при перезагрузке. Если сервер уже перезагружали, ищите ту же информацию в systemd-журнале, который переживает ребут при включённом персистентном журналировании:
Флаг -b ограничивает вывод текущей загрузкой; -b -1 покажет предыдущую, если нужно найти событие, случившееся до последнего ребута.
Здесь начинается путаница, с которой чаще всего сталкиваются именно разработчики, а не классические админы. Cgroup (control group) - механизм ядра, который ограничивает, сколько CPU, памяти и прочих ресурсов может использовать группа процессов; каждый Docker-контейнер и каждый systemd-юнит с заданным лимитом живут в своей cgroup. Флаг --memory у docker run (или mem_limit в Docker Compose) и директива MemoryMax= в unit-файле systemd делают одно и то же на уровне ядра - выставляют потолок memory.max для этой конкретной группы процессов.
Ключевой момент, который часто упускают: контейнер с лимитом может быть убит собственным OOM killer'ом cgroup задолго до того, как закончится память на всём сервере. Ядро следит за лимитом каждой cgroup отдельно, и если процессы внутри неё пытаются превысить именно её потолок - убивает процесс внутри этой группы, даже если free -h на хосте в этот момент показывает свободными половину гигабайт памяти. Это не системный OOM, а его локальная версия, ограниченная одной группой процессов.
Различаются и следы в логе. Системное убийство начинается со слов «Out of memory: Killed process» - именно оно разобрано выше. Убийство по лимиту cgroup начинается со слов «Memory cgroup out of memory: Killed process», и рядом обычно есть строка с путём вроде oom_memcg=/system.slice/docker-<id контейнера>.scope (проверено вживую 25 сентября 2026 через docker run --memory=50m: современный Docker на Ubuntu 24.04 использует systemd как cgroup-драйвер по умолчанию, поэтому путь идёт через system.slice, а не через отдельную иерархию /docker/<id>, которую иногда приводят как пример более старые источники, писавшие под cgroupfs-драйвером), указывающая, какому именно контейнеру или юниту принадлежал лимит. Увидев вторую формулировку, не тратьте время на поиск утечки на уровне всего сервера - смотрите конкретно в тот контейнер или сервис, чей путь указан в строке.
Для Docker есть отдельная и более прямая проверка, не требующая грепать dmesg вручную:
true - контейнер убит именно нехваткой памяти в своей cgroup.false при коде завершения 137 - контейнер получил SIGKILL (128 + номер сигнала 9 = 137) по какой-то другой причине, не по памяти; сам по себе код 137 ничего не доказывает без этой проверки.Для systemd-юнита с MemoryMax= симптом на первый взгляд похож - systemctl status покажет что-то вроде «Main process exited, code=killed, status=9/KILL» - но это тоже не доказывает именно память саму по себе, пока вы не подтвердили это той же строкой «Memory cgroup out of memory» в journalctl -k. Стоит знать и о более мягком пороге: помимо жёсткого memory.max, у cgroup v2 есть memory.high - при превышении этого более низкого порога ядро не убивает процесс, а начинает агрессивно тормозить и вытеснять его память, то есть контейнер сначала заметно тупит, и только потом, если ситуация не выправилась, доходит до убийства по memory.max. Это тот же паттерн «медленно, а не сразу мертво», что и с подкачкой на хосте, только в масштабе одного контейнера.
Отдельно стоит знать про systemd-oomd - пользовательскую службу, которая следит за метриками PSI (pressure stall information, доля времени, которое cgroup «теряет» из-за нехватки памяти) и убивает процессы превентивно, ещё до классического OOM killer'а ядра. На десктопных сборках Ubuntu она включена по умолчанию, на серверных образах - как правило нет, так что на типичном VPS вы почти наверняка увидите именно классический OOM killer, а не systemd-oomd; если всё же установили её сами, её решения ищутся отдельно, через journalctl -u systemd-oomd.
Если на одном VPS крутится сразу несколько контейнеров без явных лимитов - типичная ситуация из статьи про запуск нескольких сервисов через Docker Compose - именно отсутствие лимитов и есть причина, по которой убийство одного контейнера потом выглядит как загадочная нехватка памяти на всём сервере: без лимита контейнер конкурирует за общую память хоста наравне со всеми, и жертвой системного OOM killer вполне может стать не он, а соседний, ни в чём не виноватый сервис.
Параметр | OOM killer хоста | OOM killer cgroup (Docker/systemd) |
|---|---|---|
Что закончилось | RAM и своп всего сервера | лимит memory.max одной конкретной группы процессов |
Состояние хоста при срабатывании | память реально исчерпана целиком | на хосте может быть свободно много гигабайт |
Строка в dmesg / journalctl -k | «Out of memory: Killed process...» | «Memory cgroup out of memory: Killed process...» + путь oom_memcg=/system.slice/docker-<id>.scope |
Кто становится жертвой | процесс с наибольшей badness среди вообще всех процессов сервера | процесс внутри той же ограниченной группы, что упёрлась в лимит |
Доп. проверка | free -h, vmstat, /proc/<pid>/oom_score | docker inspect --format '{{.State.OOMKilled}}', systemctl status юнита |
Порядок действий, который отделяет память от CPU и от диска за пару минут: uptime или w для самого load average, vmstat 1 для колонок r/b и подкачки, free -h для запаса памяти, dmesg -T | grep -i "killed process" для истории OOM. Ниже - как читать сочетания.
Что видно в терминале | Вероятная причина |
|---|---|
load высокий, CPU почти простаивает, r низкая, b высокая, si/so = 0 | ожидание диска или сети - не память, см. хаб-статью про диагностику диска |
load высокий, CPU почти простаивает, b высокая, si/so ненулевые, available в free -h около нуля | подкачка-пробуксовка: памяти не хватает прямо сейчас |
load ~ nproc, us/sy высокие в top | реальная нехватка CPU, не память |
процесс исчез без ошибки, в dmesg строка «Out of memory: Killed process» | сработал системный OOM killer |
контейнер перезапускается, docker inspect показывает OOMKilled: true, а free -h на хосте показывает много свободной памяти | убийство по лимиту cgroup (--memory / MemoryMax=), а не нехватка на хосте |
Тут я не буду делать вид, что все три варианта равнозначны - у каждого своя честная роль, и путать их местами вредно.
Своп - затычка, а не лечение. Он откладывает момент, когда придёт OOM killer, превращая жёсткий обрыв в медленную деградацию - это лучше, чем внезапная смерть процесса, но не решает нехватку памяти как таковую: если рабочий набор процессов стабильно больше RAM, подкачка будет идти постоянно, и это само по себе тормозит систему сильнее, чем помогает. Держать своп стоит как страховку от редких кратких всплесков - разовая сборка проекта, обновление пакетов, - а не как постоянную замену недостающей оперативной памяти. Если на сервере ещё нет свопа:
fallocate -l 2G резервирует файл фиксированного размера под своп - на тарифах с диском в 10-20 ГБ не берите файл больше пары гигабайт, это тоже место на диске.chmod 600 закрывает файл от чтения другими пользователями - в своп могут попасть куски памяти чужих процессов.mkswap размечает файл под своп, swapon включает его немедленно, до перезагрузки.Чтобы своп включался и после перезагрузки, добавьте строку в /etc/fstab:
Что вы должны увидеть. Команда swapon --show после этого покажет файл /swapfile с заданным размером - если вывод пуст, своп не включился.
Лимиты на контейнеры - это не бюрократия, а способ ограничить ущерб. Оставлять контейнеры без лимитов памяти удобнее писать, но хуже в аварии: без лимита один сбойный сервис с утечкой памяти конкурирует за всю память хоста наравне со всеми остальными, и когда дело доходит до системного OOM killer, badness-оценка вполне может выбрать жертвой соседний, ни в чём не повинный контейнер, а не тот, что реально течёт. Явный --memory у Docker или MemoryMax= в unit-файле systemd превращает «сервер лёг целиком, непонятно из-за чего» в «упал ровно один сервис, ровно тот, что и должен был», плюс автоматический рестарт (restart: unless-stopped в Compose или Restart=on-failure в systemd) чинит это без вашего участия.
Апгрейд RAM - когда нехватка не разовая, а постоянная. Если пробуксовка свопа видна не раз в неделю при бэкапе, а под обычной ежедневной нагрузкой - ни своп, ни лимиты контейнеров эту проблему не решают, они лишь делают её более предсказуемой. Своп в такой ситуации будет постоянно активен, и это уже не страховка, а хроническое состояние, которое напрямую бьёт по задержке ответа. Переход на тариф с бОльшим объёмом памяти - в терминах панели HIP это вкладка «Масштаб» карточки хиплета (так HIP называет отдельный VPS в своей панели) и линейка тарифов Memory, отдельно рассчитанная под память, - не откладывает проблему, а закрывает её на уровне причины. Ожидайте, что на время применения нового тарифа сервер попросят выключить - это штатное поведение при смене конфигурации, а не сбой.
Load average считает не только процессы, готовые считать на CPU, но и процессы в состоянии D (uninterruptible sleep) - застрявшие в системном вызове, обычно из-за диска, сетевой файловой системы или подкачки. При load 15 и почти простаивающем CPU смотрите на колонки si/so в vmstat 1: нередко за этим стоит не диск, а активная подкачка из-за нехватки памяти.
Выполните sudo dmesg -T | grep -i "killed process" или journalctl -k -b | grep -i "out of memory". Строка вида «Out of memory: Killed process 1068570 (stress-ng-vm)» с временной меткой и именем процесса - прямое доказательство. Для контейнеров дополнительно проверяйте docker inspect --format '{{.State.OOMKilled}}'.
Ядро в момент нехватки памяти считает для каждого процесса оценку badness - примерно долю используемой памяти, скорректированную значением oom_score_adj от -1000 до +1000. Убивается процесс с самой высокой badness именно в этот момент, оценка пересчитывается заново при каждом срабатывании, а не берётся из заранее известного рейтинга.
Системный OOM killer срабатывает, когда исчерпаны RAM и своп всего хоста. Cgroup-OOM срабатывает раньше и локальнее - как только процесс внутри контейнера или systemd-юнита с лимитом упирается в собственный memory.max, даже если на хосте свободны гигабайты. В логе это разные строки: «Out of memory» против «Memory cgroup out of memory».
Своп откладывает срабатывание, но не лечит нехватку: когда рабочая память процессов стабильно превышает RAM, ядро начинает постоянную подкачку, каждое обращение к памяти идёт через диск, и система выглядит зависшей, хотя формально память ещё есть. Своп хорош как страховка от редких коротких всплесков, а не как постоянная замена недостающей RAM.
Ядро завершило главный процесс контейнера сигналом SIGKILL из-за нехватки памяти в его cgroup, обычно с кодом возврата 137. Проверяется командой docker inspect <container> --format '{{.State.OOMKilled}}'. Сам по себе код 137 без этой проверки ничего не доказывает - тем же кодом завершается любой SIGKILL.
si/so в vmstat - почти всегда подкачка-пробуксовка, а не нехватка ядер.dmesg -T или journalctl -k; для контейнеров - docker inspect --format '{{.State.OOMKilled}}'.--memory, systemd MemoryMax=) убивает процесс внутри лимита, даже когда на хосте полно свободной памяти - лог начинается со слов «Memory cgroup out of memory», это отдельное от системного событие.MemoryMax= и OOMPolicy=.