Kubernetes v1.37: Витіснення планувальником для зміни розміру Podʼів на місці (Alpha)

У Kubernetes виділення ресурсів історично було статичним рішенням, прийнятим під час початкового планування та розміщення Podʼа. З переходом основної можливості in-Place Pod resize до загальної доступності (GA) у v1.35, розробники застосунків та оператори кластерів отримали потужну здатність динамічно коригувати виділення CPU та памʼяті запущених контейнерів без руйнівних перезапусків або простою застосунків.

Однак зміна розміру на місці створила унікальну прогалину в плануванні ресурсів: якщо запущений Pod запитував масштабування ресурсів, що перевищувало запас ресурсів хост-вузла, доступних для розподілу, Kubelet був змушений позначити запит як Deferred. Pod залишався у цьому стані невизначений час, очікуючи, доки ресурси на вузлі не звільняться природним шляхом.

Для закриття цієї прогалини в плануванні Kubernetes v1.37 вводить витіснення планувальником для зміни розміру Podʼа на місці (Alpha), з функціональною можливістю InPlacePodVerticalScalingSchedulerPreemption. Ця можливість дозволяє планувальнику Kubernetes активно звільняти ресурси на повністю завантаженому вузлі шляхом витіснення робочих навантажень з нижчим пріоритетом, уможливлюючи успіх очікуваних in-place масштабувань критичних застосунків з вищим пріоритетом.

Проблема відкладеної («deferred») зміни розміру

Щоб зрозуміти, чому цей механізм витіснення потрібен, корисно подивитися, як Kubernetes обробляє зміну розміру запущених Podʼів. Коли користувач або контролер (такий як Vertical Pod Autoscaler) оновлює запити ресурсів активного контейнера, Kubelet оцінює, чи базовий вузол має достатньо вільної потужності для задоволення збільшення.

Якщо ресурси вузла повністю використані і не можуть задовольнити нові обмеження, Kubelet встановлює resizeStatus контейнера (повідомлений у status.containerStatuses[] Podʼа) на Deferred. На відміну від запиту на зміну розміру Infeasible (який негайно відхиляється, бо він перевищує фізичні межі машини, LimitRange простору імен або квоти допуску), статус Deferred вказує на те, що запит є дійсним, але тимчасово не може бути реалізований, очікуючи, доки потужність вузла не стане доступною.

Перед введенням цього механізму витіснення запит на in-place масштабування Podʼа міг стати постійно заблокованим, якщо вузол був інтенсивно використаний. Навіть коли критичному застосунку (такому як in-memory база даних або real-time веб-сервер) потрібно більше памʼяті для запобігання неминучому збою out-of-memory (OOM), а вузол не мав вільної потужності, resize залишався Deferred.

У цьому сценарії оператори кластера мали обмежений вибір:

  1. Вручну виселити Podʼи з нижчим пріоритетом з вузла, щоб звільнити запас ресурсів.
  2. Покладатися на кластерний автомасштабувач, який зрештою запустить більший вузол і перепланує Pod. Однак це операція, яка є надзвичайно руйнівною і порушує основну пропозицію цінності «без перезапуску» in-place масштабування.
  3. Покладатися на власне рішення автоматичного масштабування, наприклад, кластерний автомасштабувач, який може сам ініціювати операції динамічної зміни розміру вузла.

Оскільки kube-scheduler не знав про відкладені зміни розміру на запущених Podʼах, він не міг використовувати стандартне витіснення на основі пріоритетів для виселення робочих навантажень з нижчим пріоритетом і звільнення місця для зростання ресурсів запущеного Podʼа з вищим пріоритетом.

Чому це важливо

У промислових середовищах Kubernetes оператори кластерів прагнуть максимізувати використання ресурсів та ефективність. Поширена стратегія — bin-pack (упакування) невикористаної потужності на ще не повних вузлах робочими навантаженнями з нижчим пріоритетом, такими як пакетні завдання, фонова обробка даних або best-effort задачі.

Без витіснення планувальником для зміни розміру на місці це створювало велику операційну дилему. Якщо робочі навантаження з нижчим пріоритетом споживали залишковий запас на вузлі, застосунки з вищим пріоритетом, що запущені на тому самому вузлі, ставали заблокованими (Deferred), коли їм потрібно було масштабуватися для обробки раптових стрибків трафіку або сплесків використання памʼяті. Оператори були змушені вибирати між запуском кластерів з низьким використанням та вільним буферним запасом або ризиком того, що критичні робочі навантаження не зможуть масштабуватися, коли це потрібно.

