Kubernetes resource limits: как выставить requests и limits, чтобы не убить ноду и не переплатить за облако

Kubernetes resource limits: как выставить requests и limits, чтобы не убить ноду и не переплатить за облако

Коротко:

  • Без requests планировщик не знает, куда ставить под, и может перегрузить ноду. Без limits один контейнер способен съесть всю память на хосте.
  • Ставить limits равными requests - безопасно, но дорого: вы резервируете ресурсы, которые реально не используются.
  • Главные симптомы неправильной настройки - OOMKilled и CPU throttling. Оба убивают производительность, но по-разному.
  • Реальное потребление смотрят через kubectl top, Prometheus и VPA в режиме рекомендаций.
  • Goldilocks автоматически собирает рекомендации VPA по всем неймспейсам и показывает их в веб-интерфейсе.
  • Хорошая точка старта: request = p95 потребления за 7 дней, limit по CPU = 2-4x от request, limit по памяти = 1.2-1.5x от request.

Почему это вообще важно

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

С другой стороны, команды, которые боятся нестабильности, часто уходят в другую крайность: выставляют огромные значения с запасом. Под просит 4 CPU и 8 Gi памяти, реально потребляет 0.2 CPU и 300 Mi - и так по всему кластеру. Облачный счет растет, ноды простаивают, а автоскейлер не может уменьшить кластер, потому что ресурсы формально заняты.

Правильная настройка resource requests и limits решает обе проблемы: кластер остается стабильным, а вы платите за то, что реально нужно.

Как это работает: requests, limits и планировщик

Прежде чем идти к практике, стоит разобраться с механикой. В Kubernetes каждый контейнер может иметь два параметра для CPU и памяти.

Request - это минимум, который планировщик гарантирует поду при размещении на ноде. Именно по requests планировщик решает, влезет ли под на конкретный узел. Если нода имеет 4 CPU, а сумма requests всех подов на ней равна 4 CPU, новый под с request 0.5 CPU туда не попадет - даже если реальное потребление всех подов составляет 1 CPU.

Limit - это потолок потребления. Контейнер не может использовать больше, чем указано в limit. Поведение при достижении потолка разное для CPU и памяти: CPU просто троттлится (процессорное время режется), а при превышении лимита по памяти процесс убивается с кодом OOMKilled.

Это принципиальное различие часто упускают. CPU throttling - это деградация производительности, которую трудно заметить без метрик. OOMKilled - это падение пода, которое видно сразу, но причину не всегда понимают правильно.

Пример манифеста с корректно выставленными значениями:

resources:
  requests:
    cpu: "250m"
    memory: "256Mi"
  limits:
    cpu: "1000m"
    memory: "512Mi"

Здесь под гарантированно получит 0.25 CPU и 256 Mi памяти при планировании, но сможет использовать до 1 CPU и 512 Mi при наличии свободных ресурсов на ноде.

Три антипаттерна, которые встречаются чаще всего

1. Нет никаких значений

Под без requests попадает в класс QoS BestEffort - самый низкий приоритет. При нехватке памяти на ноде такие поды вытесняются первыми. Планировщик не учитывает их при размещении, что приводит к непредсказуемой загрузке нод. Один «тяжелый» под без ограничений может занять всю память узла и вызвать каскадное вытеснение соседей.

2. Limits равны requests

Это популярный совет из старых гайдов: «ставь одинаково, чтобы под получал ровно то, что просит». Технически это дает класс QoS Guaranteed - самый высокий приоритет при вытеснении. Но у подхода есть цена: вы резервируете ресурсы жестко. Под с request 1 CPU и limit 1 CPU никогда не использует больше, даже если нода пустая. В масштабах кластера это прямой kubernetes overprovision: вы платите за ресурсы, которые физически есть, но формально заняты.

3. Завышенные значения «на всякий случай»

Команда не знает реального потребления, боится OOMKilled и ставит limits с большим запасом. Под потребляет 100 Mi памяти, а limit стоит 4 Gi. Автоскейлер кластера видит, что ресурсы «заняты», и не уменьшает количество нод. Это самая дорогая ошибка в облаке - и самая незаметная, пока не придет счет.

