Learning Platform
Глоссарий Troubleshooting
Урок 14.01 · 25 мин
Продвинутый
requestslimitscgroupsOOMKilledthrottlingscheduler

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:

Две роли requests и limits
requestsГарантированный минимум. Scheduler ищет node, у которого Allocatable - sum(requests всех уже запущенных Pods) >= requests нового Pod. Это математика placement. Если по математике не сходится — Pod остаётся Pending с FailedScheduling.
limitsМаксимум, выше которого kernel не пустит. kubelet через CRI настраивает cgroup v2 параметры: cpu.max (квота CFS) и memory.max. Process в container не знает про limit — для него это просто стена.
capacity planningСумма requests всех Pods на node не может превысить Allocatable. Это и есть планирование ёмкости кластера.
runtime enforcementLimit срабатывает на самом узле в момент попытки allocate. CPU — мгновенный throttle. Memory — OOM kill.

Главное следствие: 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
NOTE

Gi != G. 1Gi = 1073741824, 1G = 1000000000. Разница 7%. На больших heap для JVM это означает, что -Xmx1G под limit 1Gi будет работать, а -Xmx1Gi (JVM не понимает Gi) — ошибка парсинга. Договоритесь в команде раз и навсегда: для Kubernetes — Mi/Gi.


Что делает scheduler с requests

Поток принятия решения kube-scheduler:

Scheduling и requests
Pod созданAPI-сервер записал в etcd Pod в Pending. spec.nodeName ещё пуст.
filtering
NodeResourcesFitPlugin фильтрует nodes: для каждого node вычисляет Allocatable - sum(requests запущенных Pods). Если остатка хватает на requests нового Pod — node проходит фильтр.
scoring
NodeResourcesBalancedAllocationСреди отфильтрованных nodes scheduler выбирает тот, где placement даст наиболее сбалансированное использование CPU и memory (или по другой scoring strategy — LeastAllocated, MostAllocated).
binding
kubelet запускает PodПосле binding kubelet выбранного node ставит Pod на запуск, поднимает container через CRI, настраивает cgroup на основе requests и limits.

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.

WARNING

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.

OOMKilled flow
process: malloc()App пытается выделить страницу. Запрос идёт в kernel page allocator.
memory cgroupCgroup memory.current уже на memory.max. Kernel не может выделить страницу в этой cgroup.
oom_kill в этой cgroupKernel выбирает victim среди процессов в cgroup (по oom_score), отправляет SIGKILL. SIGKILL невозможно перехватить — никакого graceful shutdown.
container exit 137128 + signal 9 (SIGKILL) = 137. Это и есть классический OOMKilled exit code.
kubelet видит terminatedkubelet через CRI получает event, отмечает container Terminated, reason=OOMKilled. По restartPolicy перезапускает.

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, если приложение реально требует столько памяти.

DANGER

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.


Проверка знанийKnowledge check
Pod без CPU limit на node, где сейчас простаивает 3 ядра. Container в Pod внезапно начинает использовать 4 ядра. Что произойдёт?
ОтветAnswer
Kernel CFS отдаст процессу ровно столько CPU, сколько есть свободного. Под лимит = нет верхнего предела, под shares = пропорция при контеншене. На пустом node Pod возьмёт 3 свободных ядра. Если на node проснутся другие Pods, и пойдёт competition — этот Pod получит свою долю по CPU shares (на основе request). Никакого throttling и никакой ошибки.
Проверка знанийKnowledge check
Container memory limit 256Mi. Процесс пытается malloc страницу, current usage 255.9Mi. Что произойдёт?
ОтветAnswer
Если страница не помещается в оставшийся бюджет cgroup, kernel запускает OOM killer внутри этой cgroup, выбирает victim (обычно процесс с самым большим memory footprint и наивысшим oom_score_adj), отправляет SIGKILL. Container завершается с exit code 137 (128+9). kubelet видит Terminated, reason=OOMKilled, restartPolicy решает дальнейшую судьбу. Никакого graceful shutdown — SIGKILL не перехватывается.
Проверка знанийKnowledge check
В чём принципиальная разница между CPU throttling и Memory OOM с точки зрения видимости проблемы?
ОтветAnswer
CPU throttling тихий: процесс ставится на паузу до следующего CFS period, никаких errors в логах, никаких restart-ов, только проседание latency на десятки-сотни ms. Виден только через метрику container_cpu_cfs_throttled_periods_total из cAdvisor. Memory OOM громкий: container умирает с exit 137, видно в kubectl describe pod, restart count растёт, при повторении — CrashLoopBackOff.
Проверка знанийKnowledge check
Pod с initContainer (request 500m CPU) и обычным container (request 200m CPU). Какой effective CPU request учтёт scheduler?
ОтветAnswer
500m. Для init containers scheduler берёт max среди init container requests, потому что они запускаются последовательно — в любой момент времени работает только один init. Среди обычных containers — sum. Итог = max(initContainers) + sum(containers) = 500m + 200m = 500m (max(500, 200)=500, потому что init завершится до старта обычного). На самом деле формула: effective = max(max(init), sum(regular) + sum(sidecar)). Здесь sidecars нет, sum(regular)=200m, max(init)=500m, результат — 500m.

Проверьте понимание

Результат: 0 из 0
Концептуальный
Вопрос 1 из 5. В чём принципиальная разница между request и limit для scheduling и runtime?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 5