requests vs limits в Kubernetes: реальная разница и как ставить адекватные значения
Когда приходишь в новый проект на k8s, первое, что бросается в глаза — это либо полное отсутствие resources у pod-ов, либо одинаковые requests/limits на всех контейнерах, скопированные из чужого манифеста. И то и другое — путь к ночным звонкам. Разберу, чем requests отличаются от limits, как они работают на уровне ядра и что я сам ставлю в проде.
Версия — k8s 1.30, container runtime — containerd. Всё, что ниже, проверено в стейджинге, в проде, и иногда в проде на пиках Чёрной пятницы.
Что это вообще такое
В манифесте pod есть секция resources:
resources:
requests:
cpu: 100m
memory: 256Mi
limits:
cpu: 500m
memory: 512Mirequests — это то, что pod гарантированно получит. На основе requests scheduler решает, на какую ноду pod вообще можно поставить. Если у ноды свободно 200m CPU, а pod просит 500m — он туда не уедет.
limits — это потолок. Если pod захочет больше — его либо тротлят (CPU), либо убивают (память).
Тонкость в том, что requests и limits для CPU и для памяти ведут себя по-разному. Это и приводит к большинству проблем.
CPU: requests как вес, limits как тротлинг
CPU в Linux под cgroups — это share, а не абсолютная штука. cpu: 100m — это 0.1 ядра, или 100 millicpu. На уровне cgroup v2 это превращается в cpu.weight: чем больше requests, тем больший приоритет на загруженной ноде.
Ключевая фраза — «на загруженной ноде». Если нода полупустая, pod с requests: 100m может спокойно скушать целое ядро. Никто не запрещает. Но как только пришёл сосед с requests побольше — твой share уменьшится.
А вот limits: 500m — это уже жёсткий тротлинг через CFS bandwidth. Каждые 100ms (по умолчанию cpu.cfs_period_us=100000) контейнеру выделяется 50ms процессорного времени. Использовал — жди следующего окна.
Почему limits на CPU часто делают хуже
Это контринтуитивно, но: в большинстве случаев CPU limits лучше не ставить вообще. Особенно для веб-сервисов с короткими всплесками.
Пример из жизни. Go-сервис с cpu: 200m requests и cpu: 500m limits. На входе приходит запрос, сервис должен обработать его за 50ms. Но runtime захотел в этот момент сделать GC и съел всё доступное время за 30ms. Дальше запрос ждёт 70ms тротлинга, и P99 latency прыгает с 50ms до 120ms.
Без limits тот же сервис на полупустой ноде использует свободные ядра, GC отрабатывает мгновенно, P99 ровный. Я лично давно убрал CPU limits на всех своих контейнерах и сплю спокойнее. Requests ставлю честные — этого достаточно для шедулера.
resources:
requests:
cpu: 200m
memory: 256Mi
limits:
memory: 512Mi # только памятьЕсли боишься, что один контейнер съест всё CPU ноды — поставь PodDisruptionBudget и нормальные requests на соседей. Шедулер сам не даст вкатить туда ещё кучу подов.
Память: limits — это смерть
С памятью наоборот: limits ставить обязательно. Память не share, её нельзя «потротлить». Если контейнер превысил memory.limit_in_bytes — kernel вызывает OOM-killer и убивает процесс с сигналом 9. В kubectl describe pod это будет Reason: OOMKilled, Exit Code: 137.
Тут есть нюанс: requests на память влияют только на шедулинг. Реальный лимит — это limits.memory. Если пишешь:
resources:
requests:
memory: 100Mi
limits:
memory: 1Gi...то pod может разъесться до 1 ГБ, scheduler этого не учитывает при размещении. Получается классическая overcommit-ситуация: на ноду залезло 10 подов с requests по 100Mi, но каждый кушает 800Mi, и нода сваливается в kernel OOM, начинает убивать pods вразнобой, кластер штормит.
Поэтому моё правило — requests.memory == limits.memory для всего, что не утечная игрушка. Это даёт честную картину шедулеру и предсказуемое поведение.
Как считать значения
Гадать «ну пусть будет 256Mi» — это лотерея. Реальный путь — измерить.
Шаг 1: собери метрики
Поставь metrics-server и Prometheus. Метрики, которые тебе нужны:
container_memory_working_set_bytes— реально используемая память (не RSS, а то, что не выгружается).rate(container_cpu_usage_seconds_total[5m])— CPU в ядрах за интервал.kube_pod_container_resource_requests/limits— что у тебя сейчас стоит.
Запускай нагрузку (k6, locust, хоть синтетический трафик) и смотри 95-й перцентиль за неделю.
Шаг 2: посчитай по формуле
Для CPU я беру:
requests.cpu = P95(usage) * 1.2Запас 20% — на всплески и редкие пики. Limits на CPU не ставлю.
Для памяти:
requests.memory = limits.memory = max(working_set) * 1.3 + bufferЗапас 30% и буфер на runtime (Go GC, JVM heap fragmentation, и т.д.). Для Java и .NET буфер побольше — 500Mi-1Gi сверху, эти платформы любят держать память в резерве.
Шаг 3: VPA в режиме рекомендаций
Vertical Pod Autoscaler в режиме Off не меняет requests, но генерирует рекомендации в своём CRD:
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
name: api-vpa
spec:
targetRef:
apiVersion: apps/v1
kind: Deployment
name: api
updatePolicy:
updateMode: "Off"Через неделю смотришь:
kubectl describe vpa api-vpaВ выводе будет секция Recommendation с target/lower/upper bound. Пользоваться её цифрами как стартовой точкой — самое адекватное, что можно сделать.
QoS-классы и почему это важно
k8s присваивает каждому pod-у один из трёх QoS-классов на основе resources:
- Guaranteed — requests == limits для всех контейнеров и всех ресурсов. Самые «защищённые» pods, OOM-killer убьёт их в последнюю очередь.
- Burstable — есть requests, но requests != limits, или limits не для всех ресурсов. Большинство приложений живёт здесь.
- BestEffort — нет ни requests, ни limits. Расходники, kernel убьёт первыми при нехватке памяти на ноде.
Проверить класс конкретного pod-а:
kubectl get pod <pod> -n <ns> -o jsonpath='{.status.qosClass}'Для критичных компонентов (платёжный API, auth-сервис, базовая инфраструктура) я выставляю Guaranteed: requests == limits и по CPU, и по памяти. Для всего остального — Burstable с requests.memory == limits.memory.
Типичные ошибки
Что я регулярно вижу в чужих манифестах:
- Требуется 4 ядра, поставлено
cpu: 4в requests. На нодах по 8 ядер уже не помещается, шедулинг встаёт. Считать надо в реальности — обычно достаточно 500m-2. - limits.memory: 4Gi на pod, который кушает 200Mi. Шедулер думает, что pod «занимает» 4Gi (точнее, requests.memory), и не пускает соседей. Но если requests меньше limits — overcommit, привет OOM.
- CPU limits на init-контейнерах, которые делают миграции БД. Init работает раз в стартапе, ему нужно быстро. Тротлить там нечего, только тормозить релиз.
- Одинаковые лимиты на всех контейнерах в pod-е. Sidecar (envoy, fluent-bit) и основное приложение требуют разного. Sidecar обычно 50m/100Mi достаточно, не 200m/512Mi.
Что запомнить
requests — для шедулера, limits — для kernel. CPU limits в большинстве случаев вредят, memory limits обязательны. Считай по метрикам, не угадывай. Для критичных сервисов — Guaranteed (requests == limits), для остального — Burstable с requests.memory == limits.memory.
Куда копать дальше: официальная страница про управление ресурсами, статьи Tim Hockin про CFS bandwidth (он один из мейнтейнеров k8s, объясняет внутрянку понятно). И поставьте VPA, даже если боитесь автоматического апдейта — режим Off безопасен и даёт цифры из реальной нагрузки.