Этот урок про то, что на самом деле скрывается за процентами. Когда ты разберёшь загрузку CPU в Linux на режимы - user, system, iowait, steal и остальные - ты перестанешь гадать и начнёшь чинить именно то, что сломано. Это база, без которой strace и perf из следующих глав будут стрельбой вслепую. Сразу договоримся о терминах: "загрузка процессора" - это не одно число, а распределение времени по режимам, и читать его надо именно так.
Из чего складывается нагрузка CPU в Linux
Процессор в каждый момент времени что-то делает или простаивает. Ядро ведёт учёт в тиках (jiffies) и складывает их в счётчики, которые лежат в /proc/stat (строка cpu и строки cpu0, cpu1, ...). Все утилиты - vmstat, mpstat, top - читают именно эти счётчики, берут разницу за интервал и переводят в проценты. Поэтому первое значение любой утилиты, рассчитанное "с момента загрузки", почти бесполезно - смотри на последующие интервалы.
Разберём все режимы - это и есть ключ к диагностике.
- user (us, %usr) - процессор выполняет код твоих приложений в пространстве пользователя. Тут крутятся циклы, парсинг JSON, сжатие, шифрование, расчёты. Высокий user - программа реально считает.
- system (sy, %sys) - процессор внутри ядра по запросу приложения. Сюда попадают системные вызовы (открыть файл, отправить пакет, выделить память), работа сетевого стека, файловой системы. Высокий system - приложение дёргает ядро слишком часто.
- nice (ni, %nice) - тот же user, но для процессов с пониженным приоритетом (запущенных через nice, положительный nice). Считается отдельно, чтобы фоновые задачи было видно.
- iowait (wa, %iowait) - процессор простаивает и при этом в системе есть хотя бы один незавершённый запрос дискового ввода-вывода. Запомни главное: это НЕ занятость процессора. Это разновидность простоя.
- idle (id, %idle) - процессор простаивает и ничего не ждёт. Свободен.
- steal (st, %steal) - ты в виртуалке, и время твоего виртуального процессора "украдено" гипервизором в пользу соседних виртуалок на том же физическом хосте.
- irq и softirq (hi/si в top, %irq/%soft в mpstat) - обслуживание аппаратных и программных прерываний. Чаще всего softirq растёт от интенсивного сетевого трафика или таймеров.
- guest (%guest, %gnice) - время, отданное на исполнение гостевых vCPU, если твоя машина сама гипервизор (KVM-хост). На обычном сервере это нули, но в современном sysstat колонки есть, не пугайся.

