Вход на сайт

Просмотр новости

Найдите то, что Вас интересует

Инверсия приоритетов в Kubernetes: Когда планировщик убивает бизнес, спасая «важные» поды

Дата публикации: 13-07-2026 14:50:20

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

Основное содержимое страницы с новостью.

Привет, Хабр!

Меня зовут Андрей Бирюков. Я — независимый эксперт в области ИТ и ИБ, преподаю в учебных центрах и пишу статьи и книги.

Сегодня мы будем говорить про Kubernetes, как бы не «оригинально» это ни звучало. Большинство инцидентов уровня P1 в микросервисной инфраструктуре начинаются одинаково: DevOps видит, как поды с веб‑фронтендом внезапно уходят в OOMKilled или Evicted, хотя мониторинг CPU показывает утилизацию всего 65%. Через 40 секунд поды пересоздаются, но их холодный старт (подтягивание образов, инициализация кэшей, отработка пробов) занимает порядка 2 минут. В итоге, пока «важные» поды борются за выживание, вся система ложится из‑за таймаутов пробов готовности.

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

c73fdab61419d23caf25c97cdda24308.png

Как планировщик считает «цену» пода

Давайте попробуем разобраться, как возникает эта проблема. Планировщик принимает решение не на основе текущей нагрузки, а на основе состояния очереди и оценки QoS (Quality of Service). В Kubernetes существует три класса QoS:

  1. Guaranteed — когда requests строго равны limits по CPU и RAM.

  2. Burstable — когда requests меньше limits (или задан только limits).

  3. BestEffort — когда не задано ни того, ни другого.

70ac41f00ce5e665e53b305b3f8f7c7e.png

Соответственно, здесь кроется ошибка номер один: выставлять для критичных stateful‑приложений (базы данных, очереди) requests с большим запасом, чтобы «зарезервировать» ресурсы, а для stateless‑воркеров ставить низкие requests, чтобы сэкономить.

d90b2588ee8ef3e3f73a4fe8f9118de5.png

На этом шаге вступает в силу алгоритм оценки узла (Node Score). На этапе PreFilter планировщик смотрит не на свободную память, а на величину (Allocatable - TotalRequests). То есть, если высокоприоритетный под с большим requests не может поместиться ни на один узел, планировщик запускает механизм Preemption (вытеснение). И тут начинается самое интересное: вытесняются не наименее важные поды, а те, у которых ниже приоритет и выше использование ресурсов относительно своих requests.

Представьте ситуацию: ваш критичный StatefulSet с PostgreSQL (приоритет 100) запросил 8 ГБ RAM. На узле остается 2 ГБ. Планировщик находит под‑воркер (приоритет 50) с requests: 1ГБ, который в реальности потребляет 6 ГБ из‑за утечки памяти. Планировщик считает «жертву» эффективной: выселив один под с requests 1ГБ, он освободит 6 ГБ физической памяти, так как kubelet учитывает именно usage, а не request. Выселение происходит, воркер умирает, PostgreSQL встает. Через 5 секунд воркер пересоздается на соседнем узле, но его кэш инвалидирован. Начинается каскад и все становится совсем плохо.

Эффект «Лавины регистрации»

Допустим, для решения этой проблемы, вы настроили PodDisruptionBudget (PDB), чтобы защитить критичные поды от вытеснения. Но в реальности это не особо помогает. Дело в том, что PDB защищает только от добровольных прерываний (drain узлов, апгрейды). Preemption — это вынужденное прерывание, и PDB на него не действует. Планировщик проигнорирует PDB, если альтернативы нет.

1249f0be33079ce12a3b2bab6307118a.png

Самая тяжелая техническая ошибка кроется во взаимодействии планировщика с Horizontal Pod Autoscaler (HPA) и Cluster Autoscaler (CA). Когда высокоприоритетный под не может встать из‑за нехватки CPU, CA добавляет новый узел в кластер. Новый узел поднимается 3–4 минуты. За это время HPA, видя рост нагрузки на старых узлах, начинает плодить новые реплики низкоприоритетных воркеров. Они занимают освободившееся место быстрее, чем поднимается узел.

В итоге CA видит, что под с высоким приоритетом все еще висит в очереди Pending, и добавляет еще один узел. Кластер распухает на 30% за 10 минут. А когда новые узлы наконец появляются, высокоприоритетный под встает, но вместе с ним туда же, по правилу least-requested, планировщик закидывает и низкоприоритетные поды. Происходит эффект симбиоза: высокий приоритет тянет за собой мусор, так как планировщик старается максимально заполнить узел, чтобы не тратить ресурсы зря.

Приоритет очереди и время ожидания

Теперь давайте поговорим об очередях и о том, как они влияют на работу всего кластера. Планировщик использует две очереди: activeQ (поды, готовые к назначению) и unschedulableQ (поды, которые не удалось разместить). Критическая ошибка — игнорирование параметра podMaxInUnschedulableQDuration.

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

657a1f269040158601ca86555e1239b3.png

Если ваш высокоприоритетный под не встает из‑за нехватки GPU или огромных PVC‑томов, он блокирует всю очередь. Планировщик не будет параллельно обрабатывать другие поды, если они имеют более низкий приоритет, так как алгоритм Schedule сначала пытается удовлетворить самого «жирного» клиента. Э

то приводит к тому, что простой под на 100 mCore висит в очереди 4 минуты, хотя на узле есть 2 свободных ядра, но они фрагментированы (заняты мелкими подами). Планировщик слеп к фрагментации, если не включен NodeResourcesFit с политикой MostAllocated — но эту политику включают редко, так как она убивает автоскейлинг.

Инверсия через политики «Упреждающего вытеснения»

Разобравшись с проблематикой, давайте теперь поговорим о том, что, собственно, можно сделать.

