Canary deployments с Java: как выпускать новые версии без риска для продакшена
Представьте: вы — капитан подводной лодки. Вам нужно проверить новый гидролокатор, но вы не можете просто взять и включить его на полную мощность — вдруг он ослепит всю команду или, хуже того, выдаст ложный сигнал тревоги. Что вы делаете? Выпускаете канарейку (птичку, которая в старые времены предупреждала шахтеров об утечке газа). Если птичка жива — с системой всё в порядке.
В мире Java-разработки этот принцип называется canary deployments (канареечное развертывание). Это стратегия, при которой новая версия приложения сначала отправляется небольшому проценту пользователей (обычно 1–5%). Если всё хорошо — трафик постепенно увеличивается. Если нет — откатываемся без катастрофы.
В этой статье мы разберем, как реализовать canary deployments в Java на реальных примерах: от простых конфигураций до продвинутых техник с Kubernetes и Spring Cloud. Поехали!
Почему canary, а не blue-green или rolling update?
Давайте быстро сравним три популярные стратегии деплоя:
- Blue-Green: у вас два идентичных окружения (синее и зеленое). Вы переключаете весь трафик сразу. Быстро, но если баг вылезет — пострадают все.
- Rolling update: обновляете поды/серверы один за другим. Медленнее, но тоже массово.
- Canary: вы пускаете новую версию только на 2% пользователей. Если они не жалуются — расширяете до 10%, потом до 50%, потом до 100%.
Canary — это единственная стратегия, которая позволяет A/B тестировать новую версию на реальных пользователях без риска для всех. Особенно это критично для Java-микросервисов, где одна ошибка может вызвать каскадный сбой.
Как это работает в Java? Техническая архитектура
Представим типичную Java-систему: Spring Boot приложение, которое обслуживает REST API. Чтобы внедрить canary, нам нужно:
- Маршрутизация трафика: часть запросов направлять на новую версию (v2), остальные — на старую (v1).
- Метрики и мониторинг: собирать ошибки, задержки, количество запросов.
- Автоматический откат: если процент ошибок превышает порог — откатываем canary.
- Постепенное увеличение: если всё ок — увеличиваем вес canary.
Самый простой способ сделать это — использовать Kubernetes с Service Mesh (например, Istio) или Spring Cloud Gateway с весами маршрутизации. Ниже — примеры для обоих подходов.
Пример 1: Canary с Spring Cloud Gateway (без Kubernetes)
Если у вас нет Kubernetes, но есть Spring Cloud, вы можете использовать Spring Cloud Gateway с конфигурацией весов. Вот как это выглядит:
// application.yml
spring:
cloud:
gateway:
routes:
- id: my-app-canary
uri: http://my-app-v2:8080
predicates:
- Weight=my-app, 10 # 10% трафика
filters:
- SetPath=/api/v2/**
- id: my-app-stable
uri: http://my-app-v1:8080
predicates:
- Weight=my-app, 90 # 90% трафика
filters:
- SetPath=/api/v1/**
Здесь мы используем предикат Weight — встроенную фичу Spring Cloud Gateway. Она случайным образом распределяет запросы в пропорции 10/90. Если v2 работает стабильно, меняем вес на 50/50, потом на 100/0.
Плюсы: просто, не требует внешних инструментов.
Минусы: нет автоматики — веса меняются вручную или через API.
Пример 2: Canary с Kubernetes и Istio (продвинутый)
Это золотой стандарт для Java-микросервисов. Используем Istio VirtualService для управления трафиком. Допустим, у нас есть два деплоймента: my-app-v1 и my-app-v2.
Шаг 1. Создаём VirtualService:
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: my-app
spec:
hosts:
- my-app
http:
- match:
- headers:
x-canary-user:
exact: "true" # Только для пользователей с этим заголовком
route:
- destination:
host: my-app-v2
port:
number: 8080
- route:
- destination:
host: my-app-v1
port:
number: 8080
weight: 90
- destination:
host: my-app-v2
port:
number: 8080
weight: 10
Здесь мы делаем две вещи одновременно:
- Если у запроса есть заголовок
x-canary-user: true— он всегда идет на v2 (это для внутреннего тестирования). - Остальной трафик распределяется 90/10 между v1 и v2.
Шаг 2. В Java-коде добавляем заголовок для тестовых пользователей:
// Пример: если пользователь в группе "beta", добавляем заголовок
@Component
public class CanaryFilter implements GlobalFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String userId = exchange.getRequest().getHeaders().getFirst("X-User-Id");
if (userId != null && isBetaUser(userId)) {
exchange.getRequest().mutate()
.header("x-canary-user", "true");
}
return chain.filter(exchange);
}
private boolean isBetaUser(String userId) {
// Тут может быть проверка через Redis или базу данных
return userId.hashCode() % 10 == 0; // 10% пользователей
}
}
Шаг 3. Мониторинг и автооткат:
В Istio вы можете настроить DestinationRule с трейсингом, а затем использовать Kiali или Grafana для просмотра метрик. Если процент 5xx ошибок превышает 1% — запускайте скрипт отката:
# bash-скрипт для автоматического отката
#!/bin/bash
ERROR_RATE=$(curl -s http://prometheus:9090/api/v1/query?query=... | jq '.data.result[0].value[1]')
if (( $(echo "$ERROR_RATE > 1.0" | bc -l) )); then
echo "Error rate $ERROR_RATE% — откатываем canary"
kubectl apply -f virtualservice-stable.yaml # возвращаем 100% на v1
else
echo "Canary стабилен, увеличиваем вес"
kubectl apply -f virtualservice-canary-20.yaml # увеличиваем до 20%
fi
Пример 3: Canary на уровне приложения (без инфраструктуры)