Всем привет, меня зовут Антон Малафеев, я руководитель группы инженеров IT-инфраструктуры в СберМаркете. Моя команда вот уже больше 3-х лет занимается разработкой деплоя для PaaS и сопутствующих инструментов.
В этой статье я расскажу об особенностях канареечного деплоя, о том, как этот процесс строится у нас в СберМаркете и какие технологии мы при этом используем. Материал будет актуален для тех, кто хочет узнать еще один способ деплоя через канарейку. Или для тех, кто только присматривается к такой возможности.
Но сначала немного цифр. Ниже на рисунке — наша статистика использования канареечного деплоя. На момент написания статьи у нас в проде 200+ сервисов, 5300 подов PaaS, ежемесячно мы делаем 2 тысячи деплоев в продакшн и 11 тысяч деплоев в стейдж, которые проходят через канарейку.
Возможности канареечного деплоя
У канареечного деплоя есть много всем известных плюсов и возможностей, наибольшую ценность для нас представляют:
Постепенное наращивание трафика на новую версию. У нас в данный момент есть несколько стандартных сценариев, отличающихся по времени.
Возможность задавать кастомные сценарии канарейки, если стандартных не достаточно.
Обнаружение аномалий на основе стандартных метрик, которые предоставляем из коробки, в виде обнаружения ошибок и перезапуска подов.
У разработчиков есть возможность задать свои метрики, если стандартных не достаточно.
Автооткат при отклонении этих метрик. В таком случае произойдет автоматический откат всех изменений до состояния «как было». В таком случае откат произойдет вместе со всеми вспомогательными инструментами (кодом разработчиков, настройками) и вернет сервис в изначальное состояние. Да, мы не откатываем миграции, но канарейка невозможна без совместимых миграций между версиями в принципе. Пример как это работает: если во время деплоя мы обнаружим повышение ошибок на любой из версий (новой или старой), мы все автоматически откатываем. И уже после, в спокойной обстановке, будем производить разбор ситуации.
Возможность обратиться по хедеру напрямую к новой версии. Все наши стандартные сценарии начинаются с нагрузки 1%, и мы можем обратиться сразу к новому экземпляру приложения по отдельному хедеру.
Возможность выкатить сервисы вообще без нагрузки. Обращение так же идет через отдельный хэдер.
Канарейка не только для внешнего трафика, но и для внутреннего. Многие реализации делают канареечный деплой только для входящего внешнего трафика. Наша реализация делает это и для внутреннего, включая http- и gRPC-взаимодействия.
Как мы перестроили наш пайплайн под канареечный деплой
Возьмем простейший пайплайн сборка-тесты-деплой. Когда разработчики его запускают, они рассчитывают, что у них все пройдет до конца, без их участия (конечно, если каждый шаг будет успешный).
Но, если добавить канарейку, да еще с выбором, то у нас появляется несколько вариантов. Это уже нельзя сделать автоматически, т.к. нужно принимать решение, какой именно вариант нужен.
В то же время разработчики, естественно, хотят, чтобы все было быстро, и желательно без их участия. И мы тоже хотим, чтобы разработчики не ждали прогона всех тестов, линтеров, сборок, прежде чем нажимать заветную кнопку деплоя.
Наш пайплайн отличается от общепринятых тем, то что кнопки вынесены налево — в самом заметном месте, где их не надо искать. Разработчик может выбрать нужный вариант сразу после старта пайплайна, без ожидания. Это потребовало от нас своей реализации needs внутри джоб с ожиданием нужных шагов. Деплой запустится автоматически после прохождения всех нужных сборок и тестов.
Таким образом, разработчику не надо сидеть и ждать завершения всех необходимых шагов. Также это очень положительно повлияло на пользовательский опыт — все действия легко находятся.
Какие технологии мы используем
Среди технологий, которые применяет наша команда: Gitlab, Kubernetes, Helm, Argo Rollouts, Istio, Istio Ingress (он же Ingress Gateway) и несколько самописных операторов, один из которых GW Operator (про него отдельно расскажу ниже).
С первыми тремя, думаю, все понятно. Что касается Argo Rollouts, то его мы выбрали из-за простоты интеграции с istio и из-за того ,что он отвечает всем нашим потребностям. Да, мы рассматривали и flagger, но это было на этапе зарождения PaaS, и никто уже не помнит, почему не выбрали именно его).
Istio Ingress
Istio Ingress — это по сути реализация Ingress вместо всем привычного NGINX Ingress. Его мы выбрали за полную интеграцию с istio (в отличии от nginx). С ним мы можем описывать правила для внутреннего и внешнего взаимодействия в одних и тех же манифестах, и под капотом он использует тот же envoy proxy с его преимуществами и недостатками.
Когда мы начали использовать Istio Ingress, мы почти сразу столкнулись с довольно большой проблемой: правила istio формируются и обрабатываются не привычным всем nginx-способом — в порядке наибольшего соответствия пути. Здесь просто берется первое совпадение.
К примеру, возьмем 2 совершенно разных сервиса и опишем для них VirtualService с именами virtsvc1 и virtsvc2 для одного и того же хоста.
В virtsvc1 мы открываем /api/, в virtsvc2 открываем /api/v2. В итоговом конфиге envoy они могут быть в любом порядке и мы получаем разное поведение в зависимости от их местоположения. Это обусловлено тем, что /api/ мог оказаться вверху правил и перехватывал вообще все запросы, в том числе /api/v2.
Поэтому мы решили сделать свой костыль оператор. Мы назвали его GW Operator, он написан на operatorframework.io и делает одну простую, но очень нужную нам вещь — объединяет все ручки и прописывает их в нужном порядке в едином манифесте. Для этого мы сделали свою CRD и назвали ее route. В каком-то виде оно заменило для нас virtualservice в части внешнего взаимодействия. После объединения всех путей мы получаем единый virtualservice, в котором будет правильный порядок всех этих правил.
apiVersion: api-gateway.sbermarket.ru/v1alpha1
kind: Route
...
spec:
gateway: istio-ingressgateway/api-gw
host: api.app.ru
rules:
- delegate:
name: app-http
namespace: app
match:
- uri:
prefix: /api/v2/
...
apiVersion: api-gateway.sbermarket.ru/v1alpha1
kind: Route
...
spec:
gateway: istio-ingressgateway/api-gw
host: api.app.ru
rules:
- delegate:
name: app-http
namespace: app2
match:
- uri:
prefix: /api/
...
Argo rollouts
Argo rollouts оперирует немного отличными сущностями, чем привычный всем deployment. Они называются Rollout — это, по сути, тот же самый deployment, но приправленный дополнительными опциями, которые позволяют делать канарейки. Если посмотреть на отличия, в самом начале мы видим блок canary.strategy.canary. В нем есть блок steps, который отвечает за шаги канареечного деплоя. В этом примере мы делаем сначала вес 1%, ждем 1 минуту... и уже после выкатываем 100%. При этом оператор сам управляет весами и репликами.
apiVersion: argoproj.io/v1alpha1
kind: Rollout
...
spec:
strategy:
canary:
analysis:
templates:
- templateName: success-rate-app
steps:
- setWeight: 1
- pause:
duration: 1m
- setWeight: 100
…
canary.analysis.templates указывает, какой AnalysysTemplate нам использовать при деплое. Это еще одна crd от argo rollouts, которая описывает проверки для успешного прохождения канарейки.
apiVersion: argoproj.io/v1alpha1
kind: AnalysisTemplate
...
spec:
metrics:
- failureLimit: 1
interval: 5m
name: success-rate-http
provider:
prometheus:
address: http://prometheushost:9090
query: [[метрика на расчет 500]]
successCondition: result[0] >= 0.95
- failureLimit: 1
interval: 1m
name: canary-crashloop
provider:
prometheus:
address: http://prometheushost:9090
query: [[метрика на падение пода]]
successCondition: result[0] < 3
Во время деплоя на основе AnalysisTemplate формируется AnalisysRun, в котором происходят проверки жизнеспособности и принимается решение о необходимости дальнейших шагов.
А в примере ниже — пример неудачной канарейки.
apiVersion: argoproj.io/v1alpha1
kind: AnalysisRun
...
status:
...
- count: 5
failed: 2
measurements:
...
- finishedAt: "2023-07-04T07:05:28Z"
phase: Successful
startedAt: "2023-07-04T07:05:28Z"
value: '[0]'
- finishedAt: "2023-07-04T07:06:28Z"
phase: Failed
startedAt: "2023-07-04T07:06:28Z"
value: '[3]'
- finishedAt: "2023-07-04T07:07:28Z"
phase: Failed
startedAt: "2023-07-04T07:07:28Z"
value: '[4]'
metadata:
ResolvedPrometheusQuery: |
sum(increase(kube_pod_container_status_restarts_total{namespace=~"app", pod=~"app-6fff94bbc6.*"}[3m]) >= 3) or vector(0)
name: canary-crashloop
phase: Failed
successful: 3
Второе отличие rollouts от deployments — это описание canary.trafficRouting.destinationRule. Это указатель на DestinationRule от Istio, который описывает, как нам найти приложение. Мы объявляем, какой у нас DestinationRule прошлой версии канарейки stable и новой canary.
apiVersion: argoproj.io/v1alpha1
kind: Rollout
...
spec:
…
strategy:
canary:
…
trafficRouting:
istio:
…
destinationRule:
canarySubsetName: app-canary
name: app-rollout
stableSubsetName: app-stable
…
И описываем в DestinationRule лейблы для поиска самого приложения по rollouts-pod template-hash, который проставляется самим Argo Rollout, когда канарейка деплоится.
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
...
spec:
host: app.namespace.svc.cluster.local
subsets:
- labels:
app.kubernetes.io/name: app
rollouts-pod-template-hash: 5c5657cf77
name: app-stable
- labels:
app.kubernetes.io/name: app
rollouts-pod-template-hash: 1b3195rd23
name: app-canary
...
Еще одно отличие rollouts от deployments — это описание VirtualServices, в котором как раз Argo Rollout будет управлять трафиком, проставляя веса.
apiVersion: argoproj.io/v1alpha1
kind: Rollout
...
spec:
…
strategy:
canary:
…
trafficRouting:
istio:
virtualServices:
- name: app-http
routes:
- app-http
…
В блоке ниже видно, что у нас описано два destination: в одном 100% трафика, в другом вес отсутствует — то есть 0%.
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
...
spec:
http:
- name: app-http
route:
- destination:
host: app.namespace.svc.cluster.local
port:
number: 8080
subset: app-stable
weight: 100
- destination:
host: app.namespace.svc.cluster.local
port:
number: 8080
subset: app-canary
timeout: 900s
В результате получается такая картина: деплоим Rollout, DestinationRule, AnalysisTemplate, VirtualService, Route через helm. После все это обрабатывается в GW Operator и Argo Rollout Operator, istio, и получается такая схема связей (картинка ниже)
А здесь можно увидеть процесс самого канареечного деплоя. Все выглядит как обычное обновление deployment: также появляются новые поды, выключаются старые, но все это приправлено правилами весов трафика в istio.
Заключение
Да, такая схема с канарейкой выглядит как не очень тривиальная, требует навыков работы с istio и самописными операторами. Но в итоге все это прекрасно работает с высоконагруженными сервисами, позволяя нам быстро вернуться в исходное состояние, даже когда что-то пошло не так. Мы быстро можем определить наличие проблемы, и уже после, в спокойной обстановке, проводить расследование причин такого поведения.
Буду рад, если эти идеи будут вам полезны. Если будут вопросы, с удовольствием отвечу на них в комментариях.
Tech-команда СберМаркета ведет соцсети с новостями и анонсами. Если хочешь узнать, что под капотом высоконагруженного e-commerce, следи за нами в Telegram и на YouTube. А также слушай подкаст «Для tech и этих» от наших it-менеджеров.