Understudy: нулевой простой для singleton-подов в Kubernetes

Логотип Understudy
Надпись Understudy

Максимально доступная работа с одной репликой. Запускайте одноэкземплярные (singleton) нагрузки сквозь дрейны узлов, обновления, консолидацию и возврат спот-инстансов — не платя за вторую реплику круглосуточно.

Поддерживается командой mitis — Kubernetes и FinOps.

Что подразумевается под «максимальными усилиями»: никаких простоев из-за заранее объявленных сбоев, ограниченный простой, когда жёсткий дедлайн невозможно выдержать, и никакой защиты от необъявленного отказа оборудования. Последнее — это как раз то, для чего нужна вторая реплика, и ничто этого не изменит.

В театре дублёр ждёт за кулисами и выходит на сцену, когда исполнитель главной роли уходит. Этот оператор делает то же самое для Deployment или Argo Rollout с одной репликой: когда узел уходит, он поднимает подменный под (stand-in pod), ждёт, пока тот действительно начнёт обслуживать трафик, и только тогда отпускает оригинальный под. Второй под существует лишь те несколько минут, пока нарушение фактически происходит.

apiVersion: apps.understudy.sh/v1alpha1
kind: Understudy
metadata:
  name: tenant-worker
spec:
  targetRef:
    kind: Deployment
    name: tenant-worker
  holdMode: always
  minSurgeTimeSeconds: 60
  readinessDeadlineSeconds: 600

Зачем не просто запустить две реплики

Две реплики — это арендная плата каждый час каждого дня за событие, которое длится минуты в месяц. Кроме того, при каждом дрейне вдвое сокращается запас по ёмкости, а для нагрузок, которые по своей природе являются одноэкземплярными (контроллеры с выбором лидера, планировщики, потребители очередей с фиксированным назначением), вторая реплика — это холостые расходы, ничего не дающие.

Ставка Understudy уже: ваша нагрузка может терпеть два пода в течение нескольких минут. Это то же самое свойство, которое уже требует rolling-обновление с maxSurge: 1, — так что если сегодня вы деплоите без простоев, вы соответствуете требованиям. Второй под существует только в момент, когда происходит нарушение.

От чего он защищает

Тип нарушения Примеры Доступное время

Добровольное

дрейн узла, дрифт и консолидация и истечение срока в Karpenter, обновления пула узлов, ручной kubectl drain

не ограничено, вытеснение (eviction) удерживается

Принудительное

возврат спот- и preemptible-инстансов, техническое обслуживание хоста

фиксированный дедлайн, секунды или минуты

Деплои

rolling-обновление самой нагрузки

не наша забота, Understudy отступает

Результаты

Измерено на EKS с Karpenter на спот-узлах arm64, одна реплика за балансировщиком нагрузки приложения (application load balancer), без свободной ёмкости, так что каждая замена ждала нового EC2-инстанса. Зонд доступности работал примерно с частотой три запроса в секунду.

Событие Упавшие запросы

Ничего не происходит (контроль)

0 из 120

kubectl drain

1 из 350

Нарушение Karpenter

1 из 189

Реальное прерывание спот-инстанса AWS

1 из 764

Обычное rolling-обновление без Understudy (контроль)

2 из 38

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

Требования

Understudy работает на уровне admission-контроля, поэтому API-сервер должен иметь доступ к сервису вебхука на порту 443. Это стандарт почти везде, а admission API, которое он использует, стабильно начиная с Kubernetes 1.16, однако встречаются исключения: приватная плоскость управления (control plane) с жёстко ограниченными группами безопасности или сетевая политика, не пропускающая трафик плоскости управления, заблокируют этот путь. Чарт требует Kubernetes 1.34 или новее.

Вебхук зарегистрирован с failurePolicy: Ignore намеренно, потому что вебхук, который недоступен, не должен блокировать все вытеснения в кластере. Это следствие важно сформулировать прямо: недоступный вебхук тихо деградирует до отсутствия защиты, а не до ошибки. Проверьте это один раз после установки, вытеснив защищённый под и ожидая отказа:

kubectl create --raw "/api/v1/namespaces/NS/pods/POD/eviction" -f - <<'EOF'
{"apiVersion": "policy/v1", "kind": "Eviction",
 "metadata": {"name": "POD", "namespace": "NS"}}
EOF

Рабочая установка отвечает TooManyRequests и запускает surge. Тишина означает, что API-сервер так и не достучался до вебхука.

Установка

helm install understudy oci://ghcr.io/kylan11/charts/understudy \
  --namespace understudy-system --create-namespace

Или из локальной копии репозитория:

helm install understudy ./charts/understudy \
  --namespace understudy-system --create-namespace

Обновляетесь с более ранней версии? Смотрите UPGRADING.md[UPGRADING.md].

