Ресурсы, лимиты и QoS
requests и limits — это не просто два числа: от них зависит, кого кластер убьёт первым при нехватке памяти; разбираемся в классах QoS.
QoS-класс (Quality of Service) — категория, которую Kubernetes присваивает поду по соотношению его requests и limits; именно она определяет приоритет пода при вытеснении под давлением ресурсов.
Зачем это на практике
В базовом разделе мы задавали requests и limits и считали тему закрытой. Но на проде наступает ночь, узел упирается в память, и kubelet начинает кого-то убивать — вопрос только, кого именно. Ответ напрямую следует из того, как вы выставили ресурсы. Под, которому повезло иметь равные requests и limits, выживает; под без лимитов умирает первым. Это не случайность, а механика QoS-классов, и понимать её — значит уметь защитить критичные сервисы.
requests против limits — разная роль
Эти два поля часто путают, хотя они отвечают за разное:
| Поле | Что значит | Кто использует |
requests | гарантированный минимум, бронь ресурса | планировщик (scheduler) — куда поставить под |
limits | жёсткий потолок потребления | kubelet/ядро — когда притормозить или убить |
requests — это обещание кластера: «этот под гарантированно получит столько». Планировщик ставит под только на узел, где суммы requests ещё помещаются. limits — это запрет: «больше — нельзя». Память и CPU при этом ведут себя по-разному. Превышение лимита CPU не убивает под, а троттлит его: ядро не даёт процессорное время сверх квоты, и приложение тормозит. Превышение лимита памяти убивает: память нельзя «притормозить», поэтому процесс получает OOMKilled.
apiVersion: v1
kind: Pod
metadata:
name: web
spec:
containers:
- name: app
image: myapp:1.0
resources:
requests:
cpu: "250m"
memory: "256Mi"
limits:
cpu: "500m"
memory: "256Mi"
Здесь CPU может «всплеснуть» вдвое (с 250m до 500m), а память жёстко зафиксирована на 256Mi и сверху, и снизу.
Три класса QoS
Kubernetes автоматически выводит класс пода из его ресурсов — задать его напрямую нельзя:
| Класс | Условие | Приоритет при вытеснении |
Guaranteed | requests = limits по CPU и памяти у всех контейнеров | убивают последним |
Burstable | requests заданы, но меньше limits (или limits не у всех) | в середине |
BestEffort | ни requests, ни limits не заданы вовсе | убивают первым |
Под из примера выше по памяти имеет requests = limits, но по CPU — нет (250m против 500m), поэтому он Burstable. Чтобы получить Guaranteed, нужно выровнять requests и limits и для CPU тоже. Класс пода видно прямо в его статусе:
kubectl get pod web -o jsonpath='{.status.qosClass}'
Вывод:
Burstable
Практический вывод: критичные сервисы (база, платёжный шлюз) делают Guaranteed — их кластер не тронет почти ни при каких обстоятельствах. Фоновые задачи, которые не жалко перезапустить, можно оставить Burstable. А BestEffort в проде — почти всегда ошибка: такой под умирает первым и не даёт планировщику никаких гарантий размещения.
OOMKilled: почему под умирает
Когда контейнер превышает limits.memory, ядро Linux вызывает OOM-killer в его cgroup и убивает процесс. Под перезапускается, а в его описании появляется характерная отметка:
kubectl describe pod web
# ...
# Last State: Terminated
# Reason: OOMKilled
# Exit Code: 137
Код выхода 137 (= 128 + 9, где 9 — сигнал SIGKILL) — верный признак OOM. Важно отличать это от бага: код приложения может быть корректен, ему просто не хватило выделенного лимита. Лечится либо поднятием limits.memory, либо устранением утечки. А вот если под Burstable убит не своим лимитом, а под давлением памяти на узле — это уже вытеснение по QoS, и спасает повышение класса до Guaranteed или добавление узлов.
ResourceQuota и LimitRange: правила на весь namespace
Полагаться, что каждый разработчик выставит ресурсы правильно, наивно. Поэтому на уровне namespace задают два защитных объекта. ResourceQuota ограничивает суммарное потребление всего namespace — чтобы одна команда не съела весь кластер:
apiVersion: v1
kind: ResourceQuota
metadata:
name: team-quota
namespace: team-a
spec:
hard:
requests.cpu: "10"
requests.memory: 20Gi
limits.cpu: "20"
limits.memory: 40Gi
pods: "50"
Теперь все поды namespace team-a вместе не запросят больше 10 ядер и 20Gi и не создадут больше 50 подов. Важная деталь: если в namespace действует ResourceQuota на requests/limits, то под без указанных ресурсов вообще не будет принят — квота требует, чтобы каждый под объявлял свою долю.
LimitRange работает на уровне отдельного пода: задаёт значения по умолчанию и границы для каждого контейнера:
apiVersion: v1
kind: LimitRange
metadata:
name: defaults
namespace: team-a
spec:
limits:
- type: Container
default:
cpu: "500m"
memory: 512Mi
defaultRequest:
cpu: "250m"
memory: 256Mi
max:
cpu: "2"
memory: 2Gi
Если разработчик забыл указать ресурсы, LimitRange проставит defaultRequest/default автоматически, а попытку запросить больше max отклонит. Связка очевидна: LimitRange гарантирует, что у каждого пода есть разумные requests (а значит, он не BestEffort и помещается в квоту), а ResourceQuota держит суммарный потолок namespace.
Как это работает под капотом
requests и limits — это, по сути, настройки cgroups v2 ядра Linux на узле. limits.memory пишется в memory.max cgroup контейнера; при превышении срабатывает OOM-killer именно этой группы. limits.cpu транслируется в cpu.max (квота и период): 500m означает «500 мс CPU за каждые 1000 мс», и сверх этого ядро притормаживает процесс. requests.cpu же превращается в cpu.weight — относительный вес при дележе процессора между cgroup, когда CPU в дефиците. А QoS-класс kubelet кодирует в oom_score_adj процесса: у Guaranteed он минимальный (ядро трогает их в последнюю очередь), у BestEffort — максимальный (кандидаты на отстрел номер один). То есть приоритет вытеснения — это не абстракция Kubernetes, а реальная настройка ядра.
Частые ошибки
- Лимиты без requests или наоборот. Перекос ломает планирование и QoS; задавайте оба осознанно.
- BestEffort в проде. Под без ресурсов умирает первым и не даёт планировщику гарантий — почти всегда ошибка.
- Путать OOMKilled (свой лимит) с вытеснением (давление на узле). Первое лечится лимитом памяти, второе — классом QoS или узлами.
- Жёсткий лимит CPU там, где нужны всплески. Троттлинг растит задержки, хотя память и аптайм идеальны; подбирайте лимит по профилю нагрузки.
- ResourceQuota без LimitRange. Квота начнёт отклонять поды без ресурсов, и разработчики упрутся в ошибки; LimitRange проставит дефолты и сгладит переход.
Итоги
requests— бронь для планировщика,limits— потолок для ядра; роли у них разные.- Память за лимитом убивает (OOMKilled, exit 137), CPU за лимитом лишь троттлит.
- QoS-класс выводится автоматически:
Guaranteed(requests=limits) выживает,BestEffort(без ресурсов) умирает первым. - Критичные сервисы делайте
Guaranteed;BestEffortв проде избегайте. ResourceQuotaограничивает namespace в сумме,LimitRangeзадаёт дефолты и границы поду — используйте их в паре.