История выкатки, которая затрагивала всё

kc5yq_bh4rgol9q8lebwmzsldca.jpeg
Enemies of Reality by 12f-2

В конце апреля, пока белые ходоки осаждали Винтерфелл, у нас произошло кое-что поинтереснее, мы сделали не совсем обычную выкатку. В принципе мы постоянно катим новые фичи в прод (как и все). Но эта была не такая, как все. Масштаб её был таков, что любые потенциальные ошибки, которые мы могли допустить, поаффектили бы все наши сервисы и пользователей. В итоге мы всё выкатили по плану, в запланированный и анонсированный срок даунтайма, без последствий для прода. Статья — о том, как мы этого добились и как желающие могут это повторить в домашних условиях.
Я не буду сейчас описывать принятые нами архитектурные и технические решения, рассказывать, как оно все работает. Это скорее заметки на полях о том, как проходила одна из самых сложных выкаток, которую я наблюдал и в которой принимал непосредственное участие. Не претендую на полноту или технические детали, возможно, они появятся в другой статье.

Предыстория + что же это за такая функциональность


Мы строим облачную платформу Mail.ru Cloud Solutions (MCS), где я работаю техническим директором. И вот — пришло время приделать к нашей платформе IAM (Identity and Access Management), который обеспечивает единое управление всеми пользовательскими аккаунтами, пользователями, паролями, ролями, сервисами и прочим. Зачем он нужен в облаке — вопрос очевидный: в нем хранится вся пользовательская информация.

Обычно такие вещи начинают строить на этапе самых стартов любых проектов. Но в MCS исторически сложилось немного по-другому. MCS строился двумя частями:

  • Openstack с собственным модулем авторизации Keystone,
  • Hotbox (S3-хранилище) на базе проекта Облако Mail.ru,


вокруг которых затем появились новые сервисы.

По сути, это было два разных типа авторизации. Плюс мы использовали некоторые отдельные разработки Mail.ru, например — общее хранилище паролей Mail.ru, а также самописный openid-коннектор, благодаря которому обеспечивалась SSO (сквозная авторизация) в панели Horizon виртуальных машин (нативный UI OpenStack).

Сделать IAM для нас значило соединить это всё в единую систему, полностью свою. При этом не потерять никакого функционала по дороге, создать задел на будущее, который позволит нам прозрачно его дорабатывать без рефакторинга, масштабировать по функциональности. Также на старте у пользователей появилась ролевая модель доступа к сервисам (центральный RBAC, role-based access control) и некоторые другие мелочи.

Задача оказалась нетривиальной: python и perl, несколько бекендов, независимо написанные сервисы, несколько команд разработки и админов. И главное — тысячи живых пользователей на боевой продакшен-системе. Все это надо было написать и, главное, — выкатить без жертв.

Что мы собрались выкатить


Если очень грубо, где-то за 4 месяца мы подготовили такое:

  • Сделали несколько новых демонов, которые агрегировали функции, раньше работавшие в разных уголках инфраструктуры. Остальным сервисам прописали новый бекенд в виде этих демонов.
  • Написали свое центральное хранилище паролей и ключей, доступное для всех наших сервисов, которое можно свободно модифицировать, как нам нужно.
  • Написали с нуля 4 новых бекенда для Keystone (пользователи, проекты, роли, role assignments), которые, по сути, заменили его базу, и теперь выступает единым хранилищем наших пользовательских паролей.
  • Научили все наши сервисы Openstack ходить за своими политиками в сторонний сервис политик вместо того, чтобы читать эти политики локально с каждого сервера (да-да, по-умолчанию Openstack так и работает!)


Такая большая переделка требует больших, сложных и, главное, — синхронных изменений в нескольких системах, написанных разными командами разработки. После сборки вся система должна работать.

Как выкатывать такие изменения и не облажаться? Сначала мы решили немножко заглянуть в будущее.

