requests - это объём CPU и памяти, который планировщик резервирует поду на конкретной ноде. limits - верхняя граница потребления, которую контейнер не может превысить во время работы. Первое влияет на размещение пода, второе на его поведение в runtime: выход за лимит CPU даёт троттлинг, выход за лимит памяти заканчивается OOMKilled и кодом возврата 137.
Практическое правило простое. requests ставьте по реальному потреблению в пике, limits выше requests там, где допустим кратковременный рост нагрузки, и равными requests там, где нужна предсказуемость. Обе величины указываются в одном месте: spec.containers[].resources каждого контейнера. Kubernetes позволяет выражать, запрашивать и ограничивать ресурсы рабочих нагрузок, включая специальные аппаратные устройства вроде GPU (документация Kubernetes по управлению ресурсами).
apiVersion: v1
kind: Pod
metadata:
name: api
spec:
containers:
- name: api
image: registry.example.com/api:1.4.2
resources:
requests:
cpu: "250m"
memory: "256Mi"
limits:
cpu: "500m"
memory: "512Mi"
В этом примере планировщик резервирует 250 millicores и 256 MiB, а контейнер может кратковременно разогнаться до 500 millicores и вырасти по памяти до 512 MiB. Превышение лимита памяти ядро не пропустит: процесс получит SIGKILL. Единицы измерения фиксированы: CPU в millicores, где 1000m равно одному ядру или vCPU, память в байтах с суффиксами Ki, Mi, Gi, Ti (двоичные) или k, M, G, T (десятичные).
Что такое requests и limits и зачем они нужны
Путаница между requests и limits даёт большинство инцидентов с подами: одни команды выставляют только limits и получают троттлинг, другие оставляют requests по умолчанию и удивляются Pending. Поля решают две разные задачи: requests - учёт при планировании и защита от нехватки ресурсов, limits - защита ноды от одного разросшегося контейнера.
Чем requests отличаются от limits
| Поле | Что делает | Кто использует | Последствия |
|---|---|---|---|
| requests.cpu | Гарантированный минимум CPU | kube-scheduler при выборе ноды, kubelet при расчёте вытеснения | Не помещается на нодах - под остаётся в Pending |
| requests.memory | Гарантированный минимум памяти | kube-scheduler, kubelet, расчёт oom_score_adj | Завышенный requests уменьшает плотность размещения подов |
| limits.cpu | Потолок CPU | kubelet и cgroups через CFS quota | Превышение даёт троттлинг: контейнер замирает до конца периода |
| limits.memory | Потолок памяти | Ядро через лимит cgroup memory | Превышение даёт OOMKilled, exit code 137 |
Разница видна на цифрах. Манифест с requests.cpu=250m и limits.cpu=500m разрешает занять до половины ядра, но при планировании учитываются только 250m. Манифест с requests.memory=256Mi и limits.memory=512Mi позволяет вырасти до 512 MiB, и ровно на этой границе контейнер убьют. CPU сжимаемый ресурс, память нет: превышение лимита CPU просто замедляет приложение, превышение лимита памяти его останавливает.
Где задавать ресурсы: под или контейнер
Отдельного поля resources на уровне пода не существует. Ресурсы описываются внутри каждого контейнера в spec.containers[].resources. В поде с несколькими контейнерами (основной контейнер, sidecar, init-контейнеры) каждый задаёт свои значения, а QoS-класс вычисляется по всей группе.
spec:
initContainers:
- name: migrate
image: registry.example.com/api:1.4.2
command: ["./migrate"]
resources:
requests:
cpu: "100m"
memory: "128Mi"
limits:
cpu: "200m"
memory: "256Mi"
containers:
- name: api
image: registry.example.com/api:1.4.2
resources:
requests:
cpu: "250m"
memory: "256Mi"
limits:
cpu: "500m"
memory: "512Mi"
- name: log-shipper
image: registry.example.com/fluent-bit:3.0
resources:
requests:
cpu: "50m"
memory: "64Mi"
limits:
cpu: "100m"
memory: "128Mi"
Init-контейнеры планируются отдельно и завершаются до старта основных, но их requests тоже участвуют в расчёте при размещении пода. Для sidecar без лимитов под автоматически получит класс Burstable, даже если основной контейнер настроен строго.
Как планировщик использует requests при размещении пода
kube-scheduler суммирует requests всех подов, уже привязанных к ноде, и сравнивает сумму с полем Allocatable этой ноды. Allocatable меньше Capacity: из ёмкости вычитаются резервы kube-reserved и system-reserved, а также порог срабатывания eviction. Limits в этом расчёте не участвуют вообще, поэтому на ноду можно поставить поды, сумма limits которых в разы больше её ёмкости. Такой переподписки (overcommit) никто не запрещает, но при одновременном всплеске нагрузки поды начнут мешать друг другу.
Пример: нода с Allocatable 4 CPU, системные поды и DaemonSet занимают 0.5 CPU, два приложения держат по requests.cpu=1500m, итого 3.5 CPU. Остаётся 500m, и под с requests.cpu=1000m на эту ноду уже не встанет. При трёх нодах в кластере событие будет выглядеть так:
FailedScheduling: 0/3 nodes are available: 3 Insufficient cpu.Под останется в статусе Pending, пока кто-то из соседей не освободит место.
Диагностика ошибок планирования
Первое, что смотрим при Pending, это события пода и занятые ресурсы нод.
kubectl get pods -o wide kubectl describe pod api-7d9f4c8b6d-2xkzp kubectl describe pod api-7d9f4c8b6d-2xkzp | grep -A10 Events kubectl describe node worker-02 | grep -A25 "Allocated resources"
В секции Events ищем строки Insufficient cpu, Insufficient memory, Insufficient pods или сообщение вида 0/3 nodes are available. Блок Allocated resources у ноды покажет суммарные Requests и Limits по каждому ресурсу и процент от Allocatable. Что делать дальше:
- снизить requests до фактического потребления, если значения явно завышены;
- добавить ноду в пул вручную или включить Cluster Autoscaler;
- проверить, не занимает ли место DaemonSet или дефектный под с огромным requests;
- использовать PriorityClass, чтобы важный под мог вытеснить малозначимый (preemption) и занять его место.
Отдельно проверьте лимиты namespace: если в нём настроены ResourceQuota для CPU или памяти, под не создастся даже при свободных нодах. Подробный разбор квот, LimitRange и политик распределения собран в материале про управление ресурсами, квоты и лимиты в Kubernetes.
Почему limits вызывают троттлинг CPU и как его заметить
Планировщик Linux CFS (Completely Fair Scheduler) делит время на периоды и выдаёт контейнеру квоту, равную его limits.cpu. Значения по умолчанию для CFS bandwidth: cpu.cfs_period_us = 100ms, cpu.cfs_quota_us = -1, cpu.cfs_burst_us = 0 (CFS Bandwidth Control, документация ядра Linux). При limits.cpu=500m квота составляет 50 мс на каждые 100 мс. Как только контейнер выбирает квоту, он замораживается до начала следующего периода, даже если ядро простаивает. Внешне это выглядит как рост latency и «проседание» ответов при средней загрузке CPU, далёкой от лимита.
Важно понимать, что квота CFS - это бюджет по настенным часам, а не на каждое ядро: при квоте 200ms и периоде 100ms группа может использовать 2 CPU одновременно (200% утилизации). Квота 100ms/100ms = 100% соответствует только 1 CPU, и 4 потока исчерпают её за 25ms, после чего будут throttled оставшиеся 75ms (CPU Bandwidth Control, Linux Kernel Internals).
Опасность в том, что throttling не виден ни в kubectl top, ни в обычном графике нагрузки: средние значения остаются низкими, страдают только пиковые запросы. Заниженный limits.cpu на ingress-контроллере или API-сервисе легко добавляет десятки миллисекунд к каждому ответу.
Как проверить троттлинг в контейнере
kubectl top pod api-7d9f4c8b6d-2xkzp --containers kubectl exec api-7d9f4c8b6d-2xkzp -- cat /sys/fs/cgroup/cpu.stat kubectl exec api-7d9f4c8b6d-2xkzp -- cat /sys/fs/cgroup/cpu.max
В cgroup v2 файл cpu.stat содержит строки nr_periods, nr_throttled и throttled_usec. Отношение nr_throttled к nr_periods и есть доля throttled-периодов. В cgroup v1 тот же счётчик лежит в /sys/fs/cgroup/cpu/cpu.stat. В Prometheus используем метрики cAdvisor:
rate(container_cpu_cfs_throttled_periods_total{container="api"}[5m])
/
rate(container_cpu_cfs_periods_total{container="api"}[5m])
Если доля throttled-периодов держится высокой на рабочей нагрузке, limits.cpu занижен. Единого универсального порога нет: ориентируйтесь на собственный baseline и на то, как throttling отражается на latency сервиса. Для latency-sensitive сервисов есть два рабочих варианта: выставить limits.cpu равным requests.cpu с запасом по обоим значениям либо оставить только requests.cpu без лимита. Во втором случае throttling исчезает, но контейнер теоретически может занять CPU всей ноды, поэтому так делают только для сервисов, которым доверяют, и при наличии ResourceQuota на namespace. Отсутствие лимита CPU не отменяет requests: планировщик по-прежнему резервирует указанный объём.
OOMKilled: почему под убивается по памяти и как это исправить
OOMKilled означает, что процесс убил механизм out-of-memory в ядре, потому что потребление памяти превысило limits.memory контейнера. Код возврата 137 равен 128 плюс 9, то есть SIGKILL. В kubectl describe pod это видно в секции Last State: Terminated, Reason: OOMKilled, Exit Code: 137. Логи предыдущего запуска читаются командой kubectl logs --previous.
Типовые причины:
- limits.memory выставлен по памяти в простое, без учёта пиковой нагрузки;
- JVM или .NET-приложение не знает о лимите cgroup и берёт heap по объёму памяти всей ноды;
- утечка памяти: потребление растёт линейно и упирается в потолок через несколько часов;
- внутренние кэши (HTTP-кэш, кэш ORM) без ограничения размера.
Для JVM надёжный старт даёт флаг -XX:MaxRAMPercentage=75.0: heap займёт не больше 75% лимита, остальное уйдёт на metaspace, стеки потоков и native-память. Для Node.js помогает --max-old-space-size, для Go и Rust память ограничивают на уровне приложения и не полагаются на GOMEMLIMIT как на единственную защиту.
Как отличить OOMKilled контейнера от eviction ноды
Убить под могут двое, и лечится это по-разному. Контейнерный OOMKilled означает срабатывание cgroup-лимита: под остаётся на ноде, но перезапускается, а в describe pod стоит Reason: OOMKilled. Eviction ноды запускает kubelet при MemoryPressure: под удаляется с ноды целиком, в событиях появляется причина Evicted, а перезапуск делает контроллер на другой ноде.
kubectl get events --field-selector involvedObject.name=api-7d9f4c8b6d-2xkzp kubectl get events --field-selector reason=Evicted,kubelet --sort-by=.lastTimestamp journalctl -u kubelet | grep -i evict
kubelet начинает вытеснение, когда свободной памяти на ноде становится меньше порога. Пороги жёсткого вытеснения по умолчанию: memory.available < 100Mi, nodefs.available < 10%, nodefs.inodesFree < 5%, imagefs.available < 15%. Эти значения применяются, только если ни один из параметров не изменён вручную (Node-pressure Eviction, документация Kubernetes). Второй механизм защиты от OOM на уровне ноды - oom_score_adj: kubelet устанавливает его для каждого контейнера на основе QoS-класса пода. Для Burstable-подов значение вычисляется по формуле min(max(2, 1000 - (1000 × memoryRequestBytes) / machineMemoryCapacityBytes), 999), а для контейнеров в подах с приоритетом system-node-critical kubelet ставит -997 (Node-pressure Eviction, документация Kubernetes). Чем ниже значение, тем позже процесс попадёт под системный OOM killer.
QoS-классы Kubernetes: Guaranteed, Burstable, BestEffort
Класс качества обслуживания Kubernetes присваивает автоматически по соотношению requests и limits у всех контейнеров пода. Вручную поле не задаётся, но от класса зависит порядок вытеснения и вес в oom_score_adj. Проверить результат можно одной командой:
kubectl get pod api-7d9f4c8b6d-2xkzp -o jsonpath='{.status.qosClass}'
Таблица условий присвоения QoS-класса
| Класс | Условие | CPU и память | Поведение при нехватке ресурсов |
|---|---|---|---|
| Guaranteed | У каждого контейнера requests равны limits по CPU и памяти | Например, 500m/500m и 1Gi/1Gi | Вытесняется последним, минимальный шанс попасть под OOM killer |
| Burstable | requests меньше limits хотя бы у одного контейнера, либо заданы только requests | Например, requests 100m/128Mi при limits 500m/256Mi | Вытесняется после BestEffort, приоритет зависит от превышения requests |
| BestEffort | Ни requests, ни limits не заданы ни у одного контейнера | Поля resources отсутствуют | Вытесняется первым, кандидат номер один при давлении на ноду |
Правило, о котором часто забывают: если указать только limits без requests, Kubernetes скопирует limits в requests. Под с единственным limits.cpu=500m и limits.memory=512Mi получит requests 500m и 512Mi, то есть класс Guaranteed. Обратная ситуация, когда заданы только requests, даёт Burstable без потолка потребления: контейнер может занять всю свободную память ноды и утащить за собой соседей.
Классификация применяется ко всем ресурсам, которые Kubernetes умеет учитывать, включая расширенные устройства. Расширенные ресурсы вроде nvidia.com/gpu описываются тем же способом, что CPU и память (управление ресурсами в Kubernetes).
Как QoS влияет на вытеснение подов при нехватке ресурсов
Когда на ноде заканчивается память или место на диске, kubelet выбирает жертву по строгому алгоритму. Сначала уходят поды без гарантий, потом те, кто вышел за свои requests, и только в последнюю очередь защищённые сервисы. Порядок предсказуем, и его стоит учитывать при разметке нагрузки.
Порядок вытеснения подов по QoS-классам
- BestEffort: поды этого класса имеют наибольший шанс вытеснения при node-pressure eviction.
- Burstable: kubelet ранжирует поды, у которых потребление превышает requests.
- Guaranteed: вытесняются с наименьшей вероятностью.
Внутри одного класса kubelet смотрит на PriorityClass и на превышение потребления над requests, а при необходимости выбирает поды с наименьшим приоритетом. Проверить приоритет и класс можно так:
kubectl get pod api-7d9f4c8b6d-2xkzp -o jsonpath='{.status.qosClass} {.spec.priorityClassName}'
kubectl get priorityclass
Практические выводы для продакшена: критичные сервисы (СУБД, ingress, очереди) держим в Guaranteed с явным PriorityClass; веб-приложения оставляем в Burstable, но requests задаём близко к реальному потреблению, иначе под станет первым кандидатом на вытеснение; BestEffort оставляем для дев-контуров и одноразовых задач. PodDisruptionBudget к вытеснению отношения не имеет: он защищает только от добровольных прерываний вроде drain ноды или обновления Deployment.
Готовые манифесты с проверенными значениями ресурсов
Ниже три шаблона под разные сценарии. Значения стартовые: их стоит уточнить по метрикам вашего сервиса, но структура и логика выбора класса остаются те же.
Манифест Guaranteed для базы данных
apiVersion: apps/v1
kind: Deployment
metadata:
name: postgres
spec:
replicas: 1
selector:
matchLabels:
app: postgres
template:
metadata:
labels:
app: postgres
spec:
containers:
- name: postgres
image: postgres:16
ports:
- containerPort: 5432
env:
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: pg-secret
key: password
resources:
requests:
cpu: "500m"
memory: "1Gi"
limits:
cpu: "500m"
memory: "1Gi"
requests равны limits, значит class Guaranteed: под последний в очереди на вытеснение, а его oom_score_adj близок к -997. Плата за это - невозможность кратковременно превысить 500m CPU. Для СУБД такой обмен оправдан: троттлинг предсказуемее, чем внезапный дефицит памяти на ноде. Проверка: kubectl get pod -l app=postgres -o jsonpath='{.items[0].status.qosClass}' вернёт Guaranteed. Если увеличить только limits.memory до 2Gi, класс сменится на Burstable, а под потеряет защиту при MemoryPressure.
Манифест Burstable для веб-приложения
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
spec:
replicas: 3
selector:
matchLabels:
app: web
template:
metadata:
labels:
app: web
spec:
containers:
- name: nginx
image: nginx:1.27-alpine
ports:
- containerPort: 80
resources:
requests:
cpu: "100m"
memory: "128Mi"
limits:
cpu: "500m"
memory: "256Mi"
readinessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 3
periodSeconds: 10
requests меньше limits, класс Burstable. Nginx в простое потребляет единицы millicores, а на пике раздаёт статику на 500m, поэтому широкий коридор здесь работает в плюс. Память ограничена 256Mi: если включён кэш прокси или много keep-alive соединений, потолок стоит поднять до 512Mi, иначе первый же всплеск даст OOMKilled с кодом 137. Для тестового окружения под без блока resources даст класс BestEffort, но в продакшене так делать не стоит.
Команды для проверки ресурсов и QoS после деплоя
Настройки применились, но это не значит, что они работают как задумано. Базовая проверка занимает минуту и снимает большую часть сюрпризов на проде.
kubectl get pod api-7d9f4c8b6d-2xkzp -o jsonpath='{.status.qosClass}'
kubectl get pod api-7d9f4c8b6d-2xkzp -o jsonpath='{.spec.containers[*].resources}'
kubectl describe pod api-7d9f4c8b6d-2xkzp | grep -E "Limits|Requests|Events|OOMKilled|Evicted"
kubectl top pod api-7d9f4c8b6d-2xkzp --containers
kubectl get events --field-selector involvedObject.name=api-7d9f4c8b6d-2xkzp
Команда kubectl top работает только при установленном metrics-server: без него вывод будет Empty metrics или ошибка о недоступности Metrics API. Если метрик нет вообще, ставьте metrics-server до начала настройки лимитов, иначе подбирать значения придётся вслепую.
Чек-лист проверки после деплоя
- QoS-класс совпадает с ожидаемым: Guaranteed, Burstable или BestEffort.
- requests и limits в describe pod совпадают со значениями манифеста, без скрытых подстановок от LimitRange в namespace.
- kubectl top pod показывает потребление ниже limits по CPU и памяти с запасом хотя бы 20-30%.
- В событиях нет OOMKilled, Evicted и FailedScheduling.
- nr_throttled в /sys/fs/cgroup/cpu.stat близок к нулю, доля throttled-периодов невелика относительно вашего baseline.
- Под не висит в Pending, а нода показывает запас в блоке Allocated resources.
Если пункт 3 или 5 не выполняется, возвращаемся к разделу про троттлинг и поднимаем лимит CPU. Если сработал пункт 4 с OOMKilled, смотрим раздел про память и проверяем пиковое потребление. Когда картина шире одного пода и нужно найти корневую причину роста потребления, помогает набор PromQL-запросов и алгоритм из материала про диагностику подов и узлов Kubernetes через метрики.
Как подобрать значения ресурсов для типовых сервисов
Угадывать цифры не нужно. Рабочий порядок такой: запускаем сервис с заведомо достаточными limits, собираем метрики потребления за одну-две недели, включая пиковые периоды, и только потом фиксируем значения. Практическая схема:
- Собрать потребление CPU и памяти по контейнерам: kubectl top в динамике, в продакшене - Prometheus с histogram по container_memory_working_set_bytes и container_cpu_usage_seconds_total.
- Взять p95 потребления CPU и памяти как requests: половина запросов будет обслуживаться без упора в лимит, планировщик получит реалистичный резерв.
- Добавить к p99 по памяти запас 20-30% и записать это в limits.memory. Для критичных сервисов requests.memory сделать равным limits.memory, переведя под в Guaranteed.
- Перепроверить через неделю: если nr_throttled растёт, поднять limits.cpu; если OOMKilled повторяется, поднять limits.memory или ограничить heap приложения.
Ориентиры для старта, которые проходят на большинстве небольших инсталляций:
| Сервис | requests | limits | QoS |
|---|---|---|---|
| Nginx или статический фронтенд | cpu 100m, memory 128Mi | cpu 500m, memory 256Mi | Burstable |
| PostgreSQL | cpu 500m-2, memory 1-4Gi | равны requests | Guaranteed |
| Java-сервис (Spring Boot) | memory 1Gi, MaxRAMPercentage 75 | memory равен requests | Guaranteed |
| Фоновый воркер | cpu 200m, memory 256Mi | cpu 1, memory 512Mi | Burstable |
Для рекомендаций без ручного анализа используют Vertical Pod Autoscaler в режиме Off: он считает рекомендации по истории потребления и не перезапускает поды. Связка VPA и Goldilocks закрывает задачу rightsizing для десятков сервисов. VPA требует metrics-server и работает тем точнее, чем длиннее история наблюдений. Как связать лимиты с автоскейлингом и метриками кластера в целом, разобрано в статье про производительность Kubernetes: метрики, лимиты и тюнинг кластера. И не переносите значения из чужих туториалов один в один: чужая нагрузка ничего не говорит о вашем профиле потребления.
Начните с самого нагруженного сервиса в кластере: снимите p95 и p99 по памяти, выставьте requests по p95, limits по p99 с запасом и переведите критичные поды в Guaranteed. Дальше по чек-листу проверьте QoS, троттлинг и события, и только после этого переходите к следующему сервису. Так вы закроете OOMKilled и Pending без перезагрузки всего кластера.