Максимально доступная работа с одной репликой. Запускайте одноэкземплярные (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, обновления пула узлов, ручной |
не ограничено, вытеснение (eviction) удерживается |
Принудительное |
возврат спот- и preemptible-инстансов, техническое обслуживание хоста |
фиксированный дедлайн, секунды или минуты |
Деплои |
rolling-обновление самой нагрузки |
не наша забота, Understudy отступает |
Результаты
Измерено на EKS с Karpenter на спот-узлах arm64, одна реплика за балансировщиком нагрузки приложения (application load balancer), без свободной ёмкости, так что каждая замена ждала нового EC2-инстанса. Зонд доступности работал примерно с частотой три запроса в секунду.
| Событие | Упавшие запросы |
|---|---|
Ничего не происходит (контроль) |
0 из 120 |
|
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.
Настройка
| Поле | По умолчанию | Что делает |
|---|---|---|
|
|
|
|
обязательное |
Нагрузка для защиты, в том же пространстве имён |
|
не задано |
Включает управление ёмкостью: желаемое количество реплик в устойчивом состоянии; изменения ждут во время передачи. Поддерживает ноль. Подробнее: docs/gitops-capacity.md[GitOps capacity] |
|
1 |
Сколько подменных подов добавлять |
|
60 |
Реалистичное время запуска этой нагрузки до начала обслуживания. Если дедлайн прерывания ближе этого значения, вытеснение сразу освобождается вместо удержания |
|
600 |
Как долго удерживать вытеснение без прогресса, прежде чем сдаться и пропустить его |
|
|
Какие нарушения стоит заставлять ждать. Подробнее ниже |
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 |
|
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