Недавно на одном из YouTube-каналов я подробно рассматривал работу Kubernetes Scheduler. В процессе подготовки материала я столкнулся с множеством новых и интересных фактов, которыми хотел бы поделиться с вами. В этой статье мы разберём, что именно происходит “под капотом” Kubernetes Scheduler и какие аспекты важны для понимания его функционирования. Планирую идти от простого к сложному, так что прошу отнестись с пониманием. Если вы уже знакомы с базовыми концепциями, не стесняйтесь пропустить вступительную часть и перейти сразу к ключевым деталям. Если у рядового разработчика спросить, как бы ты имплементировал работу k8s scheduler?
Ответ скорее всего будет в таком стиле:
while True: pods = get_all_pods() for pod in pods: if pod.node == nil: assignNode(pod)
Но этой статьи бы не было, если бы все было так просто.
Планировщик (Scheduler) в Kubernetes отвечает за распределение подов (Pods) по рабочим узлам (Nodes) в кластере. Основная задача планировщика — оптимизировать размещение подов, учитывая доступные ресурсы на узлах, требования каждого пода и различные другие факторы.
Если попросить меня описать функции Kubernetes Scheduler в двух словах, я бы выделил две ключевые задачи:
Если попробовать изобразить последовательность действий, которые происходят при создании пода, то получится следующая схема:
На изображении ниже показана последовательность действий, которые происходят при создании пода.
Все эти шаги называются “Extension points”(они же плагины), которые позволяют расширять функциональность планировщика. Они реализованы благодаря Scheduler Framework, о котором мы поговорим во 2‑й части статьи.
Например, вы можете добавить новые фильтры или алгоритмы ранжирования, чтобы удовлетворить специфические требования вашего приложения. На самом деле плагинов гораздо больше, мы вернемся к этому в следующей части.
Процесс повторяется: Планировщик продолжает мониторить кластер для следующего пода, который нуждается в размещении.
В упрощенном виде процесс планирования предоставлен на рисунке ниже.
В деталях мы рассмотрим этот процесс во 2-й части, а пока давайте посмотрим на то, как работает планировщик в базовом понимании.
В документации k8s этот процесс имеет ту же структуру, но отображен в более общей форме. Вместо informer отображен event handler. Informer используют обработчики событий(event handler) для запуска конкретных действий при обнаружении изменения в кластере. Например, если создан новый под, который нужно запланировать, обработчик событий информера активирует алгоритм планирования для этого конкретного пода.
Informer: Планировщик Kubernetes активно использует механизм, называемый “Informer”, для мониторинга состояния кластера. Informer — это комплекс контроллеров, которые непрерывно отслеживают определённые ресурсы в etcd. При обнаружении изменений, информация обновляется во внутреннем кэше планировщика. Этот кэш позволяет оптимизировать расход ресурсов и предоставлять актуальные данные о нодах, подах и других элементах кластера.
Schedule Pipeline: Процесс планирования в Kubernetes начинается с добавления новых подов в очередь. Эта операция осуществляется с использованием компонента Informer. Поды затем извлекаются из этой очереди и проходят через так называемый “Schedule Pipeline” — цепочку шагов и проверок, после которых происходит финальное размещение пода на подходящей ноде.
Schedule Pipeline разделен на 3 потока.
Schedule Pipeline так же использует Cache для хранения данных о подах.
Важный аспект:
Это ограничение было введено для того, чтобы избежать ситуации, когда несколько подов пытаются занять одни и те же ресурсы на ноде.
Все остальные потоки могут выполняться асинхронно.
1. Создадим новый под
Чтобы дать работу планировщику, создадим новый под с помощью команды kubectl apply.
Создадим под с помощью deployment.
Важно отметить, что планировщик работает только с подами, а за состоянием Deployment и replicaSet следит контроллер.
kubectl apply -f https://k8s.io/examples/controllers/nginx-deployment.yaml
nginx-deployment.yaml
apiVersion: apps/v1 kind: Deployment metadata: name: nginx-deployment labels: app: nginx spec: replicas: 3 selector: matchLabels: app: nginx template: metadata: labels: app: nginx spec: containers: - name: nginx image: nginx:1.14.2 ports: - containerPort: 80
2. Контроллер создает поды
На самом деле мы создадим Deployment, который в свою очередь создаст replicaSet, который в свою очередь создаст под.
Контроллер, который отвечает за состояние Deployment и replicaSet, видит соответствующие новые объекты и начинает свою работу.
Контроллер увидит deployment выше и будет создан примерно такой объект ReplicaSet
ReplicaSet
apiVersion: v1 items: - apiVersion: apps/v1 kind: ReplicaSet metadata: annotations: deployment.kubernetes.io/desired-replicas: "3" deployment.kubernetes.io/max-replicas: "4" deployment.kubernetes.io/revision: "1" labels: app: nginx name: nginx-deployment-85996f8dbd namespace: default ownerReferences: - apiVersion: apps/v1 blockOwnerDeletion: true controller: true kind: Deployment name: nginx-deployment uid: b8a1b12e-94fc-4472-a14d-7b3e2681e119 resourceVersion: "127556139" uid: 8140214d-204d-47c4-9538-aff317507dd2 spec: replicas: 3 selector: matchLabels: app: nginx pod-template-hash: 85996f8dbd template: metadata: labels: app: nginx pod-template-hash: 85996f8dbd spec: containers: - image: nginx:1.14.2 imagePullPolicy: IfNotPresent name: nginx ports: - containerPort: 80 protocol: TCP resources: {} terminationMessagePath: /dev/termination-log terminationMessagePolicy: File dnsPolicy: ClusterFirst restartPolicy: Always schedulerName: default-scheduler securityContext: {} terminationGracePeriodSeconds: 30 status: availableReplicas: 3 fullyLabeledReplicas: 3 observedGeneration: 1 readyReplicas: 3 replicas: 3 kind: List
В результате работы контроллера, который отвечает за replicaset будет создаено 3 пода. Они получают статус Pending, потому что планировщик еще не запланировал их на ноды, эти поды добавляются в очередь планировщика.
3. Планировщик вступает в дело
Таким образом это можно отобразить на нашей схеме.
Каждый под в очереди планировщика в порядке очереди извлекается и:
Возможно немного повторюсь, но тут чуть более подробно про сам пайплайн.
Filter - отсеиваем неподходящие ноды.
Например, если мы хотим разместить под на ноде, где есть GPU, то нам сразу не нужны ноды без GPU.
Далее мы убираем ноды, на которых нет достаточно ресурсов для запуска пода. Например, если под требует 2 CPU, а на ноде есть только 1 CPU, то такая нода не подходит.
Score - сортируем оставшиеся ноды.
Если нод больше чем одна, нам же нужно как-то выбрать наиболее подходящую ноду, а не просто использовать random.
Тут вступают в дело различные плагины. Например, плагин ImageLocality позволяет выбрать ноду, на которой уже есть образ контейнера, который мы хотим запустить. Это позволяет сэкономить время на скачивание образа из container registry.
Reserve - резервируем ресурсы на ноде для пода.
Чтобы в следующем потоке не увели ресурсы нашей идеальной ноды, мы бронируем эту ноду.
Un-Reserve - если что-то пошло не так на любом из этапов, ты мы вызываем этот метод, чтобы освободить ресурсы на ноде и отправить под обратно в очередь планировщика.
Permit - проверяем, что под может быть запущен на ноде.
Если все предыдущие шаги прошли успешно, то мы проверяем, что под может быть запущен на ноде. Например, если у нас есть правило affinity, которое говорит, что под должен быть запущен на ноде с определенным label, то мы проверяем, что эта нода соответствует этому правилу. Если все хорошо, то мы возвращаем статус approve, если нет, то deny.
Фаза закрепления
В этой фазе выполняем доп шаги перед окончательным закреплением ноды, само закрепление пода за нодой и необхходимые шаги после закрепления. Больше деталей про эту фазу вы читайте в части 2.
Важно отметить, что этот поток работает асинхронно.
Как только мы закрепили ноду за подом, kubelet видит эти изменения и начинает процесс запуска контейнера на ноде. Еще раз отмечу, что kubelet это компонент не из системы планировщика.
Под запущен на наиболее подходящей ноде, и мы можем увидеть это в выводе команды kubectl get pods. А значит планировщик выполнил свою работу.
kubectl get pods -o wide
Вот так выглядит Schedule Pipeline в упрощенном виде, а в деталях мы рассмотрим его во 2-й части.
В следующей части мы копнем глубже и узнаем больше о внутренней кухне планировщика.
В частности, мы:
Тут будет ссылка на 2-ю часть.