Прямой ответ
Ресурсы контейнера описываются двумя числами на CPU и память: requests (гарантированный минимум — по нему планировщик выбирает узел: сумма requests подов на узле не превысит его allocatable — «резервирование») и limits (потолок — поведение при превышении различается по ресурсу, и это главное, что путает новичков): CPU limit превышен — контейнер троттлится (cgroup-квота — процессы тормозятся, под жив — «приложение стало медленным при нагрузке» — обычно CPU-троттлинг); memory limit превышен — под убивается OOMKill (память не эластична — превысил — умер — «под перезапускается без видимых причин» — почти всегда OOM: kubectl describe pod — OOMKilled, exit code 137). Практические правила: память — requests = limits (память не сжимается; запрос меньше лимита — риск OOM при переполнении узла + планировочная иллюзия свободных ресурсов; Guaranteed-класс даёт предсказуемость — QoS ниже); CPU — requests по фактическому потреблению, limits либо щедрее requests (burst), либо не ставить вовсе (спорило сообщество годами — «CPU limits не обязательны» — троттлинг вреднее перегрузки при наличии requests); значения — не с потолка, а из замеров (kubectl top pod, метрики контейнеров — Prometheus — инструментация: p95 потребления за неделю — requests ≈ p95, лимит памяти = p95 + 20–30% запаса). Python-специфика: лимит памяти должен учитывать все процессы контейнера (gunicorn master + N воркеров — процессная модель: 4 воркера × 300 МБ + мастер — лимит «512Mi» убьёт под на ровном месте — считать на воркеры, а не на «одно приложение»; воркеров в контейнере меньше, чем «на всю машину» — K8s масштабирует подами, не воркерами до бесконечности).
Requests и limits: механика и QoS
Планирование: под «помещается» на узел, если его requests влезли в свободные allocatable (реальное потребление роли не играет — планировщик верит requests — заниженные requests = переполненный узел: памяти физически нет — kubelet вытесняет (eviction) поды по превышению — «поды случайно умирают при нагрузке соседей» — следствие общей недооценки requests в кластере); лимиты — на уровне cgroup контейнера (CPU — квота/период (200m = 0.2 ядра-эквивалент — 20% времени CPU; доли ядра реальны — 100m-воркер работает, медленно), память — жёсткий максимум). QoS-классы (порядок выживания при нехватке ресурсов на узле/эвикшенах): Guaranteed (requests = limits на всё — выживают первыми; критичные сервисы — сюда), Burstable (requests < limits — середина), BestEffort (ресурсы не заданы — убиваются первыми — «под без requests в проде» — лотерея на выживание). Диагностика ресурсных проблем: kubectl describe pod (Last State: OOMKilled/exit 137 — память; restart-паттерн), kubectl top pod --containers (живое потребление против requests/limits), метрики container_memory_working_set_bytes (то, что сравнивается с лимитом — не RSS «из top» — рабочая память с активными страницами — расхождения нормальны) и container_cpu_cfs_throttled_periods_total (доля троттлинга — растёт — CPU-лимит жмёт); kubectl get events (evictions/OOM — лента событий узла); частые сценарии: OOMKill при старте (лимит меньше пика загрузки — импорт тяжёлых библиотек/прогрев — замерить пик старта, не «установившееся потребление»), медленный рост до OOM (утечка — лимит не лечит, а диагностирует регулярностью смерти — тракторинг по траектории RSS), OOM на батчевой задаче (обработка большого файла — пик в 10× среднего — либо лимит под пик, либо потоковая обработка — стриминг вместо «всё в память»).
Подбор значений и масштабирование
Процесс подбора (не гадание): (1) замеры — неделя метрик потребления (working set по памяти, фактический CPU — p50/p95/пик старта); (2) requests: CPU ≈ p95 потребления (планировочная честность), память = p95 + запас 20–30% и сразу equals limits (или близко — Guaranteed/почти); (3) наблюдение после деплоя: троттлинг вырос — CPU-limit поднять/убрать; OOM — лимит памяти поднять и искать причину роста (утечка/пик); (4) итерация (значения живут в Helm-чарте — чарты — values per окружение: dev меньше, prod по замерам — значения ресурсов — часть кода деплоя, ревьюятся — конфиг-дисциплина). Взаимодействие с HPA (автомасштабирование по CPU — HPA): метрика utilization считается от requests («80% CPU» = 80% от request, не лимита — занизил requests — HPA скалит рано и часто; завысил — поздно); «requests = limits» + HPA — предсказуемая связка. Эвристики по типам нагрузок: веб-API (uvicorn/gunicorn) — CPU-эластичны (троттлинг терпим, лимит можно мягкий), память под воркеров — жёсткий расчёт; батчи/воркеры очередей (Celery) — пики по задачам — лимит под p99 задачи + потоковость кода; in-memory кеш в процессе (Redis не используется — кеш в RAM приложения) — лимит = кеш + приложение + запас (иначе кеш «съест» под — максимум кеша учитывать — maxsize обязателен); init-контейнеры и sidecar'ы — их ресурсы суммируются в под (Pod-основы) — «забытый sidecar-лимит» ломает расчёт. Антипаттерны: BestEffort в проде (эвикшены при первом давлении), «огромные requests на всякий случай» (узел впустую зарезервирован — кластер не масштабируется экономически), лимиты «как у соседа» (нагрузки разные — только свои замеры), игнор пика старта (OOM-цикл CrashLoopBackOff на деплое — классика — проверять именно старт).
Практический сценарий: OOM-циклы после выкатки
Исходник: сервис отчётов задеплоен в K8s (базовый деплой) с resources: requests 256Mi/100m, limits 512Mi/500m («дефолты из примера»); после выкатки — CrashLoopBackOff каждые 10–40 минут. Диагностика: describe — OOMKilled (137); kubectl top — рабочий набор уползает к 500Mi перед смертью; метрики в Grafana (дашборд): пиловидная память (рост — смерть — рост) с периодом, зависящим от трафика; разбор приложения: gunicorn 4 воркера × ~110 МБ установившиеся = 440 МБ + мастер + sidecar-логгер 60 МБ — 512Mi впритык, любой отчёт-пик (xlsxwriter на 200k строк — экспорт — +150 МБ на воркер) — гарантия OOM. Лечение: (1) расчёт по модели: 4×(110+150 пик) + мастер 40 + sidecar 60 ≈ 1.2Gi — лимит 1.5Gi, requests = limits (Guaranteed); (2) воркеры 4→2 (K8s-масштабирование подами — 3 реплики по 2 воркера вместо 1×4 — пики изолированы подами — процессная модель в K8s-контексте); (3) CPU: limits убран (замеры показали троттлинг в пиках отчётов — requests 500m оставлен планировочный); (4) проверка старта: пик загрузки моделей — 380 МБ — в лимит входит; (5) HPA на CPU 70% от requests (2–6 реплик) — дневной пик разбирается.scale-ом. Результат: OOM-циклы исчезли (ноль за 2 месяца), троттлинг CPU упал с 18% периодов до нуля, узлы кластера — utilization честный (раньше requests 256Mi при факте 500+ — узел «вмещал» подов вдвое больше, чем физически мог — латентные эвикшены при пиках соседей ушли). Записано в runbook (инцидент-практика): «resources нового сервиса — только по замерам + модель процессов, не из примеров».
FAQ
Почему под в Pending и не стартует? Планировщик не нашёл узел с достаточными свободными requests (FailedScheduling — Insufficient cpu/memory — describe покажет): либо requests завышены (проверить замеры), либо кластер заполнен (cluster-autoscaler добавит узел, если настроен — managed-кластеры обычно; self-hosted — вручную), либо nodeSelector/taints сужают выбор узлов (основы); «ресурсов полно» по top — не аргумент планировщику (он считает зарезервированные requests, не факт).
Нужно ли ставить CPU limits? Позиция практики (и долгих сообщественных дебатов): requests — обязательны (планирование/HPA/QoS), limits CPU — опциональны и часто вредны (троттлинг на коротких пиках при свободном CPU узла — латентные тормоза; «убить» процесс CPU-перебором cgroup не может — только тормозить); для latency-критичных — без CPU-limit (или ≥2×requests); для «шумных соседей» батчей — лимит оправдан (изоляция); память — лимит всегда (иначе один под съест узел — эвикшены соседей).
Как считать ресурсы для Python-приложения с воркерами? Модель «на процесс»: лимит памяти = (установившийся RSS воркера + пиковая дельта задач) × число воркеров + мастер + sidecar'ы + 15–20% запас; замеры — per-процесс (не только под-сумма — какой воркер растёт — видно в профилировании); форк-экономия памяти (общие COW-страницы после fork — GIL/процессы) реальна, но расходится со временем (каждый воркер трогает свои объекты — «4×110, а не 440» — консервативно считать полным умножением); проверка — фактический working set пода под нагрузкой, не арифметика в вакууме.
Что делает K8s при нехватке памяти на узле? Kubelet-эвикшены: при давлении памяти (memory pressure) kubelet вытесняет поды по приоритету QoS/превышению requests (BestEffort → Burstable по степени превышения → Guaranteed последними); вытесненный под пересоздаётся планировщиком (на другом узле — если есть куда); отличие от OOMKill: эвикшен — решение kubelet'а про узел (под «жертва соседей»), OOMKill — cgroup-вердикт про контейнер (под «сам превысил»); профилактика — честные requests (переполнения узла нет — эвикшенов нет) + приоритеты PriorityClass для критичных сервисов.
Источники: документация Kubernetes (manage resources, pod QoS, resource metrics) — ссылки в шапке.