Задача
На одной ноде работают два пода. Оба реально потребляют по 500 МиБ памяти. Манифесты разные:
# Под A
resources:
requests:
memory: "1Gi"
cpu: "500m"
limits:
memory: "1Gi"
cpu: "500m"# Под B
resources:
requests:
memory: "256Mi"
cpu: "100m"
limits:
memory: "2Gi"На ноде заканчивается память.
Вопрос: какой под будет убит первым и почему?
Что кажется очевидным
«Потребление одинаковое — значит, шансы равны» или «убьют того, кто просит больше памяти, то есть A».
И то, и другое неверно.
Как это работает на самом деле
Сначала — про сами поля, потому что путаница обычно начинается здесь.
requests — это обещание планировщику: «этому контейнеру нужно столько». По сумме requests планировщик решает, влезет ли под на ноду. Это не лимит — контейнер может потреблять больше.
limits — потолок. Для памяти жёсткий: превысил — OOMKilled. Для CPU — троттлинг, процесс просто замедляют.
Из соотношения этих двух полей Kubernetes выводит класс QoS — он и определяет, кого приносить в жертву при нехватке ресурсов:
| Класс | Условие | При нехватке памяти |
|---|---|---|
| Guaranteed | requests == limits для всех контейнеров и по CPU, и по памяти | вытесняют последним |
| Burstable | requests заданы, но не равны limits (или заданы частично) | вытесняют вторыми |
| BestEffort | ни requests, ни limits не заданы | вытесняют первыми |
Посмотреть класс:
kubectl get pod my-pod -o jsonpath='{.status.qosClass}'В нашей задаче: под A — Guaranteed (requests равны limits по обоим ресурсам), под B — Burstable.
Что происходит при нехватке памяти
Два разных механизма, которые часто смешивают:
1. Вытеснение kubelet’ом (eviction). Kubelet следит за ресурсами ноды. Когда свободной памяти становится мало, он начинает выселять поды — и выбирает по классу QoS: сначала BestEffort, потом Burstable (среди них — тех, кто сильнее превысил свой request), в последнюю очередь Guaranteed.
2. OOM killer ядра. Если память кончается резко, вмешивается ядро Linux. Оно оперирует значением oom_score_adj, которое kubelet проставляет исходя из QoS: у Guaranteed — наименее «убиваемое» значение, у BestEffort — наиболее.
Под B превысил свой request (256 МиБ) более чем вдвое — он и есть первый кандидат.
Ответ
Первым убьют под B. Он в классе Burstable и потребляет вдвое больше запрошенного, тогда как под A — Guaranteed и укладывается в свой запрос.
Парадокс, который стоит осознать: под A «просит больше», но именно поэтому защищён. Запрошенные ресурсы — это ваша броня, а не расточительство.
Что с этим делать
- Для критичных сервисов делайте
requests=limitsпо памяти. КлассGuaranteed— самая надёжная защита от вытеснения. Для баз данных и stateful-нагрузок это обязательно. - Никогда не оставляйте поды без
requests.BestEffort— это «убейте меня первым». Такие поды допустимы только для заведомо неважных задач. - С CPU-лимитами осторожнее. Превышение лимита памяти убивает под, а превышение CPU-лимита — троттлит его через CFS-квоту: процесс останавливают до конца периода. Для latency-чувствительных сервисов это выливается в рваные задержки. Распространённая практика: задавать
requests.cpuи не задаватьlimits.cpu. requestsдолжны отражать реальность. Заниженные — под вытеснят; завышенные — нода утилизируется вхолостую, кластер стоит дороже. Настраивайте по фактическим метрикам (VPA в режиме рекомендаций хорошо помогает).- Проверяйте класс до инцидента:Всё, что нашлось в проде, — потенциальная жертва первой волны.BASH
kubectl get pods -A -o custom-columns=\ 'NS:.metadata.namespace,NAME:.metadata.name,QOS:.status.qosClass' | grep BestEffort
Подробнее про сам механизм вытеснения и пороги — в задаче 19, а про то, что именно считается памятью контейнера, — в задаче 5.