← Весь справочник

Kubernetes: пробы, ресурсы и HPA

Liveness, readiness и startup-пробы, requests и limits, единицы CPU и памяти, классы QoS, HorizontalPodAutoscaler

Два вопроса, на которые Kubernetes нужен ответ от каждого контейнера: жив ли он и готов ли принимать трафик (пробы) и сколько ему нужно ресурсов (requests и limits). На ресурсах держится и автомасштабирование: HorizontalPodAutoscaler меняет число реплик по загрузке относительно requests.

Всё это поля манифеста. Как пробы и лимиты ведут себя на деле, видно только в работающем кластере: в песочнице их можно написать и проверить по схеме, но не запустить.

Синтаксис

apiVersion: v1
kind: Pod
metadata:
  name: web
spec:
  containers:
    - name: web
      image: nginx:1.27
      ports:
        - {name: http, containerPort: 80}
      resources:
        requests: {cpu: 100m, memory: 64Mi}   # гарантировано, по ним планируют
        limits: {memory: 128Mi}               # потолок
      readinessProbe:                         # готов ли принимать трафик
        httpGet: {path: /, port: http}
      livenessProbe:                          # жив ли; нет — перезапуск
        httpGet: {path: /, port: http}
        periodSeconds: 10
        failureThreshold: 3

Три вида проб

ПробаЧто будет при провалеКогда нужна
readinessProbeпод убирается из адресов Service — трафик на него не идёт; контейнер не перезапускаетсяпочти всегда: прогрев, временная перегрузка, потеря зависимости
livenessProbekubelet перезапускает контейнерприложение может зависнуть так, что само не упадёт
startupProbeпока не прошла, остальные пробы не запускаются; провал за отведённое время — перезапускдолгий старт: даёт время, не ослабляя liveness

Без проб контейнер считается живым, пока процесс не завершился, и готовым — сразу после старта.

Способ проверки

Ровно один на пробу:

ПолеУспех, если…Параметры
httpGetкод ответа от 200 до 399path (по умолчанию /), port (число или имя порта), scheme: HTTP|HTTPS, host, httpHeaders: [{name, value}]
tcpSocketTCP-соединение установилосьport
execкоманда завершилась с кодом 0command: [список] — без оболочки; нужна оболочка — ["sh", "-c", "…"]
grpcgRPC health check вернул SERVINGport, service

Тайминги проб

ПолеЧто значитПо умолчанию
initialDelaySecondsпауза после старта контейнера до первой проверки0
periodSecondsинтервал между проверками10
timeoutSecondsсколько ждать ответа1
failureThresholdсколько провалов подряд считать провалом пробы3
successThresholdсколько успехов подряд нужно после провала1; у liveness и startup — только 1
terminationGracePeriodSecondsсвоё время на остановку при провале liveness/startupиз пода

Время до реакции ≈ failureThreshold × periodSeconds: при значениях по умолчанию liveness перезапустит зависший контейнер примерно через 30 секунд. Startup-проба с failureThreshold: 30 и periodSeconds: 10 даёт приложению до 300 секунд на запуск.

Ресурсы: requests и limits

Поле resources у каждого контейнера:

ПолеЧто значитЧто будет при превышении
requests.cpuдоля CPU, которую планировщик резервирует на узлеможно больше, если узел свободен
requests.memoryпамять, резервируемая на узлеможно больше, но под с превышением первым кандидат на вытеснение при нехватке памяти на узле
limits.cpuпотолок CPUпроцесс притормаживается (throttling), не убивается
limits.memoryпотолок памятиконтейнер убивается: статус OOMKilled, затем перезапуск

Под попадает только на узел, где сумма requests уже размещённых подов плюс его собственные помещается в доступные ресурсы. limits на планирование не влияют. Если задан только limits, requests берутся равными ему.

Единицы