Единственный рабочий технический подход — перестать воспринимать priority как защиту. Вместо этого необходимо использовать связку:

  • Принудительное использование requests равным limits для высоконагруженных подов. Это переводит их в класс Guaranteed. Такой под никогда не будет вытеснен из‑за нехватки памяти на узле (его память зарезервирована жестко). Но за это вы платите простаивающими 20% ресурсов, которые нельзя отдать бурстовым задачам.

  • Внедрение PriorityThreshold в descheduler. Вместо того чтобы позволять планировщику вытеснять поды при дефиците, вы запускаете descheduler в режиме RemovePodsViolatingNodeAffinity с жестким порогом. Он заранее, при утилизации узла > 85%, начинает мирно эвакуировать BestEffort‑поды, используя terminationGracePeriodSeconds. Это дает им время отдать незавершенные транзакции (drain), в отличие от хардкорного SIGKILL при использовании preemption.

  • Самая нетривиальная техника — манипуляция schedulerName. Для высокоприоритетных, но «тяжелых» подов (например, Spark‑драйверов) создают отдельный экземпляр планировщика с кастомным профилем, где отключен Preemption. Эти поды просто встают в очередь и ждут, пока освободится место естественным путем, не трогая очередь основного планировщика, где крутятся веб‑сервисы.

Это разрывает цикл «приоритетный под будит CA → CA добавляет ноду → нода заполняется мусором».

Cgroups v2 и давление памяти (PSI)

На уровне ядра Linux данная ошибка проявляется еще жестче. Когда планировщик kubelet принимает решение о вытеснении, он смотрит на memory.available — это не вся свободная память, а значение, вычисленное с учетом inactive_file (кэша страниц). Если высокоприоритетный под использует memory‑mapped файлы (например, Java занимает огромный heap), ядро начинает вытеснять страницы кэша других подов в swap (если он включен) или просто убивает процесс с самым высоким oom_score_adj.

В Cgroups v2 появился механизм PSI (Pressure Stall Information). Ошибка в том, что стандартный kubelet до версии 1.26 не использовал PSI для принятия решений о вытеснении, а полагался на жесткие пороги evictionHard. Если вы не настроили evictionSoft с eviction-max-pod-grace-period, kubelet ждет до последнего, а потом убивает поды с самым высоким приоритетом первыми, потому что они потребляют больше всего памяти относительно requests.

Это противоречит логике, но это факт: при OOM на узле ядро смотрит на oom_score_adj пода. Для Guaranteed этот коэффициент равен -998, для BestEffort — 1000. Казалось бы, Guaranteed защищен. Но если у вас два Guaranteed пода, и один потребляет на 10% больше своего request (что невозможно, если requests==limits, но возможно из‑за overcommit ядра), то ядро убьет тот, у которого выше использование в абсолютных цифрах.

Как избежать инверсии через Taint и Toleration

Самый надежный промышленный паттерн — это не возиться с приоритетами, а использовать горячие и холодные ноды. Вы выполняете taint для группы узлов специальным ключом, например, critical=heavy:NoSchedule. Высокоприоритетные поды получают toleration, а все остальные — нет. Они живут в изолированном пуле, где requests жестко пересчитаны так, что сумма всех requests никогда не превышает 80% allocatable.

В этом пуле вы отключаете Preemption через конфиг планировщика (disablePreemption: true). Теперь, если высокоприоритетному поду не хватает места, он просто висит в Pending, НО он не вытесняет низкоприоритетные поды из других пулов, потому что они не имеют toleration и не могут быть сюда переброшены. И главное — этот подход убивает эффект шторма реинкарнаций, потому что eviction в этом пуле происходит только по драйну ноды, а не по алгоритму вытеснения.

Подведем итог

Контроль над приоритетами в Kubernetes — это контроль над источниками стресса планировщика. Помните: чем выше приоритет пода, тем более жестким должен быть его requests, и тем более узким должен быть круг узлов, где он имеет право жить. Если ваш «критичный» под может лечь на любой узел кластера — он критичен только по названию, но не по архитектуре.

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

Устройство Kubernetes и его ключевые абстракции подробно разобраны в отдельном гайде.


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

  • 21 июля в 20:00 — «Выбор между Serverless и Kubernetes для AI-ворклоадов: как определить оптимальную платформу под задачу». Записаться

  • 12 августа в 20:00 — «Паттерны отказоустойчивости и масштабируемости микросервисной архитектуры». Записаться

Занятия проводят преподаватели-практики. Во время эфира можно задать вопросы по теме урока и получить ответы от эксперта.

Больше бесплатных уроков июля смотрите в дайджесте.

Схожие новости

#Наименование новостиТональностьИнформативностьДата публикации
1Памятник kubelet, Kubernetes != CRI0713-07-2026
2Gateway API против Ingress: как выбрать реализацию и не пожалеть0709-07-2026
32026 Kubernetes Deployment Guide: How to, Solutions & More5803-05-2026
4Оптимизация без AI: как я автоматизировал API-ручки и типы-2529-06-2026
5What Is Kubernetes Orchestration: Tradeoffs & Management Advice0716-02-2026
6Kubernetes Support: Services, Tiers & What Enterprises Need0716-03-2026
7У семи нянек — сервер без ответственного0514-07-2026
8FinOps на практике. Серия 2: от пилота к регламентам, или как удержать экономию на облаке0514-07-2026
9Перебор IP-адресов на хостинге — и как с этим бороться0514-07-2026
10Масштабируй! Почему Cassandra 5 стала спасением, а FoundationDB прилегла в чулан5810-07-2026

Классификация: Мнения. Схожих патентов: 0. Схожих новостей: 10. Тональность: 0. Информативность: 7. Источник: habr.com.