OOMKilled и throttling: как диагностировать

OOMKilled

Под падает с этим статусом, когда контейнер превысил memory limit. Проверить просто:

kubectl describe pod 
# ищем в секции Last State:
# Reason: OOMKilled

Или через события:

kubectl get events --field-selector reason=OOMKilling

Причины бывают двух типов. Первый - limit реально занижен: приложение в норме потребляет больше, чем ему разрешено. Второй - утечка памяти: приложение со временем потребляет всё больше и в итоге упирается в потолок. Это разные проблемы с разными решениями. В первом случае нужно поднять limit, во втором - чинить код.

Чтобы отличить одно от другого, смотрите на динамику: если потребление памяти монотонно растет до момента убийства - это утечка. Если под падает сразу при старте или при пиковой нагрузке - limit занижен.

CPU throttling

Это тихий убийца производительности. Под работает, не падает, но отвечает медленнее, чем должен. В Prometheus метрика выглядит так:

rate(container_cpu_cfs_throttled_seconds_total[5m])
  / rate(container_cpu_cfs_periods_total[5m])

Если значение больше 25-30%, под регулярно не получает запрошенное процессорное время. Это прямое следствие слишком низкого CPU limit. Особенно болезненно для latency-sensitive сервисов: API, которое должно отвечать за 50 мс, начинает отвечать за 200-500 мс просто потому, что ядро режет ему время.

Важно: CPU throttling происходит даже когда нода не загружена. Это не про нехватку ресурсов на хосте - это про жесткий потолок, который вы сами выставили в манифесте. Низкий CPU limit = throttling при любой нагрузке.

Как найти реальное потребление: инструменты

kubectl top

Самый быстрый способ посмотреть текущее потребление:

kubectl top pods -n  --sort-by=memory
kubectl top nodes

Минус - показывает только текущий момент. Для выставления значений нужна история: пики нагрузки, ночные джобы, периодические задачи.

Prometheus + Grafana

Исторические данные по потреблению - основа для расчета адекватных значений. Полезные запросы:

# p95 потребления CPU за 7 дней
quantile_over_time(0.95,
  rate(container_cpu_usage_seconds_total{
    container!="", pod=~"myapp-.*"
  }[5m])[7d:5m]
)

# максимум потребления памяти за 7 дней
max_over_time(
  container_memory_working_set_bytes{
    container!="", pod=~"myapp-.*"
  }[7d]
)

Смотрите именно на container_memory_working_set_bytes, а не на container_memory_usage_bytes. Первая метрика показывает активно используемую память без кеша, и именно её сравнивает OOM killer с лимитом.

VPA в режиме рекомендаций

Vertical Pod Autoscaler умеет не только автоматически менять requests - он может работать в режиме Off, просто собирая рекомендации без применения. Это безопасный способ получить data-driven предложения по значениям.

apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
  name: myapp-vpa
spec:
  targetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: myapp
  updatePolicy:
    updateMode: "Off"

После нескольких дней сбора данных смотрим рекомендации:

kubectl describe vpa myapp-vpa

VPA покажет три значения: Lower Bound (минимум для стабильной работы), Target (рекомендуемое) и Upper Bound (максимум, который наблюдался). Это хорошая отправная точка, но не финальный ответ - VPA не знает о ваших SLO и бизнес-пиках.

Goldilocks

Goldilocks от Fairwinds - это надстройка над VPA, которая разворачивает VPA-объекты для всех деплойментов в указанных неймспейсах и показывает агрегированные рекомендации в веб-интерфейсе. Название отсылает к сказке про Машу и трёх медведей: цель - найти значения, которые «в самый раз».

Установка через Helm:

helm repo add fairwinds-stable https://charts.fairwinds.com/stable
helm install goldilocks fairwinds-stable/goldilocks \
  --namespace goldilocks --create-namespace

# включаем для нужного неймспейса
kubectl label namespace production \
  goldilocks.fairwinds.com/enabled=true