З витісненням планувальником для зміни розміру Podʼа на місці ви можете впевнено упакувати невикористаний простір у своїх кластерах робочими навантаженнями з нижчим пріоритетом, не переймаючись тим, що вони погіршать роботу Podʼів з вищим пріоритетом або блокують їхні запити на масштабування. Якщо робоче навантаження з високим пріоритетом вимагає зміни розміру на місці, що перевищує доступну потужність вузла, планувальник автоматично витісняє Podʼи з нижчим пріоритетом для звільнення запасу. Ви досягаєте високого використання кластера та ефективності витрат, зберігаючи при цьому швидкість реагування та надійність критичних сервісів.

Архітектурна механіка: Як це працює

Витіснення планувальником для зміни розміру Podʼа на місці інтегрується безпосередньо в основний цикл планування для динамічної та безпечної координації ресурсів.

Централізоване відстеження планувальником

kube-scheduler відстежує в кластері Podʼи, що виконуються зі статусом зміни розміру Deferred. Зазвичай Podʼи із заповненим полем spec.nodeName вважаються успішно розміщеними й оминають активну чергу планування. У рамках цієї функціональної можливості планувальник перехоплює Podʼи зі статусом Deferred, дозволяючи їм залишатися в активній черзі планування спеціально для запуску витіснення. Планувальник здійснює безперервне відстеження цих Podʼів, доки Kubelet не завершить успішно операцію зміни розміру.

Межа витіснення на одному вузлі

На відміну від витіснення при розміщенні, яке аналізує всі вузли в кластері для пошуку оптимального варіанту планування, витіснення для зміни розміру на місці суворо обмежується вузлом, якому наразі призначено Pod. Планувальник визначає відповідні Podʼи з нижчим пріоритетом — «жертви» — на тому самому хості та ініціює їх належне виселення, звільняючи локальні ресурси. Витіснення суворо обмежується тим самим вузлом, на якому працює Pod із відкладеним зміненням розміру; якщо вузол не може забезпечити змінення розміру навіть після виселення всіх відповідних робочих навантажень з нижчим пріоритетом, змінення розміру залишається у стані Deferred.

Безпека резервування ресурсів

Щоб запобігти станам гонитви під час планування і подвійному виділенню ресурсів, планувальник розглядає ресурси, запитувані для зміни розміру, як вже використані. Це дозволяє Kubelet запустити зміну розміру, щойно витіснення набуде чинності.

Розділення зон відповідальності та критичний допуск

Коли вузол перебуває під тиском нестачі ресурсів, Kubelet включає локальний механізм, відомий як critical Pod admission handler (обробник критичного допуску Podʼів). Під час початкового допуску Podʼа, якщо критичний системний Pod надходить на вузол, в якого бракує вільної потужності, цей локальний обробник може безпосередньо виселити Podʼи з нижчим пріоритетом на цьому вузлі для гарантування допуску критичного робочого навантаження.

Значною архітектурною перевагою цієї нової можливості є суворе розділення зон відповідальності між Kubelet та планувальником. З функціональною можливістю InPlacePodVerticalScalingSchedulerPreemption обробник допуску критичних Podʼів Kubelet НЕ виконує локальні перевірки витіснення і НЕ ініціює локальні витіснення для операцій зміни розміру на місці. Замість цього Kubelet відкладає запит і повністю делегує рішення про витіснення планувальнику. Це гарантує, що єдиний централізований оркестратор керує всією логікою витіснення, повʼязаною зі зміною розміру на місці, поважаючи глобальні пріоритети, Pod Disruption Budgets (PDB) та політики належного завершення роботи.

Керування конкурентними оновленнями та станами гонитви

Якщо під час активного циклу витіснення надходить конкуруючий запит на зміну розміру з вищим пріоритетом для іншого Podʼа, що виконується на тому самому вузлі, Kubelet надає пріоритет запиту з вищим пріоритетом. Планувальник призначений для відстеження таких оновлень і динамічно запускатиме новий цикл витіснення, якщо для досягнення нового стану знадобиться більше ресурсів.

Конфігурація витіснення на рівні вузла

