Типичная заявка: «сайт тормозит, load под десятку, добавьте ядер». Захожу на VPS — два vCPU, load 8–9, в top почти всё idle. PHP-FPM в состоянии D, MySQL тоже. Хостер клянётся, что «ресурсы выданы полностью». Дальше обычно начинается шаманство: крутят pm.max_children, рестартуют php-fpm, добавляют swap. Это почти никогда не то место, где лежит причина.

На виртуалке load врёт чаще, чем на железе. Он не отделяет «мои процессы жрут CPU» от «диск лёг» и от «гипервизор забрал кванты времени у соседа». Если не разложить эти три штуки, будете лечить не того.

Что на самом деле считает load average

Load — это не «загрузка процессора в процентах». Это средняя длина очереди: сколько задач либо готовы бежать на CPU, либо спят в uninterruptible sleep (состояние D, обычно ожидание диска или NFS).

Поэтому картина «load 12 при 70% idle» на двух ядрах — нормальная, если в D висит десяток php-fpm, а диск отвечает по 80–200 мс. Процессор свободен, очередь растёт. С steal time та же история: задача хочет CPU, гипервизор её не планирует, очередь тоже растёт, а в top вы видите idle плюс st.

Смотрю сразу три числа: load за 1/5/15 минут и сколько ядер (nproc). Load 4 на четырёх ядрах — ещё можно жить. Load 4 на одном vCPU уже несколько минут — уже инцидент. Пятнадцатиминутный load запаздывает: если тормоза начались три минуты назад, ориентируйтесь на единицу и на живые счётчики, не на «среднее за четверть часа».

Три столбца в top, без которых диагностика — гадание

В шапке top (клавиша 1 включает разбивку по ядрам) мне важна строка CPU:

%Cpu(s):  8.4 us,  3.1 sy,  0.0 ni, 41.0 id, 27.2 wa,  0.0 hi,  1.1 si, 19.2 st

Коротко по делу:

  • us + sy — ваши процессы реально крутят код. Вот это «нехватка CPU у приложения».
  • id — простой. Высокий idle сам по себе ничего не лечит и ничего не доказывает.
  • wa — iowait: процессор свободен, но есть незавершённый I/O. Это не «диск загрузил CPU», это ожидание.
  • st — steal: гипервизор отдал эти тики не вам. Ваша виртуалка хотела считать — ей не дали.
  • si — softirq. Если внезапно большой на фоне сети, смотрю пакеты и netfilter, а не PHP.

Правило, которым я пользуюсь в первые 30 секунд: если us+sy низкие, а load высокий — это не «надо больше ядер приложению». Это либо диск, либо гипервизор, либо cgroup-троттлинг, который в st может вообще не попасть.

Steal time: когда виноват не ваш код, а сосед по ноде

Steal — доля времени, когда vCPU был готов работать, но гипервизор забрал слот. На нормально выделенном KVM с гарантией CPU st обычно около нуля, редкие всплески на 1–2% можно не драматизировать. На дешёвых VPS вечером я регулярно вижу 10–30% steal пачками по несколько минут. Формально у вас «2 ядра», фактически в этот момент — полтора или меньше.

Как это ощущается снаружи: сайт отвечает рывками, CPU в панели хостера «зелёный», процессы в top не жрут по 100%, латентность прыгает без изменения трафика. Внутри в это же время растёт очередь PHP/очередь диска — не потому что MySQL внезапно стал писать в десять раз больше, а потому что кванты CPU приходят дыряво.

Подтверждаю не одним кадром top, а рядом:

nproc
uptime
mpstat -P ALL 1 30
vmstat 1 30

В mpstat смотрю столбец %steal по каждому vCPU. Если steal сидит на всех ядрах стабильно выше примерно 5–8% и при этом %usr/%sys не упёрлись в потолок — это не ваше приложение «уперлось в CPU». Это нода. Если steal скачет нуль–сорок нуль–сорок синхронно на всех vCPU — классика оверселла, «шумный сосед» или ночной бэкап гипервизора.

