Ресурсы, QoS и вытеснение: requests, limits, eviction

Рейтинг: 78.5% · 14 голосов
Kubernetes для разработчиков: поды, деплойменты, сервисы, ingress, конфиги и отладка. Уроки по главам с обсуждением.
Ответить
Аватара пользователя
anton_k8s
Сообщения: 55
Зарегистрирован: 12 май 2026, 03:23

Ресурсы, QoS и вытеснение: requests, limits, eviction

Сообщение anton_k8s »

Оглавление курса (48)
  1. Зачем нужен Kubernetes и из чего состоит кластер
  2. Поднимаем локальный кластер: minikube и kind
  3. Поды: базовая единица запуска
  4. Deployment и ReplicaSet: управляем репликами
  5. Service: сетевой доступ к подам
  6. ConfigMap и Secret: выносим конфигурацию
  7. Ingress: пускаем трафик снаружи
  8. Хранилище: Volumes и PersistentVolumeClaim
  9. Namespaces, requests и limits
  10. Health checks: liveness и readiness пробы
  11. Отладка: почему под не стартует
  12. Helm: пакетный менеджер для Kubernetes
  13. Базовая безопасность: RBAC и доступы
  14. Job и CronJob: разовые и периодические задачи
  15. StatefulSet и DaemonSet: stateful-нагрузки и системные агенты
  16. Стратегии обновления и планирование: rollout и rollback, graceful shutdown, nodeSelector, affinity, taints
  17. Автомасштабирование: HPA по метрикам, обзор VPA и Cluster Autoscaler
  18. Наблюдаемость: логи, метрики, events, обзор Prometheus и Grafana
  19. Безопасность глубже: securityContext, Pod Security Standards, NetworkPolicy, шифрование секретов
  20. Архитектура control plane: api-server, etcd, scheduler
  21. Узел кластера: kubelet, kube-proxy и container runtime
  22. Декларативная модель: reconciliation, контроллеры, CRD
  23. kubectl профессионально: get, describe, explain, jsonpath
  24. Под глубже: init, sidecar, lifecycle hooks и QoS
  25. Метки, селекторы и организация ресурсов
  26. Сервисы и kube-proxy глубже: типы, IPVS, EndpointSlices
  27. Ingress, ingress-контроллеры и Gateway API
  28. Секреты в кластере: шифрование, External Secrets, Vault
  29. Хранилище глубже: PV, StorageClass, CSI и StatefulSet
  30. Сеть кластера: CNI, NetworkPolicy и CoreDNS
  31. Планировщик: affinity, taints, topology spread
  32. Ресурсы, QoS и вытеснение: requests, limits, eviction (вы здесь)
  33. Helm глубже: шаблоны, хуки, зависимости, OCI
  34. Kustomize и управление конфигурацией без шаблонов
  35. RBAC и аутентификация глубже
  36. Admission и Pod Security: контроль на входе
  37. Policy as code: Kyverno и OPA Gatekeeper
  38. GitOps: Argo CD и Flux
  39. Операторы и CRD: расширяем Kubernetes
  40. Сервис-меш: Istio, Linkerd и когда он нужен
  41. Эксплуатация кластера: апгрейд, узлы, бэкап etcd
  42. Где запускать кластер: managed, self-hosted, k3s, Deckhouse
  43. Траблшутинг кластера: Pending, CrashLoop, узлы NotReady
  44. Стоимость и эффективность кластера: FinOps
  45. CI/CD в Kubernetes: сборка, деплой, прогрессивные релизы
  46. Лучшие практики и антипаттерны Kubernetes
  47. Сквозной проект и путь дальше: от манифеста до прода
  48. Деплой приложений через Argo CD: Application, sync и App-of-Apps
Боль: под "съел" ноду, а ты узнал об этом из алерта в три часа ночи

Типичная авария на проде выглядит так. Кто-то выкатил сервис без указания ресурсов. Под спокойно работал неделю, а в пятницу под нагрузкой раздулся по памяти, вытолкал с ноды соседей, и пол-кластера начало флапать. Или наоборот: ты поставил жесткий лимит памяти, приложение в пике вышло за него на пару мегабайт, и kubelet прибил процесс с кодом 137. Никаких логов, просто Exit Code 137 и рестарт.

