15%

Сэкономьте 15% на всех хостинговых услугах

Проверьте свои навыки и получите скидку на любой тарифный план

Используйте код:

Skills
Начать
12.12.2023

Голодание процессов в операционных системах: причины, механизмы и решения производственного уровня

Голодание процессов возникает, когда процессу бесконечно долго отказывают в процессорном времени, памяти или пропускной способности I/O, необходимых для выполнения работы — не потому, что ресурсы отсутствуют, а потому что политика планирования постоянно отдаёт предпочтение другим процессам. В отличие от взаимоблокировки, при которой все конкурирующие процессы заблокированы, голодание позволяет системе выглядеть работоспособной, незаметно деградируя или останавливая определённые рабочие нагрузки.

Это различие имеет практическое значение: голодающий процесс не генерирует ошибок на уровне ядра, не создаёт дампов памяти и может не достигать стандартных порогов оповещения — что делает его одной из наиболее коварных патологий производительности в многопользовательских средах и средах с высоким уровнем параллелизма.

Что на самом деле означает голодание на уровне ядра

Термин заимствован из экологии ресурсов: процесс «голодает», когда его постоянно вытесняют в конкуренции за ограниченный ресурс. В современных операционных системах Linux Completely Fair Scheduler (CFS), очереди приоритетов Windows NT и планировщик BSD ULE реализуют механизмы предотвращения голодания — и тем не менее оно всё равно возникает в производственных средах при определённых условиях.

На уровне ядра голодание проявляется как процесс, чьё виртуальное время выполнения (в терминологии CFS) или время ожидания растёт неограниченно, так и не будучи выбранным для исполнения. Процесс остаётся в состоянии TASK_RUNNING — он готов и имеет право на выполнение — но планировщик никогда не выделяет ему квант CPU, поскольку задачи с более высоким приоритетом или более частым запуском всегда его вытесняют.

Ключевые технические различия:

  • Взаимоблокировка (Deadlock): Два или более процессов взаимно заблокированы, каждый ожидает ресурс, удерживаемый другим. Система не делает никакого прогресса по этим задачам.
  • Голодание (Starvation): Один или несколько процессов постоянно обходятся планировщиком стороной. Другие процессы продолжают работать нормально.
  • Живая блокировка (Livelock): Процессы не заблокированы, но непрерывно меняют состояние в ответ друг на друга, не достигая реального прогресса.

Первопричины голодания процессов

Понимание голодания требует изучения конкретных механизмов, его порождающих, а не просто перечисления «ограниченных ресурсов» в качестве причины.

1. Статическая инверсия приоритетов без старения

Большинство планировщиков на основе приоритетов назначают каждому процессу фиксированный или полуфиксированный приоритет. Если процесс с низким приоритетом постоянно вытесняется потоком задач со средним и высоким приоритетом, он никогда не выполняется. Критический сбой здесь — отсутствие старения — техники, при которой эффективный приоритет процесса постепенно повышается по мере ожидания. Без старения фоновое задание с низким приоритетом на загруженном сервере может ждать бесконечно.

В Linux диапазон значений nice (от -20 до +19) и приоритеты реального времени (SCHED_FIFO, SCHED_RR) создают именно такой риск. Процесс, выполняющийся под SCHED_FIFO с приоритетом 99, будет вытеснять каждый процесс SCHED_OTHER на том же ядре CPU до тех пор, пока добровольно не уступит управление или не заблокируется.

2. Несправедливая очередь в планировщиках I/O

Голодание CPU хорошо задокументировано, но голодание I/O столь же разрушительно и часто остаётся незамеченным. Планировщик I/O Linux (исторически CFQ, теперь BFQ или mq-deadline в зависимости от версии ядра и типа хранилища) управляет порядком обслуживания запросов к блочным устройствам. При интенсивных последовательных операциях записи — характерных для серверов баз данных и приложений с интенсивным журналированием — планировщик I/O может снижать приоритет запросов случайного чтения от других процессов, фактически лишая их доступа к диску.

Это частая проблема в средах VPS Хостинга, где несколько арендаторов совместно используют базовую инфраструктуру хранения и конкуренция за I/O является реальной операционной проблемой.

