Обновления на лету (zero-downtime deployment) вообще и в Ruby on Rails

в 7:14, , рубрики: ruby on rails, ruby on rails 3, sla, Блог компании «Cloud Castle», Веб-разработка, метки: , ,

Сначала разберемся с определениями. Под обновлением на лету мы подразумеваем такое обновление системы, при котором не нарушается штатная ее работа: клиенты работают, посетители ходят и никто не наблюдает ошибок, увеличившегося времени отклика или таблички “УЧЁТ”.

Зачем это нужно? Если вы задаетесь этим вопросом — вам не нужно. Вешайте табличку, садитесь обедать.

Как это делается? Сложно. Почему? Главных причин две: — вы не можете обновить систему мгновенно и атомарно (то есть ровно между двумя HTTP запросами). При наивном подходе пользователи заметят как минимум долгое время отклика, а то и ошибку, если, к примеру, БД обновлена, а код еще нет; — состояние и конфигурация системы существуют и на клиенте и на сервере. Примеры: данные в сессии, имена полей формы, адреса в ссылках, состояние в javascript на открытой у пользователя странице.

Общее решение

В общем виде решение можно сформулировать так: необходимо обеспечить совместимость кода версии N+1 с состоянием версии N.

На практике такая вот совместимость и выливается в огромное количество (очевидных и не очень) сложностей. Разберем типичные случаи в приложении на Ruby On Rails.

Изменение схемы БД

Добавление поля в таблицу теоретически совместимо с предыдущей версией кода. Практически — тоже, если нет особенно злого мета-программирования.

Удаление поля имеет очевидную несовместимость в случае, если старый код использует это поле, и неочевидную, в любом случае: ActiveRecord кэширует список полей и перечисляет все поля, например, в запросах INSERT. Выход: сначала обновить код до промежуточного, который а) не будет испльзовать удаляемое поле; б) будет сам удалять это поле из кэша, потом обновить БД, потом обновить код до конечного.

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

Добавление и удаление индексов совместимо с предыдущей версией кода, если а) не использовать хинты с явным указанием индексов б) удаление индекса не сильно замедлит выполнение старого кода.

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

Изменение взаимодействия клиент-сервер

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

Изменение семантики данных в сессии и куках придется так же обрабатывать отдельным кодом, понимающим оба формата. Сессии живут долго, куки еще дольше. Вы же не хотите потерять данные корзины покупателя или заставлять его вводить логин-пароль лишний раз? (Хабр, shame on you!)

Изменение адресов тех или иных стрниц / action’ов приложения всегда нужно выполнять обратно-совместимым. Оставлять старые роуты, назначать на них редиректы, что угодно. URL’ы в веб-приложении должны быть самой стабильной частью системы: это ваш публичный API, которым пользуются ваши пользователи и поисковики, которые приводят ваших пользователей. Таким образом у вас не будет проблем и в части, описываемой данной статьей.

В случае использования assets pipeline не нужно удалять ассеты предыдущей версии кода. Это просто.

Перезапуск

Совместимость кода — еще не всё. Как вы вводите новый код в работу? Сколько у вас веб- или апп-серверов? Рассмотрим варианты.

Если у вас больше одного сервера, спрятаных за балансировщиком, можете дальше не читать — вы и так всё знаете :) Для остальных все тоже достаточно очевидно: тёмной ночью выбираем время наименьшей нагрузки на систему и по очереди обновляем каждый сервер, выводя его из под балансировщика на время обновления.

Если у вас один сервер на Passenger позади Nginx’а или Apache httpd, придется переехать на Unicorn. Даже Passenger 3, в котором заявлен zero-downtime restart, делает его достаточно наивно: сначала убивает старые worker’ы, потом делает новые. В результате посетители получают большое время отклика, фактически не менее времени старта вашего приложения.

Используя Unicorn, мы можем воспроизвести сценарий для нескольких серверов, но “в миниатюре”. В before_fork нужно посылать старому master процессу сигнал TTOU, в таком случае каждый новый worker будет выключать по одному старому. В конце нужно послать старому master’у QUIT, и всё. Если вам хватит памяти на двойное количество worker’ов, то можно делать проще и выводить старые процессы не постепенно, а сразу — в конце перезапуска.

Совет: используйте опцию preload_app true, даже если вы не на ruby enterprise edition — иначе вы слишком поздно узнаете о том, что новые worker’ы падают при старте из-за ошибки.

Заключение

Подумайте еще раз: вам действительно все это нужно? Точно? Может быть все-таки просто вставить в страницу-заглушку свежый выпуск +100500, запустить cap deploy и идти пить чай? Ах да… пользователи, продажи, прибыль…

Автор: flamefork

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js