Все это - про управление ресурсами. Тема, которую новички пропускают ("да зачем мне эти requests"), а потом именно она ломает прод. В этом уроке разберем механику до самого низа: чем requests отличается от limits на уровне ядра, как из этих двух чисел вычисляется QoS-класс, кто и в каком порядке выселяет поды при нехватке памяти, и как обложить namespace защитой через LimitRange и ResourceQuota. Связка requests limits kubernetes - это фундамент, на котором держится и планировщик, и автоскейлинг, и стабильность ноды.

Изображение

requests против limits: две совершенно разные сущности

Самая частая путаница новичков - думать, что requests и limits это "минимум и максимум". Это не так. Это две разные машины, которые работают на разных этапах и через разные механизмы ядра Linux.

requests - это обещание планировщику. Когда ты пишешь requests, ты говоришь scheduler-у: "зарезервируй мне столько". Планировщик смотрит на свободную (allocatable) емкость ноды и складывает requests всех уже стоящих там подов. Если твой request влезает в остаток - под едет на ноду. Если нет - под висит в Pending. Важнейший момент: планировщик считает по requests, а НЕ по реальному потреблению. Нода может быть загружена на 30% по факту, но если сумма requests упёрлась в потолок - новые поды туда не поедут.

limits - это потолок, который enforce-ит ядро в рантайме. И вот тут CPU и память ведут себя принципиально по-разному, это ключ ко всему уроку.
  • CPU limit - это троттлинг, не убийство. CPU сжимаемый (compressible) ресурс. Когда контейнер упирается в CPU limit, ядро через CFS-квоту (cgroups) просто притормаживает процесс: не дает ему планироваться на CPU до конца текущего периода. Процесс жив, но медленный. Никто его не убивает.
  • Memory limit - это жесткая стена. Память несжимаемая. Нельзя "чуть-чуть отобрать память" у работающего процесса. Когда контейнер вышел за memory limit, ядро запускает OOM-killer внутри cgroup и убивает процесс. В статусе ты увидишь OOMKilled и Exit Code 137 (это 128 + сигнал 9, SIGKILL). Это и есть классический kubernetes oom.
Запомни как мантру: CPU limit тормозит, memory limit убивает. Из этого следует главный практический совет, к которому мы вернемся в граблях.

Как это выглядит в манифесте:

Код: Выделить всё

apiVersion: apps/v1
kind: Deployment
metadata:
  name: api
spec:
  replicas: 3
  selector:
    matchLabels: { app: api }
  template:
    metadata:
      labels: { app: api }
    spec:
      containers:
      - name: api
        image: registry.example.ru/api:1.4.2
        resources:
          requests:
            cpu: "250m"
            memory: "256Mi"
          limits:
            cpu: "1"
            memory: "256Mi"
Здесь 250m - это 250 миллиядер, то есть четверть одного vCPU. memory 256Mi - это мебибайты (1Mi = 1048576 байт), не путать с 256M (мегабайты, 1000000 байт). Обрати внимание: тут memory request равен memory limit (256Mi=256Mi), а CPU - нет (250m против 1). Это намеренно, и почему так - сейчас увидим через QoS.

kubernetes qos: три класса и почему они решают, кто умрет первым

QoS (Quality of Service) - это ярлык, который kubelet вешает на под автоматически, исходя из того, как заданы requests и limits. Ты не выставляешь QoS руками, он вычисляется. И именно по этому ярлыку нода решает, кого выселять при нехватке ресурсов. Три класса:
  • Guaranteed - у КАЖДОГО контейнера в поде заданы и request, и limit, причем для CPU и памяти они равны (request == limit). Это под, которому нода гарантированно держит ресурсы. Выселяется последним. В примере выше память была бы Guaranteed-уровня, но из-за разных CPU request/limit весь под попадет в Burstable - для Guaranteed нужно равенство по ОБОИМ ресурсам у всех контейнеров.
  • Burstable - задан хотя бы один request, но условие Guaranteed не выполнено (limits нет или они больше requests). Под может "выстреливать" выше своего request, когда на ноде есть свободное. Большинство нормальных продовых подов - именно тут.
  • BestEffort - не задано вообще ничего: ни requests, ни limits. Под-побирушка. Жрет что осталось, выселяется первым, без сожалений.
Проверить класс конкретного пода:

Код: Выделить всё

