блог разработчика о насущном

Канареечное развертывание

Лирическое отступление
Релиз новых или дополнение уже существующих бизнес фич — задача, которая из раза в раз покрывается всякого рода грустными и смешными шутками из-за множества непредсказуемых факторов или неожиданных багов.

Особенно релизы болезненны для разработчиков, если деплой подразумевает замены всех старых стабильных сборок приложения на новые, пусть и протестированные. Если в новой сборке оказался критический баг, ломающий важные флоу приложения.

И потому у нас возникает справедливый вопрос — а можно если ли и допускать риск "пожара" на продакшене, но не везде сразу?

Допустим, деплоить только одну "экспериментальную" новую сборку, но все стабильные старые оставить, и пускать маленькую часть пользователей на новую, а остальных не требушить.
логирование java-разработчик баги rocket science
Рис. 1. Наглядная демонстрация, как мы можем сэкономить нервы на нерухнувшем проде и как мы можем сходить с ума на второй картинке, если новый релиз рухнул или "трещит по швам".
Это выгодно нам, разработчикам, т.к. мы экономим и нервы, и время сна. Но так же это выгодно и бизнесу — пользователи не пойдут с вилами в их офис на следующее утро, и доходы не будут падать из-за случайных ошибок.

Что же нам поможет? Правильно, канареечное развертывание!
Предыстория
Почему канарейка? Справка из википедии:

Длительное время (в Великобритании вплоть до 1987 года) канареек использовали в качестве раннего способа обнаружения в шахтах рудничного газа. Эти птицы очень чувствительны к газам, включая метан и угарный газ, и гибнут даже от незначительной примеси их в воздухе. Шахтеры брали клетку с канарейкой в шахту и во время работы следили за птицей. Также канареек часто использовали горноспасатели, спускавшиеся в аварийные шахты.

Только теперь наши канарейки — это невероятно малая часть пользователей, а опасные шахты — наши новые сборки.
логирование java-разработчик баги rocket science
Рис. 2. Peace never was an option.
Описание
Канареечное развёртывание призвано повысить доступность Сервера ЕМП во время релиза новых версий за счёт того, что одновременно будут доступны и старая и новая версия, а траффик от мобильных приложений будет перенаправляться на новую версию постепенно, растянутым во времени процессом с постоянным мониторингом ошибок обработки запросов. Перенаправляться будет не случайный процент трафика, а целиком трафик отдельных пользователей, "канареек", процент которых от общего числа пользователей будет постепенно повышаться. После того, как трафик 100% пользователей будет маршрутизироваться на новую версию, старая версия будет остановлена.

Так мы минимизируем риски поломать сразу всё и постепенно разворачиваем новое приложение.
Тем самым, мы можем достичь сразу множества полезностей и плюшек:
  • 1
    Релизиться можем часто и докидывать новые плюшки в прод, не боясь того самого неожиданного пожара Какое-то время проверили на новой сборке на тестовых пользователях и отправили в полёт. От того растет гибкость в реализации задач или быстрых багфиксов.
  • 2
    Релизиться можем в любое время дня без особых рисков поджечь всё вокруг. Раньше релиз был ночью из соображения, чтобы было время проверить работоспособность сборки на продакшене и в случае чего сразу исправить критические ошибки.
  • 3
    Если всё-таки пожар случился, то он случился не у всех. А разработчики молниеносно найдут ошибку и исправят её.
  • 4
    Перезапуск всех сборок может длиться очень долго, начиная самим деплоем сборки, настройкой новых фичей, «прогревом» сервера на запросы пользователей. С канареечной сборкой эти проблемы исчезают, т.к. экспериментальная сборка уже прогрета и может бесшовно перевестись в стабильную.
Механизм работы
Метки и статусы
Каждый сборке даётся уникальный номер сборки — buildId, потому мы сможем их идентифицировать отдельно и над каждой сборкой отдельно централизованно проводить какие-либо манипуляции.

Также к сборкам добавляется статус релиза — их статус в общем процессе канареечного развертывания.
Всего таких статусов несколько:
  • 1
    DEPLOYED
    (Подготовленное развёртывание)
    самая свежая только запущенная сборка
  • 2
    CANARY
    (Канареечное развёртывание)
    экспериментальная, которую и будем держать в отдельности и не сразу пускать на всю аудиторию
  • 3
    STABLE
    (Стабильное развёртывание)
    стабильная сборка, на которую ходят все пользователи
  • 4
    DELETED
    (Удаленное развёртывание)
    что отслужила свое и отошла в мир иной. Такой сборки больше нет в кластере
