{ "index": 129, "slug": "editorial-2024-06-practice-container-orchestration", "title": "Контейнерная нагрузка: как связать ресурсы, готовность и автомасштабирование", "excerpt": "Почему Pod может успешно разместиться, но не выдержать трафик: разбираем requests и limits, readiness и HPA на одном учебном примере.", "contentHtml": "

После выкладки Pod получает статус Running, но запросы к сервису ждут дольше обычного. Иногда HorizontalPodAutoscaler (HPA) увеличивает число реплик, а доступных backend не становится больше: новые Pod не проходят readiness. В другой версии проблемы endpoint отвечает успешно ещё до прогрева, и Service отправляет трафик в приложение, которое не готово к рабочей нагрузке. Команда видит зелёный rollout и ищет причину уже под трафиком.

\n

У этих симптомов общий источник: в манифесте смешивают четыре разные границы. requests нужны планировщику для размещения, limits ограничивают выполнение container, readiness управляет допуском к трафику, а HPA меняет число реплик по метрике. Ни одно поле не обещает пропускную способность сервиса само по себе. Поэтому разберём не «правильные числа», а последовательность, в которой каждое число связывается с наблюдаемым результатом.

\n

Сначала разделите симптомы

\n
Один симптом не заменяет проверку соседних границ
НаблюдениеЧто оно подтверждаетЧто проверить дальше
Pod остаётся PendingPod не назначен на Node; request может не помещаться в доступный ресурс.События Pod, requests всех containers, allocatable Node, taints и affinity.
Pod Running, но не ReadyПроцесс запущен, но probe не разрешает отправлять ему трафик.Смысл endpoint, результаты probe, время прогрева и состояние Service.
Pod Ready, latency растётТекущая probe пропускает трафик; она не доказывает запас capacity.CPU, memory, очередь, внешние вызовы и лимиты приложения.
HPA поднял replicas без эффектаРешение о масштабировании было принято, но новые реплики могут не стать Ready или метрика не описывает bottleneck.Метрику HPA, request-знаменатель, события, readiness и время появления backend.
Container перезапускается с OOMKilledПамять пересекла ограничение или приложение получило другую фатальную ошибку.Причину завершения, working set, limit, рост cache и политику восстановления.
\n

Начинайте с колонки «наблюдение», а не с изменения манифеста. Running описывает состояние процесса, не готовность к запросам. Значение CPU в HPA не означает процент от Node или от limit: для ресурсной метрики utilization это отношение usage к request. Эти различия и есть рабочая карта диагностики.

\n

Четыре границы одного Pod

\n

Request — заявка на ресурс. Scheduler учитывает requests контейнеров, когда выбирает Node; для обычного Pod суммарная заявка контейнеров определяет, сколько ресурса нужно зарезервировать при размещении. Request не равен фактическому usage: container может потреблять больше заявки, если это разрешают limit и свободный ресурс. Без request нельзя осмысленно интерпретировать CPU utilization HPA для такого контейнера.

\n

Limit — ограничение выполнения контейнера. Для CPU превышение limit может привести к throttling, а для memory превышение может завершить процесс с OOMKilled. Limit не является обещанием throughput и не заменяет нагрузочное измерение. Если request или limit добавляет admission-политика namespace, смотрите итоговый объект в кластере, а не только исходный файл.

\n

Readiness probe отвечает на узкий вопрос: можно ли сейчас отправить запрос этому контейнеру. При неуспешной readiness Kubernetes перестаёт считать Pod готовым endpoint для Service. Probe не измеряет запас capacity и не должна бездумно превращаться в проверку всех внешних зависимостей. Иначе краткий сбой партнёра способен убрать из трафика все реплики. Слишком ранний успешный ответ создаёт обратную проблему: приложение принимает запросы до открытия пулов, миграций или cache.

\n

HPA периодически вычисляет желаемое число реплик по метрике. Для CPU с averageUtilization: 70 контроллер сравнивает среднее потребление с CPU request, а не с limit. Pod без нужного request не даёт корректного CPU utilization; Pod, который ещё не готов или не имеет метрики, может учитываться консервативно в расчёте. Поэтому HPA нельзя читать без describe, состояния реплик и понимания того, откуда пришла метрика.

\n

Учебный манифест с явной гипотезой

\n

Ниже — минимальный serving-workload для HTTP-приложения. Предположим, что после прогрева CPU — главный ограничитель, endpoint /startup появляется один раз, /ready проверяет локальную готовность пулов, а /live отвечает только при неисправимом состоянии процесса. Значения 500m, 384Mi, две реплики и target 70% — не рекомендация и не результат измерения. Это числа для воспроизводимого учебного прогона.

\n
apiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: app\nspec:\n  replicas: 2\n  selector:\n    matchLabels:\n      app: app\n  template:\n    metadata:\n      labels:\n        app: app\n    spec:\n      containers:\n        - name: app\n          image: registry.example/app:1.4.0\n          ports:\n            - name: http\n              containerPort: 8080\n          resources:\n            requests:\n              cpu: \"500m\"\n              memory: \"384Mi\"\n            limits:\n              cpu: \"1000m\"\n              memory: \"768Mi\"\n          startupProbe:\n            httpGet:\n              path: /startup\n              port: http\n            periodSeconds: 2\n            failureThreshold: 30\n          readinessProbe:\n            httpGet:\n              path: /ready\n              port: http\n            periodSeconds: 5\n            timeoutSeconds: 2\n          livenessProbe:\n            httpGet:\n              path: /live\n              port: http\n            periodSeconds: 10\n            timeoutSeconds: 2\n---\napiVersion: v1\nkind: Service\nmetadata:\n  name: app\nspec:\n  selector:\n    app: app\n  ports:\n    - name: http\n      port: 80\n      targetPort: http\n---\napiVersion: autoscaling/v2\nkind: HorizontalPodAutoscaler\nmetadata:\n  name: app\nspec:\n  scaleTargetRef:\n    apiVersion: apps/v1\n    kind: Deployment\n    name: app\n  minReplicas: 2\n  maxReplicas: 6\n  metrics:\n    - type: Resource\n      resource:\n        name: cpu\n        target:\n          type: Utilization\n          averageUtilization: 70
\n

startupProbe отделяет медленный запуск от последующих проверок: пока она не завершилась успешно, liveness и readiness не начинают обычную работу. Это защищает процесс от преждевременного перезапуска, но не ускоряет прогрев. После прогрева readiness должна означать «этот Pod может принять обычный запрос», а не «процесс слушает порт». У HPA есть верхняя граница шесть реплик, но она не гарантирует, что шесть Pod поместятся в кластер или выдержат запросы.

\n

Иллюстрация пути запроса

\n
\"Путь
Учебная схема разделяет размещение Pod, допуск к Service и масштабирование по метрике. Она показывает порядок вопросов, а не состояние конкретного кластера.
\n

Читать схему нужно слева направо. Сначала scheduler решает, где Pod может быть размещён по requests. Затем kubelet запускает контейнер и выполняет startup, readiness и liveness в своих ролях. Service направляет запросы только к готовым backend. Параллельно HPA получает свою метрику и меняет desired replicas. При таком порядке увеличение replicas не исправляет неверный readiness, а поднятие memory limit не исправляет очередь запросов.

\n

Воспроизводимый прогон

\n

Сохраните манифест в файл app.yaml в тестовом namespace и сначала проверьте его сервером. Эта команда обращается к API и может требовать прав; локальный кластер, версия Kubernetes, policy admission и наличие Metrics API должны быть известны заранее.

\n
kubectl config current-context\nkubectl auth can-i create deployment -n demo\nkubectl describe pod -n demo -l app=app --show-events\nkubectl apply --dry-run=server -n demo -f app.yaml\nkubectl diff -n demo -f app.yaml\nkubectl apply -n demo -f app.yaml\nkubectl rollout status deployment/app -n demo --timeout=120s\nkubectl get pods -n demo -l app=app -o wide\nkubectl get endpointslice -n demo -l kubernetes.io/service-name=app\nkubectl describe hpa app -n demo\nkubectl top pods -n demo -l app=app
\n

Последняя команда требует работающего Metrics API, часто его предоставляет Metrics Server. Если она возвращает ошибку, это не доказательство нулевой нагрузки и не повод подставить число вручную. Зафиксируйте ошибку как ограничение прогона. kubectl diff и --dry-run=server также не заменяют rollout: первая сравнивает объект, вторая проверяет запрос к API, а третья показывает, что Deployment действительно продвигается. Команда describe pod -l удобна как быстрый запрос, но при нескольких Pod для детального анализа выберите конкретное имя из get pods.

\n

Как читать результат и отрицательный путь

\n
  1. Проверьте контекст. Убедитесь, что команды обращаются к ожидаемому кластеру и namespace. Не применяйте учебный файл в production только потому, что текущий context называется коротко.
  2. Отделите размещение от запуска. Для Pending прочитайте kubectl describe pod и события. Сопоставьте requests с allocatable, а не с общей памятью Node. Если Pod размещён, переходите к probes.
  3. Проверьте время. Сравните длительность startup с failureThreshold × periodSeconds. Для этого примера окно startup — до 60 секунд при последовательных неуспехах, но реальное время зависит от результата probe и приложения.
  4. Сверьте Ready и endpoint. Сопоставьте поле Ready у Pod с EndpointSlice и фактическим ответом /ready. Если Pod Ready, а latency растёт, probe выполняет свой узкий контракт; ищите bottleneck в CPU, memory, очереди или внешнем вызове.
  5. Разберите HPA. В describe hpa найдите текущую и целевую метрики, desired replicas, события и ошибки. Сопоставьте их с request. Отсутствие данных Metrics API нельзя трактовать как отсутствие нагрузки.
  6. Проверьте отрицательный путь. Если новые Pod не становятся Ready, не увеличивайте maxReplicas. Сначала исправьте контракт готовности или startup, затем повторите тот же прогон. Если HPA масштабируется, но latency не улучшается, проверьте, является ли CPU главным ограничителем.
  7. Изменяйте одну границу. Зафиксируйте гипотезу, одно изменение, окно наблюдения, критерий остановки и rollback. Иначе после одновременного изменения limit, probe и HPA нельзя понять, что именно изменило результат.
\n

Какие числа можно считать доказанными

\n

В учебном файле доказан только синтаксический и объектный контракт, если его принял API. Значение 500m становится обоснованным request лишь после повторяемого измерения representative-нагрузки с согласованным latency budget и запасом на пики. Target 70% становится рабочей гипотезой только вместе с наблюдаемым временем масштабирования, размером очереди и готовностью новых реплик. Значение 768Mi нельзя оправдать одной строкой OOMKilled: нужно понять, растёт ли cache, есть ли утечка и сколько памяти нужно процессу при штатном пике.

\n

После каждого изменения сохраняйте минимум: версию образа, итоговый Deployment, временное окно, входную нагрузку, latency/error rate, состояние Pod и HPA, а также решение об откате. Это превращает настройку из перебора коэффициентов в проверяемый эксперимент. Если не хватает разрешённого наблюдения, корректное действие — остановить эксперимент, а не додумывать результат.

\n

Ограничения применимости

\n

Схема рассчитана на stateless HTTP-сервис, который можно безопасно масштабировать горизонтально. Она не решает координацию StatefulSet, порядок миграций базы, работу с локальным диском, очередями, GPU или внешним rate limit. HPA по CPU может быть плохим сигналом для memory-heavy приложения, batch-задачи или сервиса, где bottleneck — очередь и время ответа партнёра. Для таких случаев нужна другая метрика и отдельная проверка её источника.

\n

Точный результат зависит от версии Kubernetes, API и controller flags, Metrics API, admission defaults, политики namespace, сетевого маршрута и реализации приложения. Схема также не проверяет PDB, topology spread, node autoscaling и безопасность образа. Наличие статуса Available или зелёного rollout не является доказательством производительности. Граница вывода простая: эта статья даёт порядок проверки, а не готовый production-манифест.

\n

Проверяемые источники

\n", "readingMinutes": 9 }