Canary деплой
Доля трафика через соотношение реплик: как это устроено на голом Service, где точность врёт и когда пора брать service mesh.
Что такое Canary
Canary - деплой на растущей доле трафика. Новую версию раскатывают сначала на малый процент (5-10%), смотрят метрики, ошибки, задержку p99 (время ответа, в которое укладываются 99% запросов). Если всё хорошо, долю увеличивают до 25%, 50%, 100%. Если что-то сломалось, откат быстрый: пострадала только канареечная доля пользователей.
Название пришло из шахт. Шахтёры брали клетку с канарейкой. Птица переставала петь при утечке газа, и это был сигнал уходить. В деплое тот же смысл: малая часть нагрузки ловит проблему раньше остальных.
Без service mesh canary делают через соотношение реплик. Service mesh - отдельный слой управления трафиком поверх кластера. Один Service цепляет все Pod приложения, и stable, и canary. Service - объект, который раздаёт трафик на набор Pod по меткам. Pod - наименьшая единица запуска, внутри неё работает контейнер.
kube-proxy (компонент на каждой ноде, который раздаёт трафик Service между Pod) раскидывает запросы примерно поровну между конечными точками. endpoints - список IP-адресов Pod, на которые Service шлёт трафик. Чтобы дать canary около 40%, держат 2 канареечных Pod на 3 stable.
Service app selector: app=app
stable ●●● 3 из 5 ~60%
canary ●● 2 из 5 ~40%
Минимальная конструкция
Нужны два Deployment и один Service. Deployment - объект, который держит нужное число одинаковых Pod.
Stable:
apiVersion: apps/v1
kind: Deployment
metadata:
name: app-stable
spec:
replicas: 3
selector:
matchLabels: {app: app, version: stable}
template:
metadata:
labels: {app: app, version: stable}
spec:
containers:
- name: app
image: nginxinc/nginx-unprivileged:1.25
ports:
- containerPort: 8080
Canary, одна реплика с новой версией:
metadata:
name: app-canary
spec:
replicas: 1
selector:
matchLabels: {app: app, version: canary}
template:
metadata:
labels: {app: app, version: canary}
spec:
containers:
- name: app
image: nginxinc/nginx-unprivileged:1.25
ports:
- containerPort: 8080
Service цепляет оба Deployment по общей метке:
apiVersion: v1
kind: Service
metadata:
name: app
spec:
selector:
app: app # без version, поэтому ловит обе версии
ports:
- port: 80
targetPort: 8080
Как сдвигают пропорцию
Команда kubectl scale меняет число реплик:
kubectl scale deploy app-canary --replicas=2 # 2 из 5, ~40%
kubectl scale deploy app-stable --replicas=3
Доля чистая и метрики в норме - canary увеличивают до полного размера, а stable обнуляют. После этого canary становится новым stable.
Незакрытый canary живёт вечно. Через месяц один Pod со старым образом всё ещё ловит свою долю запросов, и про него уже никто не помнит.
Гранулярность и точность
Доля трафика равна доле Pod. Чтобы дать 1%, нужен 1 Pod на 99. Чтобы дать 0.1%, нужен 1 на 999. На маленьких установках шаг грубый: 1 к 4 - это сразу 20%.
Балансировка не идеальна. На малом трафике 90/10 легко уезжает в 85/15. Причина в том, как клиенты держат соединения. Чтобы статистика выровнялась, нужны тысячи запросов.
Есть и нюанс kube-proxy. В режиме iptables (правила в ядре Linux) он не
ходит по endpoints по кругу: каждое новое соединение попадает в правило
-m statistic --mode random и получает случайный endpoint с равной
вероятностью. Настоящий round-robin делает режим IPVS. На практике
разницы нет: весов всё равно нет, доли на длинной дистанции равные,
поэтому соотношение реплик и есть соотношение трафика.
Когда нужен service mesh
Без service mesh управление сводится к числу Pod. Нельзя направить
canary 10% пользователей по userID. Нельзя послать на canary только
запросы с заголовком x-canary: true. Такое умеют отдельные
инструменты:
- Istio через VirtualService с разбивкой трафика,
- Linkerd через Gateway API: с версии 2.13 сплит описывают весами в
HTTPRoute.backendRefs, старый TrafficSplit из SMI ушёл в архив, - Argo Rollouts со встроенной логикой canary,
- Flagger для автоматизации canary поверх Istio или Linkerd.
Раньше в этот список попадали canary-аннотации ingress-nginx
(nginx.ingress.kubernetes.io/canary). Проект сняли с поддержки в марте
2026, так что на новых кластерах этот путь закрыт.
Все они дороже в обслуживании, чем kubectl scale. Поэтому в
небольших проектах начинают с канарейки на уровне Pod.
Связанные команды
kubectl scale deploy app-canary --replicas=2
kubectl get pods -l app=app --show-labels
kubectl get endpoints app
kubectl rollout status deploy/app-canary
kubectl logs -l app=app,version=canary --tail=20
Объект v1 Endpoints deprecated с 1.33, и kubectl get endpoints
печатает про это предупреждение. Тот же список адресов через актуальный
объект:
kubectl get endpointslice -l kubernetes.io/service-name=app
Что часто ломается
- В selector Service есть
version. Тогда он поймает только stable или только canary. Селектор должен быть общий, поapp. - Pod canary не появляются в endpoints. Причины: Pod не Ready, метки не совпали с selector, красная readinessProbe.
- Метрики stable и canary не размечены. В Prometheus они смешаются.
В метки метрик добавляют
version.
Домашка
Теорию закрепляет задание на своём кластере: 2 шага с проверкой. Кластер поднимается по треку Kubernetes локально, задания лежат в репозитории k8s-practice.
./run.sh start k8s-canary # развернуть стартовое состояние
./run.sh verify k8s-canary # проверить текущий шаг
Коротко
- Доля трафика на canary равна доле его Pod среди всех Pod за Service.
- Selector Service берёт только общую метку
app, безversion. - Пропорцию двигают через
kubectl scale, отдельного объекта для этого нет. - Точность грубая: на малом трафике реальное распределение уезжает от расчётного.
- Разбивка по заголовкам, userID и весам требует Istio, Linkerd, Argo Rollouts или Flagger.
- Завершённый canary масштабируют до полного размера, а старый stable обнуляют.
Прошли материал до конца? Отметьте урок пройденным.
Прогресс сохранён в этом браузере
Почистишь историю или откроешь с телефона, и отметки пропадут. Оставь почту, они переедут в аккаунт. Пароль придумывать не нужно.
Готово. теперь в аккаунте: прогресс виден с любого устройства. Мой прогресс