ЗаписьЗначение
CPU 1одно ядро (vCPU)
CPU 500m = 0.5половина ядра; m — тысячные доли, минимум 1m
память 128Mi, 1Giдвоичные: Ki = 1024, Mi = 1024², Gi = 1024³
память 128M, 1Gдесятичные: k = 1000, M = 1000², G = 1000³ (128M ≈ 122 Mi)
память 128m0,128 байта — суффикс m здесь тоже «милли»; почти всегда опечатка

Классы QoS

Kubernetes сам присваивает поду класс (status.qosClass) — от него зависит, кого вытеснять первым при нехватке памяти на узле:

КлассУсловиеВытесняется
Guaranteedу каждого контейнера заданы requests и limits по CPU и памяти, и они равныпоследним
Burstableне Guaranteed, но хотя бы у одного контейнера есть requests или limitsпосле BestEffort
BestEffortни у одного контейнера нет ни requests, ни limitsпервым

HorizontalPodAutoscaler (autoscaling/v2)

HPA каждые ~15 секунд сравнивает метрику с целью и меняет replicas у Deployment или StatefulSet: желаемое число ≈ ceil(текущие_реплики × текущее_значение / целевое). Для метрик CPU и памяти в кластере нужен metrics-server.

ПолеЧто значитПо умолчанию / заметки
spec.scaleTargetRef{apiVersion: apps/v1, kind: Deployment, name: web} — что масштабироватьобязательно
spec.minReplicasнижняя граница1
spec.maxReplicasверхняя границаобязательно
spec.metrics[]по каким метрикам считать; несколько — берётся максимум из желаемых чиселбез метрик — CPU 80%
metrics[].type: Resourceresource: {name: cpu|memory, target: …}
target: {type: Utilization, averageUtilization: 70}средняя загрузка в процентах от requestsнужен requests у контейнеров
target: {type: AverageValue, averageValue: 500Mi}среднее абсолютное значение на под
metrics[].type: Pods / Object / Externalпользовательские и внешние метрики (через адаптер метрик)
spec.behavior.scaleUp / scaleDownstabilizationWindowSeconds, policies: [{type: Pods|Percent, value, periodSeconds}], selectPolicy: Max|Min|Disabledокно стабилизации: вниз — 300 с, вверх — 0

Примеры

# requests и limits в готовом манифесте — без кластера
kubectl create deployment web --image=nginx:1.27 --port=80 --dry-run=client -o yaml > deploy.yaml
kubectl set resources -f deploy.yaml --requests=cpu=100m,memory=128Mi \
  --limits=cpu=500m,memory=256Mi --local -o yaml > deploy-res.yaml

# пробы: готового генератора нет, добавляем через yq
yq -i '.spec.template.spec.containers[0].readinessProbe = {"httpGet": {"path": "/", "port": 80}, "periodSeconds": 5}' deploy-res.yaml
yq -i '.spec.template.spec.containers[0].livenessProbe = {"tcpSocket": {"port": 80}, "initialDelaySeconds": 10}' deploy-res.yaml
yq '.spec.template.spec.containers[0] | {"resources": .resources, "probes": [.readinessProbe, .livenessProbe]}' deploy-res.yaml

# HPA: kubectl autoscale требует кластер, поэтому пишем манифест сами
cat > hpa.yaml <<'EOF'
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: web
spec:
  scaleTargetRef: {apiVersion: apps/v1, kind: Deployment, name: web}
  minReplicas: 2
  maxReplicas: 10
  metrics:
    - type: Resource
      resource:
        name: cpu
        target: {type: Utilization, averageUtilization: 70}
EOF

# раз репликами управляет HPA, replicas из Deployment убираем
yq -i 'del(.spec.replicas)' deploy-res.yaml

kubeconform -strict -summary deploy-res.yaml hpa.yaml

# в кластере (здесь не сработают):
# kubectl get hpa web --watch
# kubectl top pods
# kubectl describe pod web-…        # события Unhealthy, Liveness probe failed
# kubectl get pod web-… -o jsonpath='{.status.qosClass}'