kubectl get pod api-7d9f8c-x2k4l -o jsonpath='{.status.qosClass}{"\n"}'
# Burstable
Или человекочитаемо через describe - в выводе будет строка QoS Class: Burstable. Запомни механику: чтобы получить Guaranteed, нужно у всех контейнеров задать requests=limits и по CPU, и по памяти. Half-measures дают Burstable.

kubernetes eviction: что делает kubelet, когда нода под давлением

Тут важно разделить два совершенно разных механизма, которые оба называют "вытеснением".

1. API-инициированное вытеснение (drain). Это когда ты делаешь kubectl drain или Pod Disruption Budget разрешает эвикцию. Вежливо, через Eviction API, с уважением к PDB. Про это - в уроке про обновления нод.

2. Node-pressure eviction (kubernetes eviction по давлению). Это локальное решение kubelet, и оно нас интересует сейчас. kubelet постоянно мониторит ресурсы ноды и сравнивает их с порогами вытеснения (eviction thresholds). По умолчанию есть жесткий порог memory.available<100Mi - то есть когда свободной памяти на ноде осталось меньше 100 мебибайт, kubelet объявляет ноде состояние MemoryPressure и начинает выселять поды, чтобы спасти ноду от системного OOM, который вырубил бы все подряд, включая сам kubelet.

Вот здесь и срабатывает QoS. Порядок выселения при memory pressure:
  • Сначала kubelet сортирует поды по тому, превышают ли они свои requests. BestEffort и Burstable, которые жрут БОЛЬШЕ своего memory request - первые кандидаты.
  • Внутри них ранжирование по pod priority (PriorityClass), а затем по тому, насколько потребление превышает request: формула (usage - request), кто сильнее вылез за свой request, того и выселят раньше.
  • Guaranteed-поды и поды, которые сидят в пределах своих requests, трогают в последнюю очередь.
Практический итог: под, у которого request близок к реальному потреблению, защищен. Под без requests (BestEffort) или сильно вылезший за request - первый под нож. Поэтому "не ставить requests" - это не экономия, а билет на эшафот при первом же давлении.

Кроме памяти, kubelet точно так же реагирует на нехватку места на диске - пороги nodefs.available и imagefs.available. При DiskPressure kubelet сначала чистит мусор (мертвые контейнеры, неиспользуемые образы), а если не помогло - начинает выселять поды. Посмотреть, что нода под давлением:

Код: Выделить всё

kubectl describe node worker-3 | grep -A6 Conditions
# Conditions:
#   Type             Status    Reason
#   MemoryPressure   True      KubeletHasInsufficientMemory
#   DiskPressure     False     KubeletHasNoDiskPressure
#   Ready            True      KubeletReady
Если видишь MemoryPressure: True - kubelet прямо сейчас выселяет поды на этой ноде. А события выселения ищи так:

Код: Выделить всё

kubectl get events --field-selector reason=Evicted -A
# NAMESPACE   LAST SEEN   TYPE      REASON    OBJECT          MESSAGE
# shop        2m          Warning   Evicted   pod/worker-xx   The node was low on resource: memory. ...
Важное различие, которое путают все. OOMKilled (Exit 137) и Evicted - это РАЗНЫЕ вещи. OOMKilled - контейнер вышел за СВОЙ memory limit, его прибило ядро внутри его cgroup, под остается на ноде и рестартует. Evicted - вся НОДА осталась без памяти, kubelet выселил под целиком, и он переедет на другую ноду. Первое - проблема настройки лимита пода, второе - проблема перегрузки ноды.

LimitRange и resourcequota: оборона на уровне namespace

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

LimitRange работает на уровне ОТДЕЛЬНОГО пода/контейнера. Он умеет: проставлять дефолтные requests/limits, если разработчик их не указал (вот так BestEffort-поды автоматически превращаются в Burstable), и задавать минимум/максимум на контейнер - чтобы никто не запросил 64Gi "на всякий случай".

Код: Выделить всё

apiVersion: v1
kind: LimitRange
metadata:
  name: defaults
  namespace: shop
spec:
  limits:
  - type: Container
    default:           # станет limit, если не задан
      cpu: "500m"
      memory: "512Mi"
    defaultRequest:    # станет request, если не задан
      cpu: "100m"
      memory: "128Mi"
    max:
      cpu: "2"
      memory: "2Gi"
