Requests и limits: основа
Каждый container в Pod может декларировать resource requests и resource limits — два числа, которые определяют две принципиально разные вещи. Requests — это контракт со scheduler. Limits — это контракт с kernel. Их часто путают и пишут одинаковыми «на всякий случай», и эта симметрия скрывает важнейший факт: для CPU и memory limit работает по-разному. CPU превышение → throttling. Memory превышение → OOMKilled, exit code 137. Если этого не понимать, production будет страдать молча.
Swap, overcommit, OOM killer: что делать когда RAM не хватает
Две оси: requests vs limits
resources:
requests:
cpu: 100m
memory: 128Mi
ephemeral-storage: 1Gi
limits:
cpu: 500m
memory: 256Mi
ephemeral-storage: 2Gi
Что здесь происходит для kube-scheduler и для kubelet:
Главное следствие: request — это про «где Pod запустится», limit — про «что Pod может делать на этом node». Pod с request 100m и limit 2000m спокойно spike-нёт до 2 ядер, если node имеет spare capacity — но scheduler искал место только под 100m.
Единицы измерения
CPU измеряется в millicores (m). Базовое значение — 1 = одно vCPU (логическое ядро, для Linux scheduler это одна running task в данный момент).
1 = 1 vCPU
1000m = 1 vCPU
500m = 0.5 vCPU (полядра)
100m = 0.1 vCPU (10% одного ядра)
0.5 = эквивалент 500m
2.5 = 2 с половиной vCPU
Дробные значения легитимны, но m читаемее. Никогда не пишите 0.001 — лучше 1m.
Memory измеряется в bytes с suffixes:
Binary (multiples of 1024) Decimal (multiples of 1000)
Ki = 1024 K = 1000
Mi = 1048576 M = 1000000
Gi = 1073741824 G = 1000000000
В кластерах всегда используйте binary suffixes (Mi, Gi). Linux memory счётчики всё считают в страницах по 4Ki, поэтому 256Mi ложится ровно, а 256M — это 244Mi с хвостом, и вы рискуете получить subtle off-by-one в monitoring.
Ephemeral storage (GA с v1.25) — для container image layers, container writable layer, logs, emptyDir без medium: Memory. Запрашивается ключом ephemeral-storage. Если container превысит limit на ephemeral-storage — kubelet evict-нет Pod (reason: Evicted, Evicted because of ephemeral-storage usage exceeds limit).
HugePages (RW GA с v1.22) — отдельный тип ресурса для приложений, которые хотят прямое выделение 2 MiB / 1 GiB страниц (БД, JVM с large pages, networking apps). Запрашивается ключом hugepages-2Mi или hugepages-1Gi; requests должно равняться limits — это static reservation на ноде, не bursting. На ноде должны быть pre-allocated huge pages (hugepagesz= в kernel boot). На CKAD спрашивают редко, но знать концепцию полезно для production.
resources:
limits:
hugepages-2Mi: 100Mi # 50 страниц по 2 MiB
memory: 200Mi
requests:
hugepages-2Mi: 100Mi # обязан равняться limit
memory: 200Mi
Gi != G. 1Gi = 1073741824, 1G = 1000000000. Разница 7%. На больших heap для JVM это означает, что -Xmx1G под limit 1Gi будет работать, а -Xmx1Gi (JVM не понимает Gi) — ошибка парсинга. Договоритесь в команде раз и навсегда: для Kubernetes — Mi/Gi.
Что делает scheduler с requests
Поток принятия решения kube-scheduler:
Pod без requests проходит filter тривиально (0 >= 0), что выглядит удобно — но создаёт overcommit. Node, заполненный BestEffort Pods без requests, по математике пуст, и scheduler может посадить туда ещё Pods, у которых requests есть. Когда нагрузка реальная — node под pressure, eviction, гонка. Поэтому в production всегда requests.
CPU limit: throttling, а не error
Kubelet через CRI настраивает на cgroup v2 контейнера параметр cpu.max, который содержит два числа: quota и period. Это интерфейс CFS bandwidth controller в Linux kernel. Если process в container попытается использовать больше CPU, чем quota за один period — kernel не даёт error, не убивает, просто приостанавливает task до начала следующего period.
limit 500m → cpu.max = 50000 100000 (50ms из 100ms period)
limit 2000m → cpu.max = 200000 100000 (200ms из 100ms period — 2 ядра)
Внешне это выглядит так: latency сервиса растёт скачками с шагом 10–100ms (период CFS), GC паузы в JVM становятся в разы дольше, метрики throughput проседают — но никаких errors в логах. Это самый неприятный класс багов в Kubernetes.
CPU throttling видно через метрику container_cpu_cfs_throttled_periods_total в cAdvisor. Если в Grafana вы видите регулярные throttled periods под нагрузкой — limit слишком низкий, поднимайте или уберите вообще.
В community сложился консенсус: CPU limits для подавляющего большинства workloads — anti-pattern. Аргументы:
- Без CPU limit Pod может burst-нуть на простаивающие ядра node — это бесплатная производительность.
- Compete за CPU между Pods регулируется через requests (CPU shares в cgroup, пропорциональное распределение под нагрузкой). То есть Pod с request 500m получит гарантированно полядра, даже если другие Pods на ноде хотят больше.
- Limit добавляет artificial throttling даже при наличии свободных ядер.
Когда CPU limit нужен: multi-tenant clusters где надо жёстко изолировать tenants, batch jobs которым нельзя дать сожрать всё, и редкие случаи когда runtime сам ведёт себя плохо при burst (некоторые версии JVM/Go runtime неправильно читают cgroup и стартуют слишком много GC threads — но это уже хвост).
Memory limit: OOMKilled, exit 137
С памятью всё иначе. Memory нельзя «приостановить» — process либо получил allocation, либо нет. Поэтому при попытке allocate за пределы memory cgroup kernel запускает OOM killer, выбирает process в этой cgroup (или дочерней) и убивает SIGKILL.
kubectl describe pod покажет:
Last State: Terminated
Reason: OOMKilled
Exit Code: 137
В отличие от CPU throttling это видно сразу. Pod рестартует, считается restart count, при превышении backoff threshold переходит в CrashLoopBackOff. Если памяти не хватает системно, restart не лечит — нужна работа с приложением: профайлинг heap (pprof для Go, jmap/jcmd для Java), поиск leak, тюнинг GC, увеличение limit, если приложение реально требует столько памяти.
Memory limit обязателен почти всегда. Без него один Pod с leak может съесть всю память node, и kubelet включит node-level eviction — будут падать чужие Pods. Memory request — для scheduler. Memory limit — для защиты соседей по node.
Killer момент: requests без limits — типичный production-выбор
resources:
requests:
cpu: 100m # для scheduler
memory: 256Mi # для scheduler
limits:
memory: 512Mi # защита от OOM соседей
# cpu — НЕ указан, разрешаем burst
Это пример «защитного дефолта», который часто встречается у зрелых команд: memory limit ставится (обычно как 1.5–2x request), CPU limit опускается совсем. Pod гарантированно получает свои 100m через CPU shares от cgroup, но может burst-нуть на свободные ядра. При memory leak погибнет только сам Pod, не соседи.
Когда это не работает: jobs с непредсказуемым tail latency (CPU spike задерживает SLO-критичный соседний Pod, который запрашивает ровно столько же CPU), multi-tenancy с фиксированными квотами.
Container init и sidecars
Init containers и sidecars (с 1.29+ через initContainers с restartPolicy: Always) считаются scheduler-ом особенным образом:
- Init containers: scheduler берёт maximum requests среди init containers, потому что они стартуют последовательно. Из обычных containers — берётся сумма.
- Sidecar init containers (restartPolicy=Always): они работают параллельно с main containers, поэтому их requests прибавляются к сумме обычных.
spec:
initContainers:
- name: prep
resources: { requests: { cpu: 100m, memory: 256Mi } }
- name: warmup
resources: { requests: { cpu: 200m, memory: 128Mi } }
containers:
- name: app
resources: { requests: { cpu: 50m, memory: 128Mi } }
- name: proxy
resources: { requests: { cpu: 50m, memory: 64Mi } }
# Effective Pod requests:
# cpu = max(100m, 200m) + 50m + 50m = 300m
# memory = max(256Mi, 128Mi) + 128Mi + 64Mi = 448Mi
На CKAD редко спрашивают эту арифметику в лоб, но при отладке Pending Pod это часто всплывает: «почему scheduler видит requests 300m, если у меня в каждом контейнере 50m?» — потому что плюс init.