lenec ru

← все посты

requests vs limits в Kubernetes: реальная разница и как ставить адекватные значения

14K

Когда приходишь в новый проект на k8s, первое, что бросается в глаза — это либо полное отсутствие resources у pod-ов, либо одинаковые requests/limits на всех контейнерах, скопированные из чужого манифеста. И то и другое — путь к ночным звонкам. Разберу, чем requests отличаются от limits, как они работают на уровне ядра и что я сам ставлю в проде.

Версия — k8s 1.30, container runtime — containerd. Всё, что ниже, проверено в стейджинге, в проде, и иногда в проде на пиках Чёрной пятницы.

Что это вообще такое

В манифесте pod есть секция resources:

resources:
  requests:
    cpu: 100m
    memory: 256Mi
  limits:
    cpu: 500m
    memory: 512Mi

requests — это то, что pod гарантированно получит. На основе requests scheduler решает, на какую ноду pod вообще можно поставить. Если у ноды свободно 200m CPU, а pod просит 500m — он туда не уедет.

limits — это потолок. Если pod захочет больше — его либо тротлят (CPU), либо убивают (память).

Тонкость в том, что requests и limits для CPU и для памяти ведут себя по-разному. Это и приводит к большинству проблем.

CPU: requests как вес, limits как тротлинг

CPU в Linux под cgroups — это share, а не абсолютная штука. cpu: 100m — это 0.1 ядра, или 100 millicpu. На уровне cgroup v2 это превращается в cpu.weight: чем больше requests, тем больший приоритет на загруженной ноде.

Ключевая фраза — «на загруженной ноде». Если нода полупустая, pod с requests: 100m может спокойно скушать целое ядро. Никто не запрещает. Но как только пришёл сосед с requests побольше — твой share уменьшится.

А вот limits: 500m — это уже жёсткий тротлинг через CFS bandwidth. Каждые 100ms (по умолчанию cpu.cfs_period_us=100000) контейнеру выделяется 50ms процессорного времени. Использовал — жди следующего окна.

Почему limits на CPU часто делают хуже

Это контринтуитивно, но: в большинстве случаев CPU limits лучше не ставить вообще. Особенно для веб-сервисов с короткими всплесками.

Пример из жизни. Go-сервис с cpu: 200m requests и cpu: 500m limits. На входе приходит запрос, сервис должен обработать его за 50ms. Но runtime захотел в этот момент сделать GC и съел всё доступное время за 30ms. Дальше запрос ждёт 70ms тротлинга, и P99 latency прыгает с 50ms до 120ms.

Без limits тот же сервис на полупустой ноде использует свободные ядра, GC отрабатывает мгновенно, P99 ровный. Я лично давно убрал CPU limits на всех своих контейнерах и сплю спокойнее. Requests ставлю честные — этого достаточно для шедулера.

resources:
  requests:
    cpu: 200m
    memory: 256Mi
  limits:
    memory: 512Mi   # только память

Если боишься, что один контейнер съест всё CPU ноды — поставь PodDisruptionBudget и нормальные requests на соседей. Шедулер сам не даст вкатить туда ещё кучу подов.

Память: limits — это смерть

С памятью наоборот: limits ставить обязательно. Память не share, её нельзя «потротлить». Если контейнер превысил memory.limit_in_bytes — kernel вызывает OOM-killer и убивает процесс с сигналом 9. В kubectl describe pod это будет Reason: OOMKilled, Exit Code: 137.

Тут есть нюанс: requests на память влияют только на шедулинг. Реальный лимит — это limits.memory. Если пишешь:

resources:
  requests:
    memory: 100Mi
  limits:
    memory: 1Gi

...то pod может разъесться до 1 ГБ, scheduler этого не учитывает при размещении. Получается классическая overcommit-ситуация: на ноду залезло 10 подов с requests по 100Mi, но каждый кушает 800Mi, и нода сваливается в kernel OOM, начинает убивать pods вразнобой, кластер штормит.

