06. Проверки здоровья, ресурсы и планирование
Незнакомый термин? Откройте словарь Kubernetes и терминов курса.
Назначение, предварительные знания и результаты обучения
Нужны главы 00–05 и базовый Deployment. Эта глава связывает жизненный цикл приложения с kubelet, scheduler и cgroups Linux.
После главы вы сможете:
- спроектировать проверки startup/readiness/liveness без цикла обратной связи;
- объяснить корректное завершение процесса FastAPI;
- отличить
request/limitCPU и памяти, а также QoS; - доказать
Pendingчерез FailedScheduling и OOM черезlastState; - использовать LimitRange/ResourceQuota;
- читать селектор узла, affinity, taints/tolerations и топологическое распределение.
Ментальная модель: три независимых вопроса о здоровье
- Проверка startup: завершился ли долгий старт? До успеха readiness и liveness не управляют контейнером.
- Проверка readiness: можно ли сейчас направлять трафик? Провал убирает Pod из обычных endpoints Service, но не перезапускает процесс.
- Проверка liveness: процесс безнадёжно завис и перезапуск полезнее ожидания? Провал приводит к перезапуску контейнера.
Проверка — управляющий сигнал, а не замена мониторинга. Liveness не должна зависеть от общей БД или внешнего сервиса: отказ зависимости перезапустит все реплики и усилит аварию. Readiness тоже опасна: если все Pods откажутся из-за одной зависимости, вы создадите полный отказ. Определите, может ли Service отдавать ответ в режиме частичной деградации.
Базовый python-api:
startupProbe:
httpGet:
path: /health/live
port: http
periodSeconds: 2
timeoutSeconds: 1
failureThreshold: 30
readinessProbe:
httpGet:
path: /health/ready
port: http
periodSeconds: 3
timeoutSeconds: 1
failureThreshold: 2
livenessProbe:
httpGet:
path: /health/live
port: http
periodSeconds: 10
timeoutSeconds: 1
failureThreshold: 3
Временной бюджет проверки startup приблизительно равен
failureThreshold × periodSeconds = 60s. Тайм-аут должен учитывать локальную
задержку планирования и среды исполнения, но не скрывать зависание. Успешный
HTTP-ответ — статус 200–399.
Корректный запуск и завершение работы Python
Путь завершения:
удаление/rollout/eviction
→ Pod завершается + распространяется состояние endpoint
→ обработчик preStop (/health/drain помечает приложение неготовым, ждёт 5 с)
→ SIGTERM для uvicorn
→ прекратить приём, завершить ограниченные активные запросы и жизненный цикл FastAPI
→ выйти до terminationGracePeriodSeconds; иначе SIGKILL
Порядок и распространение изменений асинхронны: задержка preStop даёт endpoints
и LB время, но не математическую гарантию. Обработчик и завершение приложения делят
terminationGracePeriodSeconds: 30. Долгие запросы должны иметь тайм-аут меньше
оставшегося бюджета. Процесс PID 1 обязан корректно обработать SIGTERM; форма
exec для CMD в Containerfile не прячет сигнал за командной оболочкой.
Запросы, лимиты и QoS
Запрос (request) — резервирование, вход планирования и относительная доля CPU.
Лимит (limit) — верхняя граница во время исполнения. CPU 500m равен половине
процессорного времени; CPU сжимаем, поэтому избыток ограничивается.
Память 256Mi несжимаема: превышение cgroup может вызвать завершение по OOM. Запрос
памяти не является предварительным выделением и не предотвращает OOM.
resources:
requests:
cpu: 100m
memory: 128Mi
ephemeral-storage: 64Mi
limits:
cpu: 500m
memory: 256Mi
ephemeral-storage: 256Mi
QoS:
Guaranteed: у каждого контейнера запросы CPU и памяти равны лимитам;Burstable: естьrequests/limits, но класс неGuaranteed;BestEffort: ни у одного контейнера нет запросов/лимитов CPU и памяти.
QoS влияет на приоритет eviction при нехватке ресурсов, но не заменяет
PriorityClass или SLO. Базовый Pod относится к Burstable.
Ограничения Namespace
Полные манифесты:
apiVersion: v1
kind: LimitRange
metadata:
name: workload-defaults
namespace: kube-course
spec:
limits:
- type: Container
defaultRequest:
cpu: 50m
memory: 64Mi
default:
cpu: 500m
memory: 256Mi
min:
cpu: 10m
memory: 16Mi
max:
cpu: "2"
memory: 1Gi
---
apiVersion: v1
kind: ResourceQuota
metadata:
name: workload-budget
namespace: kube-course
spec:
hard:
requests.cpu: "4"
requests.memory: 8Gi
limits.cpu: "8"
limits.memory: 16Gi
pods: "50"
persistentvolumeclaims: "10"
LimitRange может добавить значения по умолчанию, изменить или отклонить отдельный объект; ResourceQuota отклоняет превышение суммы в namespace. Они не измеряют реальную стоимость и не гарантируют доступную ёмкость узлов.
Ограничения планирования
Scheduler сначала фильтрует подходящие узлы, затем оценивает их. Основные средства управления:
nodeSelector— простые обязательные метки узлов;- node affinity — выразительные обязательные или предпочтительные правила;
- affinity/anti-affinity Pod — совместное или раздельное размещение относительно Pods; вычислительно дороже;
- taint на узле отталкивает; toleration лишь разрешает, но не притягивает;
- топологическое распределение размещает подходящие Pods по hostname или зоне;
- ресурсы и топология PVC также участвуют в фильтрации.
Базовый манифест использует:
topologySpreadConstraints:
- maxSkew: 1
topologyKey: kubernetes.io/hostname
whenUnsatisfiable: ScheduleAnyway
labelSelector:
matchLabels:
app.kubernetes.io/name: python-api
app.kubernetes.io/instance: course
ScheduleAnyway — предпочтительный баланс, при котором маленький кластер не
блокируется. Для жёсткого требования доступности можно использовать
DoNotSchedule, но при недостатке зон или узлов rollout останется Pending.
Селектор правила распределения обязан совпадать с Pods, иначе ограничение
посчитает другой набор.
Полный Deployment с проверками здоровья
apiVersion: apps/v1
kind: Deployment
metadata:
name: python-api-health
namespace: kube-course
labels:
app.kubernetes.io/name: python-api
app.kubernetes.io/instance: health-lab
spec:
replicas: 2
selector:
matchLabels:
app.kubernetes.io/name: python-api
app.kubernetes.io/instance: health-lab
template:
metadata:
labels:
app.kubernetes.io/name: python-api
app.kubernetes.io/instance: health-lab
spec:
terminationGracePeriodSeconds: 30
automountServiceAccountToken: false
securityContext:
runAsNonRoot: true
runAsUser: 10001
runAsGroup: 10001
seccompProfile:
type: RuntimeDefault
containers:
- name: api
image: kube-python-course:1.0.0
ports:
- name: http
containerPort: 8000
env:
- name: DRAIN_SECONDS
value: "5"
startupProbe:
httpGet: {path: /health/live, port: http}
periodSeconds: 2
failureThreshold: 30
readinessProbe:
httpGet: {path: /health/ready, port: http}
periodSeconds: 3
failureThreshold: 2
livenessProbe:
httpGet: {path: /health/live, port: http}
periodSeconds: 10
failureThreshold: 3
lifecycle:
preStop:
httpGet: {path: /health/drain, port: http}
resources:
requests: {cpu: 100m, memory: 128Mi}
limits: {cpu: 500m, memory: 256Mi}
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
Практическое упражнение: проверки и завершение работы
Подготовка
kubectl apply -f manifests/base/configmap.yaml
kubectl apply -f manifests/base/secret.example.yaml
kubectl apply -f manifests/base/serviceaccount.yaml
kubectl apply -f manifests/base/deployment.yaml
kubectl apply -f manifests/base/service.yaml
kubectl rollout status deployment/python-api -n kube-course --timeout=180s
kubectl get pods -n kube-course -l app.kubernetes.io/instance=course \
-o custom-columns='NAME:.metadata.name,QOS:.status.qosClass,RESTARTS:.status.containerStatuses[0].restartCount,NODE:.spec.nodeName'
Ожидаются Burstable, три готовых Pod и нулевое число перезапусков.
Доказательства readiness и drain
Выберите Pod, следите за EndpointSlice и удалите его:
POD="$(kubectl get pod -n kube-course \
-l app.kubernetes.io/instance=course \
-o jsonpath='{.items[0].metadata.name}')"
kubectl get endpointslice -n kube-course \
-l kubernetes.io/service-name=python-api -w
В другом терминале:
time kubectl delete pod "$POD" -n kube-course --wait=true
kubectl rollout status deployment/python-api -n kube-course --timeout=180s
Удаление должно занять примерно не меньше окна drain и завершения работы; endpoint станет неготовым и будет удаляться, а замена перейдёт в Ready. Точное время зависит от проверок и распространения изменений API. Проверьте старые журналы, если они ещё доступны, и новый UID.
Медленный запуск без цикла liveness
kubectl patch configmap python-api-config -n kube-course --type merge \
-p '{"data":{"STARTUP_DELAY_SECONDS":"20"}}'
kubectl rollout restart deployment/python-api -n kube-course
kubectl get pods -n kube-course -w
Бюджет проверки startup равен 60 секундам, поэтому Pod должен со временем стать Ready без перезапусков из-за liveness. Восстановление:
kubectl apply -f manifests/base/configmap.yaml
kubectl rollout restart deployment/python-api -n kube-course
kubectl rollout status deployment/python-api -n kube-course --timeout=180s
Лабораторные работы с нехваткой ресурсов
Невыполнимый запрос ресурсов
kubectl apply -f manifests/broken/01-pending-cpu.yaml
kubectl get pod broken-pending -n kube-course
kubectl describe pod broken-pending -n kube-course
kubectl get events -n kube-course \
--field-selector involvedObject.name=broken-pending
Ожидается Pending, PodScheduled=False, FailedScheduling с
Insufficient cpu. Журналы образа и приложения не относятся к причине: узел
ещё не выбран. Исправление — реалистичный запрос ресурсов или увеличение ресурсов, а не
удаление Pod.
OOM
kubectl apply -f manifests/broken/06-oom.yaml
kubectl wait pod/broken-oom -n kube-course \
--for=jsonpath='{.status.phase}'=Failed --timeout=120s
kubectl get pod broken-oom -n kube-course \
-o jsonpath='{.status.containerStatuses[0].state.terminated.reason}{" exit="}{.status.containerStatuses[0].state.terminated.exitCode}{"\n"}'
Ожидается OOMKilled exit=137 (реализация может показать причину и код через
lastState перезапускаемой нагрузки). Дополняйте доказательства метриками узла и
ядра, но не делайте вывод об утечке памяти только по OOM: лимит может быть
просто ниже рабочего набора памяти.
Очистка:
kubectl delete -f manifests/broken/01-pending-cpu.yaml
kubectl delete -f manifests/broken/06-oom.yaml
Самостоятельная задача
Создайте три Pods классов Guaranteed, Burstable и BestEffort и получите
.status.qosClass. Затем добавьте обязательный nodeSelector с
несуществующей меткой, докажите FailedScheduling и исправьте его предпочтительным
affinity. Критерий приёмки: предсказание сделано до применения и подтверждено
conditions и Events.
Сломанный сценарий с проверкой
kubectl apply -f manifests/broken/05-probe.yaml
kubectl get pods -n kube-course -l app.kubernetes.io/name=broken-probe -w
Неверный порт 8001 вызывает провалы liveness и перезапуски. Соберите:
POD="$(kubectl get pod -n kube-course \
-l app.kubernetes.io/name=broken-probe \
-o jsonpath='{.items[0].metadata.name}')"
kubectl describe pod "$POD" -n kube-course
kubectl logs "$POD" -n kube-course --previous
kubectl get pod "$POD" -n kube-course \
-o jsonpath='{.status.containerStatuses[0].restartCount}{"\n"}'
Исправьте порт проверки на именованный http, не перезапускайте приложение.
Очистка:
kubectl delete -f manifests/broken/05-probe.yaml.
Дерево диагностических решений
Pod неисправен
├─ Pending → PodScheduled condition + Events
│ ├─ Insufficient resource → запросы/ёмкость
│ ├─ taint/affinity → ограничения и метки/taints узла
│ └─ PVC → топология/привязка хранилища
├─ Перезапуски
│ ├─ reason OOMKilled → рабочий набор/лимит/давление на узел
│ ├─ liveness failed → endpoint/порт/тайм-аут/зависимость
│ └─ завершение процесса → предыдущие журналы/код выхода/сигнал
└─ Running, но не Ready
├─ startup ещё активна → бюджет startup/журналы
├─ readiness failed → endpoint/зависимости
└─ у Service нет endpoint → селектор и condition Ready
Типичные ошибки: liveness проверяет БД; одинаковый агрессивный тайм-аут для всех проверок; запросы ресурсов скопированы без измерений; лимит CPU вызывает задержку, которую liveness «лечит» циклом перезапусков; отсутствуют ресурсы сайдкара; toleration считается правилом размещения; обязательный anti-affinity делает rollout непланируемым; OOM исправляют без данных о куче и рабочем наборе памяти.
Эксплуатационные компромиссы, безопасность и стоимость
Завышенные запросы ресурсов расходуют ёмкость, заниженные ухудшают планирование, eviction и расчёт HPA. Лимит CPU защищает от шумного соседа, но ограничение может увеличить хвост задержки; политика зависит от платформы. Лимит памяти необходим, но рассчитывается по перцентилю с запасом и учётом конкурентности. Проверки создают трафик и нагрузку CPU; для тысяч Pods стоимость заметна. Метки и taints узлов становятся границей доверия только при защищённой установке меток (NodeRestriction). Квоты сдерживают радиус поражения арендатора и стоимость, но требуют платформенных значений по умолчанию и процесса исключений.
Самопроверка
- Что делает провал readiness?
- Почему liveness на общей БД опасна?
- Чем срабатывание лимита CPU отличается от лимита памяти?
- Что scheduler использует: запрос ресурсов или фактическое потребление?
- Почему toleration не гарантирует размещение?
- Где искать причину Pending?
- Делит ли preStop льготный период с завершением приложения?
Ответы: 1) убирает обычный трафик Service; 2) вызывает синхронный шторм перезапусков; 3) CPU подвергается ограничению, превышение памяти может вызвать завершение по OOM; 4) запросы ресурсов; 5) только снимает фильтр taint; 6) conditions и Events; 7) да.
Резюме и источники
Проверки должны отвечать на разные вопросы, ресурсы — составлять измеренный контракт, а планирование — использовать набор ограничений. Далее: развёртывания, масштабирование и доступность.