логирование java-разработчик баги rocket science
Рис.3. Связь статусов сборки в кластере.
В проекте ЕМП допускает наличие одной стабильной, одной канареечной и сколь угодно задеплоенных сборок. Сборками мы называем конкретный сервис в кластере с определенно заданным количеством реплик.
Механизм развёртывания из административной панели сервера
Чтобы механизм развертывания привести в действие, необходимо прежде всего задеплоить новую сборку на кластер. Это делается из CI/CD пайплайна запуском задачи deploy. Docker образ сборки отправляется на кластер и запускается. Ей присваивается уникальный buildId id CI/CD пайплайна. По окончанию запуска она записывается себя в таблицу сборок в БД и помечает себя как DEPLOYED.
Теперь на отдельной странице развертывания сборок на фронтенде видна новая сборка.
логирование java-разработчик баги rocket science
Рис.4. Пять минут полёт нормальный!
Также помимо новой сборки у нас уже существует стабильная сборка, которой как раз и пользуется фронтенд.
логирование java-разработчик баги rocket science
Рис.3. Связь статусов сборки в кластере.

Помечаем новую сборку как CANARY нажатием кнопки. Теперь сборка помечена канареечной. Бежим настраивать A/B тест.
Создаём новое A/B тестирование, на котором говорим, что 18% определённых пользователей будет ходить только на канареечную сборку:
логирование java-разработчик баги rocket science
Рис.5. Определяем жертв нашей невнимательности ;)
Теперь у нас полная информация о том, какие пользователи на какую сборку будут ходить. Запускаем тест, а дальше усердно смотрим в логи.

Если что-то пошло не так и в новом релизе есть ошибки, то в A/B-тестировании для этого есть волшебный тумблер "Замена группы B". Его включение позволяет при приостановке и возобновлении после развёртывания багфикса полностью заменять пользователей-канареек на новых, которые не были затронуты предыдущей итерацией тестирования. Таким образом, условно, если у нас в новом релизе проскочила ошибка и часть пользователей с ней столкнулись, при проверке "якобы исправленной версии" у них уже нет шанса словить тот же баг второй раз.

После того, как мы протестировали нашу новую сборку на малом количестве пользователей, мы помечаем нашу канареечную сборку как новую стабильную. Старая стабильная становится в статус DEPLOYED и ждёт либо отключения, либо повторного цикла раскатывания. Если хотим удалить сборку, то мы её помечаем как удаленную (DELETED) и удаляем через задачу undeploy в CICD пайплайне сборки.
Что под капотом?
логирование java-разработчик баги rocket science
Когда приходит запрос с мобильного приложения пользователя, в первую очередь запрос прибегает на gateway.
Gateway имеет несколько фильтров, которые:
  • 1
    Идентифицируют пользователя на бэкенде и дополняют запрос особыми внутренними хэдэрами с необходимой информацией;
  • 2
    Определяют целевую сборку, куда должен пойти запрос, и задают параметры редиректа запросу;
  • 3
    В случае непредвиденных ошибок повторяем попытку запроса и уведомляем разработчиков о неудаче.
Нас интересует 2й пункт. Как мы найдём целевую сборку?
Всё просто, спросим сам бэкенд!

Gateway отсылает запрос get-target-build-id на одну из первых попавшихся в кластере реплик бэкенда и спрашивает, какая сборка должна обработать запрос пользователя.

Вспомним, что в моменте запуска бэкенд отмечает себя в БД, что он запустился, а затем мы в админ.панели отметили сборку как канареечную. Пользователь при создании теста определился либо в А, либо в B группу, и результаты тоже сохранились в БД.

Бэкенд смотрит в таблицы теста, является ли человек "канарейкой". Если да, то он ищет так же сборку, что помечена канареечной, и затем возвращает gateway buildId.

Далее, gateway, получив id целевой для пользователя сборки, определяет IP сборки и перенаправляет туда запрос.

А как gateway по buildId узнал IP целевой сборки? Всё просто! При запуске пода со сборкой в метаданные кладётся сам buildId сборки, и далее через K8S Client gateway узнаёт, какой реплике в кластере соответствует выбранный buildId и берёт информацию об его IP. Реплики единой сборки-сервиса с определённым buildId gateway выбирает случайным образом, чтобы распределить нагрузку. В идеале метод get-target-build-id должен быть перенесён в отдельный микросервис и это есть у нас в планах.

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

Так и работает наше канареечное развёртывание:)
Заключение
Заканчиваю рассказ переделкой известной цитаты Цицерона:
Лучше худое падение канарейки, чем хорошее падение всего прода.