3. Нехватка памяти и OOM Killer

Когда физическая RAM исчерпана, убийца нехватки памяти (OOM killer) ядра Linux выбирает процесс для завершения на основе oom_score. Хотя технически это завершение, а не голодание, предшествующее состояние — когда процесс многократно выгружается на диск и никогда не получает достаточно резидентной памяти для эффективного выполнения — представляет собой голодание по памяти. Процесс технически работает, но практически не продвигается из-за постоянных ошибок страниц и операций подкачки.

4. Конкуренция за блокировки и голодание мьютексов

В многопоточных приложениях голодание возникает на уровне примитивов синхронизации. Если мьютекс или семафор использует несправедливую политику захвата (последним пришёл — первым обслужен или случайный выбор среди ожидающих потоков), конкретный поток может постоянно обходиться стороной, даже если блокировка часто освобождается. Это отличается от планирования на уровне ОС и происходит полностью в пространстве пользователя или подсистеме синхронизации ядра.

5. Голодание по пропускной способности сети

В контейнеризованных и виртуализированных средах процесс или контейнер, потребляющий всю доступную пропускную способность сети, может лишить другие процессы сетевого I/O. Без формирования трафика через tc (traffic control) и cgroups один вышедший из-под контроля процесс может монополизировать пропускную способность NIC.

Голодание vs. взаимоблокировка vs. живая блокировка: техническое сравнение

СвойствоГолоданиеВзаимоблокировкаЖивая блокировка
Прогресс системыДа (другие процессы работают)Нет (заблокированные процессы остановлены)Видимый (реального прогресса нет)
Заблокированное состояниеНет (процесс готов к выполнению)Да (процесс ожидает ресурс)Нет (процесс активен)
Удерживаемый ресурсНетДа (циклическое удержание и ожидание)Нет
СамоустранениеИногда (при наличии старения)Никогда (требует вмешательства)Редко
Сложность обнаруженияВысокая (нет явной ошибки)Средняя (обнаружение цикла)Высокая (выглядит как активность)
Основная причинаНесправедливая политика планированияЦиклическая зависимость ресурсовРеактивные циклы смены состояний
Сигнал ядра LinuxНетНет (возможна мягкая блокировка)Нет

Как современные планировщики решают проблему голодания

Linux Completely Fair Scheduler (CFS)

CFS, представленный в ядре Linux 2.6.23, решает проблему голодания, отслеживая виртуальное время выполнения (vruntime) для каждого процесса. Планировщик всегда выбирает процесс с наименьшим vruntime — это означает, что процессы, получившие меньше процессорного времени, систематически получают приоритет. Такая конструкция делает чистое голодание CPU практически невозможным в CFS для процессов SCHED_OTHER.

Однако CFS не защищает от голодания со стороны процессов реального времени. Любой процесс, запланированный под SCHED_FIFO или SCHED_RR, вытесняет все задачи SCHED_OTHER. Параметр ядра /proc/sys/kernel/sched_rt_runtime_us (по умолчанию: 950 000 микросекунд в секунду) резервирует 5% процессорного времени для задач не реального времени именно для предотвращения этого.

Старение приоритетов

Классические алгоритмы старения увеличивают эффективный приоритет процесса на фиксированную величину за каждый цикл планирования, проведённый в ожидании. Как только эффективный приоритет достигает наивысшего уровня, процессу гарантируется выполнение. После запуска его приоритет сбрасывается до базового значения. Это классическое решение голодания на основе приоритетов, реализованное в различных формах в Windows NT, Solaris и более старых планировщиках Linux.

Справедливая очередь и взвешенная справедливая очередь (WFQ)

Для сетевых ресурсов и ресурсов I/O взвешенная справедливая очередь назначает каждому потоку или процессу долю пропускной способности, пропорциональную его весу. Даже если поток с высоким весом генерирует больше трафика, потокам с низким весом гарантируется минимальная скорость обслуживания. Linux реализует это через дисциплины Hierarchical Token Bucket (HTB) и Stochastic Fair Queuing (SFQ) в подсистеме tc.