Затем защитите нагрузку, создав рядом с ней ресурс Understudy. Команда kubectl get understudies покажет цель, режим, фазу и то, как долго что-либо было заблокировано.

Стандартная установка занимает один Deployment с одной репликой. Вебхук обслуживается из того же пода. Нет ни DaemonSet, ни агентов на каждом узле. Дрейны, нарушения Karpenter, уменьшение масштаба autoscaler и обновления узлов — всё это обнаруживается только через API-сервер подом оператора, и на кластерах с Karpenter это включает прерывания спот-инстансов, поскольку Karpenter реагирует на очередь прерываний, добавляя узлу taint, который оператор и видит.

На кластерах, где каждый узел несёт taint, оператору нужны соответствующий toleration и nodeSelector — так же, как и любому другому контроллеру, запускаемому там.

Облачные сентинели (опционально, по умолчанию отключены)

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

Сентинель добавляет именно этот дедлайн. Без него прерывание спот-инстанса доходит до оператора как обычный drainer-taint, неотличимый от неограниченного дрейна. Сентинель читает уведомление об остановке от облака на узле и передаёт, сколько времени реально осталось, — это и позволяет оператору выбирать между удержанием вытеснения и немедленным его освобождением. Он также сокращает время реакции примерно на 15 секунд по сравнению с путём через очередь.

Это DaemonSet только потому, что каждое облако отдаёт уведомления об остановке на link-local-эндпоинт метаданных, который ничто за пределами инстанса прочитать не может. Ограничьте его пулами узлов, которые могут быть вытеснены; запускать его где-либо ещё нет смысла.

helm upgrade understudy ./charts/understudy \
  --set sentinel.enabled=true \
  --set sentinel.cloud=aws \
  --set 'sentinel.nodeSelector.karpenter\.sh/nodepool=spot'

Зонд AWS проверен на реальных прерываниях спот-инстансов. Зонды GCP и Azure покрыты только юнит-тестами. Считайте их экспериментальными, пока кто-нибудь не проверит их на GKE и AKS.

Настройка

Поле По умолчанию Что делает

targetRef.kind

Deployment

Deployment (apps/v1) или Rollout (argoproj.io/v1alpha1)

targetRef.name

обязательное

Нагрузка для защиты, в том же пространстве имён

baselineReplicas

не задано

Включает управление ёмкостью: желаемое количество реплик в устойчивом состоянии; изменения ждут во время передачи. Поддерживает ноль. Подробнее: docs/gitops-capacity.md[GitOps capacity]

surgeReplicas

1

Сколько подменных подов добавлять

minSurgeTimeSeconds

60

Реалистичное время запуска этой нагрузки до начала обслуживания. Если дедлайн прерывания ближе этого значения, вытеснение сразу освобождается вместо удержания

readinessDeadlineSeconds

600

Как долго удерживать вытеснение без прогресса, прежде чем сдаться и пропустить его

holdMode

always

Какие нарушения стоит заставлять ждать. Подробнее ниже

holdMode определяет, удерживается ли вытеснение вообще. Surge это не затрагивает: подменный под поднимается при каждом сигнале об обречённости во всех трёх режимах, потому что поднять его заранее ничего не стоит, а проиграть гонку — дорого.

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

  • voluntary-only удерживает нарушение, которое может подождать, и никогда — то, которое не может. Узел при прерывании спот-инстанса уходит в фиксированный момент, что бы ни говорили бюджет или вебхук, поэтому вытеснение сразу разрешается, и под тратит последние секунды на чистое завершение, а не на отказ и последующее уничтожение. Используйте этот режим, если ваша нагрузка сбрасывает соединения по SIGTERM и вы предпочитаете, чтобы она начала немедленно.

  • off никогда не удерживает. Подменный под всё равно поднимается параллельно, так что нагрузка простаивает только пока идёт замена, а не пока проходит переназначение. Используйте там, где бин-пакинг важнее последней секунды uptime, или чтобы отключить удержание для одной нагрузки, не затрагивая остальной кластер.

Если вы работаете на спот-инстансах без сентинеля, установите readinessDeadlineSeconds меньше окна уведомления платформы, иначе оператор может удержать вытеснение против дедлайна, которого не видит.

Argo Rollouts

Защитите Argo Rollout, задав targetRef.kind: Rollout:

apiVersion: apps.understudy.sh/v1alpha1
kind: Understudy
metadata:
  name: tenant-worker
spec:
  targetRef:
    kind: Rollout
    name: tenant-worker
  holdMode: always

Поддерживаются стратегии canary и blue/green после полного продвижения (fully promoted). Understudy масштабирует spec.replicas Rollout; Argo Rollouts продолжает управлять своими ReplicaSet и маршрутизацией трафика. Только готовые поды из стабильной ревизии (или активной ревизии при blue/green) считаются подменными. Готовые preview-поды и старые ревизии не могут снять удержание вытеснения с производственного пода.