Стратегия выкатки


  • Можно было бы сделать выкатку в несколько этапов, но это бы увеличило срок разработки раза в три. Кроме того, на какое-то время у нас была бы полная рассинхронизация данных в базах. Пришлось бы писать свои инструменты синхронизации и долго жить с несколькими хранилищами данных. А это создаёт самые разнообразные риски.
  • Всё, что могли подготовить прозрачно для пользователя, сделали заранее. На это ушло 2 месяца.
  • Мы позволили себе даунтайм в течение нескольких часов — только на операции пользователей по созданию и изменению ресурсов.
  • Для работы всех уже созданных ресурсов даунтайм был недопустим. Мы запланировали, что при выкате ресурсы должны работать без даунтайма и аффекта для клиентов.
  • Чтобы снизить влияние на наших заказчиков, если что-то пойдёт не так, мы решили выкатываться вечером в воскресенье. Ночью меньше заказчиков занимается управлением виртуальными машинами.
  • Всех наших клиентов мы предупредили о том, что в выбранный для выкатки период управление сервисами будет недоступно.


Отступление: что есть выкатка?


<осторожно, философия>

Каждый айтишник легко ответит, что такое выкатка. Ставишь CI/CD, и автоматом все доставляется на прод. :)

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

А вся картинка такова. Выкатка состоит из четырёх больших аспектов:

  1. Доставка кода, включая изменение данных. Например, их миграции.
  2. Откат кода — возможность вернуться, если что-то пойдёт не так. Например, через создание бэкапов.
  3. Время каждой операции выкатки / отката. Надо понимать тайминг любой операции первых двух пунктов.
  4. Затронутый функционал. Нужно обязательно оценить как ожидаемый позитивный, так и возможный негативный эффект.


Все эти аспекты необходимо учесть для успешной выкатки. Обычно оценивают только первый, в лучшем случае второй пункт, и тогда выкатка считается успешной. Но третий и четвертый даже более важны. Какому пользователю понравится, если выкатка займет 3 часа вместо минуты? Или если на выкатке зааффектится что-то лишнее? Или даунтайм одного сервиса приведет к непредсказуемым последствиям?

Акт 1…n, подготовка к релизу


Сначала я думал кратко описать наши встречи: всей командой, ее частями, кучи обсуждений в кофе поинтах, споров, тестов, брейнштормов. Потом подумал, что это будет лишним. Четыре месяца разработки всегда состоят из такого, особенно когда пишешь не то, что можно доставлять постоянно, а одну большую фичу на живую систему. Которая затрагивает все сервисы, но у пользователей ничего не должно поменяться, кроме «одной кнопки в веб-интерфейсе».

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

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

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

Мы автоматизировали все операции по выкатке, которые только было можно. Любую операцию скриптовали, даже самую простую, постоянно гоняли тесты. Спорили, как лучше выключать сервис — опускать демон или закрывать доступ к сервису фаерволом. Создали чеклист команд на каждый этап выкатки, постоянно его актуализировали. Нарисовали и постоянно актуализировали диаграмму Ганта по всем работам по выкатке, с таймингами.

И вот…

Акт финальный, перед выкаткой


… настало время выкатываться.

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

Мы приняли решение, что готовы выкатываться, когда убедились, что мы сделали всё возможное, чтобы закрыть все риски для наших пользователей, связанные с неожиданными аффектами и даунтаймами. То есть — что угодно может пойти не так, кроме:

  1. Аффекта (священной для нас, драгоценнейшей) пользовательской инфраструктуры,
  2. Функциональностей: использование нашего сервиса после выкатки должно быть таким же, как и до неё.


Выкатка


mxvtyqnl26yvqbxhego52zj9d8c.png
Двое катят, 8 не мешают

Берем даунтайм на все запросы от пользователей в течение 7 часов. На это время у нас есть как план выкатки, так и план отката.

  • Сама выкатка занимает примерно 3 часа.
  • 2 часа — на тестирование.
  • 2 часа — запас на возможный откат изменений.