Диагностика голодания в производственных системах Linux

Выявление голодания требует одновременной корреляции нескольких источников данных.

Анализ планирования CPU

# Check per-process CPU wait time and scheduling statistics
cat /proc/<PID>/schedstat

# Monitor scheduler latency with perf
perf sched latency --sort max

# Identify processes with high voluntary/involuntary context switches
pidstat -w 1 10

# Check real-time process priorities that may be starving others
ps -eo pid,comm,cls,pri,ni --sort=-pri | head -20

Вывод schedstat предоставляет накопленное время ожидания процесса в очереди на выполнение (run_delay в наносекундах) — прямой показатель голодания при планировании.

Индикаторы голодания по памяти

# Check swap activity — high si/so values indicate memory starvation
vmstat 1 10

# Identify processes with high major page fault rates
pidstat -r 1 10

# Check OOM kill history
dmesg | grep -i "oom|killed process"

# Inspect per-process memory pressure
cat /proc/<PID>/status | grep -E "VmRSS|VmSwap|VmPeak"

Обнаружение голодания I/O

# Per-process I/O wait statistics
iotop -b -n 5

# Block device queue depth and wait times
iostat -x 1 5

# Check I/O scheduler in use for each block device
cat /sys/block/sda/queue/scheduler

# Identify processes blocked on I/O
ps aux | awk '$8 ~ /D/ {print}'

Процессы в состоянии D (непрерываемый сон) заблокированы на I/O. Устойчивое присутствие процессов в состоянии D является сильным индикатором голодания I/O или насыщения подсистемы хранения.

Производственные решения и стратегии снижения рисков

Внедрение cgroups v2 для изоляции ресурсов

Контрольные группы (cgroups v2) обеспечивают наиболее надёжный механизм предотвращения голодания в многопроцессных и контейнеризованных средах. Назначая явные квоты CPU, памяти и I/O группам процессов, вы гарантируете минимальное выделение ресурсов независимо от нагрузки на систему.

# Create a cgroup with CPU weight (higher weight = more CPU share)
mkdir /sys/fs/cgroup/my_service
echo "100" > /sys/fs/cgroup/my_service/cpu.weight

# Set memory limit to prevent memory starvation of other groups
echo "2G" > /sys/fs/cgroup/my_service/memory.max

# Assign process to cgroup
echo <PID> > /sys/fs/cgroup/my_service/cgroup.procs

Вес CPU в cgroups v2 использует диапазон от 1 до 10000, где значение по умолчанию равно 100. Группа процессов с весом 200 получает вдвое большую долю CPU, чем группа с весом 100 при конкуренции.

Настройка планировщика Linux под вашу рабочую нагрузку

# Increase scheduler migration cost to reduce cache thrashing (latency-sensitive workloads)
echo 500000 > /proc/sys/kernel/sched_migration_cost_ns

# Reduce scheduler granularity for more frequent preemption (throughput workloads)
echo 1000000 > /proc/sys/kernel/sched_min_granularity_ns

# Ensure real-time tasks cannot starve normal tasks
echo 950000 > /proc/sys/kernel/sched_rt_runtime_us

Применение соответствующих политик планирования для каждого процесса

# Set a process to batch scheduling (explicitly low-priority, won't starve interactive tasks)
chrt -b -p 0 <PID>

# Set a CPU-intensive background job to idle scheduling class
chrt -i -p 0 <PID>

# Adjust nice value for a running process
renice -n 10 -p <PID>

# Run a new command with reduced priority
nice -n 15 ./my_background_script.sh

Класс SCHED_IDLE (chrt -i) — правильный инструмент для действительно фоновых задач: он выполняется только тогда, когда нет других готовых к выполнению процессов, полностью исключая возможность голодания других рабочих нагрузок.

Выбор планировщика I/O

# For NVMe SSDs (low-latency, no rotational penalty): use none or mq-deadline
echo "mq-deadline" > /sys/block/nvme0n1/queue/scheduler

# For HDDs with mixed workloads: use bfq for fairness
echo "bfq" > /sys/block/sda/queue/scheduler