Адміністратори та автоматизовані контролери (наприклад, автомасштабувальник кластера) можуть вимкнути витіснення спеціально для зміни розміру на місці на окремих вузлах. Це налаштовується за допомогою нового поля spec.podPreemptionPolicy у специфікації вузла:

apiVersion: v1
kind: Node
metadata:
  name: batch-workload-node
spec:
  podPreemptionPolicy:
    disableResizePreemption:
      - "cluster-autoscaler.kubernetes.io/disable-preemption"
      - "operator.example.com/policy-override"

Приклад випадку використання цієї політики — коли контролер надає перевагу зміні розміру інших Podʼів або динамічній корекції потужності власного вузла, коли це можливо, вмикаючи витіснення планувальником лише як останній засіб.

Спробуйте це!

Для використання витіснення планувальника для зміни розміру Podʼа на місці:

  • Ваш кластер має працювати на Kubernetes версії v1.37 або новішій, як на панелі управління, так і на всіх робочих вузлах.
  • Функціональна можливість InPlacePodVerticalScalingSchedulerPreemption має бути увімкнена на всіх компонентах панелі управління (kube-apiserver, kube-scheduler) та kubelet.

Міні-посібник: Спостереження за витісненням планувальника для зміни розміру на місці в дії

Щоб побачити цю можливість в дії локально, ви можете протестувати витіснення планувальника на одновузловому кластері kind з обмеженим запасом CPU.

1. Створіть kind-кластер з увімкненим витісненням планувальника для зміни розміру на місці

Створіть файл конфігурації кластера kind з назвою kind-config.yaml з увімкненою функціональною можливістю InPlacePodVerticalScalingSchedulerPreemption:

# kind-config.yaml
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
featureGates:
  InPlacePodVerticalScalingSchedulerPreemption: true

Створіть кластер, використовуючи цю конфігурацію, передаючи прапорець --image, щоб переконатися, що кластер працює на Kubernetes v1.37 (або новішій):

kind create cluster --config kind-config.yaml --image kindest/node:v1.37.0

Примітка:

Переконайтеся, що образ вузла, який ви вказуєте, відповідає Kubernetes-кластеру v1.37 або новішому (такому як kindest/node:v1.37.0). Старіші релізи Kubernetes не підтримують функціональну можливість InPlacePodVerticalScalingSchedulerPreemption.

Після готовності кластера перевірте вузол, щоб дізнатися, скільки ядер CPU, доступних для розподілу, він має:

kubectl get nodes -o custom-columns=NAME:.metadata.name,ALLOCATABLE_CPU:.status.allocatable.cpu

У стандартному локальному середовищі kind вивід показує 8 ядер CPU, доступних для розподілу:

NAME                 ALLOCATABLE_CPU
kind-control-plane   8

2. Створіть PriorityClasses та розгорніть Podʼи

Створіть два PriorityClasses та розгорніть Pod з низьким пріоритетом (що запитує 3 CPU) поруч з Podʼом з високим пріоритетом (що запитує 4 CPU). Разом ці робочі навантаження споживають 7 з 8 доступних ядер CPU, залишаючи 1 CPU вільного запасу, доступного для розподілу, на вузлі.

# preemption-demo.yaml
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
  name: high-priority
  value: 1000000
  globalDefault: false
  description: "High priority workload"
---
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
  name: low-priority
  value: 1000
  globalDefault: false
  description: "Low priority workload"
---
apiVersion: v1
kind: Pod
metadata:
  name: low-priority-pod
spec:
  priorityClassName: low-priority
  containers:
  - name: worker
    image: nginx
    resources:
      requests:
        cpu: "3"
        memory: "500Mi"
      limits:
        cpu: "3"
        memory: "500Mi"
---
apiVersion: v1
kind: Pod
metadata:
  name: high-priority-pod
spec:
  priorityClassName: high-priority
  containers:
  - name: app
    image: nginx
    resources:
      requests:
        cpu: "4"
        memory: "1Gi"
      limits:
        cpu: "4"
        memory: "1Gi"

Збережіть цей маніфест у preemption-demo.yaml і застосуйте його:

kubectl apply -f preemption-demo.yaml

Почекайте, поки обидва Podʼи запустяться на вузлі:

kubectl get pods

Вивід:

NAME                READY   STATUS    RESTARTS   AGE
high-priority-pod   1/1     Running   0          9s
low-priority-pod    1/1     Running   0          9s

