Больше не нужен рестарт: как Kubernetes позволяет менять ресурсы контейнеров «на лету»

от автора

Многие из вас наверняка сталкивались с ситуацией, когда аккуратно настроили поды в кластере, выставили CPU и память с точностью до 10 МБ, но вдруг приходит большой трафик и приложение начинает потреблять намного больше памяти, чем ожидалось. Тогда начинается игра в рулетку с VPA — удастся ли ему правильно подобрать новые значения ресурсов, так как режим Recreate иногда преподносит неожиданные сюрпризы? Например, при наплыве трафика пересоздаст под, что под нагрузкой может вызвать перегрузку остальных компонентов и цепную ошибку. Или же можно сразу выделять запас ресурсов (если есть возможность)?

Всем привет! Меня зовут Юрий Лосев, я Technical Product Manager в команде Deckhouse компании «Флант». В этой статье я разберу, как бороться с проблемой нехватки ресурсов в контейнерах. Впервые решение появилось в версии Kubernetes 1.27. Речь про функционал, с помощью которого можно изменять вычислительные ресурсы у уже работающих подов без их перезапуска. В 1.33 этот функционал значительно улучшили и сделали доступным по умолчанию.

Рассмотрим, как это работает, где на практике особенно полезно и какие существуют ограничения.

Изменение запросов и лимитов без пересоздания пода

KEP-1287 добавил возможность изменять поля resources.requests и resources.limits в спецификации пода, которые раньше были неизменяемыми (immutable fields), без перезапуска пода. То есть теперь они меняются «на лету» или по месту. 

Как выглядит этот процесс по шагам:

  1. Пользователь меняет ресурсы контейнера (CPU или память) в манифесте пода и отправляет изменения в API-сервер Kubernetes (kube-api).

  2. kube-api передаёт обновлённую спецификацию пода на соответствующий узел.

  3. На узле kubelet выполняет быструю проверку ресурсов:

    capacity узла — сумма всех выделенных на контейнеры ресурсов >= новые ресурсы

    Если условие выполняется — изменения применяются, если нет — под получает статус PodResizePending.

  4. После проверки kubelet использует CRI, чтобы сообщить среде выполнения контейнеров (например, containerd, cri-o или podman), например: «Этому контейнеру нужно больше/меньше памяти».

  5. Среда выполнения (runtime) автоматически подстраивает cgroups контейнера, увеличивая или уменьшая выделенные ресурсы без перезапуска контейнера. Этот процесс происходит асинхронно и не блокирует выполнение — kubelet продолжает заниматься другими задачами.

Процесс изменения ресурсов в спецификации пода

Процесс изменения ресурсов в спецификации пода

Совместимость Container Runtime

В таблице ниже указаны минимальные версии популярных Container Runtime, которые полностью поддерживают CRI (или не все) и работают с современными версиями Kubernetes:

Container Runtime

Compatible Version

Release Notes

containerd

v1.6.9+

containerd v1.6.9 Release Notes

CRI-O

v1.24.2+

CRI-O v1.24.2 Release Notes

Podman

v4.0.0+

Podman v4.0.0 Release Notes

Docker

Полностью не поддерживается

Но кто использует Docker как CRI в 2025-м?

Сценарии, в которых новая функция обновления ресурсов спасает приложение

Посмотрим на реальные случаи, где эта возможность будет использоваться на 100 %.

Базы данных
Например, инстанс PostgreSQL вдруг требует больше памяти, чтобы потянуть новый отчёт. Раньше приходилось перезапускать под, что вызывало простой и потерю соединений. Теперь ресурсы можно увеличить «на лету» — без остановки, а пользователи этого даже и не заметят.

Stateless веб-приложения
Приложения на NodeJS, Golang, Python спокойно используют дополнительные CPU и память без рестартов. Это идеально подходит для динамического масштабирования во время пикового трафика.

Для JVM-приложений просто увеличить лимиты памяти пода недостаточно, так как параметр -Xmx (максимальный размер кучи) обычно задаётся при старте. Хотя CPU и не-heap-память можно менять «на лету», для полного использования новых лимитов Java-приложению всё равно потребуется рестарт. Увы, идеального решения пока нет.

ML-сервисы
Сервисы на базе TensorFlow, которым вдруг понадобилась поддержка более тяжёлых моделей или запросов большего объёма, теперь могут получить больше ресурсов без остановки обработки.

Прокси-сайдкары в service mesh
Envoy-прокси в Istio и других mesh-системах можно адаптировать под изменяющийся трафик без сбоев и влияния на основное приложение. Это одна из лучших возможностей, ведь трафик часто сложно предсказать заранее.