Поэтому моё правило — requests.memory == limits.memory для всего, что не утечная игрушка. Это даёт честную картину шедулеру и предсказуемое поведение.

Как считать значения

Гадать «ну пусть будет 256Mi» — это лотерея. Реальный путь — измерить.

Шаг 1: собери метрики

Поставь metrics-server и Prometheus. Метрики, которые тебе нужны:

  • container_memory_working_set_bytes — реально используемая память (не RSS, а то, что не выгружается).
  • rate(container_cpu_usage_seconds_total[5m]) — CPU в ядрах за интервал.
  • kube_pod_container_resource_requests/limits — что у тебя сейчас стоит.

Запускай нагрузку (k6, locust, хоть синтетический трафик) и смотри 95-й перцентиль за неделю.

Шаг 2: посчитай по формуле

Для CPU я беру:

requests.cpu = P95(usage) * 1.2

Запас 20% — на всплески и редкие пики. Limits на CPU не ставлю.

Для памяти:

requests.memory = limits.memory = max(working_set) * 1.3 + buffer

Запас 30% и буфер на runtime (Go GC, JVM heap fragmentation, и т.д.). Для Java и .NET буфер побольше — 500Mi-1Gi сверху, эти платформы любят держать память в резерве.

Шаг 3: VPA в режиме рекомендаций

Vertical Pod Autoscaler в режиме Off не меняет requests, но генерирует рекомендации в своём CRD:

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

Через неделю смотришь:

kubectl describe vpa api-vpa

В выводе будет секция Recommendation с target/lower/upper bound. Пользоваться её цифрами как стартовой точкой — самое адекватное, что можно сделать.

QoS-классы и почему это важно

k8s присваивает каждому pod-у один из трёх QoS-классов на основе resources:

  • Guaranteed — requests == limits для всех контейнеров и всех ресурсов. Самые «защищённые» pods, OOM-killer убьёт их в последнюю очередь.
  • Burstable — есть requests, но requests != limits, или limits не для всех ресурсов. Большинство приложений живёт здесь.
  • BestEffort — нет ни requests, ни limits. Расходники, kernel убьёт первыми при нехватке памяти на ноде.

Проверить класс конкретного pod-а:

kubectl get pod <pod> -n <ns> -o jsonpath='{.status.qosClass}'

Для критичных компонентов (платёжный API, auth-сервис, базовая инфраструктура) я выставляю Guaranteed: requests == limits и по CPU, и по памяти. Для всего остального — Burstable с requests.memory == limits.memory.

Типичные ошибки

Что я регулярно вижу в чужих манифестах:

  1. Требуется 4 ядра, поставлено cpu: 4 в requests. На нодах по 8 ядер уже не помещается, шедулинг встаёт. Считать надо в реальности — обычно достаточно 500m-2.
  2. limits.memory: 4Gi на pod, который кушает 200Mi. Шедулер думает, что pod «занимает» 4Gi (точнее, requests.memory), и не пускает соседей. Но если requests меньше limits — overcommit, привет OOM.
  3. CPU limits на init-контейнерах, которые делают миграции БД. Init работает раз в стартапе, ему нужно быстро. Тротлить там нечего, только тормозить релиз.
  4. Одинаковые лимиты на всех контейнерах в pod-е. Sidecar (envoy, fluent-bit) и основное приложение требуют разного. Sidecar обычно 50m/100Mi достаточно, не 200m/512Mi.

Что запомнить

requests — для шедулера, limits — для kernel. CPU limits в большинстве случаев вредят, memory limits обязательны. Считай по метрикам, не угадывай. Для критичных сервисов — Guaranteed (requests == limits), для остального — Burstable с requests.memory == limits.memory.

Куда копать дальше: официальная страница про управление ресурсами, статьи Tim Hockin про CFS bandwidth (он один из мейнтейнеров k8s, объясняет внутрянку понятно). И поставьте VPA, даже если боитесь автоматического апдейта — режим Off безопасен и даёт цифры из реальной нагрузки.

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

  • Будьте первым, кто оставит комментарий.

Войдите, чтобы оставить комментарий.