После этого любой под в namespace shop без ресурсов получит request 100m/128Mi и limit 500m/512Mi автоматически. Это самый дешевый способ убить класс багов "забыли проставить requests".

ResourceQuota работает на уровне ВСЕГО namespace - суммарно. Это про деньги и про справедливость между командами. resourcequota ограничивает общую сумму requests/limits и количество объектов:

Код: Выделить всё

apiVersion: v1
kind: ResourceQuota
metadata:
  name: team-shop
  namespace: shop
spec:
  hard:
    requests.cpu: "20"
    requests.memory: 40Gi
    limits.cpu: "40"
    limits.memory: 80Gi
    pods: "100"
Критичная грабля: как только в namespace появилась ResourceQuota на CPU или память, Kubernetes ТРЕБУЕТ, чтобы каждый под указывал соответствующие requests/limits. Под без ресурсов в таком namespace просто не создастся - получишь ошибку "must specify limits.cpu". Поэтому ResourceQuota почти всегда ставят в паре с LimitRange: LimitRange проставит дефолты, ResourceQuota посчитает сумму. Проверить текущее потребление квоты:

Код: Выделить всё

kubectl describe resourcequota team-shop -n shop
# Resource         Used   Hard
# requests.cpu     12     20
# requests.memory  22Gi   40Gi
# pods             47     100
Колонка Used против Hard сразу показывает, сколько команда уже выбрала. Когда Used упрется в Hard - новые поды встанут в очередь с ошибкой превышения квоты.

Overcommit, right-sizing и диагностика CPU-троттлинга

Overcommit (переподписка) - это когда сумма limits на ноде больше ее физической емкости. Это нормально и даже желательно: не все поды жрут пик одновременно. Нода с 16 ядрами может держать поды с суммой CPU limits в 40 ядер - пока они не упрутся все разом. Опасен overcommit по памяти: память несжимаема, и если все поды разом потянутся к своим limits - получишь node-pressure eviction. Правило: по CPU overcommit-ить можно агрессивно, по памяти - осторожно.

Right-sizing - подбор адекватных цифр. Гадать вредно. Сними реальные метрики:

Код: Выделить всё

kubectl top pod -n shop --sort-by=memory
# NAME              CPU(cores)   MEMORY(bytes)
# api-7d9f8c-x2k4l  340m         412Mi
# worker-5c8b-aa1   12m          88Mi
И поставь requests близко к реальному устоявшемуся потреблению, а memory limit - с запасом 20-50% над пиком (чтобы пережить всплески, но не дать утечке убить ноду). Автоматизирует это VPA (Vertical Pod Autoscaler): в режиме рекомендаций (updateMode: "Off") он не трогает поды, а просто считает и кладет в свой статус рекомендованные requests на основе исторического потребления - бесплатный аудит right-sizing. А с 1.35 in-place pod resize стал stable: VPA умеет менять requests/limits живого пода через resize-субресурс часто без рестарта (kubectl patch pod ... --subresource resize), раньше любое изменение ресурсов требовало пересоздания пода.

Диагностика CPU-троттлинга. Если сервис "тормозит без причины", а CPU вроде не в потолке по top - проверь троттлинг. Метрика container_cpu_cfs_throttled_periods_total из cAdvisor показывает, сколько CFS-периодов контейнер был притормозлен квотой. Высокое отношение throttled к total периодам = ты упёрся в CPU limit, даже если средняя загрузка низкая (всплески внутри 100ms-периодов). Лечится поднятием CPU limit или полным его снятием (оставив только request).

Типичные грабли и антипаттерны
  • Нет requests вообще. Под становится BestEffort, планировщик не резервирует под него ничего, и он первым летит при eviction. Никогда не катите прод без requests.
  • Жесткий memory limit вплотную к потреблению. Любой всплеск - OOMKilled (Exit 137) и рестарт. Ставь memory limit с запасом над реальным пиком, а не "впритык".
  • CPU limit там, где он не нужен. Многие команды на проде вообще снимают CPU limits (оставляя requests), чтобы убрать вредный троттлинг латентных сервисов. CPU сжимаем - сосед в любом случае не отберет у тебя request, а пиком ты воспользуешься свободным CPU. Memory limit при этом оставляют всегда.
  • memory request != memory limit при желании получить стабильность. Если хочешь предсказуемости для критичного сервиса - делай его Guaranteed (req=lim по памяти), тогда он выселяется последним.
  • Mi/M и Gi/G путаница. 1Gi=1073741824 байт, 1G=1000000000. Разница ~7%, и на этом ловят OOM, когда думали что дали достаточно.
  • ResourceQuota без LimitRange. Половина деплоев начинает падать с "must specify requests", потому что забыли дефолты. Ставь их вместе.