Обновление ресурсов «на лету» на практике

Но достаточно теории, мы ведь любим практику. Давайте потрогаем этот функционал своими руками.

Есть некоторая разница между Kubernetes 1.27–1.32 и Kubernetes 1.33. Чтобы понять её и почему это сделано, начнём эксперимент с версии 1.27.

Обновление ресурсов в K8s v1.27

В Kubernetes 1.27 завезли FeatureGate InPlacePodVerticalScaling, который позволяет изменять поле .spec.containers[*].resources у подов. Чтобы эта функция работала, её надо выставить не только у control-plane-ресурсов (apiserver, controller-manager, scheduler), но и для kubelet. При этом после включения флага InPlacePodVerticalScaling kubelet начинает отображать статистику выделенных ресурсов в статусе контейнера:

status:   containerStatuses:   - allocatedResources:       cpu: 150m       memory: 128Mi     containerID: containerd://xxx     image: docker.io/library/ubuntu:22.04     imageID: docker.io/library/ubuntu@xxx8b1649e5b269eee     name: resource-watcher     ready: true     resources:       limits:         cpu: 150m         memory: 128Mi       requests:         cpu: 150m         memory: 128Mi     restartCount: 0

А вот дальше становится интересно. После указания данных флагов мы можем менять не только поля image, tolerations, terminationGracePeriodSeconds у пода (как в обычном состоянии), но и resources:

The Pod "resize-demo" is invalid: spec: Forbidden: pod updates may not change fields other than `spec.containers[*].image`,`spec.initContainers[*].image`,`spec.activeDeadlineSeconds`,`spec.tolerations` (only additions to existing tolerations),`spec.terminationGracePeriodSeconds` (allow it to be set to 1 if it was previously negative),`spec.containers[*].resources` (for CPU/memory only)

Для проверки задеплоим следующий под:

apiVersion: v1 kind: Pod metadata:   name: resize-demo spec:   containers:   - name: resource-watcher     image: ubuntu:22.04     command:     - "/bin/bash"     - "-c"     - |       apt-get update && apt-get install -y procps bc       echo "=== Pod Started: $(date) ==="        # Functions to read container resource limits       get_cpu_limit() {         if [ -f /sys/fs/cgroup/cpu.max ]; then           # cgroup v2           local cpu_data=$(cat /sys/fs/cgroup/cpu.max)           local quota=$(echo $cpu_data | awk '{print $1}')           local period=$(echo $cpu_data | awk '{print $2}')            if [ "$quota" = "max" ]; then             echo "unlimited"           else             echo "$(echo "scale=3; $quota / $period" | bc) cores"           fi         else           # cgroup v1           local quota=$(cat /sys/fs/cgroup/cpu/cpu.cfs_quota_us)           local period=$(cat /sys/fs/cgroup/cpu/cpu.cfs_period_us)            if [ "$quota" = "-1" ]; then             echo "unlimited"           else             echo "$(echo "scale=3; $quota / $period" | bc) cores"           fi         fi       }        get_memory_limit() {         if [ -f /sys/fs/cgroup/memory.max ]; then           # cgroup v2           local mem=$(cat /sys/fs/cgroup/memory.max)           if [ "$mem" = "max" ]; then             echo "unlimited"           else             echo "$((mem / 1024 / 1024)) MiB"           fi         else           # cgroup v1           local mem=$(cat /sys/fs/cgroup/memory/memory.limit_in_bytes)           echo "$((mem / 1024 / 1024)) MiB"         fi       }        # Print resource info every 5 seconds       while true; do         echo "---------- Resource Check: $(date) ----------"         echo "CPU limit: $(get_cpu_limit)"         echo "Memory limit: $(get_memory_limit)"         echo "Available memory: $(free -h | grep Mem | awk '{print $7}')"         sleep 5       done     resizePolicy:     - resourceName: cpu       restartPolicy: NotRequired     - resourceName: memory       restartPolicy: NotRequired     resources:       requests:         memory: "128Mi"         cpu: "100m"       limits:         memory: "128Mi"         cpu: "100m"

Обратите внимание на нюансы: мы создаём под с установленными requests и limits: 

    resources:       requests:         memory: "128Mi"         cpu: "100m"       limits:         memory: "128Mi"         cpu: "100m"

Попробуем изменить ресурсы этого пода:

