QoS classes: Guaranteed, Burstable, BestEffort
QoS class — это метка, которую Kubernetes ставит на Pod автоматически, исходя из того, как заданы requests и limits. Вручную выставить QoS нельзя (поле status.qosClass, read-only). Эта метка определяет две вещи: порядок eviction Pods под memory pressure на node и значения oom_score_adj для процессов в container, то есть кого первым убьёт kernel OOM killer. На CKAD QoS — частая теоретическая тема, и одновременно фундамент production-устойчивости.
Приоритеты и nice: как влиять на планировщик из user-space
Три класса
Примеры:
# Guaranteed: request == limit для обоих ресурсов
resources:
requests:
cpu: 500m
memory: 512Mi
limits:
cpu: 500m
memory: 512Mi
# Также Guaranteed: только limits, kubelet проставит requests=limits
resources:
limits:
cpu: 500m
memory: 512Mi
# Burstable: request < limit хотя бы для одного ресурса
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 256Mi
# Также Burstable: указан только request
resources:
requests:
cpu: 100m
# BestEffort: пусто
# (resources не указан вообще, или resources: {})
QoS определяется на уровне Pod, но проверка идёт по каждому container. Если в Pod два container, один Guaranteed и один BestEffort — итоговый Pod = Burstable (потому что Guaranteed требует, чтобы ВСЕ container были Guaranteed). Подмешать BestEffort container в Guaranteed Pod нельзя случайно — поднимется до Burstable.
Посмотреть QoS:
kubectl get pod <name> -o jsonpath='{.status.qosClass}'
kubectl describe pod <name> | grep "QoS Class"
Eviction order под memory pressure
Kubelet на каждом node постоянно мониторит сигналы (memory.available, nodefs.available, imagefs.available). Когда memory.available падает ниже eviction threshold (по умолчанию около 100Mi), kubelet сам инициирует eviction Pods, чтобы освободить ресурсы и не дать упасть критичным компонентам node (kubelet, container runtime, kernel).
Eviction kubelet делает SIGTERM → grace period → SIGKILL. Pod помечается как Failed с reason Evicted. По restartPolicy controller (Deployment/StatefulSet/etc.) пересоздаст реплику — но на другом node, не на том же.
Eviction отличается от OOMKill. Eviction — это решение kubelet (workload terminate gracefully where possible, status=Failed Evicted). OOMKill — решение kernel (мгновенный SIGKILL процессу, container exit 137). Eviction срабатывает раньше, на уровне node pressure; OOMKill — на уровне cgroup конкретного container.
OOM score и приоритет внутри cgroup
Внутри cgroup memory limit (когда process в одном Pod упёрся в собственный limit) Linux OOM killer выбирает victim среди процессов этой cgroup, и тоже использует приоритеты. Они задаются через oom_score_adj — число от -1000 (никогда не убивать) до 1000 (убить в первую очередь).
Kubelet проставляет oom_score_adj процессам Pod исходя из QoS class:
Guaranteed: -997 (защищён)
BestEffort: 1000 (убивать первым)
Burstable: рассчитывается формулой
oom_score_adj = max(2, min(999, 1000 - (1000 * memory_request_bytes / node_memory_bytes)))
Burstable формула: чем больший memory request относительно ёмкости node, тем меньше oom_score_adj, тем менее охотно kernel выберет process. То есть Burstable с большим request защищён почти как Guaranteed.
Это объясняет, почему сам факт указать memory request снижает шанс быть убитым — даже без limit. Pod, скромно попросивший 1Gi на node с 32Gi, получит oom_score_adj ≈ 970 (плохо, но не худший), а BestEffort — гарантированно 1000.
CPU manager policy и static CPU pinning
С cpuManagerPolicy: static (включается на kubelet) Guaranteed Pod с integer CPU request получает эксклюзивно закреплённые vCPUs (cpuset). Это убирает context switching между ядрами, выгодно для latency-критичных приложений: low-latency trading, video processing, real-time inference.
Условия для pinning:
# Guaranteed, request=limit=integer для CPU
resources:
requests:
cpu: 2 # ровно 2 ядра, integer, НЕ 2000m в строке (тоже OK, но integer)
memory: 4Gi
limits:
cpu: 2
memory: 4Gi
С cpu: 500m или cpu: "1.5" static policy не сработает — нужны integer. Burstable и BestEffort всегда работают в shared CPU pool (SharedCPUs).
Это редко требуется на CKAD, но в production знание помогает: «почему мы видим cache line thrashing на high-frequency сервисе» → потому что Pod Burstable, и kernel перемещает task между ядрами.
Killer момент: BestEffort — приговор в production
# Pod без resources — BestEffort
apiVersion: v1
kind: Pod
metadata:
name: yolo
spec:
containers:
- name: app
image: my-app
# никаких resources
Что произойдёт с этим Pod в плохой день:
- Scheduler посадит его на любой node с самым большим запасом — но запас фейковый, scheduler не знает, сколько Pod реально использует.
- При первом скачке нагрузки на node — Pod первый в очереди на eviction.
- При cgroup memory pressure где-то рядом — Pod первый в очереди на OOM kill (
oom_score_adj=1000). - Даже если node нормальный, при system OOM Pod умрёт раньше любого Pod, у которого хотя бы заявлен memory request.
В production BestEffort приемлем только для нагрузок типа «curl debug Pod», «короткий job», «опциональная вспомогательная задача, которую можно потерять». Никогда не для приложений с пользовательским трафиком.
Никогда не выкатывайте Deployment в production без хотя бы memory request. Без request Pod становится BestEffort, и при первой же memory pressure он умрёт раньше всех, не получив shutdown gracefully — пользователи увидят 500/504 без возможности retry на старом instance. Минимальный production-контракт: указать оба request, и memory limit (≈ 1.5–2x request).