Мини-лаба: пощупать OOM, QoS и eviction руками

Нужен любой кластер - подойдет kind, minikube, k3s или managed (Yandex Managed Kubernetes, VK Cloud). По шагам:
  • Создай namespace и повесь LimitRange с дефолтами и ResourceQuota (манифесты выше). Примени, выполни kubectl describe resourcequota - убедись, что Used нулевой.
  • Запусти под БЕЗ ресурсов: kubectl run be --image=nginx -n shop. Проверь, что LimitRange проставил ему дефолты (kubectl get pod be -n shop -o jsonpath='{.spec.containers[0].resources}') и какой QoS он получил.
  • Спровоцируй OOM: запусти под с memory limit 64Mi и нагрузи память через stress (image polinux/stress, аргумент --vm-bytes 200M). Через kubectl get pod -w увидишь OOMKilled и RESTARTS, а в describe - Last State: Terminated, Reason: OOMKilled, Exit Code: 137.
  • Сравни QoS: создай три пода - один без ресурсов (BestEffort), один с requests<limits (Burstable), один с req==lim (Guaranteed). Проверь .status.qosClass у каждого. Подумай, кого kubelet выселит первым при давлении.
Контрольные вопросы
  • Чем enforcement memory limit отличается от CPU limit на уровне ядра? Что увидишь в статусе пода в каждом случае?
  • У пода заданы requests=limits по памяти, но по CPU request меньше limit. Какой у него QoS-класс и почему не Guaranteed?
  • В каком порядке kubelet выселяет поды при MemoryPressure и какую роль играет величина (usage - request)?
  • Зачем ResourceQuota почти всегда ставят в паре с LimitRange? Что сломается, если поставить только Quota?
Итог

requests - обещание планировщику и основа автоскейлинга, limits - потолок, который ядро enforce-ит в рантайме: CPU троттлит, память убивает через OOM (Exit 137). Из соотношения requests и limits вычисляется QoS-класс, а по классу kubelet решает, кого выселить при node-pressure eviction: BestEffort падает первым, Guaranteed - последним. LimitRange проставляет дефолты и границы на под, ResourceQuota держит сумму по namespace, и ставить их надо вместе. Правильный right-sizing (адекватные requests, memory limit с запасом, CPU limit под вопросом) - это и есть граница между стабильным продом и ночными алертами.
👍2 ❤️3 🔥2 😄 🤔1
Аватара пользователя
bash_ops
Сообщения: 1
Зарегистрирован: 30 май 2026, 00:09

Re: Ресурсы, QoS и вытеснение: requests, limits, eviction

Сообщение bash_ops »

Вот это про Exit 137 наконец дошло. У меня воркер месяц рестартился по OOMKilled, а я грешил на eviction и копал не туда. Оказалось просто memory limit стоял впритык, поднял с запасом - ушло.
👍 ❤️2 🔥 😄 🤔1
Аватара пользователя
neonex
Сообщения: 1
Зарегистрирован: 02 июн 2026, 05:30

Re: Ресурсы, QoS и вытеснение: requests, limits, eviction

Сообщение neonex »

Вопрос по практике: если на латентном API снять CPU limit совсем (оставить только request) - это вообще нормальная стратегия для прода или так делают от безысходности? Боюсь что один под выжрет все CPU ноды.
👍 ❤️ 🔥 😄 🤔
Ответить
← Предыдущая глава
Планировщик: affinity, taints, topology spread
Следующая глава →
Helm глубже: шаблоны, хуки, зависимости, OCI

Все главы курса «Kubernetes: оркестрация контейнеров от основ до продакшена»

Поделиться темой: ✈ Telegram VK

Вернуться в «Kubernetes на практике»

Кто сейчас на конференции

Сейчас этот форум просматривают: нет зарегистрированных пользователей и 1 гость