Files

9 lines
22 KiB
JSON
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
"index": 129,
"slug": "editorial-2024-06-practice-container-orchestration",
"title": "Контейнерная нагрузка: как связать ресурсы, готовность и автомасштабирование",
"excerpt": "Почему Pod может успешно разместиться, но не выдержать трафик: разбираем requests и limits, readiness и HPA на одном учебном примере.",
"contentHtml": "<p>После выкладки Pod получает статус <code>Running</code>, но запросы к сервису ждут дольше обычного. Иногда HorizontalPodAutoscaler (HPA) увеличивает число реплик, а доступных backend не становится больше: новые Pod не проходят readiness. В другой версии проблемы endpoint отвечает успешно ещё до прогрева, и Service отправляет трафик в приложение, которое не готово к рабочей нагрузке. Команда видит зелёный rollout и ищет причину уже под трафиком.</p>\n<p>У этих симптомов общий источник: в манифесте смешивают четыре разные границы. <code>requests</code> нужны планировщику для размещения, <code>limits</code> ограничивают выполнение container, readiness управляет допуском к трафику, а HPA меняет число реплик по метрике. Ни одно поле не обещает пропускную способность сервиса само по себе. Поэтому разберём не «правильные числа», а последовательность, в которой каждое число связывается с наблюдаемым результатом.</p>\n<h2>Сначала разделите симптомы</h2>\n<div class=\"table-scroll\"><table><caption>Один симптом не заменяет проверку соседних границ</caption><thead><tr><th scope=\"col\">Наблюдение</th><th scope=\"col\">Что оно подтверждает</th><th scope=\"col\">Что проверить дальше</th></tr></thead><tbody><tr><td>Pod остаётся <code>Pending</code></td><td>Pod не назначен на Node; request может не помещаться в доступный ресурс.</td><td>События Pod, requests всех containers, allocatable Node, taints и affinity.</td></tr><tr><td>Pod <code>Running</code>, но не <code>Ready</code></td><td>Процесс запущен, но probe не разрешает отправлять ему трафик.</td><td>Смысл endpoint, результаты probe, время прогрева и состояние Service.</td></tr><tr><td>Pod <code>Ready</code>, latency растёт</td><td>Текущая probe пропускает трафик; она не доказывает запас capacity.</td><td>CPU, memory, очередь, внешние вызовы и лимиты приложения.</td></tr><tr><td>HPA поднял replicas без эффекта</td><td>Решение о масштабировании было принято, но новые реплики могут не стать Ready или метрика не описывает bottleneck.</td><td>Метрику HPA, request-знаменатель, события, readiness и время появления backend.</td></tr><tr><td>Container перезапускается с <code>OOMKilled</code></td><td>Память пересекла ограничение или приложение получило другую фатальную ошибку.</td><td>Причину завершения, working set, limit, рост cache и политику восстановления.</td></tr></tbody></table></div>\n<p>Начинайте с колонки «наблюдение», а не с изменения манифеста. <code>Running</code> описывает состояние процесса, не готовность к запросам. Значение CPU в HPA не означает процент от Node или от <code>limit</code>: для ресурсной метрики utilization это отношение usage к request. Эти различия и есть рабочая карта диагностики.</p>\n<h2>Четыре границы одного Pod</h2>\n<p><strong>Request</strong> — заявка на ресурс. Scheduler учитывает requests контейнеров, когда выбирает Node; для обычного Pod суммарная заявка контейнеров определяет, сколько ресурса нужно зарезервировать при размещении. Request не равен фактическому usage: container может потреблять больше заявки, если это разрешают limit и свободный ресурс. Без request нельзя осмысленно интерпретировать CPU utilization HPA для такого контейнера.</p>\n<p><strong>Limit</strong> — ограничение выполнения контейнера. Для CPU превышение limit может привести к throttling, а для memory превышение может завершить процесс с <code>OOMKilled</code>. Limit не является обещанием throughput и не заменяет нагрузочное измерение. Если request или limit добавляет admission-политика namespace, смотрите итоговый объект в кластере, а не только исходный файл.</p>\n<p><strong>Readiness probe</strong> отвечает на узкий вопрос: можно ли сейчас отправить запрос этому контейнеру. При неуспешной readiness Kubernetes перестаёт считать Pod готовым endpoint для Service. Probe не измеряет запас capacity и не должна бездумно превращаться в проверку всех внешних зависимостей. Иначе краткий сбой партнёра способен убрать из трафика все реплики. Слишком ранний успешный ответ создаёт обратную проблему: приложение принимает запросы до открытия пулов, миграций или cache.</p>\n<p><strong>HPA</strong> периодически вычисляет желаемое число реплик по метрике. Для CPU с <code>averageUtilization: 70</code> контроллер сравнивает среднее потребление с CPU request, а не с limit. Pod без нужного request не даёт корректного CPU utilization; Pod, который ещё не готов или не имеет метрики, может учитываться консервативно в расчёте. Поэтому HPA нельзя читать без <code>describe</code>, состояния реплик и понимания того, откуда пришла метрика.</p>\n<h2>Учебный манифест с явной гипотезой</h2>\n<p>Ниже — минимальный serving-workload для HTTP-приложения. Предположим, что после прогрева CPU — главный ограничитель, endpoint <code>/startup</code> появляется один раз, <code>/ready</code> проверяет локальную готовность пулов, а <code>/live</code> отвечает только при неисправимом состоянии процесса. Значения <code>500m</code>, <code>384Mi</code>, две реплики и target 70% — не рекомендация и не результат измерения. Это числа для воспроизводимого учебного прогона.</p>\n<pre><code>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</code></pre>\n<p><code>startupProbe</code> отделяет медленный запуск от последующих проверок: пока она не завершилась успешно, liveness и readiness не начинают обычную работу. Это защищает процесс от преждевременного перезапуска, но не ускоряет прогрев. После прогрева readiness должна означать «этот Pod может принять обычный запрос», а не «процесс слушает порт». У HPA есть верхняя граница шесть реплик, но она не гарантирует, что шесть Pod поместятся в кластер или выдержат запросы.</p>\n<h2>Иллюстрация пути запроса</h2>\n<figure><img src=\"/assets/editorial/2024/container-orchestration-2024-pod-lifecycle.svg\" alt=\"Путь от request и запуска контейнера к readiness, Service и решению HPA\" loading=\"lazy\" /><figcaption>Учебная схема разделяет размещение Pod, допуск к Service и масштабирование по метрике. Она показывает порядок вопросов, а не состояние конкретного кластера.</figcaption></figure>\n<p>Читать схему нужно слева направо. Сначала scheduler решает, где Pod может быть размещён по requests. Затем kubelet запускает контейнер и выполняет startup, readiness и liveness в своих ролях. Service направляет запросы только к готовым backend. Параллельно HPA получает свою метрику и меняет desired replicas. При таком порядке увеличение replicas не исправляет неверный readiness, а поднятие memory limit не исправляет очередь запросов.</p>\n<h2>Воспроизводимый прогон</h2>\n<p>Сохраните манифест в файл <code>app.yaml</code> в тестовом namespace и сначала проверьте его сервером. Эта команда обращается к API и может требовать прав; локальный кластер, версия Kubernetes, policy admission и наличие Metrics API должны быть известны заранее.</p>\n<pre><code>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</code></pre>\n<p>Последняя команда требует работающего Metrics API, часто его предоставляет Metrics Server. Если она возвращает ошибку, это не доказательство нулевой нагрузки и не повод подставить число вручную. Зафиксируйте ошибку как ограничение прогона. <code>kubectl diff</code> и <code>--dry-run=server</code> также не заменяют rollout: первая сравнивает объект, вторая проверяет запрос к API, а третья показывает, что Deployment действительно продвигается. Команда <code>describe pod -l</code> удобна как быстрый запрос, но при нескольких Pod для детального анализа выберите конкретное имя из <code>get pods</code>.</p>\n<h2>Как читать результат и отрицательный путь</h2>\n<ol><li><strong>Проверьте контекст.</strong> Убедитесь, что команды обращаются к ожидаемому кластеру и namespace. Не применяйте учебный файл в production только потому, что текущий context называется коротко.</li><li><strong>Отделите размещение от запуска.</strong> Для <code>Pending</code> прочитайте <code>kubectl describe pod</code> и события. Сопоставьте requests с allocatable, а не с общей памятью Node. Если Pod размещён, переходите к probes.</li><li><strong>Проверьте время.</strong> Сравните длительность startup с <code>failureThreshold × periodSeconds</code>. Для этого примера окно startup — до 60 секунд при последовательных неуспехах, но реальное время зависит от результата probe и приложения.</li><li><strong>Сверьте Ready и endpoint.</strong> Сопоставьте поле Ready у Pod с EndpointSlice и фактическим ответом <code>/ready</code>. Если Pod Ready, а latency растёт, probe выполняет свой узкий контракт; ищите bottleneck в CPU, memory, очереди или внешнем вызове.</li><li><strong>Разберите HPA.</strong> В <code>describe hpa</code> найдите текущую и целевую метрики, desired replicas, события и ошибки. Сопоставьте их с request. Отсутствие данных Metrics API нельзя трактовать как отсутствие нагрузки.</li><li><strong>Проверьте отрицательный путь.</strong> Если новые Pod не становятся Ready, не увеличивайте <code>maxReplicas</code>. Сначала исправьте контракт готовности или startup, затем повторите тот же прогон. Если HPA масштабируется, но latency не улучшается, проверьте, является ли CPU главным ограничителем.</li><li><strong>Изменяйте одну границу.</strong> Зафиксируйте гипотезу, одно изменение, окно наблюдения, критерий остановки и rollback. Иначе после одновременного изменения limit, probe и HPA нельзя понять, что именно изменило результат.</li></ol>\n<h2>Какие числа можно считать доказанными</h2>\n<p>В учебном файле доказан только синтаксический и объектный контракт, если его принял API. Значение <code>500m</code> становится обоснованным request лишь после повторяемого измерения representative-нагрузки с согласованным latency budget и запасом на пики. Target 70% становится рабочей гипотезой только вместе с наблюдаемым временем масштабирования, размером очереди и готовностью новых реплик. Значение <code>768Mi</code> нельзя оправдать одной строкой <code>OOMKilled</code>: нужно понять, растёт ли cache, есть ли утечка и сколько памяти нужно процессу при штатном пике.</p>\n<p>После каждого изменения сохраняйте минимум: версию образа, итоговый Deployment, временное окно, входную нагрузку, latency/error rate, состояние Pod и HPA, а также решение об откате. Это превращает настройку из перебора коэффициентов в проверяемый эксперимент. Если не хватает разрешённого наблюдения, корректное действие — остановить эксперимент, а не додумывать результат.</p>\n<h2>Ограничения применимости</h2>\n<p>Схема рассчитана на stateless HTTP-сервис, который можно безопасно масштабировать горизонтально. Она не решает координацию StatefulSet, порядок миграций базы, работу с локальным диском, очередями, GPU или внешним rate limit. HPA по CPU может быть плохим сигналом для memory-heavy приложения, batch-задачи или сервиса, где bottleneck — очередь и время ответа партнёра. Для таких случаев нужна другая метрика и отдельная проверка её источника.</p>\n<p>Точный результат зависит от версии Kubernetes, API и controller flags, Metrics API, admission defaults, политики namespace, сетевого маршрута и реализации приложения. Схема также не проверяет PDB, topology spread, node autoscaling и безопасность образа. Наличие статуса <code>Available</code> или зелёного rollout не является доказательством производительности. Граница вывода простая: эта статья даёт порядок проверки, а не готовый production-манифест.</p>\n<h2>Проверяемые источники</h2>\n<ul><li><a href=\"https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/\" target=\"_blank\" rel=\"noopener noreferrer\">Kubernetes: Resource Management for Pods and Containers</a> — назначение requests и limits, их связь с размещением и поведением контейнеров.</li><li><a href=\"https://kubernetes.io/docs/concepts/workloads/pods/probes/\" target=\"_blank\" rel=\"noopener noreferrer\">Kubernetes: Liveness, Readiness, and Startup Probes</a> — роли probe, порядок startup и влияние readiness на доступность Pod.</li><li><a href=\"https://kubernetes.io/docs/concepts/workloads/autoscaling/horizontal-pod-autoscale/\" target=\"_blank\" rel=\"noopener noreferrer\">Kubernetes: Horizontal Pod Autoscaling</a> — вычисление desired replicas, CPU utilization относительно request и обработка неполных метрик.</li><li><a href=\"https://kubernetes.io/docs/concepts/workloads/controllers/deployment/\" target=\"_blank\" rel=\"noopener noreferrer\">Kubernetes: Deployments</a> — rollout Deployment и связь желаемого состояния с ReplicaSet.</li><li><a href=\"https://kubernetes.io/docs/concepts/services-networking/endpoint-slices/\" target=\"_blank\" rel=\"noopener noreferrer\">Kubernetes: EndpointSlices</a> — представление backend Service и условий готовности endpoint.</li><li><a href=\"https://kubernetes.io/docs/reference/kubectl/generated/kubectl_top/\" target=\"_blank\" rel=\"noopener noreferrer\">Kubernetes: kubectl top</a> — назначение команды и требование Metrics API.</li></ul>",
"readingMinutes": 9
}