iowait это НЕ загрузка процессора, а простой в ожидании
Это место, где спотыкаются почти все новички, поэтому остановимся отдельно. Представь повара (процессор) и духовку (диск). Повар поставил блюдо в духовку и стоит, ждёт. Кухня выглядит занятой - повар на месте, не уходит. Но повар-то ничего не делает, он простаивает в ожидании духовки. Вот это и есть iowait: процессор свободен, но в системе висит запрос на диск, и ядро отдельно учитывает этот простой как "ждём io".
Тонкость, которую полезно знать: iowait не означает, что именно столько процентов времени диск был узким местом. Это лишь доля idle, во время которого был хоть один незавершённый io-запрос. Если в этот же момент появится готовая к счёту задача, ядро отдаст ей ядро, и iowait упадёт, хотя диск медленнее не стал. Поэтому iowait - это подсказка, а не диагноз: увидел высокий wa - иди подтверждать его на стороне диска (iostat, await, %util из следующих глав).
Практический вывод жёсткий: если у тебя высокий iowait в Linux, добавлять ядра процессора БЕСПОЛЕЗНО. Узкое место - диск (или сеть). Лечится это быстрым NVMe, оптимизацией запросов, кешированием, индексами в базе - но не процессором. А ещё iowait обманчив на многоядерных машинах: если одно ядро из 16 ждёт диск, общий iowait будет около 6%, хотя для этого конкретного приложения диск - стена. Поэтому смотри на iowait в разбивке по ядрам, а не только среднее.
Практика: читаем загрузку CPU по колонкам
Начнём с vmstat - он показывает систему в динамике. Аргумент 1 значит "обновляй раз в секунду".
Код: Выделить всё
vmstat 1
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
r b swpd free buff cache si so bi bo in cs us sy id wa st
2 0 0 412300 18400 980200 0 0 12 34 210 450 35 8 55 2 0
1 1 0 410800 18400 981000 0 0 4096 0 900 1500 9 12 20 59 0
Слева важнейшие колонки r и b. r - сколько задач прямо сейчас готовы выполняться (бегут на ядре или стоят в очереди на процессор, runnable). b - сколько застряло в непрерываемом сне (состояние D, uninterruptible sleep), почти всегда это ожидание диска или другого медленного драйвера. Колонка in - аппаратные прерывания в секунду, cs - контекст-переключения в секунду, к ним вернёмся.
Теперь mpstat -P ALL - он раскладывает по ядрам. Это лучший инструмент, чтобы поймать, что нагружено не "вообще", а конкретное ядро.
Код: Выделить всё
mpstat -P ALL 1
CPU %usr %nice %sys %iowait %irq %soft %steal %guest %gnice %idle
all 28.50 0.00 9.20 0.50 0.00 1.30 12.00 0.00 0.00 48.50
0 30.00 0.00 8.00 1.00 0.00 0.50 11.00 0.00 0.00 49.50
1 95.00 0.00 3.00 0.00 0.00 0.00 2.00 0.00 0.00 0.00
Команда top тоже показывает разбивку - строка %Cpu(s) вверху: us, sy, ni, id, wa, hi (hardware irq), si (softirq), st. Это те же режимы, просто короткими именами. По нажатию клавиши 1 в top строка разворачивается по ядрам, а в btop (популярная замена top в 2026) ядра видны графиками сразу.
Чтобы найти виновный процесс, а не только режим, бери pidstat из того же пакета sysstat:
Код: Выделить всё
pidstat -u 1
07:12:30 UID PID %usr %system %guest %wait %CPU CPU Command
07:12:31 1000 8123 96.00 2.00 0.00 0.00 98.00 1 ffmpeg
steal cpu, run queue и контекст-переключения
steal. Если ты на VPS или в облаке и видишь %steal стабильно выше 5-10%, это значит, что физический хост перепродан (oversubscribed): на одном железе слишком много виртуалок, и гипервизор не успевает выдавать тебе процессорное время. Твоё приложение тормозит, но это не его вина и не вина твоего кода - тебя обделяет хост провайдера. Лечение: писать в саппорт, мигрировать на другой хост или тариф. Внутри виртуалки steal не убрать. На голом железе steal всегда 0, его там просто нет. Важная оговорка 2026: в контейнерах с cgroup-лимитами на CPU (Kubernetes, systemd CPUQuota) троттлинг выглядит иначе - steal там может быть нулём, а тормоза реальны; ищи их в cgroup, об этом ниже.
Контекст-переключения (cs). Процессор умеет делать только одно дело за раз на ядро. Чтобы крутить сотни задач, ядро быстро переключает их: сохраняет состояние одной, грузит состояние другой. Это и есть контекст-свитч, и он не бесплатный - сбрасываются кеши и TLB процессора, тратятся такты впустую. Норма - единицы и десятки тысяч cs в секунду, это ок. А вот сотни тысяч cs при невысоком полезном us/sy - симптом: слишком много потоков дерутся за процессор, либо лок-контеншн, либо приложение лупит мелкими блокирующими операциями (например, синхронные сетевые запросы по одному). Смотри cs вместе с колонкой r: если r стабильно больше числа ядер - очередь на процессор переполнена, CPU реально насыщен (это уже CPU-bound).
Run queue и насыщение (2026, eBPF). Колонка r и load average говорят "очередь длинная", но не говорят, насколько больно от этого конкретным задачам. В 2026 это закрывают штатные eBPF-инструменты из пакетов bpfcc-tools и bpftrace, они есть в Astra/RED OS и обычных дистрибутивах:
Код: Выделить всё
runqlat 5 1 # гистограмма задержки ожидания в очереди (мкс)
runqlen 5 1 # длина очереди по ядрам
PSI - давление на ресурсы (актуально на 2026). Самый честный сигнал насыщения сегодня - Pressure Stall Information, файлы в /proc/pressure/ (нужен CONFIG_PSI=y, в современных ядрах включён):
Код: Выделить всё
cat /proc/pressure/cpu
some avg10=23.10 avg60=18.40 avg300=9.05 total=84522331
Как отличить CPU-bound от io-bound одной формулой:
- Высокий us (плюс sy), idle низкий, wa низкий, r больше числа ядер, /proc/pressure/cpu some растёт - CPU-bound. Помогут профайлеры (perf из след. глав) и больше ядер/оптимизация горячего кода.
- Высокий wa, b больше нуля, us низкий - io-bound. Подтверди через iostat, помогут диск, индексы, кеш. Ядра не спасут.
- Высокий sy без явной причины - много системных вызовов или прерываний, дальше идём в strace или bpftrace по syscall.
- Высокий st - проблема не у тебя, а у хоста; в контейнере вместо st смотри cpu.pressure и троттлинг.
Типичные грабли и заблуждения
- "CPU 100%, надо ядер" - сначала глянь, из чего эти 100%. Если там iowait - ядра не при делах.
- Считать iowait долгом процессора. wa - это простой ядра, а не работа; и сам по себе он лишь подсказка, диагноз ставит iostat.
- Смотреть только среднюю загрузку. Среднее по 16 ядрам спрячет одно перегруженное ядро. Всегда есть mpstat -P ALL и pidstat.
- Путать load average и загрузку CPU. Load average учитывает ещё и процессы в io-ожидании (состояние D), поэтому high load при низком CPU - это часто диск, а не процессор.
- Игнорировать steal на виртуалке и неделями оптимизировать свой код, который не виноват.
- В контейнере искать steal вместо троттлинга. Лимит CPU в cgroup v2 душит сервис тихо - смотри cpu.pressure и cpu.stat (nr_throttled, throttled_usec).
- Первая строка vmstat и первый интервал mpstat - это среднее за весь аптайм, по ним нельзя судить о текущем моменте.
- Запусти vmstat 1 в одном терминале. Во втором создай нагрузку на user: Смотри, как растёт us. Потом убей:
Код: Выделить всё
yes > /dev/null &Код: Выделить всё
kill %1 - Создай нагрузку на iowait. Если есть пакет stress-ng: Следи за колонкой wa и за b в vmstat, параллельно глянь
Код: Выделить всё
stress-ng --io 4 --hdd 2 --timeout 30sУвидь своими глазами, что us при этом маленький.Код: Выделить всё
cat /proc/pressure/io - Запусти mpstat -P ALL 1 и параллельно (один процесс). Найди ядро, которое улетело в 100% %usr, пока остальные отдыхают. Затем pidstat -u 1 - найди по имени сам процесс.
Код: Выделить всё
yes > /dev/null & - Перегрузи планировщик: запусти процессов yes больше, чем ядер, и посмотри и
Код: Выделить всё
runqlat 5 1- как растут задержка очереди и some.Код: Выделить всё
cat /proc/pressure/cpu - Если ты на VPS - просто посмотри свой %steal в mpstat 1. Ноль это хорошо.
- Почему при высоком iowait добавление ядер процессора обычно не помогает и почему wa - это подсказка, а не окончательный диагноз?
- Чем режим user отличается от system, и о чём говорит стабильно высокий system?
- Что означает steal, почему его невозможно убрать изнутри виртуальной машины и на что смотреть вместо него в контейнере с cgroup-лимитом?
- Как по выводу vmstat (колонки r, b, wa, cs) и по /proc/pressure/cpu отличить CPU-bound нагрузку от io-bound?
Загрузка CPU - это не одна цифра, а сумма режимов из /proc/stat. user - код приложения, system - ядро и системные вызовы, iowait - простой в ожидании io (а не работа процессора), steal - украдено гипервизором в виртуалке. Высокий sys гони в strace/bpftrace, высокий iowait - подтверждай через iostat и иди к диску, высокий steal - к провайдеру, высокий user при r больше числа ядер - это настоящий CPU-bound. Всегда смотри разбивку по ядрам через mpstat -P ALL и виновника через pidstat, а для честной картины насыщения в 2026 - /proc/pressure/cpu, runqlat и cpu.pressure контейнера.