# Make persistent across reboots (add to /etc/udev/rules.d/)
echo 'ACTION=="add|change", KERNEL=="sda", ATTR{queue/scheduler}="bfq"' 
  > /etc/udev/rules.d/60-scheduler.rules

BFQ (Budget Fair Queuing) специально разработан для предотвращения голодания I/O, гарантируя каждому процессу пропорциональную долю пропускной способности диска. Это рекомендуемый планировщик для сред общего хостинга и серверов баз данных.

Управление пропускной способностью сети с помощью tc

# Create a root HTB qdisc on the primary interface
tc qdisc add dev eth0 root handle 1: htb default 30

# Add a parent class with total bandwidth
tc class add dev eth0 parent 1: classid 1:1 htb rate 1gbit

# Add child classes with guaranteed minimums (prevents starvation)
tc class add dev eth0 parent 1:1 classid 1:10 htb rate 100mbit ceil 1gbit
tc class add dev eth0 parent 1:1 classid 1:20 htb rate 100mbit ceil 1gbit

# Add SFQ leaf to each class for per-flow fairness
tc qdisc add dev eth0 parent 1:10 handle 10: sfq perturb 10
tc qdisc add dev eth0 parent 1:20 handle 20: sfq perturb 10

Эта конфигурация гарантирует каждому классу минимум 100 Мбит/с, позволяя при этом использовать всю пропускную способность канала 1 Gbps в пиковые моменты, когда полоса пропускания доступна.

Настройка переподписки памяти и подкачки

# Reduce swappiness to minimize swap-induced memory starvation
echo 10 > /proc/sys/vm/swappiness

# Enable memory overcommit accounting (prevents OOM from surprising processes)
echo 2 > /proc/sys/vm/overcommit_memory

# Set overcommit ratio (total allocatable = RAM * ratio + swap)
echo 80 > /proc/sys/vm/overcommit_ratio

Установка vm.swappiness=10 указывает ядру предпочитать освобождение кэша страниц вместо выгрузки памяти процессов, значительно снижая вероятность голодания по памяти при умеренной нагрузке.

Голодание в виртуализированных и контейнеризованных средах

На выделенных серверах, работающих с гипервизорами (KVM, VMware ESXi, Hyper-V), голодание может возникать на двух различных уровнях:

Голодание на уровне гипервизора: виртуальной машине отказывают в циклах CPU со стороны планировщика гипервизора. KVM использует CFS хост-ядра для планирования vCPU, что означает: виртуальная машина с меньшим весом доли CPU может испытывать голодание со стороны виртуальных машин с большими весами при конкуренции. DRS (Distributed Resource Scheduler) VMware использует доли, резервирования и ограничения для управления этим.

Голодание на уровне гостевой ОС: внутри самой виртуальной машины применяются те же динамики планирования на уровне ОС. Контейнеризованная рабочая нагрузка, работающая под Docker или Kubernetes без явных ограничений ресурсов, может монополизировать CPU и память гостевой ОС, лишая совместно размещённые контейнеры ресурсов.

Для сред Kubernetes всегда определяйте как requests, так и limits в спецификациях подов:

resources:
  requests:
    cpu: "250m"
    memory: "512Mi"
  limits:
    cpu: "1000m"
    memory: "1Gi"

Значение requests определяет размещение при планировании и вес доли CPU в cgroup. Без него планировщик Kubernetes не имеет основы для справедливого размещения, а среда выполнения контейнеров назначает веса по умолчанию (равные) — что всё равно допускает голодание, если один контейнер постоянно насыщает свой лимит CPU.

Голодание в серверах баз данных и приложений

Движки баз данных реализуют собственные внутренние планировщики, независимые от планировщика ОС. PostgreSQL использует модель «один процесс на соединение», где каждый процесс-обработчик конкурирует за ресурсы ОС в обычном режиме, но конкуренция за блокировки внутри базы данных (блокировки строк, рекомендательные блокировки) может вызывать голодание на уровне приложения, при котором конкретные запросы бесконечно ждут получения блокировки.

