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 — трафик на него не идёт; контейнер не перезапускается | почти всегда: прогрев, временная перегрузка, потеря зависимости |
livenessProbe | kubelet перезапускает контейнер | приложение может зависнуть так, что само не упадёт |
startupProbe | пока не прошла, остальные пробы не запускаются; провал за отведённое время — перезапуск | долгий старт: даёт время, не ослабляя liveness |
Без проб контейнер считается живым, пока процесс не завершился, и готовым — сразу после старта.
Способ проверки
Ровно один на пробу:
| Поле | Успех, если… | Параметры |
|---|---|---|
httpGet | код ответа от 200 до 399 | path (по умолчанию /), port (число или имя порта), scheme: HTTP|HTTPS, host, httpHeaders: [{name, value}] |
tcpSocket | TCP-соединение установилось | port |
exec | команда завершилась с кодом 0 | command: [список] — без оболочки; нужна оболочка — ["sh", "-c", "…"] |
grpc | gRPC health check вернул SERVING | port, 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) |
память 128m | 0,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: Resource | resource: {name: cpu|memory, target: …} | |
target: {type: Utilization, averageUtilization: 70} | средняя загрузка в процентах от requests | нужен requests у контейнеров |
target: {type: AverageValue, averageValue: 500Mi} | среднее абсолютное значение на под | |
metrics[].type: Pods / Object / External | пользовательские и внешние метрики (через адаптер метрик) | |
spec.behavior.scaleUp / scaleDown | stabilizationWindowSeconds, 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