3. Запит на in-place scale-up

Застосуйте patch до Podʼа з високим пріоритетом, щоб збільшити його запит CPU з 4 до 6 (+2 CPU). Оскільки на вузлі вільний лише 1 CPU, цей запит на зміну розміру перевищує залишкову доступну ємність:

kubectl patch pod high-priority-pod --subresource resize --patch \
  '{"spec":{"containers":[{"name":"app", "resources":{"requests":{"cpu":"6"}, "limits":{"cpu":"6"}}}]}}'

4. Перевірка події витіснення на Podʼі з низьким пріоритетом

Якщо увімкнено InPlacePodVerticalScalingSchedulerPreemption, планувальник перехоплює стан зміни розміру Deferred у high-priority-pod і вибирає low-priority-pod для витіснення.

Щоб переконатися, що планувальник дійсно витіснив Pod з низьким пріоритетом, перегляньте його події:

kubectl get events --field-selector involvedObject.name=low-priority-pod

У потоці подій (або через kubectl describe pod low-priority-pod) ви побачите подію Preempted, згенеровану планувальником:

LAST SEEN   TYPE     REASON      OBJECT                 MESSAGE
5s          Normal   Preempted   pod/low-priority-pod   Preempted by pod 97dba925-6b5f-4e2f-99f9-d51c30016586 on node kind-control-plane
5s          Normal   Killing     pod/low-priority-pod   Stopping container worker

5. Відстеження життєвого циклу події зміни розміру на Podʼі з високим пріоритетом

Далі перегляньте історію подій на high-priority-pod, щоб спостерегти, як зміна розміру перейшла від стану deferred до успішного завершення:

kubectl get events --field-selector involvedObject.name=high-priority-pod

Ви спостерігатимете послідовність подій, коли Kubelet координується з планувальником:

LAST SEEN   TYPE      REASON            OBJECT                  MESSAGE
33s         Warning   ResizeDeferred    pod/high-priority-pod   Pod resize OutOfcpu: {"containers":[{"name":"app","resources":{"limits":{"cpu":"6","memory":"1Gi"},"requests":{"cpu":"6","memory":"1Gi"}}}],"generation":2,"error":"Node didn't have enough resource: cpu, requested: 6000, used: 3950, capacity: 8000"}
32s         Normal    ResizeStarted     pod/high-priority-pod   Pod resize started: {"containers":[{"name":"app","resources":{"limits":{"cpu":"6","memory":"1Gi"},"requests":{"cpu":"6","memory":"1Gi"}}}],"generation":2}
32s         Normal    ResizeCompleted   pod/high-priority-pod   Pod resize completed: {"containers":[{"name":"app","resources":{"limits":{"cpu":"6","memory":"1Gi"},"requests":{"cpu":"6","memory":"1Gi"}}}],"generation":2}
  1. ResizeDeferred: Kubelet початково позначає запит на зміну розміру як deferred (Warning) через нестачу запасу CPU на вузлі (OutOfcpu).
  2. ResizeStarted: Як тільки планувальник витісняє low-priority-pod і потужність звільняється, Kubelet приймає нове виділення і починає застосування зміни розміру.
  3. ResizeCompleted: Kubelet успішно оновлює обмеження cgroup контейнера через середовище виконання контейнерів без перезапуску Podʼа.

Нарешті, перевірте, що виділений CPU на контейнері відображає новий запит (додавання {"\n"} до JSONPath запиту забезпечує завершальний новий рядок у вашому терміналі):

kubectl get pod high-priority-pod -o jsonpath='{.status.containerStatuses[0].allocatedResources.cpu}{"\n"}'

Вивід:

6

Це підтверджує, що зміна розміру на місці вдалася.

Як долучитися

Ця можливість є значним кроком вперед у сфері планування ресурсів, оскільки забезпечує динамічне масштабування ресурсів із контролем щільності та пріоритезацією робочих навантажень на рівні корпоративних систем. Ми запрошуємо операторів кластерів, архітекторів платформ та розробників увімкнути функціональну можливість InPlacePodVerticalScalingSchedulerPreemption у своїх тестових середовищах та поділитися своїми відгуками.

Якщо ви хочете поділитися своїм досвідом з цією функцією, будь ласка, звʼяжіться зі спільнотою через канали SIG Scheduling або SIG Node!