Многие из вас наверняка сталкивались с ситуацией, когда аккуратно настроили поды в кластере, выставили 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.

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

Комментарии (0)