В vmstat те же буквы в последнем столбце st, плюс r (run queue) и b (blocked on IO). Высокий r при низком us и ненулевом st — задачи ждут планировщика, а не вашего кода.

Важный нюанс: на части облаков CPU режут не через steal, а через кредиты/квоты. Тогда st может быть нулевым, а приложение всё равно ползёт. Это уже следующий раздел, не путайте с «честным» KVM-steal.

Iowait: load большой, процессор скучает, диск не отвечает

Вторая частая ловушка. Load 10, idle 60%, wa 30–40%. В ps куча процессов в D. Человек видит load и думает «CPU». Нет: очередь — на I/O.

ps -eo pid,user,state,wchan:24,etime,cmd | awk 'NR==1 || $3 ~ /D/'
iostat -xz 1 20
vmstat 1 20

В iostat мне важны не «красивые» MB/s, а await, aqu-sz и %util. Если на vda/vdb await уехал в десятки–сотни миллисекунд при скромном объёме записи — это не «MySQL плохо настроен», это сторадж ноды. На дешёвом VPS диск почти всегда общий. Сосед запустил бэкап — ваш InnoDB начинает ждать fsync.

Отдельно проверяю swap:

free -h
vmstat 1 10

Ненулевые si/so в vmstat плюс высокий iowait — это уже не «мало CPU», это память кончилась и система молотит страницы. Добавление ядер тут бесполезно. И увеличение pm.max_children в такой ситуации обычно делает хуже: больше процессов — больше давления на диск и RAM.

NFS, CIFS, зависший iSCSI дают ту же картину: D-state, растущий load, CPU почти idle. wchan в ps часто прямо намекает (io_schedule, nfs, wait_on_page). Не лечите это рестартом php-fpm, пока не поняли, на чём они спят.

Реальная нагрузка: когда процессы правда жрут CPU

Это самый скучный и самый честный случай. us+sy под 80–100% на всех ядрах, steal около нуля, iowait низкий, в top по CPU видны конкретные pid: php-fpm, mysqld, node, python. Load примерно согласован с числом ядер и run queue.

Тогда уже имеет смысл:

pidstat -u 1 10
ps -eo pid,user,pcpu,pmem,cmd --sort=-pcpu | head
# если есть perf и можно ненадолго:
# perf top -g

Искать медленный запрос, воркера без лимита, cron, который внезапно переложил всю таблицу. Здесь апгрейд CPU поможет. На steal и на дохлый диск — нет, вы просто будете дороже ждать то же самое.

Не путайте короткий всплеск (деплой, прогрев кэша, один тяжёлый отчёт) с системной нехваткой. Смотрите 5-минутное окно, не один кадр.

Четвёртая ловушка: cgroup throttle без steal

На контейнерах (LXC, часть «VPS из панели», Kubernetes) CPU часто режут квотой cgroup, а не классическим steal. В top тогда st=0, idle может быть приличным, а латентность всё равно плохая: вас просто не пускают на CPU сверх квоты.

На cgroup v2:

cat /sys/fs/cgroup/cpu.stat
cat /sys/fs/cgroup/cpu.max
cat /proc/pressure/cpu

В cpu.stat смотрю nr_throttled и throttled_usec. Если throttled растёт на глазах — вас режут. cpu.max вида 20000 100000 на двух видимых vCPU означает, что «два ядра» в nproc — картинка, а лимит другой.

PSI (/proc/pressure/cpu и io) удобен тем, что показывает stall some/full за 10/60/300 секунд. full по CPU — никто не мог работать. Это ближе к правде, чем load, когда хочется понять «прямо сейчас нас душили или пять минут назад».

На старых OpenVZ/Virtuozzo картина ещё мутнее: иногда steal есть, иногда только failcnt в beancounters. Если попали на такое — не делайте выводы по одному top, смотрите лимиты панели и cpu.stat.