MySQL/InnoDB использует пул потоков с настраиваемыми ограничениями параллелизма (innodb_thread_concurrency). Слишком низкое значение вызывает голодание запросов, поскольку потоки выстраиваются в очередь в ожидании слотов выполнения. Слишком высокое значение вызывает перегрузку CPU. Рекомендуемое начальное значение — 2 × number of CPU cores.

Для веб-серверов Nginx и Apache имеют различные профили голодания. Событийно-ориентированная модель Nginx по своей природе устойчива к голоданию воркеров, но исчерпание пула соединений к вышестоящему серверу (например, к PHP-FPM или серверному API) создаёт голодание на уровне приложения. Prefork MPM Apache может исчерпать лимит MaxRequestWorkers, вызывая бесконечное накопление новых соединений в очереди — форму голодания соединений.

Эти соображения непосредственно актуальны при настройке VPS с cPanel для рабочих нагрузок общего веб-хостинга, где несколько сайтов конкурируют за пулы воркеров PHP-FPM и лимиты соединений MySQL.

Инфраструктура мониторинга для предотвращения голодания

Реактивной диагностики недостаточно для производственных систем. Проактивный стек мониторинга должен включать:

Метрики Prometheus + Node Exporter для отслеживания:

  • node_schedstat_waiting_seconds_total — накопленное время ожидания в очереди на выполнение CPU на каждый CPU
  • node_vmstat_pgmajfault — серьёзные ошибки страниц, указывающие на нехватку памяти
  • node_disk_io_time_weighted_seconds_total — насыщение очереди I/O
  • node_pressure_cpu_waiting_seconds_total — давление CPU по Linux PSI (Pressure Stall Information)
  • node_pressure_memory_full_seconds_total — полное время простоя памяти по PSI

Linux PSI (доступен начиная с ядра 4.20) — наиболее прямой индикатор голодания, доступный в ядре. Он сообщает процент времени, в течение которого задачи простаивали в ожидании ресурсов CPU, памяти или I/O:

# Real-time PSI monitoring
cat /proc/pressure/cpu
cat /proc/pressure/memory
cat /proc/pressure/io

Формат вывода: some avg10=X.XX avg60=X.XX avg300=X.XX total=NNNN, где some указывает на то, что хотя бы одна задача простаивала. Значения выше 10–15% по avg60 требуют немедленного расследования.

Для команд, управляющих панелями управления VPS или пользовательскими серверными стеками, интеграция метрик PSI в дашборды Grafana обеспечивает раннее предупреждение до того, как голодание ухудшит производительность для конечных пользователей.

Практическая матрица решений: выбор правильного механизма защиты от голодания

СимптомТип ресурсаРекомендуемый инструментЦель настройки
Фоновые задания никогда не завершаютсяCPUSCHED_IDLE или nice +19Устранить конкуренцию фоновых задач за CPU
Скачки задержки интерактивных операций под нагрузкойCPUНастройка CFS + вес CPU в cgroups v2Гарантировать долю интерактивного процесса
Тайм-аут запросов к базе данныхCPU + блокировкаinnodb_thread_concurrency, тайм-аут блокировкиОграничить время ожидания блокировки
Дисковые задания блокируют веб-обслуживаниеI/OПланировщик BFQ + io.weight cgroups v2Пропорциональное распределение I/O
OOM-завершение контейнеров под нагрузкойПамятьmemory.min + vm.swappiness cgroups v2Гарантировать минимальную резидентную память
Сетевой процесс лишает ресурсов другиеСетьHTB + SFQ через tcГарантия пропускной способности для каждого класса
Голодание виртуальной машины со стороны гипервизораvCPUРезервирования/доли CPU гипервизораЗарезервировать минимальные циклы vCPU