Медленный старт: startup-проба даёт до 5 минут, потом работают liveness и readiness; exec-проба для фоновой задачи без HTTP:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: api
spec:
  replicas: 2
  selector:
    matchLabels: {app: api}
  template:
    metadata:
      labels: {app: api}
    spec:
      containers:
        - name: api
          image: registry.example.com/api:3.1
          ports:
            - {name: http, containerPort: 8080}
          resources:                    # requests = limits → класс Guaranteed
            requests: {cpu: 250m, memory: 256Mi}
            limits: {cpu: 250m, memory: 256Mi}
          startupProbe:
            httpGet: {path: /healthz, port: http}
            periodSeconds: 10
            failureThreshold: 30        # 30 × 10 с = до 5 минут на старт
          livenessProbe:
            httpGet: {path: /healthz, port: http}
            periodSeconds: 10
            timeoutSeconds: 2
          readinessProbe:
            httpGet: {path: /ready, port: http}
            periodSeconds: 5
            failureThreshold: 2
        - name: worker
          image: registry.example.com/worker:3.1
          resources:
            requests: {cpu: 100m, memory: 128Mi}
            limits: {cpu: 100m, memory: 128Mi}
          livenessProbe:
            exec:
              command: ["sh", "-c", "test $(( $(date +%s) - $(stat -c %Y /tmp/heartbeat) )) -lt 60"]
            periodSeconds: 30

HPA по CPU и памяти с плавным уменьшением:

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: api
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: api
  minReplicas: 2
  maxReplicas: 20
  metrics:
    - type: Resource
      resource:
        name: cpu
        target: {type: Utilization, averageUtilization: 60}
    - type: Resource
      resource:
        name: memory
        target: {type: AverageValue, averageValue: 200Mi}
  behavior:
    scaleDown:
      stabilizationWindowSeconds: 600
      policies:
        - {type: Pods, value: 1, periodSeconds: 60}   # убирать не больше 1 пода в минуту

Подводные камни

  • Liveness проверяет зависимости. Если liveness-проба ходит в базу, то при падении базы kubelet начнёт перезапускать все поды приложения — это не поможет и только добавит нагрузки. Liveness — «жив ли сам процесс», зависимости — в readiness.
  • Одинаковые liveness и readiness без startup у медленно стартующего приложения: liveness убивает контейнер раньше, чем он успевает подняться, — бесконечный CrashLoopBackOff. Добавь startupProbe или initialDelaySeconds.
  • timeoutSeconds: 1 по умолчанию. Под нагрузкой ответ дольше секунды считается провалом, и liveness начинает перезапускать живые, но занятые поды.
  • exec-проба без оболочки. command: ["test -f /tmp/ok"] ищет программу с именем test -f /tmp/ok. Аргументы — отдельными элементами, а $(…), &&, | — только через ["sh", "-c", "…"].
  • Порт пробы — порт контейнера, а не Service. Имя порта (port: http) работает, только если в ports контейнера есть name: http.
  • successThreshold больше 1 у liveness и startup API отвергнет, а kubeconform пропустит.
  • 128m памяти — это 0,128 байта. Схема не ловит и такое: формат quantity она не разбирает. Память пиши с Mi/Gi.
  • M и Mi — разные единицы: 512M — это примерно 488Mi. В пределах одного проекта держись одной системы.
  • Лимит памяти — жёсткий. При превышении контейнер убивается с OOMKilled (код 137), а не «притормаживается», как с CPU.
  • Низкий limits.cpu вызывает тормоза без ошибок: троттлинг не виден в логах и статусе, только в метриках. Многие команды лимит CPU не ставят вовсе, оставляя только requests.
  • HPA по Utilization без requests не работает: процент считать не от чего, в событиях HPA будет missing request for cpu.
  • replicas в манифесте Deployment при работающем HPA каждый kubectl apply возвращает число реплик к написанному. Убери поле из манифеста.
  • HPA не реагирует мгновенно: уменьшение ждёт окно стабилизации (300 с по умолчанию), а изменения в пределах ~10% от цели игнорируются.
  • Без metrics-server HPA показывает цель как <unknown> и ничего не масштабирует.

См. также

kubectl · kubeconform · yq · k8s-workloads · k8s-networking · k8s-config · curl · top