Песнь о могучем Деплое: безостановочное прозрачное развёртывание веб-сервиса
Пришло время, когда нам — команде Эльбы — захотелось поделиться с миром подробностями некоторых магических и загадочных деталей нашего продукта. Решили начать с одного из самых сложных проектов, который является предметом особой гордости и лёгкого поклонения. Он покрыт налётом тайны и окутан ореолом тёмной магии. Легенды о нём передаются из уст в уста. Лишь малая часть знаний задокументирована в вики или ютреке, большая же — сокрыта в исходниках системы контроля версий. Премудрых старцев, умеющих расшифровать этот тайный код, в проекте всё меньше и меньше. Пришла пора записать все магические заклинания в подробном манускрипте. Речь пойдёт о системе развёртывания Эльбы — о могучем Деплое.
Эльба — это веб-сервис для предпринимателей, который помогает вести бизнес. А работа могучего Деплоя — обновлять Эльбу. Когда будет готова новая версия сервиса, Деплой должен явить её миру, т.е. пользователям. Это значит, что все части системы должны быть обновлены и запущены, а все данные — конвертированы и подготовлены. Наш Деплой справляется со своей работой хорошо — обновления Эльбы проходят абсолютно незаметно для пользователей, не прерывая их работы. Это позволяет выкладывать новые релизы так часто, как нужно — по мере их готовности. О том, как мы этого достигли, как безостановочно и прозрачно для пользователей обновляется веб-сервис, и будет рассказано далее.Глава первая. Происхождение
В давние времена Эльба состояла из небольшого веб-приложения на ASP.Net Web Forms и БД на MS SQL Server. Приложение позволяло предпринимателям с помощью небольшого визарда подготовить и сдать декларацию УСН в налоговую инспекцию. С тех давних пор сменилось много эпох — сейчас Эльба позволяет делать гораздо больше. Что именно она умеет и чем помогает предпринимателям, расскажет наш маркетинг и увлекательное чтение промосайта.
Нас больше интересует техническая сторона. Когда Эльба была маленькой, процесс доставки был простой и наивный. Приложение жило в датацентре на двух хостах, спрятанных за балансировщиком. Чтобы обновить оба приложения, программист заходил руками по RDP на продакшн-сервер и копировал в папки на обоих хостах заранее скомпилированный код. Добрый IIS тут же подхватывал изменения и перезапускал систему. Чтобы обновить данные в базе, приходилось вручную запускать скрипты на SQL-сервере.
Со временем кодовая база разрасталась и набирала силу. Появилось много фронтов, возникли отдельно живущие на собственных хостах сервисы, роботы, заменяющие человеков и мечтающие однажды захватить мир, добавились интеграции со сторонними продуктами и сервисами, ещё много всего разного и интересного. Сначала для релизов была написана пачка батников, которые упрощали ручной труд при копировании. Батники раскладывали новый дистрибутив по всем хостам и запускали. Но вскоре кода стало так много, что время его копирования на все хосты неприлично возросло. Батниками стало сложно управлять: поддерживать в них актуальный список сервисов и топологию. К тому же с увеличением количества пользователей вырос и объём данных, который требовалось обновлять перед новым релизом. Настал час автоматизации.
Те стародавние времена были очень суровы. Готовых инструментов никто не знал, гуглить не умел и всё равно чужое использовать не стал бы. Программисты любят писать код. Решили написать сами. И написали.
Первые версии Деплоя действовали как топор. Человек запускал консольку, которая сперва собирала весь продукт в релизный вид. Это включало в себя компиляцию кода и статики сервисов/роботов/фронтов и самого Деплоя. Потом готовились продакшен-настройки для всего этого добра: топология площадки, адреса, явки, пароли, флажки, тумблеры и магические константы. Подготовленный дистрибутив передавался в датацентр специально обученному удалённому сервису, который только и умел, что после окончания передачи запустить тот самый волшебный Деплой.
Первым делом на фронтах появлялась заглушка для внешнего мира «Идут технические работы». Под её прикрытием Деплой убивал всё, до чего мог дотянуться. Гасил всю площадку. Убивал роботов, сервисы, фронты, все работающие процессы, не щадя ни родительские, ни дочерние, оставляя за собой выжженную землю. Эльба больше не работала. И на этих руинах Деплой начинал строить новый, лучший мир. Не спеша раскидывал дистрибутив в соответствии с заданной топологией, подменял настройки, выполнял скрипты, чтобы обновить данные в базе. Затем потихоньку запускал всё хозяйство обратно. После старта самого мееедленного сервиса заглушка снималась, и радостные пользователи, наконец, могли попасть в обновлённую систему.
Очевидно, что не все пользователи и далеко не всегда были рады. В самом удачном случае время простоя занимало минут 10–20 поздним вечером… И хорошо, когда всё проходило хорошо. При неудачном положении звёзд Деплой неожиданно умирал в самом неподходящем месте, не закончив работу, и команда оставалась с разломанной в хлам площадкой. Где-то что-то работало, где-то нет, а что-то работало не как задумано. И в ближайшие часы очередной бессонной ночи приходилось срочно приводить всё в порядок. Техподдержка с трудом сдерживала гнев пользователей, успокаивала самых расстроенных, отпаивая их валерьянкой прямо по телефону горячей линии.
В тот период эволюция разработки в Эльбе достигла той стадии, когда хотелось релизить больше, чаще и лучше. Строить очередь из релизов уже не хотели, напрягать пользователей и техподдержку тем более.
Сформировались новые требования к Деплою:
- минимальное время простоя Эльбы, в идеале — его отсутствие;
- прозрачное обновление данных;
- система должна продолжать работать в случае неудачного релиза;
- полная автоматизация или минимум ручных действий, чтобы исключить человеческий фактор;
- простой код для дальнейшего улучшения и развития.
Понимание требований быстро привело к осознанию того, что без поддержки в основном приложении затея обречена на провал. Не получится сделать отдельно стоящую систему развёртывания. Надо в самой системе, в самом базовом коде, поддерживать сценарии релизов, дописывать инфраструктуру, нужно устанавливать требования к коду, к данным, к топологии.
Большую часть работы по развёртыванию новой версии Эльбы можно выполнять параллельно с работающей. Деплой получит на вход скомпилированный дистрибутив и топологию, указывающую на хост и порт, где живёт конкретный сервис, после чего раскидает и запустит все необходимые сервисы. В нужный момент останется только дёрнуть рубильник и переключить всю площадку.
На берегу всё звучало как магия, неподвластная смертным.
Настал тот момент, когда программистам в очередной раз предстояло засучить рукава и явить миру новый, «более лучший» Деплой, с большей магической силой, избавленный от недостатков и слабостей своих предшественников.
Долго ли, коротко ли, с помощью великого gusev_p и вашего покорного слуги из пыли и пепла родился он — могучий неостановимый Деплой.
Глава вторая. ОкружениеПеред описанием процесса развёртывания и сугубо технических подробностей, стоит рассказать, с чем мы имели дело:
— Эльба — веб-сервис, продукт для Windows-стека, код написан на C#;
— основное приложение (фронт) — ASP.Net веб-сайт для IIS. Частично Web Forms, частично MVC;
— десяток постоянно поднятых standalone сервисов с http-интерфейсом. Сервисы реплицированы для балансировки и отказоустойчивости;
— десяток роботов, запускаемых планировщиком по расписанию;
— основное хранилище — БД MS SQL Server;
— MongoDB для денормализованных данных и агрегаций;
— ElasticSearch для полнотекстового поиска;
— RabbitMQ для месседжинга.
База SQL используется как append-only. В коде приложения не используются UPDATE и DELETE, а модификация и удаление сущностей происходит через добавление новой ревизии с изменёнными полями или признаком удаления. Номер ревизии — инкрементальный счётчик, сквозной для одного типа сущностей. С помощью ORM внутри кода приложения из хранилища выбираются только последние неудалённые ревизии каждой конкретной сущности. В результате такой схемы получается версионирование и история изменений всех данных, что даёт +86 к магии, о которой будет рассказано ниже.
Глава третья. Школа магииПриступаем к описанию работы Деплоя. Для начала весь процесс развёртывания релиза был разбит примерно на 40 шагов. Каждый шаг делает малую часть большой работы и отвечает только за неё. Следующий шаг запускается только после удачного завершения предыдущего. Большинство шагов может выполняться параллельно работающей Эльбе. В случае неудачи шаг, по возможности, возвращает систему к исходному состоянию.
Как и раньше, маленький специально обученный удалённый сервис получает новый дистрибутив Эльбы и запускает Деплой. Тот, не теряя времени, начинает делать свои первые шаги.
Начальные шаги не столь интересны. Проверяется текущая конфигурация, подготавливается файловая система, новый код сервисов копируется в новые папки на соответствующих хостах, готовятся задачи в планировщике. Однотипные задачи распараллелены по всем хостам.
Далее Деплою необходимо заняться миграцией данных. В этот момент он вступает на территорию магии.
В новом релизе программисты хорошо поработали, написали много нового кода, создали новые сущности и знатно порефакторили существующие. В некоторых из них изменились умолчания, появились новые свойства или удалились старые. Теперь вместо старой сущности может использоваться набор из нескольких новых, связанных между собой, либо наоборот. Где была связь «один-к-одному», могла появиться «многие-ко-многим» и т.д. Проблема в том, что новый код не умеет работать со старыми данными. Перед запуском нового кода существующие данные необходимо подготовить — мигрировать. При подготовке нового релиза программисты пишут соответствующие скрипты и утилиты, которые изменяют структуру данных в хранилище, модифицируют и трансформируют текущие сущности.
Но старый код тоже не умеет работать с новыми данными. И если вперёд данные мигрировать обычно легко, то обратная миграция сложна, а то и невозможна. Если новый код запишет сущности по-новому, обратить их в старые не выйдет. Откатывать миграции данных не стоит. Поэтому пусть старый код работает со старыми данными, а новый с новыми — лишь бы они не пересекались во времени и пространстве.
Так родился первый постулат Деплоя: миграция данных не должна ломать работающий код. Создавать новые колонки и таблицы можно, модифицировать текущие данные — нельзя. Если нужно их преобразовать, то следует сделать новые колонки и таблицы и положить результат туда. Благо, ORM в коде приложения хорошо абстрагирует от хранилища и позволяет не замечать такие изменения.
Заклинание #1После первичной миграции Деплой останавливает всех роботов — их работой на время релиза можно пожертвовать. Затем сервисы. Но сервисы нужны для работы системы, нельзя остановить их все. Мы не захотели менять топологию и настройки при каждом релизе. Практически всегда топология нового релиза совпадает с топологией уже развёрнутой и работающей Эльбы. Большинству релизов это требование не мешает, ибо топология меняется редко, а при необходимости немного ручных манипуляций её легко модифицируют.
Очень часто миграция — длительный и не всегда детерминированный по времени процесс. Может понадобиться перекачать миллионы строк таблиц и гигабайты данных. Но пользователи не хотят ждать, простой сервиса — это плохо. И тут Деплой воспользовался первым магическим заклинанием — версионной структурой хранилища. Вся миграция была разделена на два этапа.Первый этап — практически неограниченный по времени — обновляет схему данных и занимается миграцией основного объёма сущностей. Он добавляет колонки и таблицы и запоминает текущие последние ревизии всех типов сущностей. После запускает скрипты и утилиты по трансформации данных, которые подготовят сущности к работе с новым кодом. Главное, чтобы первый этап не мешал работе существующего кода, а модифицированные данные появлялись в новых полях, невидимых из старого кода.
Второй этап Деплой выполняет отдельным шагом перед самым переключением версий. На нём происходит домиграция новых ревизий сущностей, которые появились после первого этапа. Для этого выполняются все те же скрипты и утилиты трансформации, но уже с указанием запомненных ранее ревизий. Это обеспечивает обработку только накопившихся изменений.
Так появился второй постулат Деплоя — запускать на одном и том же хосте сервис с новым кодом вместо старого. Для простоты решили на время развёртывания останавливать половину реплик работающих сервисов и вместо них запускать новые. Пока старый сервис работал, обновлённая версия уже была скопирована рядом. Текущий сервис останавливается Деплоем, новый — запускается. При этом вся система продолжает работать на временно уменьшенном количестве реплик.
Заклинание #2
Пришлось доработать базовую сетевую инфраструктуру системы. Каждый сервис запускается на определённом наборе настроек, в которых теперь появилась версия релиза, возрастающая с каждым новым развёртыванием. Правильно указать версию в настройках сервиса и поддерживать журнал — задача Деплоя. Все сервисы, включая фронт, умеют общаться только в пределах одной версии. Если один сервис делает запрос на другой более старый или более новый, вместо обработки запроса возвращается специфичный ответ. Затем этот сервис попадает в чёрный список, а запрос уходит на другую реплику.
Туман сгущается. Деплой вплотную подошёл к территории самой тёмной магии. Подошла очередь обновлять фронты. С ними, по понятным причинам, возникли основные сложности. Во-первых, из-за долгого старта нового приложения. Неясно, что такого IIS делает, но поднятие всех сборок в память, кэширование, обновление метабазы и т.п. съедает приличное количество секунд. Во-вторых, из-за http-запросов от пользователей в работающем приложении. Мы всё ещё не хотим их обрывать, возвращать заглушку, ошибку или таймаут. Все текущие запросы должны быть обработаны, так же как и все новые.
Деплой разворачивает новые фронты на IIS на всех хостах, где запущены старые. Но на отдельном порту. Непрогретое приложение тупит при старте, поэтому оно научилось само себя прогревать. Для этого при запуске симулируются заходы пользователей на самые популярные страницы, что позволяет сделать IIS-у все нужные действия по кэшированию. Деплой терпеливо дожидается, пока новые вспомогательные фронты прогреются. В итоге на каждом фронте продолжает работать старое приложение, постоянно обрабатывая входящие запросы пользователей, и уже заработало новое приложение, которое пока ждёт своего часа.
Совсем скоро начнётся переключение релизов. Требуется выполнить полную подготовку данных для нового кода и преобразовать имеющиеся ревизии всех сущностей. Этому процессу никто и ничто не должно помешать. Никаких более свежих записей появиться не должно. Для фиксации этого постулата было решено установить некий «блок» для всех входящих пользовательских запросов. Это очень сильное заклинание, состоящее из множества разнообразных эффектов.
Все фронты и работающие сервисы получают от Деплоя сигнал о начале переключения релизов, что приводит к заметным метаморфозам в их работе. С момента получения сигнала внутри кода приложения устанавливается тот самый «блок». Все новые http-запросы на фронтах встают на паузу, ожидая его снятия. «Блок» запрещает добавление новых записей в базу данных, чтобы обеспечить свободу для финальной миграции. Если текущий запрос, попавший в приложение до установки «блока», пытается осуществить запись, то из самых далёких глубин инфраструктурного кода вылетает исключение, выполнение запроса на фронте повторяется, и он упирается в тот же «блок», как все новые.
Деплой выполняет второй, заключительный, этап миграции данных. С этих пор все имеющиеся данные готовы к обработке новым кодом. После завершения миграции подаётся сигнал фронтам о снятии «блока». Все запросы, нервно толпящиеся на входе в «старое» приложение, проксируются на уже разогретое «вспомогательное» новое, а его ответ проксируется обратно клиенту. Таким образом пользовательские запросы уже обрабатываются новым кодом в новом «вспомогательном» приложении, а старое приложение занимается только проксированием. Старый код не работает с новыми данными. И ни один пользовательский запрос не был потерян.
Весь процесс от установки до снятия «блока» занимает буквально пару секунд. Вся затея служит для определения того единственного момента, когда можно будет запускать обработку запросов в новом приложении, и к этому моменту уже мигрированы абсолютно все существующие данные. В каждый момент времени новые записи попадают в базу либо из старого кода, либо из нового, но никогда одновременно. Это исключает несовместимость кода по данным, упрощает и ускоряет переключение релизов.
Заклинание #3Работа Деплоя пока не закончена. Жить с двумя приложениями и проксированием не очень-то удобно. Деплой переключает дефолтное приложение в IIS на папку с новым приложением — ту самую, из которой запущено «вспомогательное». Этот процесс снова занимает у IIS время, обычно исчисляемое секундами. Всё это время запросы поступают в старое приложение и проксируются на «вспомогательное». Новое «рабочее» приложение тем временем стартует, и, как обычно после старта, ему надо дать время на «прогрев». Поэтому поступающие http-запросы новое приложение не обрабатывает, а на всё время прогрева также проксирует на «вспомогательное».
Шаг со снятием «блока» для запросов считается финальным шагом развёртывания. Мир изменился. Работает новый код, и возврат к прошлому невозможен. Любой шаг до этого можно без последствий откатить. При возникновении любой исключительной ситуации до момента переключения фронтов Деплой откатывает шаги в обратном порядке, от текущего к первому, возвращая всю систему в работоспособный вид. Выключаются новые сервисы, включаются старые, запускаются остановленные роботы и т.п.
Заклинание #4На последних шагах Деплою осталось остановить всё ещё работающую половину реплик старых сервисов, запустить вместо них новые, включить в планировщике новых роботов, дождаться прогрева новых фронтов и написать в логе об успехе релиза. Deploy done! Эпилог
В какой-то момент на одном хосте в одном IIS работают одновременно три приложения Эльбы. Старое, которое успело принять входящий запрос до переключения IIS и проксирует его на «вспомогательное» новое. Третье, также новое, только что запущенное после переключения, полностью идентично второму «вспомогательному». Третье проксирует запросы второму на время своего прогрева. После окончательного «прогрева» приложение прекращает проксирование и начинает обрабатывать запросы самостоятельно. Остальные приложения отмирают, когда поток запросов к ним иссякает. В конце остаётся только одно.
В этой статье описаны основные идеи и довольно специфичные приёмы, которые позволили Эльбе за непродолжительное время внедрить практику быстрого безостановочного обновления.
Осталось за кадром, как мы мигрируем данные в монге и эластике, обеспечиваем правильный откат записей, синхронизируем пресловутый «блок», боремся с IIS, делаем UI для Деплоя и решаем множество других мелких технических проблем. Если вы заинтересуетесь подробностями, мы с удовольствием ответим в комментариях.
Изредка Эльба обновляется под «заглушкой», но это обычно связано с переездом железа, апгрейдом ОС, инфраструктурных сервисов и, уже почти никогда, с релизами новой функциональности.
В итоге разработки нового Деплоя мы получили практически то, чего желали, — безостановочное развёртывание нового релиза, полностью прозрачное для пользователей. Клиент может зайти на страницу в старом приложении, обновить её и оказаться в новом. Весь процесс развертывания занимает меньше 10 минут, а максимальное время задержки с ответом на запрос не больше 3–4 секунд. При этом, весь код Деплоя — наш. Мы продолжаем его дорабатывать и улучшать, внедрять новые фичи и развивать его функциональность.