Составлена диаграмма Ганта на каждое действие, сколько времени оно занимает, что идет последовательно, что производится параллельно.

gl4v2pcxxorqbzzhuvlbdsi0jtg.png
Кусочек диаграммы Ганта выкатки, одна из ранних версий (без пераллельного исполнения). Ценнейший инструмент синхронизации

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

Хроника событий


Итак, 15 человек приехало на работу в воскресенье 29 апреля, в 10 вечера. Помимо ключевых участников, некоторые приехали просто на поддержку команды, за что им отдельное спасибо.

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

00:00. Стоп
Останавливаем пользовательские запросы, вешаем шильдик, мол, технические работы. Мониторинг вопит, но все штатно. Проверяем, что ничего не упало, кроме того, что должно было. И начинаем работы по миграции.

У всех есть распечатанный план выкатки по пунктам, все знают, кто что делает и в какой момент. После каждого действия сверяемся с таймингами, что не превышаем их, и все идет по плану. Те, кто не участвует в выкатке непосредственно на текущем этапе, готовятся, запустив онлайн-игрушку (Xonotic, типа 3 кваки), чтобы не мешать коллегам. :)

02:00. Выкатили
Приятный сюрприз — заканчиваем выкатку на час раньше, за счет оптимизации наших баз и скриптов миграции. Всеобщий клич, «выкатили!» Все новые функции в проде, но в интерфейсе пока видим только мы. Все переходят в режим тестирования, разбираются на кучки, и начинают смотреть, что в итоге получилось.

Получилось не очень, это понимаем минут через 10, когда в проектах участников команды ничего не подключено и не работает. Быстрый синк, озвучиваем наши проблемы, расставляем приоритеты, разбиваемся на команды и уходим в дебаг.

02:30. Две большие проблемы vs четыре глаза
Обнаруживаем две большие проблемы. Поняли, что заказчики не увидят некоторые подключенные сервисы, и возникнут проблемы с аккаунтами партнёров. Обе связаны с несовершенством скриптов миграции для некоторых краевых случаях. Надо фиксить сейчас.

Пишем запросы, которые это фиксят, минимум в 4 глаза. Прокатываем на предпроде, чтобы удостовериться, что они работают и ничего не ломают. Можно катить дальше. Параллельно запускается наше обычное интеграционное тестирование, которое обнаруживает еще несколько проблем. Все они мелкие, но тоже надо чинить.

03:00. -2 проблемы +2 проблемы
Две предыдущие большие проблемы пофикшены, почти все мелкие тоже. Все незанятые в фиксах активно работают в своих аккаунтах и репортят, что находят. Приоритезируем, распределяем по командам, некритичное оставляем на утро.

Опять запускаем тесты, они обнаруживают две новые большие проблемы. Не все политики сервисов доехали правильно, так что некоторые пользовательские запросы не проходят авторизацию. Плюс новая проблема с аккаунтами партнёров. Бросаемся смотреть.

03:20. Экстренный синк
Одна новая проблема исправлена. Для второй мы устраиваем экстренный синк. Понимаем, что происходит: предыдущий фикс починил одну проблему, но создал другую. Берем паузу, чтобы разобраться как сделать правильно и без последствий.

03:30. Шесть глаз
Осознаём, какое должно быть итоговое состояние базы, чтобы все было хорошо у всех партнёров. Пишем запрос в 6 глаз, прокатываем на предпроде, тестируем, катим на прод.

04:00. Всё работает
Все тесты прошли, критичных проблем не видно. Периодически в команде у кого-то что-то не работает, оперативно реагируем. Чаще всего тревога ложная. Но иногда что-то не доехало, где-то не работает отдельная страница. Сидим, фиксим, фиксим, фиксим. Отдельная команда запускает последнюю большую фичу — биллинг.