Ключевые технические выводы

  • Никогда не полагайтесь на планирование по умолчанию для серверов со смешанными рабочими нагрузками. Явно классифицируйте процессы с помощью chrt, nice и cgroups v2 на основе их чувствительности к задержкам и бизнес-приоритета.
  • Включите мониторинг PSI (/proc/pressure/*) на всех производственных системах Linux. Это наиболее точный индикатор голодания в реальном времени в ядре с практически нулевыми накладными расходами.
  • Используйте BFQ для вращающихся дисков и любых устройств NVMe, обслуживающих смешанные случайные/последовательные рабочие нагрузки в многопользовательских средах. Гарантии справедливости стоят незначительных накладных расходов на пропускную способность.
  • Устанавливайте запросы ресурсов Kubernetes без исключений. Неустановленный requests.cpu — это не «неограниченный» — это обязательство при планировании, которое допускает голодание CPU на уровне контейнера.
  • Различайте голодание и взаимоблокировку перед вмешательством. Завершение и перезапуск голодающего процесса не устраняет базовый дисбаланс планирования; это лишь временно убирает симптом.
  • Проверяйте назначения приоритетов реального времени (SCHED_FIFO/SCHED_RR) на любой системе, где они используются. Один неправильно настроенный процесс реального времени может бесконечно лишать ресурсов все рабочие нагрузки с обычным приоритетом на ядре CPU.
  • Для сред общего веб-хостинга применяйте квоты CPU и I/O на уровне cgroup для каждой учётной записи, а не полагайтесь исключительно на ограничение скорости на уровне приложения.

Часто задаваемые вопросы

В чём разница между голоданием и взаимоблокировкой в операционной системе?

Взаимоблокировка возникает, когда два или более процессов постоянно заблокированы, каждый удерживает ресурс, необходимый другому — ни один процесс не продвигается. Голодание возникает, когда процесс постоянно обходится планировщиком стороной, несмотря на готовность к выполнению; другие процессы продолжают нормально выполняться. Взаимоблокировка требует разрыва циклической зависимости; голодание требует исправления политики планирования, как правило, путём реализации старения или справедливой очереди.

Как планировщик Linux CFS предотвращает голодание CPU?

CFS отслеживает виртуальное время выполнения (vruntime) для каждого процесса и всегда выбирает для выполнения процесс с наименьшим vruntime. Это гарантирует систематическое предпочтение процессам, получающим меньше процессорного времени, делая бесконечное голодание CPU процессов SCHED_OTHER практически невозможным. Однако процессы реального времени (SCHED_FIFO, SCHED_RR) полностью обходят CFS и всё равно могут лишать ресурсов обычные процессы, если параметр sched_rt_runtime_us настроен неправильно.

Как определить, испытывает ли процесс голодание на сервере Linux?

Прочитайте /proc/<PID>/schedstat для проверки накопленного времени ожидания в очереди на выполнение. Отслеживайте /proc/pressure/cpu для получения метрик простоя PSI. Используйте perf sched latency --sort max для выявления процессов с аномально высокой задержкой планирования. Процессы в постоянном состоянии D, видимые в выводе ps aux, указывают на голодание I/O, а не CPU.

Влияет ли голодание процессов на VPS и облачные серверы иначе, чем на физические серверы?

Да. На VPS голодание может возникать как на уровне гипервизора (планировщик гипервизора отказывает вашей виртуальной машине в vCPU-времени), так и внутри гостевой ОС. Голодание на уровне гипервизора невидимо для стандартных инструментов мониторинга ОС и требует специфических для гипервизора метрик или заметного времени похищения (%st в выводе top). Высокое время похищения — как правило, выше 5–10% в устойчивом режиме — указывает на то, что гипервизор не предоставляет вашей виртуальной машине положенные ей циклы vCPU.

Каков самый быстрый способ предотвратить голодание других процессов конкретным процессом на загруженном сервере?

Назначьте ему класс планирования SCHED_IDLE с помощью chrt -i -p 0 <PID>. Этот класс выполняется только тогда, когда нет других готовых к выполнению процессов, гарантируя, что он не сможет лишить ресурсов никакую другую рабочую нагрузку. Для фоновых процессов с интенсивным I/O дополнительно установите приоритет I/O в класс idle: ionice -c 3 -p <PID>. Комбинирование обоих подходов устраняет процесс как источник голодания CPU и I/O двумя командами без каких-либо изменений в приложении.

15%

Сэкономьте 15% на всех хостинговых услугах

Проверьте свои навыки и получите скидку на любой тарифный план

Используйте код:

Skills
Начать