Во время обновления, паузы или отмены Understudy отступает и пропускает вытеснения. Если релиз начинается во время surge Understudy, он убирает surge и снимает удержание. При использовании baselineReplicas применяется последний запрошенный baseline; иначе восстанавливается исходное количество реплик. Прогрессивная доставка остаётся ответственностью Argo.

Argo Rollouts должен быть установлен заранее. Его API определяется при запуске оператора; перезапустите Understudy, если установите CRD Rollout позже. Кластеры без Argo Rollouts продолжают поддерживать Deployments. Как и с Deployments, другой reconciler (например, HPA или инструмент GitOps) не должен перезаписывать временное увеличение реплик.

GitOps capacity

Argo CD синхронизирует Kubernetes с Git; Argo Rollouts управляет выпусками приложений. Это разные задачи. Для Deployment или Rollout, управляемых Argo CD, задайте spec.baselineReplicas в ресурсе Understudy и делегируйте эффективное spec.replicas нагрузки Understudy.

Изменения ёмкости в Git тогда сходятся автоматически. Во время передачи при дрейне Understudy сохраняет захваченный baseline плюс surge и применяет последний запрошенный baseline после завершения. BaselineApplied=False сообщает об ожидающем изменении. Смотрите docs/gitops-capacity.md[полную конфигурацию владения и жизненный цикл].

Как это работает

Кратко: отвечать на вытеснение у входа кодом HTTP 429, поднимать surge, когда узел объявляется обречённым, разрешать повторную попытку, как только замена проходит свои readiness-гейты, и масштабировать обратно, удаляя нужный под. PodDisruptionBudget не создаётся. Полное объяснение с диаграммами и обоснованием каждого решения — в docs/how-it-works.md[docs/how-it-works.md].

Сигналы поступают в оператор через четыре адаптера; ядро не содержит кода, специфичного для провайдера:

Адаптер Что видит

Cordon watch

kubectl drain, обновления GKE, AKS и EKS, cluster-autoscaler, kured

Taint watch

Karpenter, cluster-autoscaler, а также любой ключ, который вы настроите

Eviction webhook

каждую попытку вытеснения от любого drainer, включая те, которые ничто другое не обнаруживает

Cloud sentinel (опционально, по умолчанию отключён)

уведомления о прерывании спот- и preemptible-инстансов вместе с их дедлайнами

Безопасность

Настройте алерт на understudy_oldest_blocked_eviction_seconds. Он нужен для того, чтобы удержанное вытеснение всегда было видимым и ограниченным.

Если surge не может продвинуться вперёд, удержание снимается, а не стопорит дрейн навсегда. Вебхук работает в режиме fail-open: если все реплики оператора недоступны, Kubernetes разрешает вытеснения в обычном режиме по истечении таймаута вебхука. Чтобы немедленно отключить удержания:

kubectl delete validatingwebhookconfiguration understudy-eviction-hold

Оператор отказывается управлять собой, пропускает цели, которым не может помочь, и отступает, пока идёт rollout.

Для продакшена используйте replicaCount: 2 и размещайте реплики на разных узлах. Каждая реплика следит за сигналами узлов и обслуживает вебхук, даже несмотря на то что reconciliation защищён лидер-локом. Если узел, на котором находится единственный под оператора, дренируется, его собственное вытеснение разрешается и защита подов, стоящих дальше в очереди этого дрейна, может исчезнуть.

Ограничения

Understudy реагирует на нарушения; он никогда их не инициирует. Поскольку никакого бюджета для предварительной проверки нарушителем нет, консолидация, дрифт и истечение срока могут выбрать узел как обычно. Нарушитель начинает работу, повторяет попытки admission-удержания, пока поднимается подменный под, и продолжает, как только тот готов.

Запросы, находящиеся в полёте, не сохраняются. Understudy гарантирует готовую замену, но не запросы, уже направленные к уходящему поду. Добавьте поду preStop-хук, который длится дольше, чем задержка дерегистрации балансировщика нагрузки; ни один механизм surge сделать это за вас не может.

Принудительный дедлайн — это гонка с платформой. AWS даёт около двух минут, и быстро стартующая нагрузка может её выиграть, даже если сначала нужно подготовить новый узел. GCP и Azure дают около тридцати секунд — выиграть с холодного старта невозможно, поэтому там ценность — это упорядоченное освобождение и ранняя замена, а не нулевой простой.

Нагрузки, которые не могут работать в двух экземплярах одновременно, выходят за рамки области применения — включая StatefulSet с одной репликой, — и это никогда не изменится.

Разработка

make test    # envtest suite, including the upstream Argo Rollout CRD
make lint
make build

Лицензия

Apache 2.0

© 2026 meganuke