kubectl patch pod resize-demo  --patch \   '{"spec":{"containers":[{"name":"resource-watcher", "resources":{"requests":{"cpu":"200m"}, "limits":{"cpu":"200m"}}}]}}'  pod/resize-demo patched

Наш запрос успешно выполнился и ресурсы пода изменились. Кажется, что всё работает так, как и ожидалось, но не всё так просто, следующий запрос падает:

kubectl patch pod resize-demo  --patch \   '{"spec":{"containers":[{"name":"resource-watcher", "resources":{"requests":{"cpu":"150m"}, "limits":{"cpu":"200m"}}}]}}'  The Pod "resize-demo" is invalid: metadata: Invalid value: "Guaranteed": Pod QoS is immutable

Почему же так происходит? При изменении пода Kubernetes автоматически заполняет некоторые поля. Если изначально значения requests и limits были равны, то QoS-класс пода был Guaranteed. После изменения, когда requests и limits становятся разными, QoS-класс должен измениться на Burstable. Однако это поле является immutable, то есть его нельзя изменить, поэтому такой запрос на изменение не пройдёт.

Но с некоторой подготовкой — точным определением профиля использования и необходимыми гарантиями в runtime — мы можем менять ресурсы контейнера. Например:

    resources:       requests:         cpu: 100m         memory: 128Mi       limits:         cpu: 200m         memory: 256Mi

У нас есть заданные ресурсы, но мы поняли, что не вписываемся в реальное потребление приложения, и решили пропорционально всё увеличить:

kubectl patch pod resize-demo  --patch \   '{"spec":{"containers":[{"name":"resource-watcher", "resources":{"requests":{"cpu":"200m", "memory": "200Mi"}, "limits":{"cpu":"500m", "memory": "500Mi"}}}]}}'  pod/resize-demo patched

…или увеличить лимиты, чтобы не поймать OOM:

kubectl patch pod resize-demo \   --type='json' \   -p='[     {       "op": "add",       "path": "/spec/containers/0/resources/limits/memory",       "value": "750Mi"     },     {       "op": "add",       "path": "/spec/containers/0/resources/limits/cpu",       "value": "750m"     }   ]'  pod/resize-demo patched

Иногда использовать jsonpatch удобнее, если не хочется писать имя контейнера.

Результат:

kubectl get pods resize-demo -o yaml     containerStatuses:   - allocatedResources:       cpu: 100m       memory: 128Mi     containerID: containerd://xxx     image: docker.io/library/ubuntu:22.04     imageID: docker.io/library/ubuntu@xxx     name: resource-watcher     ready: true     resources:       limits:         cpu: 750m         memory: 750Mi       requests:         cpu: 100m         memory: 128Mi     restartCount: 0

Как я уже писал, задача resize происходит асинхронно, поэтому новые значения не применяются мгновенно. В Kubernetes 1.27 пока нет специального поля, которое показывало бы статус этой операции. Поэтому просто повторите команду через 30 секунд или используйте режим наблюдения с помощью команды:

kubectl get pods resize-demo -o yaml -w

Как видим, ресурсы контейнера успешно меняются, при этом значение restartCount остаётся равным 0, то есть контейнер не перезагружался.

Также мы видим изменения в статусе пода с точки зрения Kubernetes. Но важно понять, меняется ли что-то в самом поде. Ведь главное — чтобы CRI действительно обновил текущие cgroups. Для проверки мы запустили под, который выводит в логи текущее значение из cgroup. Посмотрим логи этого пода:

kubectl logs resize-demo --tail=4 ---------- Resource Check: Tue Jun 22 12:41:15 UTC 2025 ---------- CPU limit: .750 cores Memory limit: 750 MiB Available memory: 3.9Gi

Просмотрите всю историю логов, чтобы увидеть момент изменения:

kubectl logs resize-demo --tail=-1 ... ---------- Resource Check: Tue Jun 22 12:41:10 UTC 2025 ---------- CPU limit: .100 cores Memory limit: 128 MiB Available memory: 3.9Gi ---------- Resource Check: Tue Jun 22 12:41:15 UTC 2025 ---------- CPU limit: .750 cores Memory limit: 750 MiB Available memory: 3.9Gi

Как видим, containerd всё поменял и наш контейнер работает с новыми лимитами.

resizePolicy

Возможно, вы обратили внимание на часть спеки:

resizePolicy:     - resourceName: cpu       restartPolicy: NotRequired     - resourceName: memory       restartPolicy: NotRequired

Это новое поле, которое появилось при включении FeatureFlag InPlacePodVerticalScaling. Оно указывает, что делать контейнеру при изменении размера. По умолчанию стоит restartPolicy: NotRequired, то есть контейнер не будет перезагружаться при изменении лимитов ресурсов. 

