План
Очередь планировщика. Кто последний в очереди?
Scheduler framework и его компоненты.
Informer
Продолжение предыдущей части
Когда я начал погружаться в k8s и успешно сдал k8s CKA, я подумал что k8s scheduler работает как описано в части 1 и больше мне знать не обязательно, чтобы понимать как действительно работает k8s и почему поды планируются именно таким образом. Оказалось, что я упустил важный пласт знаний, который скрывается отчасти в документации, отчасти разбросан по англоязычным статьям в www. Сегодня мы наверстаем упущенное вместе.
Итоги предыдущей части:
k8s Scheduler непрерывно следит за появлением новых подов и нод, и если есть поды(в специальной очереди), которые не закреплены за нодой, то scheduler пытается закрепить их за правильно подобранной нодой.
Тезисно, k8s нужен для:
Учета имеющихся ресурсов
Учета требований к ресурсам
Учета ограничений по ресурсам (affinity, anti-affinity, etc)
Решение, где запустить контейнеры (на основе ограничений и требований)
Scheduler framework и его роль в процессе планирования
Kubernetes в корне своей идеологии использует модульную архитектуру для увелечения гибкости, поддерживаемости и уменьшения сложности кода. k8s Scheduler - не исключение.
Kubernetes использует в своей имплементации Scheduler framework(был придуман как раз для эффективной имплементации k8s планировщика). Это расширяемая архитектура, представленная в Kubernetes, которая позволяет разработчикам настраивать планировщик через написание и включение собственных плагинов. Этот фреймворк состоит из набора точек расширения (extension points), которые позволяют вмешиваться в процесс планирования на различных этапах:
Схема Scheduler framework с официального сайта:
![Scheduling Framework Scheduling Framework](https://habrastorage.org/getpro/habr/upload_files/7cf/507/85f/7cf50785f9bc3b9315fc82194732087a.png)
На схеме видно, что зеленым цветом отмечены блоки, которые можно самостоятельно расширять и менять. Желтым цветом отмечены элементы, которые недоступны для модификации. В официальной документации k8s очень не плохо расписано, за что отвечает каждый элемент, однако, чтобы раскрыть все взаимосвязи между объектами, одной документации недостаточно. Именно по этой причине я подготовил расширенную схему ниже и эту статью.
![Общий алгоритм работы k8s планировщика. Расширенная схема kubernetes scheduling process. Общий алгоритм работы k8s планировщика. Расширенная схема kubernetes scheduling process.](https://habrastorage.org/getpro/habr/upload_files/9f7/b96/127/9f7b9612788f8d1084b4c2d4bdb746c5.png)
PreEnqueue
Это первый плагин планировщика, который вызывается в начале каждого цикла планирования. Его целью является фильтрация подов, которые должны быть запланированы. Например, если у POD'а не указаны требования к ресурсам, то этот POD не будет запланирован. Дальнейшее планирование POD'а возможно, только при условии, что PreEnqueue вернет success в результате своих проверок.
Комментарий
Это некоторая оптимизация, которая позволяет проводить базовые проверки для подов перед помещением объекта POD'а в основную активную очередь. Эта оптимизация нужна для экономии ресурсов, т.е. если POD не удовлетворяет базовым проверкам, то и в очереди ему делать нечего.
Sort(QueueSort)
QueueSort — это функция (или точка расширения в планировщике Kubernetes), которая определяет, как поды должны быть упорядочены в ActiveQ. Сортировка очереди важна, потому что порядок, в котором поды рассматриваются для планирования, может повлиять на общую производительность и распределение ресурсов в кластере. Например, вы можете захотеть, чтобы более критичные поды или поды, ожидающие больше всего времени, были рассмотрены для планирования в первую очередь.
Комментарий
QueueSort не хранит поды; он лишь определяет логику их сортировки в рамках очереди. Очереди подов хранятся в ActiveQ.
Scheduler Queue
В Kubernetes планировщик (kube-scheduler) использует несколько очередей для управления подами, ожидающими назначения на узел. Каждая очередь представляет собой список подов, ожидающих назначения на узел. Всего имеется 3 типа очереди: ActiveQ, UnschedulableQ и BackoffQ. Подробнее тут.
Active Queue
Это активная очередь планирования, в которой хранятся все поды, ожидающие назначения на ноду. Поды в ActiveQ ожидают обработки планировщиком, чтобы быть проверенными по соответствию требованиям, ресурсам нод и так далее, перед тем как будут назначены на подходящую ноду для запуска.
Комментарий
Поды в очереди ActiveQ могут быть как новые, так и перепланированные. Перепланированные(не смог подобрать более подходящего слова для это сущности) поды - это поды, которые уже были запланированы, но по каким-либо причинам были перепланированы. Например, если нода, на которой был запланирован под, упала, то этот POD будет перепланирован на другую здоровую ноду. Кроме этого, QueueSort
UnschedulableQueue (unschedulableQ)
UnschedulableQueue предназначена для хранения подов, ожидающих каких-либо событий, в результате которых эти поды могли бы перейти в activeQ.
Например, эта очередь используется для подов которые планировщик не смог разместить на доступных узлах кластера из-за различных ограничений, таких как недостаток ресурсов, требования к affinity, taints и tolerations и другие пользовательские ограничения. Поды в этой очереди считаются "unschedulable" (не готовые к процессу планирования или нераспределяемыми).
Функциональность:
Поды помещаются в UnschedulableQueue, когда планировщик определяет, что в текущий момент нет узла, который мог бы их принять.
Поды остаются в этой очереди до тех пор, пока не изменится состояние кластера, что может сделать их готовыми к планированию(schedulebled). Например, это может произойти, когда другие поды завершаются и освобождают ресурсы, или когда в кластер добавляются новые узлы.
Планировщик периодически пересматривает поды в UnschedulableQueue, чтобы попытаться найти для них подходящую ноду, особенно после событий, которые могут повлиять на доступность ресурсов или местоположение подов.
Комментарий
UnschedulableQueue
- это ещё 1 способ для оптимизации процесса планирования, который позволяет избежать ненужных циклов планирования подов в условиях, когда их невозможно распределить, и одновременно обеспечивает механизм для их переоценки при изменении условий кластера.
PodBackoffQueue (podBackoffQ)
PodBackoffQueue - для обработки и управления повторными попытками планирования подов, которые не удалось запланировать в предыдущих попытках.
Эта очередь имеет механизм экспоненциальной задержки между повторными попытками, чтобы предотвратить чрезмерную нагрузку на планировщик и ресурсы кластера.
Функциональность:
Поды, которые не удалось запланировать, входят в фазу отката, где они ожидают некоторое время перед следующей попыткой планирования.
Время ожидания определяется экспоненциальным алгоритмом: каждая последующая неудачная попытка планирования увеличивает задержку до следующей попытки.
Этот процесс помогает снизить нагрузку на планировщик и даёт время для возможных изменений в кластере, которые могут позволить успешно запланировать под.
Комментарий про экспоненциальную задержку
После первой неудачной попытки планирования POD'а, он помещается в
PodBackoffQueue
с начальной задержкой.Если POD все еще не может быть запланирован после истечения задержки, время ожидания увеличивается экспоненциально, до определённого максимума.
flushUnschedulableQLeftover и flushBackoffQCompleted: Механизмы отвечающие за перемещение подов обратно в активную очередь
Эти механизмы помогают убедиться, что поды, которые по тем или иным причинам временно не могли быть запланированы, не остаются без внимания и имеют возможность быть запланированными, когда условия в кластере меняются.
flushUnschedulableQLeftover
Этот механизм отвечает за перемещение подов из UnschedulableQueue (очередь нераспределяемых подов) обратно в активную очередь (ActiveQ).
Поды помещаются в UnschedulableQueue, когда планировщик определяет, что в текущий момент нет подходящего узла для их размещения, например, из-за недостатка ресурсов.
Когда в кластере происходят изменения (например, освобождаются ресурсы на узлах или добавляются новые узлы), flushUnschedulableQLeftover активируется, чтобы переместить поды из UnschedulableQueue обратно в ActiveQ для повторной оценки и планирования.
flushBackoffQCompleted
Этот механизм управляет подами в BackoffQueue (очередь отката), которая содержит поды, временно отложенные из-за предыдущих неудачных попыток планирования.
Поды попадают в BackoffQueue, когда они неоднократно не могут быть запланированы. Для этих подов применяется механизм экспоненциального отката, который увеличивает время ожидания перед каждой новой попыткой планирования.
Когда время ожидания в BackoffQueue истекает, flushBackoffQCompleted переносит поды обратно в ActiveQ, чтобы они могли быть повторно рассмотрены для планирования.
Schedule pipeline
Schedule Pipeline - представляет собой центральную логику планировщика Kubernetes. Schedule Pipeline - это цепочка шагов и проверок, после которых pod должен быть назначен на одну из нод. Schedule Pipeline разделен на 3 потока. Про потоки писал ранее тут.
PreFilter
Этот этап процесса планирования выполняется до основных этапов фильтрации и выбора узла и служит для выполнения быстрых предварительных проверок, чтобы определить, соответствует ли POD базовым критериям для планирования.
Основная функция PreFilter
Предварительная проверка подов: PreFilter позволяет планировщику быстро определить, соответствует ли POD базовым требованиям, необходимым для его планирования на узел. Это могут быть проверки на соответствие требованиям ресурсов, ограничениям по объему памяти или CPU, etc.
Оптимизация процесса планирования: Если на этапе PreFilter выясняется, что pod не может быть запланирован (например, если в кластере нет узлов с достаточным количеством ресурсов), то он может быть исключен из дальнейшего рассмотрения без необходимости перехода к более трудоемким этапам фильтрации и ранжирования.
Уменьшение нагрузки на планировщик: Это помогает уменьшить общую нагрузку на планировщик, избегая лишних вычислений для подов, которые явно не подходят для планирования в текущем состоянии кластера.
В случае неудачи pod помещается в UnschedulableQueue, где он ожидает, пока не изменится состояние кластера, что может позволить ему быть запланированным.
Filter
Filter является ключевой точкой расширения (extension point) в Scheduling Framework. Он отвечает за фильтрацию узлов, которые не могут принять pod, и выбор узла, который может принять pod.
Фильтрация узлов: На этапе Filter, планировщик проходит через все доступные узлы и применяет заданные фильтры к каждому из них. Цель этого процесса - исключить те узлы, которые по тем или иным причинам не подходят для размещения POD'а.
Критерии фильтрации: Это могут быть проверки на соответствие требованиям ресурсов POD'а (например, CPU, RAM), а также другие пользовательские ограничения.
Процесс фильтрации
Каждый узел проверяется: Для каждого POD'а планировщик последовательно рассматривает каждый узел и проверяет, соответствует ли он всем критериям фильтрации.
Исключение неподходящих узлов: Узлы, которые не удовлетворяют условиям фильтра, исключаются из дальнейшего рассмотрения для данного POD'а.
Результат фильтрации: После завершения процесса фильтрации остаются только те узлы, которые потенциально подходят для размещения POD'а.
Комментарий о расширяемости
т.к. этот плагин (extension point) помечен зеленым(как и другие подобные на схеме), то разработчики разработчики могут создавать и внедрять свои собственные фильтры в рамках Scheduling Framework. Это позволяет адаптировать процесс планирования к специфическим требованиям и условиям кластера.
PreScore
Этот этап предназначен для подготовки необходимых данных или выполнения предварительных расчетов, которые будут использоваться на этапе ранжирования узлов.
Это еще 1 плагин призванный оптимизировать процесс, его назначение очень похоже на PreFilter по сути.
Пояснение
Выполнение предварительных операций: Планировщик выполняет любые необходимые операции перед тем, как начать ранжирование узлов. Это могут быть расчеты, сбор статистики, или другие операции, которые помогут определить, как лучше всего ранжировать узлы для конкретного POD'а.
Оптимизация процесса ранжирования: PreScore позволяет сделать процесс ранжирования более эффективным, уменьшая количество необходимых вычислений во время самого этапа Score.
Score
Цель этапа Score заключается в оценке и ранжировании узлов, которые успешно прошли этап фильтрации, чтобы определить, какой из них лучше всего подходит для размещения данного POD'а.
Каждому узлу присваивается оценка на основе различных критериев, таких как доступные ресурсы, близость к необходимым сервисам, требования к affinity/anti-affinity и другие факторы. Эти оценки затем используются для ранжирования узлов, чтобы определить, какой из них лучше всего подходит для размещения POD'а.
Пояснение о метриках и оценках
Множественные метрики: Планировщик использует различные метрики и алгоритмы для оценки узлов. Разработчики могут настраивать или добавлять свои собственные метрики в процесс ранжирования.
Суммирование оценок: Оценки от всех применяемых критериев и плагинов складываются, чтобы определить общую оценку каждого узла.
Reserve
Цель этапа Reserve заключается в резервировании необходимых ресурсов на узле для POD'а, который планируется к запуску. Это может включать, например, выделение определенного объема памяти или CPU. Этап Reserve гарантирует, что все ресурсы, необходимые для POD'а, будут доступны на выбранном узле перед его окончательным назначением.
Это необходимо для того, чтобы другие поды не могли занять эти ресурсы(предотвращение ситуации race condition). Этот плагин реализует так же метод UnReserve.
UnReserve
Если на каком-либо позднем этапе планирования (например, на этапе Permit или Bind) возникают проблемы и POD не может быть успешно размещен на узле, UnReserve используется для отката действий, выполненных на этапе Reserve. UnReserve освобождение ресурсы, делает отмену изменений, сделанных на узле в процессе резервирования, что позволяет этим ресурсам быть доступными для других подов.
Другими словами: Этот этап гарантирует, что ресурсы узлов не остаются беспричинно заблокированными в случае неудачи планирования, повышая тем самым общую эффективность и надежность кластера.
Это метод часть плагина Reserve. Этот метод может быть вызван не зависимо от очереди выполнения из любых других плагинов(после этапа Reserve).
Permit
Этот этап позволяет планировщику синхронизировать свои действия с внешними системами, прежде чем разместить pod на узле.
Основная задача: решить, следует ли разрешить или отложить размещение пода (pod) на узле (node), основываясь на текущем состоянии кластера или внешних факторах.
Этап Permit работает в своем отдельном потоке, что означает, что он выполняется параллельно с основным(main) потоком планировщика. Это подчеркивает асинхронный и неблокирующий характер этой фазы.
Permit может сделать одну из 3-х вещей:
approve - Все предыдущие плагины подтвердили, что POD может быть запущен на ноде. Значит финальное решение для POD'а - approve.
deny - Один из предыдущих плагинов вернул не положительный результат. Значит финальное решение для POD'а - deny.
wait - Если плагин permit возвращает “wait”, то POD остается в фазе permit, пока POD не получит approve или deny статус. Если происходит тайм-аут, “wait” становится “deny”, и POD возвращается в очередь планирования, активируя метод Un-reserve в фазе Reserve.
Пояснение:
Управление разрешениями на размещение POD'а: Permit дает планировщику возможность отложить размещение POD'а на узле до тех пор, пока не будут выполнены определенные условия. Это может быть использовано для синхронизации с внешними системами, проверки состояния кластера или для реализации сложных политик планирования.
Предотвращение конфликтов: Этот этап может помочь избежать потенциальных конфликтов, например, когда несколько подов одновременно пытаются занять одни и те же ресурсы на узле.
PreBind
Все семейство плагинов Bind работает в отдельном потоке, как изображено на схеме.
На этапе PreBind планировщик выполняет любые необходимые действия или проверки перед тем, как pod будет фактически привязан к узлу. Это может включать конфигурацию необходимых ресурсов, проверку совместимости или синхронизацию с внешними системами.
Кроме этого, PreBind позволяет гарантировать, что все необходимые условия выполнены, что снижает вероятность отказа или проблем после привязки POD'а к узлу.
Простой пример - PreBind может убедиться в доступности внешнего сетевого хранилища перед привязкой.
Bind
На этом этапе планировщик фактически привязывает POD к узлу. Это может включать в себя различные действия, такие как резервирование ресурсов, настройка сети, запуск контейнеров и т.д.
Окончательное назначение POD'а на узел: После того, как POD прошел через все предыдущие этапы планирования (фильтрация, ранжирование, проверки Permit и PreBind), этап Bind заключается в формальном назначении этого POD'а на выбранный узел.
Обновление состояния кластера: Во время этого этапа планировщик обновляет состояние кластера, отмечая, что POD теперь назначен и должен быть запущен на конкретном узле.
Процесс привязки:
API-запрос на привязку: Планировщик отправляет запрос в API сервер Kubernetes, чтобы привязать pod к выбранному узлу. Это обычно включает обновление объекта Pod (в etcd конечно) с информацией о том, к какому узлу он привязан.
Запуск POD'а (этот пункт вне k8s scheduler, указан тут для наглядности всего процесса): После успешной привязки в работу вступает kubelet на соответствующем узле и начинает процесс запуска POD'а.
Важные замечания
Pod удаляется из активной очереди (ActiveQ) после успешного выполнения этапа Bind, но до начала этапа PostBind. Этот процесс нужен, чтобы показать, что под, который уже был успешно назначен на узел, больше не рассматривается для дальнейшего планирования.
Взаимодействие Scheduling Pipeline и Кэша
Сбор Информации: В начале Scheduling Pipeline, планировщик собирает информацию о всех доступных узлах и подах в кластере. Эта информация обычно извлекается из кэша планировщика, который поддерживает актуальное состояние кластера.
Обновление Кэша: Кэш постоянно обновляется по мере изменения состояния узлов и подов в кластере. Эти обновления могут поступать от различных компонентов Kubernetes, таких как kubelet или контроллеры. Обновленная информация критически важна для точного и эффективного планирования.
Informer
Informer в Kubernetes представляет собой мощный инструмент для наблюдения за ресурсами, кэширования их состояния и уведомления об их изменениях, что критически важно для реактивности и эффективности работы различных компонентов системы.
При запуске планировщик Kubernetes извлекает данные, необходимые для планирования, с сервера API Kubernetes через informer с помощью API List и Watch. Эти данные содержат информацию о модулях, узлах, PV, PVC, etc...
Роль и функционал Informer'а
Наблюдение за ресурсами: Informer подписывается на обновления определенных ресурсов Kubernetes через Kubernetes API Server. Это позволяет ему отслеживать изменения в этих ресурсах в реальном времени.
Кэширование состояния ресурсов: Informer сохраняет полученные данные о состоянии ресурсов в кэше(который изображен на общей схеме). Это позволяет другим компонентам системы быстро получать актуальную информацию без необходимости постоянных запросов к API серверу.
Уведомление об изменениях: Когда ресурсы в кластере изменяются (например, создание нового POD'а или обновление узла), Informer уведомляет зарегистрированные в нем обработчики событий (event handlers) об этих изменениях.
P.S.: Статья вышла больше, чем я ожидал, поэтому сюда не включен разбор конкретных плагинов и пример написания собственного планировщика (если будет запрос в комментариях, то эта статья появится на хабре).
Пожалуйста, не стесняйтесь указывать на неточности и ошибки в комментариях, я буду исправлять и дополнять статью. K8S достаточно сложная тема, информацию о ней пришлось собирать из разных источников.
Полезные ссылки:
Также хочу порекомендовать бесплатный вебинар про хаос инжиниринг, который проведет мой коллега из OTUS dmitriizolotov. Узнать подробности и зарегистрироваться можно по этой ссылке.