04:30. Точка невозврата
Близится точка невозврата, то есть время, когда, если начнем откатываться, не уложимся в выданный нам даунтайм. Есть проблемы с биллингом, который всё знает и записывает, но упорно не хочет списывать деньги с клиентов. Есть несколько багов на отдельных страничках, действиях, статусах. Основной функционал работает, все тесты проходят успешно. Принимаем решение, что выкатка состоялась, откатываться не будем.

06:00. Открываем на всех в UI
Баги пофикшены. Какие-то, не аффектящие пользователей, оставлены на потом. Открываем интерфейс всем. Продолжаем колдовать над биллингом, ждем фидбека пользователей и результатов мониторинга.

07:00. Проблемы с нагрузкой на API
Становится ясно, что мы немного неправильно распланировали нагрузку на наше API и тестирование этой нагрузки, которое не смогло выявить проблему. В итоге ≈5% запросов фейлится. Мобилизуемся, ищем причину.

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

08:00. Фикс API
Выкатили фикс для нагрузки, фейлы ушли. Начинаем расходиться по домам.

10:00. Всё
Все пофикшено. В мониторинге и у заказчиков тихо, команда постепенно уходит спать. Остался биллинг, его будем восстанавливать уже завтра.

Далее в течение дня были выкатки, которые починили логи, нотификации, коды возврата и кастомы у некоторых наших клиентов.

Итак, выкатка прошла успешно! Могло бы быть, конечно, и лучше, но мы сделали выводы о том, чего нам не хватило, чтобы достигнуть совершенства.

Итого


В течение 2 месяцев активной подготовки к моменту выкатки выполнено 43 задачи длительностью от пары часов до нескольких дней.

Во время выкатки:

  • новых и измененных демонов — 5 штук, заменивших 2 монолита;
  • изменений внутри баз данных — все 6 наших баз с данными пользователей затронуто, выполнены выгрузки из трех старых баз в одну новую;
  • полностью переделанный фронтэнд;
  • количество выкаченного кода — 33 тысячи строк нового кода, ≈ 3 тысяч строк кода в тестах, ≈ 5 тысяч строк кода миграции;
  • все данные целы, ни одна виртуалка заказчика не пострадала. :)


Хорошие практики для хорошей выкатки


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

  1. Первое, что надо — понять, как выкатка может повлиять или повлияет на пользователей. Будет ли даунтайм? Если будет, то даунтайм чего? Как это отразится на пользователях? Какие возможны наилучшие и наихудшие сценарии? И закрывать риски.
  2. Всё спланировать. На каждом этапе нужно понимать все аспекты выкатки:
    • доставка кода;
    • откат кода;
    • время каждой операции;
    • затронутый функционал.
  3. Проиграть сценарии до тех пор, пока не станут очевидны все этапы выкатки, а также риски на каждом из них. Если в чем-то есть сомнения, можно взять паузу и исследовать сомнительный этап отдельно.
  4. Каждый этап можно и нужно улучшить, если это поможет нашим пользователям. Например, уменьшит даунтайм или уберет какие-то риски.
  5. Тестирование отката гораздо важнее, чем тестирование доставки кода. Нужно обязательно проверить, что в результате отката система вернётся в первоначальное состояние, подтвердить это тестами.
  6. Все, что может быть автоматизировано, должно быть автоматизировано. Все, что не может быть автоматизировано, должно быть заранее написано на шпаргалке.
  7. Зафиксировать критерий успешности. Какой функционал должен быть доступен и в какое время? Если этого не происходит, запускайте план отката.
  8. И самое главное — люди. Каждый должен быть в курсе, что делает, для чего и что от его действий зависит в процессе выкатки.


А если одной фразой, то с хорошим планированием и проработкой можно выкатить без последствий для прода всё что угодно. Даже то, что аффектит все ваши сервисы в проде.

© Habrahabr.ru