Но есть приложения, которым при изменении лимитов нужен рестарт, например JVM с опцией -Xmx. Для таких контейнеров можно установить restartPolicy: RestartContainer. Это позволяет гибко настроить приложение. Например, сайдкар с Envoy-прокси можно перезапускать без рестарта, а «тяжёлое» Java-приложение — только с рестартом, если это необходимо.

Для таких подов перезапуск будет виден при выводе списка:

kubectl get pods resize-demo NAME          READY   STATUS    RESTARTS      AGE resize-demo   1/1     Running   1 (42s ago)   4m32s

Это также отразится в статусе контейнера:

    resources:       limits:         cpu: 200m         memory: 512Mi       requests:         cpu: 50m         memory: 64Mi     restartCount: 1

Обновление ресурсов в K8s v1.33

Теперь посмотрим, что интересного привезли в Kubernetes 1.33:

  1. FeatureFlag InPlacePodVerticalScaling включили по умолчанию.

  2. Появился новый subresource /resize (доступен начиная с kubectl 1.32), который позволяет изменять ресурсы пода без проблем с остальными полями в спецификации.

  3. В статус пода добавились новые состояния, показывающие процесс изменения ресурсов:

  • type: PodResizePending — kubelet не может сразу удовлетворить запрос на изменение ресурсов. В поле message можно увидеть детали и причины:

    • reason: Infeasible — запрошенное изменение невозможно на текущем узле (например, запрошено больше ресурсов, чем доступно);

    • reason: Deferred — запрошенное изменение сейчас невозможно, но может стать возможным позже (например, если будет удалён другой pod). kubelet будет пытаться повторять изменение ресурсов.

  • type: PodResizeInProgress — kubelet принял запрос на изменение и выделил ресурсы, но изменения ещё применяются. Обычно это происходит быстро, но иногда может занять больше времени в зависимости от типа ресурса и поведения среды выполнения (runtime). Любые ошибки при применении изменений будут отражены в поле message (вместе с reason: Error).

Теперь изменение ресурсов работает без жёстких ограничений, которые были раньше, — например, связанных с классом QoS или ошибками из-за resizePolicy:

kubectl patch pod resize-demo --subresource resize --patch \   '{"spec":{"containers":[{"name":"resource-watcher", "resources":{"requests":{"memory":"256Mi"}, "limits":{"memory":"256Mi"}}}]}}' pod/resize-demo patched

Однако некоторые ограничения все-таки остались.

Ограничения In-Place Pod Resize

  • Если под использует swap, то изменить память без перезагрузки нельзя — потребуется рестарт контейнера.

  • Не работает на Windows-узлах.

  • Можно изменять только CPU и память.

  • Memory limit нельзя понижать без рестарта, а вот memory request — можно. Это связано с повышенным риском возникновения ошибок OOM, если лимит будет слишком низким.

  • QoS-класс пода не меняется. То есть, как бы вы ни изменили параметры, QoS останется таким же, каким был при старте. Это не блокирует обновление ресурсов, но изменить класс QoS пока нельзя.

  • Init- и ephemeral-контейнеры нельзя рестартить.

  • Нельзя полностью удалить requests или limits, можно только изменить их значения.

  • Поле resizePolicy нельзя изменять после создания пода.

Также, начиная с версии 1.33, поддерживается VPA версии 1.4 и выше с новым режимом mode: InPlaceOrRecreate. Он позволяет применять рекомендации по ресурсам «на лету». Работает через subresource resize, поэтому в более старых версиях его не включить. Это можно исправить, но VPA упирается в ограничения, которые были описаны выше, и не может нормально поправить поды.

Процесс работы VPA в режиме InPlaceOrRecreate

Процесс работы VPA в режиме InPlaceOrRecreate

Поэтому светлое будущее автоматического изменения ресурсов ещё не наступило, но уже стоит на пороге.

Заключение 

Функция обновления ресурсов контейнеров «на лету» — это большой шаг вперёд для гибкости и надёжности Kubernetes-кластеров. Теперь можно оперативно реагировать на изменения нагрузки и не опасаться простоев и цепных сбоев из-за пересоздания подов. Да, у технологии ещё есть ограничения, не все сценарии покрыты идеально, но реализация in-place resize по умолчанию в последнем релизе говорит о быстром темпе развития.

P. S.

Читайте также в нашем блоге:


ссылка на оригинал статьи https://habr.com/ru/articles/936724/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *