Всем привет, меня зовут Антон Малафеев, я руководитель группы инженеров IT-инфраструктуры в СберМаркете. Моя команда вот уже больше 3-х лет занимается разработкой деплоя для PaaS и сопутствующих инструментов.

В этой статье я расскажу об особенностях канареечного деплоя, о том, как этот процесс строится у нас в СберМаркете и какие технологии мы при этом используем. Материал будет актуален для тех, кто хочет узнать еще один способ деплоя через канарейку. Или для тех, кто только присматривается к такой возможности.

Но сначала немного цифр. Ниже на рисунке — наша статистика использования канареечного деплоя. На момент написания статьи у нас в проде 200+ сервисов, 5300 подов PaaS, ежемесячно мы делаем 2 тысячи деплоев в продакшн и 11 тысяч деплоев в стейдж, которые проходят через канарейку.

Возможности канареечного деплоя

У канареечного деплоя есть много всем известных плюсов и возможностей, наибольшую ценность для нас представляют:

  1. Постепенное наращивание трафика на новую версию. У нас в данный момент есть несколько стандартных сценариев, отличающихся по времени.

  2. Возможность задавать кастомные сценарии канарейки, если стандартных не достаточно.

  1. Обнаружение аномалий на основе стандартных метрик, которые предоставляем из коробки, в виде обнаружения ошибок и перезапуска подов. 

  1. У разработчиков есть возможность задать свои метрики, если стандартных не достаточно.

  1. Автооткат при отклонении этих метрик. В таком случае произойдет автоматический откат всех изменений до состояния «как было». В таком случае откат произойдет вместе со всеми вспомогательными инструментами (кодом разработчиков, настройками) и вернет сервис в изначальное состояние. Да, мы не откатываем миграции, но канарейка невозможна без совместимых миграций между версиями в принципе. Пример как это работает: если во время деплоя мы обнаружим повышение ошибок на любой из версий (новой или старой), мы все автоматически откатываем. И уже после, в спокойной обстановке, будем производить разбор ситуации.

  2. Возможность обратиться по хедеру напрямую к новой версии. Все наши стандартные сценарии начинаются с нагрузки 1%, и мы можем обратиться сразу к новому экземпляру приложения по отдельному хедеру.

  3. Возможность выкатить сервисы вообще без нагрузки. Обращение так же идет через отдельный хэдер.

  4. Канарейка не только для внешнего трафика, но и для внутреннего. Многие реализации делают канареечный деплой только для входящего внешнего трафика. Наша реализация делает это и для внутреннего, включая 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-менеджеров.

Комментарии (0)