Планировщик Kubernetes — один из ключевых компонентов Kubernetes. Он подбирает узлы для запуска подов, при этом поды обрабатываются по очереди, друг за другом. В таких условиях чем больше кластер, тем выше требования к пропускной способности планировщика.
За годы существования Kubernetes SIG Scheduling значительно повысила пропускную способность планировщика. Эта статья описывает ещё одно улучшение, которое вошло в Kubernetes v1.32: элемент контекста планирования под названием QueueingHint.
Очередь планирования
Планировщик хранит все незапланированные поды во внутреннем компоненте — очереди планирования.
Она состоит из следующих структур:
ActiveQ — содержит только что созданные поды или поды, готовые к повторному планированию.
BackoffQ — содержит поды, которые готовы к повторным попыткам планирования, но ожидают окончания backoff-периода. Backoff-период зависит от числа неудачных попыток планирования для конкретного пода.
-
Пул непланируемых подов — содержит поды, которые планировщик не будет пытаться запланировать по одной из следующих причин:
Планировщик ранее пытался, но не смог запланировать под. С того момента в кластере не произошло весомых изменений, которые позволили бы его запланировать.
Поды блокируются от планирования плагинами PreEnqueue. Например, есть шлюз планирования (scheduling gate — набор условий в спецификации пода, таких как доступность ресурсов или готовность компонентов, которые отслеживаются внешними контроллерами), который блокирует их планирование.
Фреймворк планирования и плагины
Планировщик Kubernetes соответствует фреймворку планирования Kubernetes.
Все функции планирования реализованы в виде плагинов (например, за аффинностью подов следит плагин InterPodAffinity).
Планировщик обрабатывает ожидающие поды в рамках так называемых циклов следующим образом:
1. Цикл планирования: планировщик поочерёдно забирает ожидающие поды из компонента ActiveQ очереди планирования. Для каждого пода применяется логика фильтрации/оценки от каждого плагина планирования. Затем планировщик выбирает наилучший узел для пода или решает, что под в тот момент не может быть запланирован.
Во втором случае под попадает в пул непланируемых подов. Если же подходящий узел найден, под переходит в цикл привязки (binding cycle).
2. Цикл привязки: планировщик передаёт своё решение API-серверу Kubernetes. В результате под привязывается к выбранному узлу.
За некоторыми исключениями, большинство незапланированных подов попадают в пул непланируемых подов после каждого цикла планирования. Пул непланируемых подов крайне важен, поскольку в цикле планирования поды обрабатываются один за другим. Если бы планировщику приходилось постоянно повторять попытки разместить непланируемые поды вместо того, чтобы просто выгрузить их в пул, куча времени тратилась бы впустую.
Как QueueingHint оптимизировала процедуру повторного планирования подов
Непланируемые поды возвращаются в компоненты ActiveQ или BackoffQ очереди планирования, только если изменения в кластере потенциально могут привести к тому, что у планировщика получится разместить эти поды на узлах.
До версии 1.32 каждый плагин планировщика с помощью механизма EnqueueExtensions
(также известного как EventsToRegister
) мог регистрировать, какие изменения в кластере (cluster events) способны сделать под планируемым. Эти изменения включали создание, обновление или удаление объектов Kubernetes. При наступлении любого такого события под, который не удалось разместить во время предыдущего цикла, отправлялся на перепланирование.
Кроме того, внутренняя функция preCheck
помогала дополнительно фильтровать события, тем самым повышая эффективность перепланирования. Она опиралась на базовые ограничения планировщика Kubernetes. Например, preCheck
могла отбросить события, связанные с узлом, статус которого NotReady
.
Однако у этих подходов было две проблемы:
-
Логика возвращения подов в очередь на основании событий была слишком расплывчатой, что приводило к повторным попыткам планирования без веской причины:
Новый запланированный под мог решить (или не решить) проблему с InterPodAffinity. Например, при запуске нового пода, но без лейбла, необходимого для аффинности (InterPodAffinity) с непланируемым подом, тот под по-прежнему не смог бы быть запланирован.
preCheck
полагался на логику in-tree-плагинов и не расширялся на кастомные плагины (см., например, issue #110175).
Именно здесь на помощь приходит новая фича. QueueingHint подписывается на определённый тип кластерных событий и принимает решение о том, способно ли входящее событие сделать под планируемым.
В качестве примера рассмотрим некоторый под pod-a
. У него есть определённые требования к аффинности. Во время цикла планирования плагин InterPodAffinity отверг pod-a, поскольку на узлах не нашлось пода, который удовлетворил бы его требования к аффинности.
Под pod-a
помещается в пул непланируемых подов. Очередь планирования «запоминает» плагин, который помешал планированию пода. В случае pod-a
очередь планирования «запоминает» плагин InterPodAffinity.
Pod-a
не станет планируемым до тех пор, пока не будет устранена проблема с InterPodAffinity. В некоторых случаях проблема может разрешиться: например, один из существующих подов получит лейбл, нужный для InterPodAffinity. В этом сценарии коллбэк-функция QueueingHint плагина InterPodAffinity проверяет обновление лейблов на всех подах в кластере. Если какой-либо под получает лейбл, соответствующий требованиям аффинности pod-a
, функция QueueingHint плагина InterPodAffinity сообщает очереди планировщика, что pod-a
теперь может быть запланирован и его можно поместить в ActiveQ или BackoffQ.
История работы над QueueingHint и как фича изменилась в версии 1.32
Мы в SIG Scheduling разрабатываем QueueingHint, начиная с Kubernetes v1.28.
Хотя QueueingHint никак не взаимодействует с пользователями, был добавлен функциональный шлюз SchedulerQueueingHints
в качестве меры предосторожности. В версии 1.28 QueueingHints работали с несколькими in-tree-плагинами в экспериментальном режиме, а функциональный шлюз был включён по умолчанию.
Однако пользователи сообщили об утечке памяти, поэтому мы отключили его в патч-версии 1.28. С версии 1.28 до версии 1.31 продолжались работа над реализацией QueueingHint в остальных in-tree-плагинах и исправление ошибок.
И вот в v1.32 эта фича снова включена по умолчанию. Мы успешно интегрировали QueueingHint со всеми плагинами и выявили причину утечки памяти!
Спасибо всем, кто принимал участие в разработке этой новой функции, а также тем, кто сообщал о проблемах и помогал их устранить.
P. S.
Читайте также в нашем блоге: