Прямой ответ
У каждого контейнера в Kubernetes два параметра ресурсов: requests и limits. Они решают разные задачи, и путаница между ними — основная причина проблем с производительностью и внезапных перезапусков.
Requests — заявка планировщику. Кубернетес ищет узел, где сумма requests всех подов плюс новый под не превысит доступную ёмкость. Requests не ограничивают потребление: контейнер может использовать больше, если на узле есть свободные ресурсы.
Limits — жёсткий потолок через cgroups. Поведение при превышении различается по типу ресурса:
| Ресурс | Что происходит при превышении limits |
|---|---|
| CPU | Троттлинг: контейнер получает меньше процессорного времени, растёт задержка |
| Память | OOMKill: контейнер завершается и перезапускается |
Это различие принципиально: CPU — сжимаемый ресурс, память — несжимаемый. Превышение по CPU ухудшает производительность, по памяти — убивает процесс.
Классы качества обслуживания (QoS) определяются автоматически:
| Класс | Условие | Порядок вытеснения |
|---|---|---|
| Guaranteed | requests == limits для CPU и памяти во всех контейнерах | Вытесняется последним |
| Burstable | Задан хотя бы один requests, но не равен limits | Вытесняется вторым |
| BestEffort | Не задано ничего | Вытесняется первым |
При нехватке ресурсов на узле kubelet вытесняет поды в порядке от BestEffort к Guaranteed. Поэтому приложение, которому не задали ресурсы, перестаёт работать первым — при том, что выглядит «настроенным так же, как остальные».
resources:
requests:
cpu: 250m # 0,25 ядра: гарантия планирования
memory: 512Mi
limits:
cpu: "1" # потолок: выше — троттлинг
memory: 768Mi # потолок: выше — OOMKill
Важно: limits не может быть меньше requests — манифест не пройдёт валидацию. CPU задаётся в долях ядра (250m = 0,25), память — в байтах с двоичными (Mi, Gi) или десятичными (M, G) приставками; разница между Mi и M составляет около 5 %.
Практика: как подбирать значения
1. Requests по памяти — критично. Если сумма requests ниже фактического потребления, узел уходит в состояние давления памяти и начинает вытеснять поды. Практика: requests по памяти ставят близко к фактическому рабочему объёму, с запасом 10–20 %.
2. Limits по CPU часто мешают. Троттлинг проявляется как рост p99 задержки при невысокой средней загрузке CPU — симптом, который трудно связать с причиной. Многие команды отказываются от лимита CPU, оставляя только requests.
3. Limits по памяти обязательны. Без них один контейнер с утечкой занимает всю память узла и провоцирует каскадное вытеснение.
4. Измеряйте перед настройкой. Значения «на глаз» почти всегда неверны. Ориентироваться нужно не на среднее, а на 95-й процентиль потребления за представительный период, включая пики суточной нагрузки и моменты сборки мусора:
kubectl top pod -n billing --sort-by=memory
kubectl describe node worker-3 | sed -n '/Allocated resources/,/Events/p'
5. Автомасштабирование опирается на requests. HPA вычисляет загрузку как отношение фактического потребления к requests, а не к limits. Заниженный requests даёт ложную картину «высокой загрузки» и преждевременное масштабирование — см. HPA.
6. JVM и рантаймы с собственной кучей. Если процесс управляет памятью сам, лимит контейнера должен учитывать не только кучу, но и метаспейс, стеки потоков и нативные аллокации. Установка -Xmx равной лимиту контейнера — типичная причина OOMKill: нативные структуры размещаются вне кучи, и суммарное потребление превышает потолок. Практическое правило — оставлять не менее 20–25 % лимита контейнера за пределами кучи.
7. Пересматривайте значения после изменений в коде. Добавление кэша, нового пула соединений или фоновой задачи меняет профиль потребления. Значения, подобранные полгода назад, перестают соответствовать действительности, и расхождение обнаруживается вытеснением или троттлингом, а не на этапе ревью.
Рекомендации:
- Начинайте с requests и оставляйте CPU без limits. Это предсказуемое поведение и отсутствие троттлинга; ограничение памяти защищает узел.
- Проверяйте класс QoS для критичных сервисов. Если важно, чтобы под вытеснялся последним, нужны равные requests и limits, то есть класс Guaranteed.
- Учитывайте overhead пауз сборки мусора. Пики потребления памяти при GC выше среднего значения; requests, выставленный по среднему, приводит к вытеснению в момент пика.
- Не копируйте значения между сервисами. Профиль потребления у API с короткими запросами и у воркера пакетной обработки различается на порядок.
- Свяжите с планированием ёмкости. Сумма requests определяет, сколько подов помещается на узел; ошибки в requests приводят либо к простоивающему железу, либо к постоянному вытеснению — о методике оценки в статье про capacity planning.
Практический сценарий: периодические перезапуски пода без видимых причин
Контекст: под API-сервиса перезапускался два-три раза в сутки с кодом 137. В логах приложения ошибок не было.
- Диагностика.
kubectl describe podпоказал причинуOOMKilled. Лимит памяти составлял 512 Mi, requests — 128 Mi. - Наблюдение. Метрики показали, что потребление держалось на уровне 420–470 Mi, но в момент сборки мусора достигало 560 Mi. То есть превышение было коротким и периодическим.
- Причина. Лимит выставили по среднему потреблению, а не по пику. Дополнительно JVM была настроена с
-Xmx512m, что не оставляло места на метаспейс и нативные структуры. - Действия. (а) Лимит памяти увеличили до 768 Mi, requests подняли до 512 Mi, чтобы планировщик учитывал реальный объём; (б)
-Xmxснизили до 480 m, оставив запас внутри контейнера; (в) добавили алерт на потребление выше 80 % лимита; (г) для воркеров пакетной обработки значения подобрали отдельно по их профилю. - Результат. Перезапуски прекратились; p99 задержки снизился, потому что прекратились холодные старты после OOMKill.
Вывод: значение limits по памяти должно отражать пик потребления, а не среднее. Перезапуск пода из-за OOMKill выглядит как случайный сбой, но всегда имеет измеримую причину.
FAQ
Что произойдёт, если не задать resources вовсе? Под получит класс BestEffort и будет вытеснен первым при нехватке ресурсов на узле. Кроме того, HPA не сможет вычислить загрузку, а планировщик разместит под без учёта его потребления. В namespace с квотой такой под вообще не создастся: при действующей квоте на CPU или память указание requests становится обязательным.
Можно ли задать только limits без requests? Можно: в этом случае requests автоматически приравнивается к limits, и под получает класс Guaranteed. Такое поведение удобно для критичных сервисов, но требует точного знания потребления.
Почему CPU-лимит вызывает задержки при низкой загрузке? Троттлинг применяется в рамках коротких периодов квотирования: процесс может исчерпать квоту за долю периода и ожидать до его конца. В метриках это выглядит как низкая средняя загрузка при высоких хвостовых задержках.
Как узнать фактическое потребление, если под постоянно перезапускается? Через метрики до перезапуска либо запуском копии с повышенным лимитом в тестовом окружении под нагрузкой. Оценка по «спокойному» состоянию даёт заниженные значения.
Источники: Kubernetes docs (manage resources, QoS classes, node-pressure eviction) — ссылки в шапке.