Blue/Green deployment в Flink K8s Operator
В уроке про upgrade modes мы видели, что любой upgrade (даже savepoint mode) подразумевает downtime: пока новый кластер поднимается из savepoint, старого уже нет, ничего не работает. Для большинства пайплайнов 30-60 секунд приемлемо, но для критичных бизнес-сценариев (биллинг, антифрод, real-time bidding) это слишком долго.
Решение из мира классических веб-сервисов — Blue/Green deployment. Поднимаем новую версию рядом со старой, прогреваем её, переключаем трафик одним щелчком. Flink Kubernetes Operator 1.14 поддерживает Blue/Green встроенно, и в этом уроке разберём, как именно это работает для стримового джоба со стейтом.
Зачем Blue/Green для Flink
Классический Blue/Green:
- Blue — текущая prod-версия. Принимает весь трафик.
- Green — новая версия. Поднимается рядом, прогревается.
- В нужный момент трафик переключается с Blue на Green одним движением (изменение selector в Service, например).
- Blue остаётся работать на случай отката.
Для stateful Flink-джоба это сложнее, чем для stateless HTTP-сервиса. Потому что:
- Оба кластера не могут одновременно потреблять из Kafka с одним и тем же consumer group ID — будет split brain (часть партиций уходит к Blue, часть к Green).
- Оба кластера не могут одновременно писать в sink (Kafka, Iceberg) с EXACTLY_ONCE — конфликт transaction id.
- Стейт обоих должен быть идентичен в момент переключения, иначе будут потери или дубли.
Flink Operator 1.14 решает это через хитрый flow с savepoint и атомарным cutover.
Blue/Green в Operator 1.14 — это spec.job.deploymentMode: blue-green (флаг feature gate). До этого был только rolling-replace (которому посвящён прошлый урок про upgrade modes). Blue/Green снижает downtime до ~2-5 секунд.
Как работает Blue/Green в операторе
Идея: оператор поднимает второй кластер (Green) с новым образом, восстанавливает его из того же savepoint, что и Blue. Подгоняет к realtime через прогрев. Дальше — координированный cutover: Blue делает финальный savepoint, Green подцепляет его, начинает потреблять с тех же offset-ов. Между cutover-ами — секунды простоя на джоб, а не минуты.
Детали: как избежать конфликта sink-ов
Самая хитрая часть — что делать с sink-ами в момент, когда оба кластера работают параллельно. Несколько вариантов, как Operator 1.14 это решает.
Kafka sink с транзакциями
При EXACTLY_ONCE Kafka sink использует transactional producer с уникальным transactional.id per parallelism slot. Если Blue и Green будут писать в один топик с одинаковыми transactional.id — Kafka брокер обнаружит конфликт (fencing through producer epoch) и отклонит транзакции от одного из них.
Operator 1.14 присваивает Blue и Green разные transactional.id префиксы (например, flink-blue-orders- и flink-green-orders-). Оба могут писать параллельно, без конфликта. Когда Blue cancel-ится, его транзакции аккуратно завершены, Green продолжает свои.
Если ваш downstream consumer читает с isolation.level = read_committed, в период параллельной работы Blue и Green он увидит дублирование записей: и Blue, и Green обработают одни и те же входные события (Blue из старого offset-а, Green — из catch-up). Это не баг — это правда того, что было прочитано. После cutover Blue остановится, дубли прекратятся.
Если для вас дубли в краткий период критичны (например, биллинг) — Blue/Green вам не подходит. Используйте обычный savepoint upgrade с 60-секундным downtime.
Iceberg sink
Apache Iceberg использует optimistic concurrency control: commits are file-based, и при коллизии commits Iceberg выкидывает retry. Blue и Green могут параллельно писать файлы данных, но commits координируются через snapshot ID. В Operator 1.14 для Iceberg sink Green явно ждёт, пока Blue прекратит commits, прежде чем начать свои — это small atomic gap.
Internal/custom sink-и
Если ваш sink не идемпотентен и не транзакционен — Blue/Green не безопасен. Сначала сделайте sink либо idempotent, либо переключите на savepoint upgrade.
Манифест FlinkDeployment с Blue/Green
apiVersion: flink.apache.org/v1beta1
kind: FlinkDeployment
metadata:
name: orders-to-iceberg
namespace: flink-jobs
spec:
image: my-registry/orders-to-iceberg:v0.4.2
flinkVersion: v2_2
flinkConfiguration:
state.backend.type: rocksdb
state.checkpoints.dir: s3://flink-state/orders/checkpoints
state.savepoints.dir: s3://flink-state/orders/savepoints
high-availability.type: kubernetes
high-availability.storageDir: s3://flink-state/orders/ha
execution.checkpointing.interval: "30s"
execution.checkpointing.unaligned: "true"
jobManager:
replicas: 2
resource:
memory: "2048m"
cpu: 1
taskManager:
replicas: 4
resource:
memory: "4096m"
cpu: 2
job:
jarURI: local:///opt/flink/usrlib/orders-to-iceberg.jar
entryClass: com.acme.flink.OrdersToIceberg
parallelism: 8
upgradeMode: savepoint
# Blue/Green specific
deploymentMode: blue-green
blueGreen:
maxCatchUpTime: 5m
cutoverStrategy: atomic
Что важно:
deploymentMode: blue-greenвключает Blue/Green flow вместо обычного replace.blueGreen.maxCatchUpTime: 5m— максимум, который Green может догонять. Если за 5 минут не сравнялся с Blue по lag — оператор abort upgrade.blueGreen.cutoverStrategy: atomic— координированный final savepoint + handover. Альтернатива —progressive, где cutover делается партициям-по-партиции (это для оооочень больших джобов, обычно не нужно).
Также при Blue/Green оператор требует двойного запаса ресурсов в кластере: на момент catch-up в namespace должны быть слоты и для Blue, и для Green. Проверьте ResourceQuota.
Сам процесс cutover: что вы увидите
При kubectl apply -f с новым образом и deploymentMode: blue-green:
# Перед apply
kubectl -n flink-jobs get pods -l app=orders-to-iceberg
# orders-to-iceberg-jobmanager-blue-0 1/1 Running
# orders-to-iceberg-taskmanager-blue-0 1/1 Running
# orders-to-iceberg-taskmanager-blue-1 1/1 Running
# После apply
kubectl apply -f orders-to-iceberg.yaml
# Через 30 секунд (savepoint и pull нового образа)
kubectl -n flink-jobs get pods -l app=orders-to-iceberg
# orders-to-iceberg-jobmanager-blue-0 1/1 Running
# orders-to-iceberg-taskmanager-blue-0 1/1 Running
# orders-to-iceberg-taskmanager-blue-1 1/1 Running
# orders-to-iceberg-jobmanager-green-0 0/1 ContainerCreating
# orders-to-iceberg-taskmanager-green-0 0/1 Pending
# Через 2 минуты (catch-up)
kubectl get flinkdeployment orders-to-iceberg -o jsonpath='{.status.phase}'
# CATCHING_UP
# Через 5 минут (cutover завершён)
kubectl -n flink-jobs get pods -l app=orders-to-iceberg
# orders-to-iceberg-jobmanager-green-0 1/1 Running
# orders-to-iceberg-taskmanager-green-0 1/1 Running
# orders-to-iceberg-taskmanager-green-1 1/1 Running
# (Blue pods удалены)
Метки blue и green — это flink.apache.org/deployment-color label, которые оператор добавляет автоматически. По ним можно фильтровать в Prometheus, чтобы графики Blue и Green не сливались.
Откат при провале upgrade
Если Green не догоняет за maxCatchUpTime, или падает на старте — оператор сам отменяет upgrade. Blue остаётся работать, Green удаляется. Вы видите в events:
Type Reason Message
---- ------ -------
Warning UpgradeAborted Green failed to catch up within 5m (lag still 10000 records)
Normal RollbackToBlue Reverted to blue deployment v0.4.1
Если Green догнался и cutover успешно прошёл, а потом обнаружилась проблема в новом коде — стандартный откат:
spec:
image: my-registry/orders-to-iceberg:v0.4.1 # старый
job:
initialSavepointPath: s3://flink-state/orders/savepoints/savepoint-pre-upgrade-XXX
deploymentMode: blue-green
Оператор увидит изменение, запустит Blue/Green flow в обратную сторону — теперь “Green” будет старая версия, а текущая (новая) — “Blue”, и через 2-5 секунд активной станет старая.
Каждый Blue/Green upgrade оставляет в S3 несколько savepoint-ов: pre-upgrade, final-cutover. В CI/CD регистрируйте эти точки в external metadata store с тегом версии — для быстрого rollback не придётся лезть в kubectl get -o yaml.
Когда Blue/Green НЕ подходит
- Не идемпотентные sink-и: дубли в downstream неприемлемы — используйте обычный upgrade.
- Очень дорогой стейт: 100+ ГБ state с долгим savepoint и долгим catch-up — может быть дешевле жить с минутой downtime, чем держать удвоенные ресурсы.
- Limited cluster capacity: если в namespace нет слотов на удвоенный TM-пул — оператор не сможет deploy Green.
- Жёсткий single-writer requirement: некоторые системы (банковские) требуют гарантию одного writer-а — Blue/Green с временной двойной записью не подходит.
Ключевые выводы
-
Blue/Green deployment — паттерн для zero-downtime upgrade: новый кластер поднимается рядом, прогревается, потом атомарный cutover.
-
Operator 1.14 поддерживает встроенно через
spec.job.deploymentMode: blue-green. До этого был только rolling-replace. -
Flow в 6 шагов: apply -> savepoint Blue -> deploy Green -> catch-up -> atomic cutover -> cleanup Blue.
-
Конфликт sink-ов решается через разные transactional.id для Kafka, optimistic concurrency для Iceberg. Custom non-idempotent sink-и несовместимы.
-
Downtime ~2-5 секунд вместо 30-60 секунд при обычном upgrade. Цена — удвоенные ресурсы на период catch-up.
-
Откат через initialSavepointPath работает так же, как при обычном upgrade — Blue/Green просто запускается в обратную сторону.
-
Не для всех случаев: дубли downstream, очень большой стейт, ограниченные ресурсы кластера — лучше обычный savepoint upgrade.