Сначала разберемся с определениями. Под обновлением на лету мы подразумеваем такое обновление системы, при котором не нарушается штатная ее работа: клиенты работают, посетители ходят и никто не наблюдает ошибок, увеличившегося времени отклика или таблички “УЧЁТ”.
Зачем это нужно? Если вы задаетесь этим вопросом — вам не нужно. Вешайте табличку, садитесь обедать.
Как это делается? Сложно. Почему? Главных причин две: — вы не можете обновить систему мгновенно и атомарно (то есть ровно между двумя 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