Порядок, которым я отличаю гипервизор от реальной нагрузки

Не распухающий чеклист, а последовательность на пять минут.

  1. Снять размер машины и load: nproc, uptime, free -h. Понять, не swap ли это вообще.
  2. Живой top, клавиша 1. Записать us/sy/id/wa/st. Не скриншот «процессы», а шапку CPU.
  3. mpstat -P ALL 1 30 и vmstat 1 30 — steal стабильный или вспышками, run queue, blocked.
  4. Если wa заметный — iostat -xz 1 20 и список D-процессов с wchan.
  5. Если st≈0 и CPU не упирается — cpu.stat / cpu.max / /proc/pressure/cpu.
  6. Только если us+sy высокие и лимиты не душат — иду в pidstat и уже в приложение.

За эти пять минут обычно ясно, в какую из четырёх корзин класть инцидент. Дальше уже либо тикет хостеру с цифрами, либо диск, либо квота, либо профилирование PHP/SQL.

Что не делать, пока не поставили диагноз

Не поднимать pm.max_children «чтобы очередь рассосалась». На iowait и на steal это увеличивает очередь, а не уменьшает.

Не добавлять swap «для запаса», если si/so уже ненулевые. Вы лечите нехватку RAM ещё большим iowait.

Не рестартить MySQL по кругу, если await диска сотни миллисекунд. Рестарт в этот момент — лотерея с порчей и долгим recovery.

Не просить «ещё два ядра» у хостера, имея на руках только load average. Load на оверсолде растёт и от steal, и от диска. Без mpstat/iostat вам продадут тот же оверсолл подороже — или, что чаще, просто перекинут на такую же ноду.

Как разговаривать с хостером, чтобы это не было «сайт тормозит»

Тикет без цифр на оверсолде заканчивается шаблоном «на ноде всё хорошо, смотрите приложение». Я прикладываю 30 секунд mpstat -P ALL, кусок vmstat, если надо — iostat, время (UTC), и формулировку в духе: «на 2 vCPU стабильно %steal 15–25 при %usr+%sys < 30, load растёт, iowait такой-то. Прошу проверить contention на ноде / переезд». Не имена клиентов, не внутренние IP, не «у нас прод упал».

Если хостер нормальный, на устойчивом steal они переезжают. Если нет — имеет смысл менять площадку, а не тюнить opcache. Оптимизация приложения на 10% не лечит потерю четверти CPU каждую минуту.

Для себя оставляю те же логи локально на время разбора. Через час steal может исчезнуть, и вы останетесь с «ну у нас сейчас всё зелёное».

Коротко: как не перепутать корзины

Реальная нагрузка: us+sy высокие, st≈0, wa низкий, в top видны конкретные процессы. Лечите код, запросы, лишние воркеры. Ядра помогут.

Нехватка CPU у гипервизора: st стабильно заметный, us+sy не упираются, load и латентность прыгают без роста трафика. Лечите площадку, не php.ini.

Диск: wa высокий, процессы в D, await большой. Часто на тех же дешёвых VPS соседствует со steal — нода оверsold и по CPU, и по стораджу. Сначала цифры, потом выводы.

Квота cgroup: st=0, cpu.stat throttled растёт, PSI full по CPU. «Два ядра» в панели ≠ два ядра в планировщике.

Пока эти четыре строки не разложены, любой совет «увеличьте лимиты PHP» — гадание. Снимается это стандартными утилитами за несколько минут, без агентов и без панелей. Панель хостера в этот момент почти всегда врёт в оптимистичную сторону: она рисует «выданные» ядра, а не те, которые вам реально отдали.

Нужна профессиональная удалённая помощь с сервером, сайтом, компьютером или ноутбуком?

Свяжитесь со мной любым удобным для вас способом, и получите её быстро и не дорого.

Обсудить задачу

Помогла статья? Поблагодари автора!

Остались вопросы, или есть что добавить? Добро пожаловать в комментарии.

Угостить автора чашечкой кофе