После этого Goldilocks создаст VPA-объекты для каждого деплоймента в неймспейсе production и начнет собирать данные. Через несколько дней в дашборде появятся рекомендации с разбивкой по контейнерам и классам QoS.

Когда использовать Goldilocks: если в кластере десятки или сотни деплойментов с неизвестным или устаревшим профилем потребления. Вручную профилировать каждый нереально - Goldilocks делает это автоматически и дает единую точку просмотра.

Пошаговый процесс выставления адекватных значений

  1. Собрать исторические данные. Минимум 7 дней, лучше 30. Включить все типичные сценарии нагрузки: дневные пики, ночные джобы, периодические задачи. Использовать Prometheus или включить VPA в режиме Off.
  2. Определить request по CPU. Берем p95 потребления за период наблюдения. Это значение гарантирует, что под получит достаточно ресурсов в 95% времени. Если сервис latency-sensitive, можно взять p99.
  3. Определить request по памяти. Берем типичное рабочее потребление (не пик). Память - не CPU: она не «возвращается» автоматически, поэтому request должен покрывать нормальный рабочий объем.
  4. Выставить CPU limit. Хорошее соотношение - 2-4x от request. Это дает поду возможность использовать свободные ресурсы ноды при пиках, но не позволяет монополизировать CPU. Если видите throttling выше 25% - поднимайте limit, а не request.
  5. Выставить memory limit. 1.2-1.5x от request. Память нельзя давать слишком щедро: если под реально потребляет столько, сколько указано в limit, это сигнал либо к утечке, либо к пересмотру архитектуры. Большой разрыв между request и limit по памяти маскирует проблемы.
  6. Проверить результат. После применения новых значений наблюдать за throttling и OOMKilled в течение нескольких дней. Смотреть на метрику kube_pod_container_status_restarts_total - рост рестартов сигнализирует о проблеме.
  7. Повторять регулярно. Профиль потребления меняется с каждым релизом. Значения, выставленные полгода назад, могут быть неактуальны. Goldilocks или периодический аудит через VPA помогают держать это под контролем.

Классы QoS: что выбрать осознанно

Kubernetes автоматически назначает каждому поду класс Quality of Service на основе того, как выставлены requests и limits. Это влияет на приоритет при вытеснении.

Класс QoSУсловиеПриоритет при нехватке памяти
Guaranteedrequests == limits для всех контейнеровВытесняется последним
Burstablerequests < limits, или задан только один параметрВытесняется после BestEffort
BestEffortНет ни requests, ни limitsВытесняется первым

Для production-сервисов рекомендуется Burstable с разумным соотношением request/limit. Guaranteed подходит для критичных компонентов (например, системных подов), где предсказуемость важнее экономии. BestEffort - только для задач, которые можно безболезненно прервать.

LimitRange и ResourceQuota: защита на уровне неймспейса

Индивидуальная настройка каждого пода - это хорошо, но в реальных кластерах нужна защита от случайных деплойментов без ресурсных параметров. Для этого есть два объекта.

LimitRange задает дефолтные и максимальные значения для контейнеров в неймспейсе. Если под задеплоен без requests/limits, LimitRange автоматически подставит дефолты:

apiVersion: v1
kind: LimitRange
metadata:
  name: default-limits
  namespace: production
spec:
  limits:
  - type: Container
    default:
      cpu: "500m"
      memory: "256Mi"
    defaultRequest:
      cpu: "100m"
      memory: "128Mi"
    max:
      cpu: "4"
      memory: "4Gi"

ResourceQuota ограничивает суммарное потребление всего неймспейса. Это защита от ситуации, когда одна команда случайно задеплоила 50 реплик с большими requests и занял ресурсы всего кластера.

Типичные ошибки при настройке

ОшибкаПоследствиеКак исправить
Нет requests у подовНепредсказуемое планирование, вытеснение при нагрузкеДобавить requests на основе реального потребления
CPU limit слишком низкийПостоянный throttling, деградация latencyПоднять limit или убрать его для CPU (спорно, но допустимо)
Memory limit == memory request с большим запасомПереплата за зарезервированные, но неиспользуемые ресурсыСнизить request до реального потребления
Значения не обновляются после релизовУстаревший профиль, накопленный overprovisionРегулярный аудит через VPA или Goldilocks
Одинаковые значения для всех сервисовОдни сервисы задыхаются, другие простаиваютПрофилировать каждый сервис отдельно

