Kube/Pythonproduction course
Курс · RU06-health-resources-and-scheduling.md

06. Проверки здоровья, ресурсы и планирование

Незнакомый термин? Откройте словарь Kubernetes и терминов курса.

Назначение, предварительные знания и результаты обучения

Нужны главы 00–05 и базовый Deployment. Эта глава связывает жизненный цикл приложения с kubelet, scheduler и cgroups Linux.

После главы вы сможете:

  • спроектировать проверки startup/readiness/liveness без цикла обратной связи;
  • объяснить корректное завершение процесса FastAPI;
  • отличить request/limit CPU и памяти, а также 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). Квоты сдерживают радиус поражения арендатора и стоимость, но требуют платформенных значений по умолчанию и процесса исключений.

Самопроверка

  1. Что делает провал readiness?
  2. Почему liveness на общей БД опасна?
  3. Чем срабатывание лимита CPU отличается от лимита памяти?
  4. Что scheduler использует: запрос ресурсов или фактическое потребление?
  5. Почему toleration не гарантирует размещение?
  6. Где искать причину Pending?
  7. Делит ли preStop льготный период с завершением приложения?

Ответы: 1) убирает обычный трафик Service; 2) вызывает синхронный шторм перезапусков; 3) CPU подвергается ограничению, превышение памяти может вызвать завершение по OOM; 4) запросы ресурсов; 5) только снимает фильтр taint; 6) conditions и Events; 7) да.

Резюме и источники

Проверки должны отвечать на разные вопросы, ресурсы — составлять измеренный контракт, а планирование — использовать набор ограничений. Далее: развёртывания, масштабирование и доступность.