Почему маленькие частые релизы безопаснее редких больших
Интуиция подсказывает обратное: реже выкатываем — реже ломаем. На практике всё наоборот, и объяснить почему — хороший способ показать инженерную зрелость.
Размер изменения определяет и вероятность отказа, и стоимость диагностики. В релизе из двухсот коммитов авария означает долгий поиск виновного изменения; в релизе из одного коммита причина очевидна. Кроме того, редкие релизы делают сам процесс выкатки редким событием, а редкое событие всегда выполняется хуже отработанного.
Отсюда стандартный набор метрик, о котором полезно знать: частота развёртываний, время от коммита до продакшена, доля неудачных изменений и время восстановления сервиса. Ключевое наблюдение отраслевых исследований в том, что скорость и стабильность не противоречат друг другу — команды, выкатывающие часто, восстанавливаются быстрее и ломают реже.
Практический вывод для проектирования: любая стратегия выкатки должна делать откат дешёвым и быстрым. Возможность быстро вернуться назад ценнее попыток никогда не ошибаться.
Rolling, blue-green, canary
Rolling update — экземпляры заменяются постепенно, по одному или небольшими группами. Дёшево по ресурсам и является поведением по умолчанию в оркестраторах. Главное следствие, о котором забывают: во время выкатки в проде одновременно работают старая и новая версии, поэтому они обязаны быть совместимы между собой и по API, и по схеме данных. Откат — тоже постепенный, то есть не мгновенный.
Blue-green — рядом поднимается полный новый комплект, трафик переключается разом, старый остаётся горячим. Плюсы: мгновенное переключение и мгновенный откат обратно. Минусы: двойные ресурсы на время выкатки и по-прежнему нерешённый вопрос общей базы — она одна на оба комплекта, поэтому схема всё равно обязана быть совместима с обеими версиями. Проблема «одновременно работают две версии» не исчезает, а лишь сжимается по времени.
Canary — новая версия получает малую долю трафика (скажем, 1%), её метрики сравниваются с базовой линией, и при отсутствии деградации доля постепенно растёт. Это единственная из трёх стратегий, которая ограничивает радиус поражения: неудачное изменение затронет процент пользователей, а не всех. Условие применимости — работающая наблюдаемость: без разделения метрик по версиям канарейка бессмысленна, сравнивать не с чем.
Практический выбор обычно такой: rolling по умолчанию, canary для рискованных изменений в критичных сервисах, blue-green когда нужна возможность мгновенно вернуться и есть ресурсы на второй комплект.
Деплой и релиз — разные события
Одна из самых полезных идей в этой теме: выкатка кода и включение функциональности не обязаны совпадать. Код уезжает в прод выключенным за флагом, а включается отдельным действием — возможно, через неделю, возможно, сначала для сотрудников.
Что это даёт. Ветки живут недолго, потому что незаконченная работа безопасно едет в мастер под выключенным флагом. Включение и выключение занимают секунды и не требуют выкатки — это самый быстрый механизм отката из существующих. Появляется возможность постепенного раскатывания по сегментам пользователей и A/B-экспериментов.
Цена — реальная и её надо назвать. Каждый флаг удваивает число путей исполнения, а несколько флагов дают комбинаторный взрыв состояний, которые никто не тестирует. Забытые флаги превращаются в мёртвый код, который все боятся трогать. Поэтому у флага должен быть владелец и срок жизни, а удаление флага после полного раската — обязательная часть работы, а не необязательная уборка.
Стоит различать типы флагов: короткоживущие релизные (выключаются и удаляются после раската) и долгоживущие операционные, например рубильник для отключения тяжёлой функции под нагрузкой. К первым дисциплина удаления применяется строго, вторые живут осознанно.
Миграции схемы без простоя
Самая частая причина простоя при выкатке — не код, а база. Пока приложение обновляется постепенно, старая и новая версии работают одновременно с одной схемой, поэтому схема обязана быть совместима с обеими. Отсюда железное правило: миграция схемы и выкатка кода — это два разных релиза, а не один.
Работающая техника — expand–contract, она же parallel change, в три фазы. Expand: добавляем новое, ничего не ломая (новая колонка nullable, новая таблица, новый индекс), — старый код этого просто не замечает. Migrate: выкатываем код, который пишет и в старое, и в новое место, и фоновым процессом переносим исторические данные порциями; затем переключаем чтение на новое. Contract: когда в проде не осталось версий, читающих старое, удаляем старую колонку — отдельным, более поздним релизом.
Разберём переименование колонки, любимый пример: прямой RENAME сломает работающие экземпляры старой версии немедленно. Правильно — добавить новую колонку, писать в обе, перелить данные, перевести чтение, дождаться полного раската, удалить старую. Пять шагов вместо одного, зато без простоя и с возможностью остановиться на любом этапе.
Практические грабли, о которых стоит упомянуть: добавление колонки с NOT NULL и значением по умолчанию на старых версиях СУБД переписывало всю таблицу под блокировкой; создание индекса блокирует запись, если не использовать неблокирующий вариант (CONCURRENTLY в PostgreSQL); длинная миграция в одной транзакции держит блокировки и мешает всем. Поэтому массовый бэкфилл делают порциями с паузами, а не одним UPDATE.
Инструменты версионирования схемы (Flyway, Liquibase) решают вопрос воспроизводимости и порядка применения, но не отменяют совместимости: миграция, ломающая предыдущую версию кода, останется ломающей, даже будучи аккуратно версионированной.
-- Contract-фаза отдельным, более поздним релизом:
-- 1) expand
ALTER TABLE users ADD COLUMN full_name text; -- nullable, безопасно
CREATE INDEX CONCURRENTLY idx_users_full_name ON users(full_name);
-- 2) migrate: код пишет в оба поля, бэкфилл порциями
UPDATE users SET full_name = name
WHERE full_name IS NULL AND id BETWEEN :from AND :to; -- батчами, не одним UPDATE
-- 3) contract — только когда старых версий в проде не осталось
ALTER TABLE users DROP COLUMN name;
Откат
Стратегия отката должна выбираться до выкатки, а не во время инцидента. Вариантов два, и у каждого своя область применения.
Rollback — вернуть предыдущую версию кода. Быстро и предсказуемо, но работает только если новая версия не оставила несовместимых следов: не удалила колонку, не записала данные в формате, который старый код не прочитает, не отправила события новой схемы.
Roll-forward — быстро выкатить исправление вперёд. Единственный вариант, когда назад дороги нет: миграция уже необратима или данные уже мигрировали. Именно поэтому необратимые шаги (удаление колонки, удаление данных) откладывают в отдельный поздний релиз, когда уверенность высока.
Отсюда общее правило проектирования выкатки: сначала все обратимые шаги, необратимые — последними и с задержкой. Если операция принципиально необратима, до неё делают резервную копию и явно проговаривают точку невозврата.
И последнее: откат надо репетировать. Механизм отката, ни разу не выполнявшийся в мирное время, во время аварии не сработает.
Как отвечать на собеседовании
Начните с тезиса, что маленькие частые релизы безопаснее редких больших, и обоснуйте через размер изменения и стоимость диагностики. Это неочевидно и сразу выделяет ответ.
Сравнивая стратегии выкатки, обязательно назовите ключевое следствие rolling: в проде одновременно живут две версии, поэтому совместимость обязательна. И отметьте, что blue-green сжимает эту проблему по времени, но не устраняет — база-то общая.
На вопрос «как выкатить переименование колонки без простоя» отвечайте пятью шагами expand–contract. Это самый частый практический вопрос темы, и полный ответ с фазой удаления в отдельном позднем релизе выглядит очень убедительно.
Про feature flags скажите не только про пользу, но и про цену — комбинаторный взрыв путей и мёртвый код, — и про обязательное удаление после раската. Завершите стратегией отката: обратимые шаги сначала, необратимые последними, механизм отката отрепетирован.