Чеклист: перед тем как выставить значения в продакшен

  • Собраны метрики потребления за минимум 7 дней (p95 CPU, max и типичное потребление памяти)
  • Проверено, нет ли OOMKilled в истории подов за последние 2 недели
  • Проверен уровень CPU throttling через Prometheus
  • VPA запущен в режиме Off и дал рекомендации
  • CPU limit выставлен как минимум в 2x от request
  • Memory limit выставлен в 1.2-1.5x от request
  • В неймспейсе настроен LimitRange с дефолтами
  • После применения значений настроен алерт на рост рестартов подов
  • Запланирован повторный аудит после следующего крупного релиза

FAQ

Что будет, если не выставить CPU limit совсем?

Контейнер сможет использовать столько CPU, сколько есть на ноде. Это не всегда плохо: при отсутствии конкуренции под работает быстрее. Но при высокой нагрузке один «жадный» контейнер может вытеснить соседей по CPU. Для production-сервисов без limits по CPU - приемлемо, если вы осознаете риск и мониторите потребление. Для памяти - нет: отсутствие memory limit опасно.

Чем VPA отличается от HPA?

HPA (Horizontal Pod Autoscaler) масштабирует количество реплик пода в зависимости от нагрузки. VPA меняет размер самого пода - его requests и limits. Они решают разные задачи и могут использоваться вместе, но с осторожностью: VPA в режиме Auto перезапускает поды для применения новых значений, что может конфликтовать с HPA.

Как понять, что кластер страдает от overprovision?

Смотрите на соотношение allocatable ресурсов ноды и реального потребления. Если ноды загружены на 10-20% по факту, но планировщик не может разместить новые поды - это классический kubernetes overprovision. Команда kubectl describe node покажет Allocated resources vs реальное потребление через kubectl top node.

Что такое OOMKilled и как отличить его от обычного краша?

OOMKilled - это принудительное завершение процесса ядром Linux, когда контейнер превысил memory limit. В отличие от обычного краша приложения, здесь нет stacktrace и нет логов о причине - под просто исчезает. Диагностируется через kubectl describe pod: в секции Last State будет Reason: OOMKilled и Exit Code 137.

Goldilocks или VPA напрямую - что выбрать?

Для одного-двух деплойментов достаточно VPA в режиме Off. Goldilocks оправдан, когда нужно профилировать десятки сервисов сразу: он автоматически создает VPA-объекты и дает единый дашборд. По сути Goldilocks - это удобная обертка над VPA, а не альтернатива.

Можно ли использовать VPA в режиме Auto в продакшене?

Технически да, но с осторожностью. VPA Auto перезапускает поды для применения новых значений - это означает кратковременную недоступность реплики. Для stateless-сервисов с несколькими репликами это обычно приемлемо. Для stateful-сервисов или сервисов с одной репликой - рискованно. Хорошая практика: Auto в staging, Off или Initial в production.

Итог

Правильная настройка ресурсов - это не разовая задача, а часть операционной культуры. Значения, которые работали при запуске сервиса, устаревают с каждым релизом. Профиль потребления меняется, нагрузка растет, появляются новые фичи - и то, что казалось щедрым запасом, превращается либо в узкое место, либо в деньги, выброшенные на ветер.

Хороший процесс выглядит так: собрать метрики, получить рекомендации от VPA или Goldilocks, осознанно выставить значения с учетом SLO и бизнес-пиков, настроить алерты на OOMKilled и throttling, повторить после следующего крупного изменения. Это занимает несколько часов, но экономит деньги и нервы при каждом инциденте.

Главное, что стоит запомнить: requests влияют на планирование и стоимость, limits - на стабильность. Занижать первое опасно для кошелька, занижать второе - для надежности. Найти баланс помогают не интуиция, а данные.